Python script to detect a prior OpenSSL Heartbleed exploit from NetShark or PCAP file

Update: SteelCentral NetShark (formerly known as Cascade Shark) supports BPF expressions natively, thus avoiding downloading large PCAP files for offline analysis.

 

P.J. Malloy wrote a great post on the Riverbed blog describing a BPF expression that can be used to filter out possible exploited heartbleed packets.   The BPF filter expression can be fed directly to tcpdump to filter existing packet capture files.  In this post, I'll share a script that leverages this BPF filter via a simple script.

 

Save the script below as 'heartbleed.py'.  It supports two basic modes:

  • analyze an existing pcap file
  • query a SteelCentral NetShark device for packets, download a pcap file and then perform the analysis

 

This script requires that the SteelScript (formerly FlyScript) SDK is installed as well as tcpdump and tshark (comes with wireshark).

 

Analyzing an existing pcap file

 

Run the heartbleed script at the command line as follows:

$ python heartbleed.py --pcap <filename>

Filtering PCAP: <filename>

reading from file <filename>, link-type EN10MB (Ethernet)

 

Matching packets from: <filename>.filtered.pcap

1    Apr  9, 2014 10:49:07.148451000    168.75.167.166

Done

 

First the script filters the input file using P.J.'s BPF expression.  This file is written to <filename>.filtered.pcap.  The resulting filtered pcap is passed to tshark to dump a few interesting fields.

 

If there is no match, you will not see any lines between "Matching" and "Done".

 

Analyzing packets from a SteelCentral NetShark

 

In the second mode, the pcap to analyze is pulled from a SteelCentral NetShark by providing the following:

  • SteelCentral NetShark and login credentials (username/password)
  • Capture Job
  • Timeframe (start / duration)

 

$ python heartbleed.py -u <username> -p <password> --shark <device> \

      --jobname <jobname>

      --start '12:00pm' --duration '1s'

      --ip <ipaddress> --sslport <sslport>

 

Downloading PCAP from shark job: tcp, indexing, dpi

Timeframe: 2014-04-10 12:00:00-04:00 - 2014-04-10 12:00:01-04:00

Filter: (ip.src="<ipaddress>") & (tcp.src_port=<sslport>)

Filtering PCAP: /tmp/job-00000001-20140410-120000.pcap

reading from file /tmp/job-00000001-20140410-120000.pcap, link-type EN10MB (Ethernet)

 

Matching packets from: /tmp/job-00000001-20140410-120000.pcap.filtered.pcap

Done

 

This mode takes a few extra parameters to reduce the size of the capture:

  • --ip <ipaddress>  -- optionally specify an IP address as a prefilter.  This will allow you to scan certain servers
  • --sslport <port> -- optionally choose an alternate port, the default is 443

 

If you don't know the right capture job, use '--list-jobs' to print out the list of available capture jobs.

 

Once run, this will create a trace clip on the shark for the target IP, port, and timerange, then download that pcap file for offline analysis.

 

The Script: heartbleed.py

 

Usage:

$ python heartbleed.py -h

Usage: heartbleed.py [options]

 

Options:

  -h, --help            show this help message and exit

 

  PCAP file:

    --pcap=PCAP         Packet capture to analyze

 

  Shark Datasource:

    --shark=SHARK       Shark device hostname or IP address

    -u USERNAME, --username=USERNAME

                        Username

    -p PASSWORD, --password=PASSWORD

                        Password

    -j JOBNAME, --jobname=JOBNAME

                        Capture job to extract packets from

    --ip=IP             IP address filter

    --start=START       start time to extract

    --duration=DURATION

                        duration of time to extract

    --filter=FILTER     Additional shark filter expression

    --list-jobs  

 

  TShark Analysis optins:

    --badlen=BADLEN     Invalid TCP payload length to flag, defaults to 69

    --sslport=SSLPORT   SSL port filter

    --field=FIELDS      Wireshark field to display for each matched packet

 

Source code:

 

#!/usr/bin/env python

""" heartbleed.py - filter a pcap file or packets from a Cascade Shark for heartbleed exploits """

import datetime
import os
import optparse
import sys

import rvbd.shark
import rvbd.common
from rvbd.shark.filters import SharkFilter, TimeFilter, BpfFilter
from rvbd.common.timeutils import parse_timedelta, TimeParser, tzlocal

parser = optparse.OptionParser()

group = optparse.OptionGroup(parser, "PCAP file")
group.add_option('--pcap', dest='pcap',
                  help='Packet capture to analyze')
parser.add_option_group(group)

group = optparse.OptionGroup(parser, "Shark Datasource")
group.add_option('--shark', dest='shark',
                  help='Shark device hostname or IP address')
group.add_option('-u', '--username', dest='username',
                  help='Username')
group.add_option('-p', '--password', dest='password',
                  help='Password ')
group.add_option('-j', '--jobname', dest='jobname',
                  help='Capture job to extract packets from ')
group.add_option('--ip', dest='ip', default=None,
                  help='IP address filter ')
group.add_option('--start', dest='start', default='1s',
                  help='start time to extract')
group.add_option('--duration', dest='duration', default='1h',
                  help='duration of time to extract')
group.add_option('--filter', dest='filter', default=None,
                  help='Additional shark filter expression')
group.add_option('--list-jobs', dest='list_jobs', action='store_true')
parser.add_option_group(group)

group = optparse.OptionGroup(parser, "TShark Analysis optins")
group.add_option('--badlen', dest='badlen', default=69,
                  help='Invalid TCP payload length to flag, defaults to 69')
