diff options
author | Remy <relst@relst.nl> | 2015-05-16 14:57:27 +0200 |
---|---|---|
committer | Remy <relst@relst.nl> | 2015-05-16 14:57:27 +0200 |
commit | 588bf45cff95761bf17acd1e26ff57796ae8251b (patch) | |
tree | daa4a9a814043d810217b2b569af8e29238389f5 /inc/heartbleed.py | |
parent | 15e230a85ea76ec7b7c5405ff8cf0953b85503e6 (diff) | |
download | ssl-decoder-588bf45cff95761bf17acd1e26ff57796ae8251b.zip ssl-decoder-588bf45cff95761bf17acd1e26ff57796ae8251b.tar.gz ssl-decoder-588bf45cff95761bf17acd1e26ff57796ae8251b.tar.bz2 |
Add heartbleed test
Diffstat (limited to 'inc/heartbleed.py')
-rw-r--r-- | inc/heartbleed.py | 383 |
1 files changed, 383 insertions, 0 deletions
diff --git a/inc/heartbleed.py b/inc/heartbleed.py new file mode 100644 index 0000000..8d194d5 --- /dev/null +++ b/inc/heartbleed.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python2 +# https://github.com/musalbas/heartbleed-masstest +# cc0 license +# Based on the original code by Jared Stafford. + +# This code has been modified to test for OpenSSL versions vulnerable to +# Heartbleed without exploiting the server, therefore the heartbeat request +# does _not_ cause the server to leak any data from memory or expose any data +# in an unauthorized manner. +# Based on: https://github.com/dchan/metasploit-framework/blob/master/modules/auxiliary/scanner/ssl/openssl_heartbleed.rb +# See: https://blog.mozilla.org/security/2014/04/12/testing-for-heartbleed-vulnerability-without-exploiting-the-server/ + +# Usage example: python ssltest.py example.com + +import sys +import struct +import socket +import time +import select +import re +import threading +import netaddr +import json +import os +import datetime +import signal +from optparse import OptionParser +from collections import defaultdict +from multiprocessing.dummy import Pool + +host_status = {} +hosts_to_skip = [] +counter = defaultdict(int) +lock = threading.Lock() + +options = OptionParser(usage='%prog <network> [network2] [network3] ...', description='Test for SSL heartbleed vulnerability (CVE-2014-0160) on multiple domains') +options.add_option('--port', '-p', dest="port", default=443, help="Port to scan on all hosts or networks, default 443") +options.add_option('--input', '-i', dest="input_file", default=[], action="append", help="Optional input file of networks or ip addresses, one address per line") +options.add_option('--logfile', '-o', dest="log_file", default="results.txt", help="Optional logfile destination") +options.add_option('--resume', dest="resume", action="store_true", default=False, help="Do not rescan hosts that are already in the logfile") +options.add_option('--timeout', '-t', dest="timeout", default=5, help="How long to wait for remote host to respond before timing out") +options.add_option('--threads', dest="threads", default=100, help="If specific, run X concurrent threads") +options.add_option('--json', dest="json_file", default=None, help="Save data as json into this file") +options.add_option('--only-vulnerable', dest="only_vulnerable", action="store_true", default=False, help="Only scan hosts that have been scanned before and were vulnerable") +options.add_option('--only-unscanned', dest="only_unscanned", action="store_true", default=False, help="Only scan hosts that appear in the json file but have not been scanned") +options.add_option('--summary', dest="summary", action="store_true", default=False, help="Useful with --json. Don't scan, just print old results") +options.add_option('--verbose', dest="verbose", action="store_true", default=False, help="Print verbose information to screen") +options.add_option('--silent', dest="silent", action="store_true", default=False, help="Don't give any output to stdout/stderr. Usefull with the --json option") +options.add_option('--max', dest="max", default=None, help="Exit program after scanning X hosts. Useful with --only-unscanned") +opts, args = options.parse_args() + +threadpool = Pool(processes=int(opts.threads)) + + +def h2bin(x): + return x.replace(' ', '').replace('\n', '').decode('hex') + +hello = h2bin(''' +16 03 03 00 dc 01 00 00 d8 03 03 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 +''') + +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: + try: + data = s.recv(remain) + except Exception, e: + return None + # EOF? + if not data: + return None + rdata += data + remain -= len(data) + return rdata + + +def recvmsg(s): + hdr = recvall(s, 5) + if hdr is None: + return None, None, None + typ, ver, ln = struct.unpack('>BHH', hdr) + pay = recvall(s, ln, 10) + if pay is None: + return None, None, None + return typ, ver, pay + + +def hit_hb(s): + while True: + typ, ver, pay = recvmsg(s) + if typ is None: + return False + + if typ == 24: + return True + + if typ == 21: + return False + +def unpack_handshake(pay): + """ + Unpack the SSL handshake in Multiple Handshake Message + """ + paylen = len(pay) + offset = 0 + payarr = [] + + while offset < paylen: + h = pay[offset:offset + 4] + t, l24 = struct.unpack('>B3s', h) + l = struct.unpack('>I', '\x00' + l24)[0] + payarr.append(( + t, + l, + pay[offset+4:offset+4+l] + )) + offset = offset+l+4 + return payarr + +def is_vulnerable(host, timeout, port=443): + """ Check if remote host is vulnerable to heartbleed + + Returns: + None -- If remote host has no ssl + False -- Remote host has ssl but likely not vulnerable + True -- Remote host might be vulnerable + """ + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(int(timeout)) + try: + s.connect((host, int(port))) + except Exception, e: + return None + s.send(hello) + + while True: + typ, ver, pay = recvmsg(s) + if typ is None: + return None + + if typ == 22: + payarr = unpack_handshake(pay) + # Look for server hello done message. + finddone = [t for t, l, p in payarr if t == 14] + if len(finddone) > 0: + break + + # construct heartbeat request packet + ver_chr = chr(ver&0xff) + hb = h2bin("18 03") + ver_chr + h2bin("40 00 01 3f fd") + "\x01"*16381 + hb += h2bin("18 03") + ver_chr + h2bin("00 03 01 00 00") + + s.send(hb) + return hit_hb(s) + + +def store_results(host_name, current_status): + current_time = time.time() + with lock: + counter[current_status] += 1 + counter["Total"] += 1 + if host_name not in host_status: + host_status[host_name] = {} + host = host_status[host_name] + # Make a note when this host was last scanned + host['last_scan'] = current_time + + # Make a note if this host has never been scanned before + if 'first_scan' not in host: + host['first_scan'] = current_time + elif host.get('status', 'never been scanned') != current_status: + # If it has a different check result from before + host['changelog'] = host.get('changelog', []) + changelog_entry = [current_time, current_status] + host['changelog'].append(changelog_entry) + host['status'] = current_status + with open(opts.log_file, 'a') as f: + message = "{current_time} {host} {current_status}".format(**locals()) + f.write(message + "\n") + return message + + +def scan_host(host): + """ Scans a single host, logs into + + Returns: + list(timestamp, ipaddress, vulnerabilitystatus) + """ + if opts.max and int(opts.max) >= counter["Total"]: + return + host = str(host) + if host in hosts_to_skip: + return + result = is_vulnerable(host, opts.timeout, opts.port) + message = store_results(host, result) + if opts.verbose: + print message + return message + + +def scan_hostlist(hostlist, threads=5): + """ Iterates through hostlist and scans them + + Arguments: + hostlist -- Iterable with ip addresses + threads -- If specified, run in multithreading mode + """ + task = threadpool.map_async(scan_host, hostlist) + while True: + if not opts.silent: + print counter['Total'], "hosts done" + task.wait(1) + if task.ready() or hasattr(threadpool, 'done'): + return + threadpool.close() + threadpool.join() + + +def clean_hostlist(args): + """ Returns list of iterables + Examples: + >>> hostlist = ["127.0.0.1", "127.0.0.2"] + >>> clean_hostlist(hostlist) + """ + hosts = [] + networks = [] + for i in args: + # If it contains any alphanumerics, it might be a domain name + if any(c.isalpha() for c in i): + # Special hack, because alexa top x list is kind of weird + i = i.split('/')[0] + hosts.append(i) + # If arg contains a / we assume its a network name + elif '/' in i: + networks.append(netaddr.IPNetwork(i)) + else: + hosts.append(i) + result = [] + for network in networks: + if network.size >= opts.threads: + result.append(network) + else: + for i in network: + hosts.append(str(i)) + if hosts: + result.append(hosts) + return result + + +def import_json(filename): + """ Reads heartbleed data in json format from this file """ + with open(filename) as f: + json_data = f.read() + data = json.loads(json_data) + for k, v in data.items(): + host_status[k] = v + + +def export_json(filename): + """ Save scan results into filename as json data + """ + json_data = json.dumps(host_status, indent=4) + with open(filename, 'w') as f: + f.write(json_data) + + +def print_summary(): + """ Print summary of previously stored json data to screen """ + if not opts.json_file: + pass + #options.error("You need to provide --json with --summary") + else: + import_json(opts.json_file) + counter = defaultdict(int) + for host, data in host_status.items(): + friendly_status = "unknown" + status = data.get('status', "Not scanned") + if status is None: + friendly_status = "SSL Connection Failed" + elif status is True: + friendly_status = "Vulnerable" + elif status is False: + friendly_status = "Not Vulnerable" + else: + friendly_status = str(status) + last_scan = int(float(data.get('last_scan',0))) + last_scan = datetime.datetime.fromtimestamp(last_scan).strftime('%Y-%m-%d %H:%M:%S') + counter[friendly_status] += 1 + counter['Total'] += 1 + if opts.only_vulnerable and not status: + continue + elif opts.only_unscanned and 'status' in data: + continue + if not opts.silent: + print "%s %-20s %5s" % (last_scan, host, friendly_status) + if not opts.silent: + print "------------ summary -----------" + for k,v in counter.items(): + if not opts.silent: + print "%-7s %s" % (v, k) + return + +def signal_handler(signal, frame): + if not opts.silent: + print "Ctrl+C pressed.. aborting..." + threadpool.terminate() + threadpool.done = True + +def main(): + if opts.summary: + print_summary() + return + + if not args and not opts.input_file and not opts.json_file: + options.print_help() + return + + + # If any input files were provided, parse through them and add all addresses to "args" + for input_file in opts.input_file: + with open(input_file) as f: + for line in f: + words = line.split() + if not words: + continue + # If input file is in masscan's portscan format + if line.startswith("Discovered open port"): + args.append(words.pop()) + elif len(words) == 1: + args.append(words[0]) + else: + if not opts.silent: + print "Skipping invalid input line: " % line + continue + if opts.json_file: + try: + import_json(opts.json_file) + except IOError: + if not opts.silent: + print opts.json_file, "not found. Not importing any data" + + + for host_name, data in host_status.items(): + if opts.only_unscanned and 'status' in data: + continue + if data.get('status', None) is True or not opts.only_vulnerable: + args.append(host_name) + + # For every network in args, convert it to a netaddr network, so we can iterate through each host + remote_networks = clean_hostlist(args) + for network in remote_networks: + scan_hostlist(network, threads=opts.threads) + + if opts.json_file: + export_json(opts.json_file) + if not opts.silent: + print_summary() + +if __name__ == '__main__': + signal.signal(signal.SIGINT, signal_handler) + main() + |