Connected is a FreePBX box, and both halves are about trusting the wrong thing. The foothold chains two fresh CVEs — an unauthenticated stacked SQL injection (CVE-2025-57819) that writes a brand-new admin straight into the users table, and an authenticated firmware-upload path traversal (CVE-2025-61678) that drops a webshell in the web root. Root is prettier: FreePBX ships a root-run "sysadmin" hook system that runs module scripts when a file lands in an incron-watched spool. It does hash-check each hook against a signed module.sig — but it never actually verifies the signature, so a user who can write the module directory can rewrite a hook, patch its hash, and have it run as root.
SSH plus a CentOS Apache serving HTTP and HTTPS, redirecting to connected.htb. The TLS cert (pbxconnect) and the /admin title give it away — this is FreePBX.
CVE-2025-57819 — an unauthenticated stacked SQL injection in the endpoint module loader's brand parameter. Because it's stacked, you don't just read data — you run a second statement that INSERTs a fresh admin row into ampusers.
CVE-2025-61678 — an authenticated arbitrary file upload. Logged in as the admin you just created, the Endpoint Manager's firmware uploader (upload_cust_fw) trusts a fwbrand path, and a traversal walks the uploaded file out into the web root as a .php webshell.
The exploit does all of it and hands you command execution. One transient note: the first run timed out on the login POST (the box is slow to answer); the retry sailed through.
FreePBX RCE
$ python3 exploit.py --rhost <TARGET_IP> --command 'id'[*] [CVE-2025-57819] creating admin via stacked SQLi: svc_p2v21:...
[+] admin row inserted into ampusers
[+] authenticated as svc_p2v21
[*] [CVE-2025-61678] uploading webshell -> /3jjb7bz2oi/t5rxo97i.php
[+] webshell live
uid=999(asterisk) gid=1000(asterisk) groups=1000(asterisk)
RCE as asterisk. Rather than juggle a reverse shell, I used the single-command mode to plant an SSH key and get a stable session — user flag's right there:
SUID hunting turns up a vulnerable-looking pkexec, but its exploit doesn't land. The real path is FreePBX's own automation glue. /etc/incron.d defines incron watches — inotify-driven cron — and one of them is the whole game:
So whenever a file appears in /var/spool/asterisk/incron, root runs sysadmin_manager with that filename. That binary (its logic lives in /usr/lib/sysadmin/includes.php) parses the filename as <module>.<hook> and, after a series of checks, runs the module's hook script as root:
includes.php — the payoff
$sigfile = "/var/www/html/admin/modules/$module/module.sig";
$hookfile = "/var/www/html/admin/modules/$module/hooks/$hook";
$verify = $g->checkSig($sigfile); # parses the signed hash list
...
if (hash_file('sha256', $hookfile) !== $verify['hashes'][$signame]) exit; # hash gate
...
system("$hookfile $params"); # runs as ROOT
On paper that's locked down: the hook has to match a sha256 listed inside a GPG-clearsigned module.sig that must be signed by a whitelisted FreePBX key. But look at what the code actually checks — it asserts the [hashes] and [config][signedwith] blocks exist and are whitelisted, and that the hook's hash matches. It never checks that the GPG signature actually verified.checkSig happily parses the hash list out of a clearsigned file whose body has been edited.
And the modules directory is ours: /var/www/html/admin/modules is owned asterisk:asterisk, mode 775. So pick a module with a hook — the api module has a logrotate one — rewrite the hook to drop a SUID bash, recompute its sha256, patch that one line in module.sig, and touch the trigger:
The hook system did the hard part — GPG-signing releases, whitelisting keys, hashing every file — and then forgot to enforce it. A hash pinned inside a signature you never validate is just a checksum, and a checksum you can rewrite is no control at all. Verify the signature's result, and don't let the account that serves the web app own the directory the root daemon trusts.
Why it worked
Stage
Root cause
→ admin
Unauthenticated stacked SQL injection in the endpoint module loader inserts an admin — CVE-2025-57819 (CWE-89)
→ asterisk
Authenticated firmware-upload path traversal writes a PHP webshell to the web root — CVE-2025-61678 (CWE-22 / CWE-434)
→ root
Root sysadmin hook trusts an editable module.sig it never GPG-verifies, and the web user owns the module dir (CWE-347 / CWE-732)