group.add_option('--sslport', dest='sslport', default=443,
                  help='SSL port filter')
group.add_option('--field', dest='fields', action='append',
                  help='Wireshark field to display for each matched packet')
parser.add_option_group(group)

(options, args) = parser.parse_args()

def get_shark(opts):
    # Create a Shark device with the given credentials
    if opts.shark is None or opts.username is None or opts.password is None:
        print "Must specify Shark device with --shark as well as username (-u) and password (-p)"
        sys.exit(1)

    return rvbd.shark.Shark(opts.shark,
                            auth=rvbd.common.UserAuth(opts.username,
                                                      opts.password))

def pcap_from_job(opts):
    # Query the Shark device for packets from a specific capture job

    jobname = opts.jobname
    shark = get_shark(opts)

    job = shark.get_capture_job_by_name(jobname)

    print "Analyzing shark job: %s" % jobname

    start = TimeParser.parse_one(opts.start).replace(tzinfo=tzlocal())
    delta = parse_timedelta(opts.duration)
    timefilter = TimeFilter(start, start + delta)
    print "Timeframe: %s - %s" % (str(timefilter.start), str(timefilter.end))
    filters = [timefilter]

    filterparts = []
    if opts.ip:
        filterparts.append('(ip.src="{ip}")'.format(ip=opts.ip))
    if opts.sslport:
        filterparts.append('(tcp.src_port={port})'.format(port=opts.sslport))
    if opts.filter:
        filterparts.append('({filter})'.format(filter=opts.filter))

    if filterparts:
        f = ' & '.join(filterparts)
        print "Filter: %s" % f
        filters.append(SharkFilter(f))

    # Analyze a pcap file using the BPF expression
    bpf = ('((not ether proto 0x8100) and (tcp src port {sslport} and (tcp[((tcp[12] & 0xF0) >> 4 ) * 4] = 0x18) and (tcp[((tcp[12] & 0xF0) >> 4 ) * 4 + 1] = 0x03) and (tcp[((tcp[12] & 0xF0) >> 4 ) * 4 + 2] < 0x04) and ((ip[2:2] - 4 * (ip[0] & 0x0F)  - 4 * ((tcp[12] & 0xF0) >> 4) > {badlen})))) or (vlan and (tcp src port {sslport} and (tcp[((tcp[12] & 0xF0) >> 4 ) * 4] = 0x18) and (tcp[((tcp[12] & 0xF0) >> 4 ) * 4 + 1] = 0x03) and (tcp[((tcp[12] & 0xF0) >> 4 ) * 4 + 2] < 0x04) and ((ip[2:2] - 4 * (ip[0] & 0x0F)  - 4 * ((tcp[12] & 0xF0) >> 4) > {badlen}))))'
           .format(sslport=opts.sslport, badlen=opts.badlen))
    filters.append(BpfFilter(bpf))

    clip = shark.create_clip(job, filters=filters, description='heartbleed clip')

    filename = ('/tmp/job-{jobid}-{timestr}.pcap'
                .format(jobid=job.id, timestr=start.strftime('%Y%m%d-%H%M%S')))

    if os.path.exists(filename):
        os.unlink(filename)

    clip.download(filename)
    clip.delete()

    return filename


def analyze_file(opts, filename):
    # Analyze a pcap file using the BPF expression
    bpf = ('((not ether proto 0x8100) and (tcp src port {sslport} and (tcp[((tcp[12] & 0xF0) >> 4 ) * 4] = 0x18) and (tcp[((tcp[12] & 0xF0) >> 4 ) * 4 + 1] = 0x03) and (tcp[((tcp[12] & 0xF0) >> 4 ) * 4 + 2] < 0x04) and ((ip[2:2] - 4 * (ip[0] & 0x0F)  - 4 * ((tcp[12] & 0xF0) >> 4) > {badlen})))) or (vlan and (tcp src port {sslport} and (tcp[((tcp[12] & 0xF0) >> 4 ) * 4] = 0x18) and (tcp[((tcp[12] & 0xF0) >> 4 ) * 4 + 1] = 0x03) and (tcp[((tcp[12] & 0xF0) >> 4 ) * 4 + 2] < 0x04) and ((ip[2:2] - 4 * (ip[0] & 0x0F)  - 4 * ((tcp[12] & 0xF0) >> 4) > {badlen}))))'
           .format(sslport=opts.sslport, badlen=opts.badlen))

    outfilename = filename + '.filtered.pcap'

    command = ('tcpdump -r {filename} -w {outfilename} "{bpf}"'
               .format(filename=filename, outfilename=outfilename, bpf=bpf))


    print "Filtering PCAP: %s" % filename
    os.system(command)

    tshark_filter = ('tcp.srcport == {port} and tcp.data[0-1] == 18:03 and tcp.data[2] < 0x04 and tcp.len > {badlen}'
                     .format(port=opts.sslport, badlen=opts.badlen))

    fields = opts.fields or ['frame.number', 'frame.time', 'ip.src']
    command = ('tshark -r {filename} -T fields {fields}'
               .format(filename=outfilename,
                       fields=' '.join(['-e "%s"' % f for f in fields])))

    print "\nMatching packets from: %s" % outfilename
    os.system(command)
    print "Done"


if options.list_jobs:
    shark = get_shark(options)
    for j in shark.get_capture_jobs():
        print "Job %s: '%s'" % (j.id, j)

elif options.jobname:
    filename = pcap_from_job(options)
    analyze_file(options, filename)

elif options.pcap:
    analyze_file(options, options.pcap)