Over seven years later, the Heartbleed vulnerability still offers important lessons in application security.
Heartbleed is a serious vulnerability discovered in the openssl open source software component in April 2014.
This article is a deep dive on Heartbleed and its broader implications for application security:
Heartbleed is a vulnerability in an open source software component called openssl. As with many other open source software components, openssl is available for anyone in the world to use as part of an application (subject to its licensing, of course).
The openssl component is an implementation of the transport layer security (TLS) network protocol, which is used by many kinds of applications. TLS provides authentication, integrity, and confidentiality for network communication. For example, whenever you visit an https://… URL, your browser uses TLS to verify the server’s identity, set up an encrypted connection, and retrieve pages and other resources over the encrypted connection.
TLS is complicated and hard to get right. For application developers, it makes perfect sense to use an open source implementation such as openssl. As long as the license terms of the component are acceptable, using an open source component gives application developers a robust, mature implementation with very little initial expense.
The TLS protocol dictates how conversations happen between two network endpoints: a client and a server. You can think of the client as a web browser and the server as a web server. Fundamentally, any TLS conversation has two phases:
The TLS protocol has gone through several major versions, and openssl has been updated to keep pace. Heartbleed was created when openssl was updated for TLS 1.2 in 2012.
Specifically, TLS 1.2 added a new type of message: the heartbeat. The intention was that either side could ask the other “Are you there?” A heartbeat request message from one side is acknowledged with a heartbeat response from the other side.
Heartbeat messages are very simple, consisting of a length field and a payload. The payload in the request is echoed back in the response.
For example, the following might be the content of a heartbeat request, shown as hexadecimal:
The heartbeat response should contain exactly the same length and payload.
Unfortunately, the openssl developers made one critical error when implementing heartbeat messages. They fully trusted the length sent in the request. This is the Heartbleed vulnerability.
If the request lies about its payload length, openssl still returns the requested length, which includes stack memory. This type of vulnerability is “information leakage.” The data returned might contain sensitive information like recently used credentials or even the private cryptographic key of the server.
For example, a badly formed request could easily claim a length longer than the given payload:
In this case, openssl might respond as follows:
The extra 16 (hex 10) bytes in the heartbeat response come directly from the server’s stack memory.
Even worse, the initial implementation of heartbeat messages allowed requests and responses in the unencrypted part of the TLS conversation, making exploitation very simple.
Because the payload length is 32 bits, an attacker could send heartbeat request messages with a false payload length of 65,535 bytes (64k, hex FFFF) and an empty payload, essentially prompting the victim system to dump out 64k chunks of memory. The attacker can examine these chunks of data for credentials, the server’s private key, and other sensitive information.
To see how Heartbleed works in practice, you just need a vulnerable server and an exploit script.
If you’d like to try this out for yourself, all the necessary scripts are here:
The scripts work in a Linux environment with Docker installed. You can get the code as follows:
$ git clone https://github.com/jknudsen-synopsys/heartbleed-box $ cd heartbleed-box
To run a vulnerable server, you can retrieve an old version of openssl and build it yourself. I’m using a Docker container for this purpose; the relevant excerpt from the Dockerfile is as follows. Most notably, the git checkout command retrieves the 1.0.1f version, the last version vulnerable to Heartbleed.
RUN git clone https://github.com/openssl/openssl && \ cd openssl && \ git checkout OpenSSL_1_0_1f && \ ./config && \ make && \ make install_sw
If you’re following along, you don’t need to run any of this manually. Just build the container like this:
Be prepared to wait a minute or two.
If you inspect the Dockerfile, you’ll notice that it also runs a command to generate a key pair and certificate for a server. Finally, the ENTRYPOINT is configured to run openssl s_server, which implements a simple TLS-enabled web server.
Once you’ve built the container, you can run it as follows:
$ ./run.sh Using default temp DH parameters Using default temp ECDH parameters ACCEPT
The container starts up and runs the server, which is ready to accept incoming connections on port 4433.
In this example, we’ll exploit Heartbleed to retrieve user credentials. First, though, we need to simulate a user logging in to the server. One way this could happen in a web application is with a login form. This is often an HTML form whose input gets POSTed to the web application.
We can simulate submitting a login form using curl. Our example server, which is really openssl s_server, doesn’t actually have any pages. That doesn’t matter because we are just demonstrating how credentials that are submitted with a web form are available in the server memory to be poached by exploiting Heartbleed.
The following command, for example, simulates a login form that POSTs to /submit with an example username and password:
$ curl -k -s -m .1 -d "username=jknudsen&password=secret123" https://localhost:4433/submit
The options are as follows:
I’ve encapsulated this command in a script, so you can simulate a login like this:
$ ./simulate-post.sh jonathan secret123 Simulating login with username=jonathan and password=secret123
Keep in mind that these credentials are encrypted before being transmitted to the server.
Now you will see how Heartbleed can be exploited to pluck the plaintext user credentials out of the server’s memory.
Because vulnerable versions of openssl respond to heartbeat messages in the unencrypted negotiation phase of the TLS conversations, exploitation is pretty simple.
First, we connect to the TCP port and send a valid TLS ClientHello message, which is enough to convince openssl that we are starting a TLS conversation. After that, we can send a malformed heartbeat request message, as previously described.
In Python, the ClientHello and heartbeat request messages can be defined as follows:
Once the messages are defined, having a TLS conversation is pretty easy:
The most complicated part of this exploit (and it’s not very complicated) is searching through the heartbeat response message to locate credentials. I won’t reproduce the code here, but in essence we just need to look for username= in the response and try to locate the end of that string.
I’ve wrapped all this up in exploit-01.py, which you can easily run as follows:
$ python3 exploit-01.py [Connecting to localhost port 4433] username=jonathan&password=secret123 [Closing socket]
Seven years later, Heartbleed still has important lessons for all of us.
First, managing open source software components is critically important for application security. While using open source components is a practical and fruitful strategy for application developers, those components do have to be managed properly. You have to know which components you’ve used in your applications, and you must be aware of any known vulnerabilities in those components. When new vulnerabilities are published about the software components you’ve used, you need to know right away so you can take action if necessary. (Likewise, you should know the software licenses of those components to ensure you are not using something improperly, but that is not the focus of this article.) A software composition analysis (SCA) solution like Black Duck automates much of this work.
Second, you cannot rely on anyone upstream for security. While open source components often provide high-quality implementations of functionality that your application needs, mistakes do happen, and open source developers might have a different idea of acceptable risk than you do. For the software components you use in your application, it is your responsibility to understand what kind of security testing has been done and decide if you need to augment that testing with your own evaluations. For critical components like openssl that are part of the attack surface of your applications, performing your own security testing is prudent. Depending on the type of component, security testing can include static analysis (Coverity, for example), software composition analysis (Black Duck), interactive analysis (Seeker), and fuzzing (Defensics). It was Defensics fuzz testing, in fact, that uncovered Heartbleed.
Finally, application security is inseparable from application development. When you are designing new applications or new features of existing applications, you must harden your design by doing threat modeling to think about threats, exploits, and security controls. During development and testing, you must automate and integrate security testing tools so that developers fix security defects as they go. Deployment practices should follow the same rigor, with special attention given to cloud configurations, container base images, and application configuration. Finally, after release, newly discovered vulnerabilities, in either the application itself or its supply chain of open source components, must be promptly identified, evaluated, and addressed.
Jonathan Knudsen likes to break things. He has tested all kinds of software, from network infrastructure and medical devices to cryptocurrency nodes. Jonathan has worked as a developer, consultant, and author. He has published books about 2D graphics, cryptography, and Lego robots, and has written more than one hundred articles on a wide range of technical subjects.