- 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>
170 lines
5.1 KiB
Python
170 lines
5.1 KiB
Python
"""
|
|
FastRDP-NG: Blazing-fast async RDP scanner.
|
|
Connects to thousands of IPs concurrently to find live RDP hosts.
|
|
Supports optional proxy routing via ProxyManager.
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
import time
|
|
from typing import Set, Tuple, List, Optional
|
|
|
|
logger = logging.getLogger("FastRDP-NG")
|
|
|
|
# RDP ports to scan
|
|
RDP_PORTS = [3389, 3390, 3391]
|
|
|
|
# First 3 bytes of an RDP Negotiation Response (T.125)
|
|
# Actual RDP servers respond with 0x03 (TPKT version 3)
|
|
RDP_BANNER_SIG = b'\x03\x00'
|
|
|
|
|
|
async def check_rdp_port(
|
|
ip: str,
|
|
port: int,
|
|
timeout: float = 2.0,
|
|
proxy_manager=None
|
|
) -> Tuple[str, int, bool]:
|
|
"""
|
|
Rapid async TCP connect check.
|
|
Supports optional proxy routing.
|
|
|
|
Returns (ip, port, is_open).
|
|
No banner reading - pure connection speed.
|
|
"""
|
|
open_conn = asyncio.open_connection
|
|
if proxy_manager and proxy_manager.enabled:
|
|
open_conn = proxy_manager.open_connection
|
|
|
|
try:
|
|
_, writer = await asyncio.wait_for(
|
|
open_conn(ip, port),
|
|
timeout=timeout
|
|
)
|
|
writer.close()
|
|
await writer.wait_closed()
|
|
return (ip, port, True)
|
|
except (asyncio.TimeoutError, ConnectionRefusedError,
|
|
OSError, ConnectionError):
|
|
return (ip, port, False)
|
|
|
|
|
|
async def check_rdp_with_banner(
|
|
ip: str,
|
|
port: int,
|
|
connect_timeout: float = 2.0,
|
|
banner_timeout: float = 1.0,
|
|
proxy_manager=None
|
|
) -> Tuple[str, int, bool]:
|
|
"""
|
|
Connect + read initial bytes to confirm it's actually RDP.
|
|
Supports optional proxy routing.
|
|
Slower but more accurate - catches non-RDP services on 3389.
|
|
"""
|
|
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=connect_timeout
|
|
)
|
|
try:
|
|
data = await asyncio.wait_for(
|
|
reader.read(4),
|
|
timeout=banner_timeout
|
|
)
|
|
is_rdp = data[:2] == RDP_BANNER_SIG # TPKT header
|
|
return (ip, port, is_rdp)
|
|
except (asyncio.TimeoutError, ConnectionError, OSError):
|
|
# Connected but no banner = port open but not RDP
|
|
return (ip, port, False)
|
|
finally:
|
|
writer.close()
|
|
await writer.wait_closed()
|
|
except (asyncio.TimeoutError, ConnectionRefusedError,
|
|
OSError, ConnectionError):
|
|
return (ip, port, False)
|
|
|
|
|
|
async def scan_ips(
|
|
ips: List[str],
|
|
ports: List[int] = None, # type: ignore
|
|
max_concurrent: int = 5000,
|
|
connect_timeout: float = 2.0,
|
|
banner_check: bool = False,
|
|
progress_callback=None,
|
|
live_callback=None,
|
|
proxy_manager=None
|
|
) -> Set[Tuple[str, int]]:
|
|
"""
|
|
Scan a list of IPs across given ports as fast as possible.
|
|
Uses asyncio semaphore to control concurrency.
|
|
|
|
- progress_callback(checked, total, live_count, rate): called periodically
|
|
- live_callback(ip, port): called IMMEDIATELY when a live host is found
|
|
- proxy_manager: optional ProxyManager for routing through proxies
|
|
|
|
Returns set of (ip, port) tuples for live RDP hosts.
|
|
"""
|
|
if ports is None:
|
|
ports = RDP_PORTS
|
|
|
|
sem = asyncio.Semaphore(max_concurrent)
|
|
live_hosts: Set[Tuple[str, int]] = set()
|
|
checked = 0
|
|
total = len(ips) * len(ports)
|
|
start_time = time.time()
|
|
|
|
checker = check_rdp_with_banner if banner_check else check_rdp_port
|
|
|
|
if progress_callback and total > 0:
|
|
if total <= 1000:
|
|
progress_every = max(1, total // 50 or 1)
|
|
elif total <= 10000:
|
|
progress_every = 100
|
|
else:
|
|
progress_every = 1000
|
|
else:
|
|
progress_every = 0
|
|
|
|
async def check_one(ip: str, port: int):
|
|
nonlocal checked
|
|
async with sem:
|
|
_, p, alive = await checker(ip, port, connect_timeout,
|
|
proxy_manager=proxy_manager)
|
|
if alive:
|
|
live_hosts.add((ip, p))
|
|
# Notify immediately — spray can start right away
|
|
if live_callback:
|
|
live_callback(ip, p)
|
|
checked += 1
|
|
if progress_callback and progress_every:
|
|
if checked == total or checked % progress_every == 0:
|
|
elapsed = time.time() - start_time
|
|
rate = checked / elapsed if elapsed > 0 else 0
|
|
progress_callback(checked, total, len(live_hosts), rate)
|
|
|
|
# Fire all tasks concurrently
|
|
tasks = []
|
|
for ip in ips:
|
|
for port in ports:
|
|
tasks.append(asyncio.create_task(check_one(ip, port)))
|
|
|
|
# Run in batches to avoid memory issues with massive task lists
|
|
batch_size = 50000
|
|
for i in range(0, len(tasks), batch_size):
|
|
batch = tasks[i:i + batch_size]
|
|
if batch:
|
|
await asyncio.gather(*batch, return_exceptions=True)
|
|
|
|
elapsed = time.time() - start_time
|
|
rate = checked / elapsed if elapsed > 0 else 0
|
|
logger.info(
|
|
f"Scan complete: {len(live_hosts)} live hosts from {checked} checks "
|
|
f"in {elapsed:.1f}s ({rate:.0f} checks/sec)"
|
|
)
|
|
|
|
return live_hosts
|