githubEditar

HackTheBox - Pterodactyl

Writeup de la máquina Pterodactyl de HackTheBox

  • Dificultad medium

  • Tiempo aprox. ~6h

  • Datos Iniciales: 10.129.4.98

Nmap Scan

Empezamos haciendo un escaneo de todos los puertos en TCP:

$ nmap -sT -Pn -n -p- --open 10.129.4.98 -v                
Starting Nmap 7.98 ( https://nmap.org ) at 2026-02-24 17:47 -0500
Initiating Connect Scan at 17:47
Scanning 10.129.4.98 [65535 ports]
Discovered open port 22/tcp on 10.129.4.98
Discovered open port 80/tcp on 10.129.4.98
RTTVAR has grown to over 2.3 seconds, decreasing to 2.0
RTTVAR has grown to over 2.3 seconds, decreasing to 2.0
RTTVAR has grown to over 2.3 seconds, decreasing to 2.0
RTTVAR has grown to over 2.3 seconds, decreasing to 2.0
...

Como al parecer tarda mucho, limitamos la cantidad de puertos a la predeterminada de nmap (Top 1000 puertos) y escaneamos servicios después:

$ nmap -sT -Pn -n --open 10.129.4.98
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

$ nmap -sT -Pn -n -p22,80 -sVC --open 10.129.4.98
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6 (protocol 2.0)
| ssh-hostkey: 
|   256 a3:74:1e:a3:ad:02:14:01:00:e6:ab:b4:18:84:16:e0 (ECDSA)
|_  256 65:c8:33:17:7a:d6:52:3d:63:c3:e4:a9:60:64:2d:cc (ED25519)
80/tcp open  http    nginx 1.21.5
|_http-server-header: nginx/1.21.5
|_http-title: Did not follow redirect to http://pterodactyl.htb/

Añadimos pterodactyl.htb a /etc/hosts.

Vemos los siguientes puertos abiertos:

  • 22/TCP (SSH): Versión vulnerable a algunos ataques MitM y a RegreSSHionarrow-up-right, difícilmente explotable.

  • 80/TCP (HTTP): Vulnerable a DoS y a otros ataques no relevantes.

No hay gran cosa, tendremos que ir a por el puerto 80.

Puerto 80, HTTP

Antes de entrar a la página principal, dado que nmap nos ha indicado que se utilizan vhosts (al indicar el did not follow redirect to http://pterodactyl.htb), es posible que haya más en la máquina, así que primero buscamos vhosts:

Y encontramos panel.pterodactyl.htb, lo añadimos a /etc/hosts.

Dominio principal pterodactyl.htb

Al entrar, encontramos una invitación para unirnos a un servidor de Minecraft MonitorLand.

Aquí vemos dos cosas:

  • Un subdominio play.pterodactyl.htb (que redirige exactamente a la misma página)

  • Unos changelogs en http://pterodactyl.htb/changelog.txt:

De aquí ya podemos apuntar que se está usando Pterodactyl Panel v1.11.10, aunque todavía no sabemos exactamente qué es. Además se usa MariaDB 11.8.3 como backend.

Subdominio panel.pterodactyl.htb

Al entrar, nos encontramos un panel de login

No tenemos credenciales, pero, de todas formas, primero convendría saber qué es Pterodactyl para poder ver cómo enfrentarnos a él. Al mirar en Internet encontramos varias páginasarrow-up-right que nos indican de qué se trata:

Pterodactyl is a free, open-source game server management panel built with PHP, React, and Go. Designed with security in mind, Pterodactyl runs all game servers in isolated Docker containers while exposing a beautiful and intuitive UI to end users.

Pterodactyl consists of two core components that work together: the Panel (web interface) and Wings (server daemon). The Panel provides the management interface, while Wings handles the actual game server operations on each node.

CVE-2025-49132, Revshell

Al mirar en Internet, vemos que existe una vulnerabilidad crítica (10.0 CRITICAL en Github) de Unauthenticated RCE: CVE-2025-49132arrow-up-right. Afecta a las versiones anteriores a la v1.11.11, entre las que se encuentra la que hemos visto que corre en este servidor: v1.11.10.

Usaremos un exploit públicoarrow-up-right:

Apuntamos las credenciales de MariaDB pterodactyl:PteraPanel. Ahora creamos un payload para el revshell:

Lo ejecutamos:

Pero en el listener no recibimos nada, solo vemos el output del comando ejecutado inmediatamente antes. Se está usando bash para el reverse shell, que casi seguro existe en el sistema, pero no está de más comprobar:

Pero si probamos con cualquiera de las rutas absolutas, seguimos sin recibir nada. Probamos a ver si al menos podemos usar bash:

Comprobamos, al mandar command -v perl, que también existe perl , así que probamos a usar un revshellarrow-up-right de perl:

Y sigue sin funcionar.

Debugging

Aviso: Si valoras tu tiempo, aviso de que todo lo que hago a partir de aquí hasta justo antes de Plan B, Webshell lleva hasta un reverse shell que no funciona. Si simplemente quieres saber cómo llegar a rootear la máquina, puedes saltar esta parte. Si te interesa cómo intento debuggearlo, eres libre de quedarte.

Tras probar con puertos comunes (80,443) para la reverse shell, sigue sin ir. Comprobamos si hay conectividad alguna o si hay algún firewall bloqueando conexiones:

Y en nuestro server http:

Tenemos conexión saliente del servidor, pero por algún motivo bash no puede iniciar la conexión. Tras buscar un rato, veo que puede ser que bash esté compilado en el container de Pterodactyl sin soporte para /dev/tcp (una práctica común) con el fin de reducir superficie de ataque. Como sabemos que PHP está instalado y funciona (el propio CVE se basa en PHP), iniciaremos la conexión con PHP y desde ahí iniciaremos bash (Similar a un staged payload).

  1. Guardamos un archivo revshell.sh con el reverse shell de PHP:

  1. Abrimos el puerto 80 con un server HTTP de python, sirviendo revshell.sh; y el puerto 443 preparado para la revshell.

  2. Hacemos que la víctima haga curl a nuestro payload y lo ejecute en memoria:

Pero en ambos puertos en escucha vemos lo mismo: Llegan 12 solicitudes http, y al handler le llegan shells inválidas:

El problema posiblemente sea que, aunque la primera revshell que llegue sea buena, el resto que llega pisa la conexión anterior y hace que al final todas las conexiones resulten inválidas. Además, al morir el proceso padre php, el resto de shells mueren. Estos dos problemas los podemos solucionar de forma sencilla.

Revshell definitivo (que no va)

  1. Añadimos una comprobación para que solo se inicie la primera conexión

  2. Añadimos nohup para que cuando muera el proceso padre no mueran los hijos y filtramos errores y output.

Ahora hacemos lo mismo que antes:

Pero si miramos el puerto en escucha:

Aunque parece que hemos avanzado bastante, tenemos un problema. Si intentamos escribir cualquier cosa, el shell no responde, está congelado. Tenemos un revshell pero ni siquiera funciona.

Plan B, Webshell

Por algún motivo, el revshell que hemos conseguido antes no iba y, antes de ponerme a debuggear otra vez, busco una alternativa.

Primero se me ocurre pasar un binario de chisel a la máquina (recordemos que curl sí funcionaba), hacer port-forwarding de la base de datos (para la que teníamos credenciales pterodactyl:PteraPanel), y rezar por que hubiese un hash de contraseña de un usuario del sistema, pero luego pienso en otra alternativa más fácil antes.

La revshell podía fallar por muchos motivos, y la forma más sencilla de eliminar esos motivos es usar una webshellarrow-up-right PHP y ejecutar la revshell desde ahí:

Aunque ponga Command not found, en nuestro servidor python hemos recibido las solicitudes. Ahora accedemos al webshell y ejecutamos nuestro reverse shell:

Y por fin tenemos un shell estable.

Realmente podríamos haber hecho bastante enumeración desde el webshell o incluso desde el exploit (que nos dan un nivel de interactividad similar), pero es bastante más comodo tener un shell completo.

Privesc (desde wwwrun)

Posiblemente el vector de escalada esté en unas credenciales guardadas en MariaDB para el panel de login que se reutilicen para un user del sistema, así que probamos:

Al parecer las credenciales no son esas. Antes de enumerar por otro lado, busco dónde están guardadas las credenciales de la DB en los archivos de Pterodactyl (por si han cambiado) y casualmente me encuentro con este post que me salva mucho tiempoarrow-up-right.

De nuevo, probamos:

Bingo.

  • information_schema es la default de MySQL, tiene metadatos.

  • test está vacía.

  • panel parece (y es) la importante.

Hay users y user_ssh_keys, parece que tenemos lo que buscábamos. Echamos un vistazo y conseguimos la siguiente info:

Los metemos a hashcat:

Y sacamos la contraseña de phileasfogg3: !QAZ2wsx

Privesc (desde phileasfogg3)

Vamos a SSH:

Ejecutamos linPEAS:

  • PATH: /home/phileasfogg3/bin:/usr/local/bin:/usr/bin:/bin.

    • El primer directorio en el que se buscan los programas es el ~bin de phileasfogg3.

  • Puertos en local abiertos:

Redis (rabbit hole)

Entramos primero a redis:

Vemos que la versión de Redis es la 8.2.1, según Internet, potencialmente vulnerable a CVE-2025-49844 "Redishell"arrow-up-right

Redis is an open source, in-memory database that persists on disk. Versions 8.2.1 and below allow an authenticated user to use a specially crafted Lua script to manipulate the garbage collector, trigger a use-after-free and potentially lead to remote code execution

Problema: Redis se ejecuta con la cuenta de servicio redis, así que no vamos a conseguir elevar privilegios. De hecho, conseguir ser redis posiblemente nos limite más porque sus capacidades estarán intencionalmente restringidas.

Sudo (rabbit hole)

Tras echar un vistazo a linPEAS otra vez sin ver nada, miramos la versión de sudo:

Y al buscar en google:

La versión sudo 1.9.15p5 (y versiones anteriores hasta la 1.9.14) está afectada por dos vulnerabilidades críticas descubiertas en 2025 que permiten la escalada de privilegios a root.

La más relevante es CVE-2025-32463arrow-up-right, cuyo exploitarrow-up-right usaremos. Como no tenemos gcc en la máquina (y el exploit lo usa), compilamos la librería de forma local con las flags que usa el exploit:

  1. Creamos woot1337.c:

  1. Lo compilamos igual que en el exploit y lo servimos

  1. Editamos el .sh:

Pero lo ejecutamos:

Y no funciona, tenemos que buscar otra vía.

Analizando programas instalados con Trivy

Tras haber buscado binarios con SUID, cronjobs, permisos sudo, servicios en ejecución, archivos con credenciales que podamos leer, haber ejecutado linPEAS y más, seguimos sin encontrar nada que nos permita elevar privilegios.

No quedan muchas alternativas, pero podemos mirar qué paquetes hay instalados por si hay alguno vulnerable:

Con casi 900 paquetes instalados es inviable analizarlos manualmente, pero sí podemos usar un escáner automático como trivy para hacer el trabajo:

Trivy necesita descargar su DB, pero la máquina no tiene acceso a Internet, así que la descargamos localmente en nuestra máquina y la comprimimos para subirla:

Y ya con el binario de trivy en la víctima, descargamos la db también:

Ahora ejecutamos y buscamos vulnerabilidades en el fs raíz, (rootfs), en modo offline y usando nuestra db descargada (--offline-scan --cache-dir /tmp/triv/), que sean CRITICAL o HIGH (--severity CRITICAL,HIGH) y de paquetes del SO (--vuln-type os). Luego filtramos por privesc (grep "privilege esc"), ordenamos y quitamos repetidos (sort -u):

Y tenemos 2 vulnerabilidades, una de open-vm-tools y otra de libblockdev (Udisks2). Posiblemente la de open-vm-tools tenga que ver con que la propia máquina es una VM (como el resto de máquinas de HTB), así que lo que queda es el CVE-2025-6019.

CVE-2025-6019

Se trata de un fallo de seguridad en el modo en que libblockdev interactúa con udisks2 al redimensionar sistemas de archivos, lo que permite ejecutar código con privilegios de root a través de un sistema de archivos especialmente preparado. El atacante crea una imagen XFS que contiene un binario SUID con permisos de root y engaña a udisks para montarla sin las protecciones habituales (al redimensionarla), ejecutando así el shell SUID.

Encontramos este exploitarrow-up-right público, lo descargamos y subimos a la máquina. Al ejecutarlo:

No ha funcionado, pero, por suerte, en el propio PoC se contemplaba esta posibilidad y se indica que puede solucionarse de forma fácil:

Luego nos reconectamos

Y tenemos root.

Post-Root: CVE, D-Bus y Polkit.

Tras ejecutar el PoC del CVE-2025-6019 y conseguir el shell como root, surgen varias preguntas:

Cómo es posible que estemos explotando una vulnerabilidad que nos permita llegar a ser root en un programa que ni podemos ejecutar como root (no tenemos permisos sudo ni hay SUID bit o capabilities), ni se está ejecutando como root (no aparece al usar**** ps aux)?

La respuesta es el D-Busarrow-up-right. En Linux, los procesos están aislados por seguridad, y si un proceso necesita hacer algo importante, no puede hacerlo directamente, necesita pedírselo al sistema. El D-Bus es el sistema de mensajería interna (IPC) de Linux que permite que los procesos se comuniquen entre sí. Los servicios pueden registrarse en los D-Bus y exponer funcionalidades y señales y permitir que otros procesos las soliciten. Al registrarse, también pueden pedir que el sistema les "despierte" cuando llegue una señal específica al D-Bus.

En Linux hay un System D-Bus (global para la máquina) y otro Session D-Bus para cada sesión de usuario (con servicios no root).

Podemos ver qué servicios hay registrado en el System D-Bus con busctl list (para el D-Bus de la sesión actual sería busctl --user list):

Si nos fijamos, entre todo esto, vemos la siguiente línea:

UDisks2 era el servicio vulnerable, y antes de ejecutar el exploit, su CONNECTION está puesta a (activatable), en lugar de tener un ID como el resto de servicios del D-Bus. Tampoco tiene un PID ni un usuario asociado. Esto se debe a que, aunque el servicio está registrado en el D-Bus, no está activo, ni siquiera tiene un proceso vivo, por eso no veíamos nada relacionado con UDisks2 al usar ps aux.

Como UDisks2 es un programa que opera con discos y particiones (cosa que no siempre se usa), el servicio pasa la mayor parte del tiempo inactivo, pero registrado en el D-Bus y a la espera de que un programa le mande una señal que haga que se despierte, de ahí el (activatable). Cuando alguien mande un mensaje a org.freedesktop.UDisks2, D-Bus le dirá a systemd que inicie el binario de UDisks2, le asigne un PID y usuario y procese la petición, de ahí que pueda hacer cosas como root.

Y es exactamente por eso que, tras haber ejecutado el exploit, si volvemos a mirar:

Ya existe un proceso por debajo.

Ahora que sabemos por qué no lo veíamos antes y cómo y por qué el programa se despertaba (y que lo hacía como root), cómo funcionaba el CVE?

Partimos de que UDisks2 es un servicio y una herramienta de Linux que permite gestionar dispositivos de almacenamiento como HDDs, SSDs y demás. Ofrece, como hemos dicho, una interfaz en el D-Bus para que los programas puedan montar y desmontar particiones o formatear discos (entre otros) sin necesidad de ser root.

En UDisks2, cuando un usuario solicita redimensionar un sistema de archivos XFSarrow-up-right, udisks cede la tarea a la biblioteca libblockdev (la que ha detectado Trivy). Para redimensionar el fs., libblockdev necesita montarlo temporalmente en /tmp. La vulnerabilidad consiste en que ese montaje temporal se realiza sin aplicar flags de seguridad como nosuid (que hace que los binarios del fs. con el bit SUID/SGID no tengan tal efecto en el sistema global).

Aprovechando ese fallo, un atacante puede preparar un exploit como el sacado de Github, que contenía 4 archivos:

  • build_poc.sh: Compila localmente catcher y crea exploit.img para subirlos al server de la víctima después.

  • exploit.sh: Solicita a UDisks2 que redimensiona exploit.img e inmediatamente después inicia el catcher.

  • exploit.img: Imagen XFS maliciosa con un binario bash con SUID bit puesto.

  • catcher: Se mantiene a la escucha hasta que libblockdev monta la imagen para redimensionarla (sin el nosuid)

Al ejecutar exploit.sh, este pide a UDisks2 que redimensione la imagen. El catcher se mantiene en escucha y cuando ve que libblockdev ha montado el fs., inmediatamente ejecuta el shell bash con SUID que había dentro.

Por qué ha fallado la primera vez? En qué consistía la solución?

Aquí entra en juego Polkitarrow-up-right, otro sistema (que también veíamos en el D-Bus como org.freedesktop.PolicyKit1) encargado de controlar las autorizaciones (quién puede hacer qué). Permite establecer reglas estrictas para cada usuario sin necesidad de ejecutar todo el programa como root.

El proceso de funcionamiento de Polkit es el siguiente (p.ej para añadir una impresora):

  1. La aplicación de config. manda un mensaje por D-Bus a CUPS, solicitando añadir una impresora

  2. CUPS recibe la solicitud y sabe que añadir una impresora requiere permisos especiales, pero no sabe si el usuario los tiene.

  3. CUPS pregunta a Polkit (via D-Bus): "El usuario X quiere hacer Y acción, debería dejarle?"

  4. Polkit revisa sus reglas guardadas en /etc/polkit-1/rules.d/ o /usr/share/polkit-1/actions/, de ahí puede ver varias cosas:

  • Sí, el usuario tiene permiso, CUPS puede añadir la impresora.

  • No, el usuario no tiene permiso, CUPS no debería añadir la impresora (aunque CUPS puede ignorarle)

  • Autenticación Requerida: Puede, pero debe autenticarse (se pide contraseña y se delega la comprobación a PAM).

  1. En función de lo recibido, CUPS hará una cosa u otra

Cuando lo que se quiere es montar o trabajar con discos, la política de Polkit es clara:

  • Si un usuario está conectado por SSH, se le considera "sesión inactiva" y se requiere que introduzca la contraseña de root para interactuar con el hardware. Por eso cuando se ha intentado crear el disco para redimensionarlo, Polkit nos ha parado.

Para solucionar esto, hemos tenido que añadir 2 variables a ~/.pam_environment:

  • XDG_SEAT=seat0: Indica el "contexto de hardware", seat0 hace referencia a teclado y ratón físicos.

  • XDG_VTNR=1: Indica número de terminal. Se pone tty1 en este caso, que hace referencia a la terminal común para logins GUI.

Estas dos variables en el archivo explotan otra vulnerabilidad (CVE-2025-6018) que engaña a PAM para que marque la sesión remota por SSH como una sesión activa:

Esto hace que, al ejecutar el exploit, Polkit no nos pare a mitad, permitiéndonos cargar el filesystem con el binario de bash y finalmente obtener un shell como root.

Última actualización