429 lines
16 KiB
Python
429 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright (c) 2018, salesforce.com, inc.
|
|
# All rights reserved.
|
|
# Licensed under the BSD 3-Clause license.
|
|
# For full license text, see the LICENSE file in the repo root
|
|
# or https://opensource.org/licenses/BSD-3-Clause
|
|
|
|
import argparse
|
|
import pyshark
|
|
import os
|
|
import json
|
|
import logging
|
|
import textwrap
|
|
from hashlib import md5
|
|
|
|
__author__ = "Adel '0x4D31' Karimi"
|
|
__email__ = "akarimishiraz@salesforce.com"
|
|
__version__ = "1.1"
|
|
__copyright__ = "Copyright (c) 2018, salesforce.com, inc."
|
|
__license__ = "BSD 3-Clause License"
|
|
__credits__ = ["Ben Reardon", "Adel Karimi", "John B. Althouse",
|
|
"Jeff Atkinson", "Josh Atkins"]
|
|
|
|
HASSH_VERSION = '1.0'
|
|
CAP_BPF_FILTER = 'tcp port 22 or tcp port 2222'
|
|
DECODE_AS = {'tcp.port==2222': 'ssh'}
|
|
|
|
protocol_dict = {}
|
|
|
|
|
|
class color:
|
|
CL1 = '\u001b[38;5;81m'
|
|
CL2 = '\u001b[38;5;220m'
|
|
CL3 = '\u001b[38;5;181m'
|
|
CL4 = '\u001b[38;5;208m'
|
|
END = '\033[0m'
|
|
|
|
|
|
def process_packet(packet, logf, fingerprint, pout):
|
|
logger = logging.getLogger()
|
|
global protocol_dict
|
|
|
|
if not packet.highest_layer == 'SSH':
|
|
return
|
|
# Extract SSH identification string and correlate with KEXINIT msg
|
|
if 'protocol' in packet.ssh.field_names:
|
|
protocol = packet.ssh.protocol
|
|
srcip = packet.ip.src
|
|
dstip = packet.ip.dst
|
|
sport = packet.tcp.srcport
|
|
dport = packet.tcp.srcport
|
|
key = '{}:{}_{}:{}'.format(srcip, sport, dstip, dport)
|
|
protocol_dict[key] = protocol
|
|
if 'message_code' not in packet.ssh.field_names:
|
|
return
|
|
if packet.ssh.message_code != '20':
|
|
return
|
|
if ("analysis_retransmission" in packet.tcp.field_names or
|
|
"analysis_spurious_retransmission" in packet.tcp.field_names):
|
|
event = event_log(packet, event="retransmission")
|
|
if logf == 'json':
|
|
logger.info(json.dumps(event))
|
|
return
|
|
# Client HASSH
|
|
if ((fingerprint == 'client' or fingerprint == 'all')
|
|
and int(packet.tcp.srcport) > int(packet.tcp.dstport)):
|
|
record = client_hassh(packet)
|
|
if logf == 'json':
|
|
logger.info(json.dumps(record))
|
|
elif logf == 'csv':
|
|
csv_record = csv_logging(record)
|
|
logger.info(csv_record)
|
|
# Print the result
|
|
if not pout:
|
|
return
|
|
tmp = textwrap.dedent("""\
|
|
[+] Client SSH_MSG_KEXINIT detected
|
|
{cl1}[ {sip}:{sport} -> {dip}:{dport} ]{cl1e}
|
|
[-] Identification String: {cl2}{proto}{cl2e}
|
|
[-] hassh: {cl2}{hassh}{cl2e}
|
|
[-] hassh Algorithms: {cl3}{hasshv}{cl3e}""")
|
|
tmp = tmp.format(
|
|
cl1=color.CL1,
|
|
sip=record['sourceIp'],
|
|
sport=record['sourcePort'],
|
|
dip=record['destinationIp'],
|
|
dport=record['destinationPort'],
|
|
cl1e=color.END,
|
|
cl2=color.CL2,
|
|
hassh=record['hassh'],
|
|
cl2e=color.END,
|
|
cl3=color.CL3,
|
|
hasshv=record['hasshAlgorithms'],
|
|
cl3e=color.END,
|
|
proto=record['client'])
|
|
print(tmp)
|
|
# Server HASSH
|
|
elif ((fingerprint == 'server' or fingerprint == 'all')
|
|
and int(packet.tcp.srcport) < int(packet.tcp.dstport)):
|
|
record = server_hassh(packet)
|
|
if logf == 'json':
|
|
logger.info(json.dumps(record))
|
|
elif logf == 'csv':
|
|
csv_record = csv_logging(record)
|
|
logger.info(csv_record)
|
|
# Print the result
|
|
if not pout:
|
|
return
|
|
tmp = textwrap.dedent("""\
|
|
[+] Server SSH_MSG_KEXINIT detected
|
|
{cl1}[ {sip}:{sport} -> {dip}:{dport} ]{cl1e}
|
|
[-] Identification String: {cl4}{proto}{cl4e}
|
|
[-] hasshServer: {cl4}{hasshs}{cl4e}
|
|
[-] hasshServer Algorithms: {cl3}{hasshsv}{cl3e}""")
|
|
tmp = tmp.format(
|
|
cl1=color.CL1,
|
|
sip=record['sourceIp'],
|
|
sport=record['sourcePort'],
|
|
dip=record['destinationIp'],
|
|
dport=record['destinationPort'],
|
|
cl1e=color.END,
|
|
cl4=color.CL4,
|
|
hasshs=record['hasshServer'],
|
|
cl4e=color.END,
|
|
cl3=color.CL3,
|
|
hasshsv=record['hasshServerAlgorithms'],
|
|
cl3e=color.END,
|
|
proto=record['server'])
|
|
print(tmp)
|
|
|
|
|
|
def event_log(packet, event):
|
|
"""log the anomalous packets"""
|
|
if event == "retransmission":
|
|
event_message = "This packet is a (suspected) retransmission"
|
|
# Report the event (only for JSON output)
|
|
msg = {"timestamp": packet.sniff_time.isoformat(),
|
|
"eventType": event,
|
|
"eventMessage": event_message,
|
|
"sourceIp": packet.ip.src,
|
|
"destinationIp": packet.ip.dst,
|
|
"sourcePort": packet.tcp.srcport,
|
|
"destinationPort": packet.tcp.dstport}
|
|
return msg
|
|
|
|
|
|
def client_hassh(packet):
|
|
"""returns HASSH (i.e. SSH Client Fingerprint)
|
|
HASSH = md5(KEX;EACTS;MACTS;CACTS)
|
|
"""
|
|
srcip = packet.ip.src
|
|
dstip = packet.ip.dst
|
|
sport = packet.tcp.srcport
|
|
dport = packet.tcp.srcport
|
|
protocol = None
|
|
key = '{}:{}_{}:{}'.format(srcip, sport, dstip, dport)
|
|
if key in protocol_dict:
|
|
protocol = protocol_dict[key]
|
|
# hassh fields
|
|
ckex = ceacts = cmacts = ccacts = ""
|
|
if 'kex_algorithms' in packet.ssh.field_names:
|
|
ckex = packet.ssh.kex_algorithms
|
|
if 'encryption_algorithms_client_to_server' in packet.ssh.field_names:
|
|
ceacts = packet.ssh.encryption_algorithms_client_to_server
|
|
if 'mac_algorithms_client_to_server' in packet.ssh.field_names:
|
|
cmacts = packet.ssh.mac_algorithms_client_to_server
|
|
if 'compression_algorithms_client_to_server' in packet.ssh.field_names:
|
|
ccacts = packet.ssh.compression_algorithms_client_to_server
|
|
# Log other kexinit fields (only in JSON)
|
|
clcts = clstc = ceastc = cmastc = ccastc = cshka = ""
|
|
if 'languages_client_to_server' in packet.ssh.field_names:
|
|
clcts = packet.ssh.languages_client_to_server
|
|
if 'languages_server_to_client' in packet.ssh.field_names:
|
|
clstc = packet.ssh.languages_server_to_client
|
|
if 'encryption_algorithms_server_to_client' in packet.ssh.field_names:
|
|
ceastc = packet.ssh.encryption_algorithms_server_to_client
|
|
if 'mac_algorithms_server_to_client' in packet.ssh.field_names:
|
|
cmastc = packet.ssh.mac_algorithms_server_to_client
|
|
if 'compression_algorithms_server_to_client' in packet.ssh.field_names:
|
|
ccastc = packet.ssh.compression_algorithms_server_to_client
|
|
if 'server_host_key_algorithms' in packet.ssh.field_names:
|
|
cshka = packet.ssh.server_host_key_algorithms
|
|
# Create hassh
|
|
hassh_str = ';'.join([ckex, ceacts, cmacts, ccacts])
|
|
hassh = md5(hassh_str.encode()).hexdigest()
|
|
record = {"timestamp": packet.sniff_time.isoformat(),
|
|
"sourceIp": packet.ip.src,
|
|
"destinationIp": packet.ip.dst,
|
|
"sourcePort": packet.tcp.srcport,
|
|
"destinationPort": packet.tcp.dstport,
|
|
"client": protocol,
|
|
"hassh": hassh,
|
|
"hasshAlgorithms": hassh_str,
|
|
"hasshVersion": HASSH_VERSION,
|
|
"ckex": ckex,
|
|
"ceacts": ceacts,
|
|
"cmacts": cmacts,
|
|
"ccacts": ccacts,
|
|
"clcts": clcts,
|
|
"clstc": clstc,
|
|
"ceastc": ceastc,
|
|
"cmastc": cmastc,
|
|
"ccastc": ccastc,
|
|
"cshka": cshka}
|
|
return record
|
|
|
|
|
|
def server_hassh(packet):
|
|
"""returns HASSHServer (i.e. SSH Server Fingerprint)
|
|
HASSHServer = md5(KEX;EASTC;MASTC;CASTC)
|
|
"""
|
|
srcip = packet.ip.src
|
|
dstip = packet.ip.dst
|
|
sport = packet.tcp.srcport
|
|
dport = packet.tcp.srcport
|
|
protocol = None
|
|
key = '{}:{}_{}:{}'.format(srcip, sport, dstip, dport)
|
|
if key in protocol_dict:
|
|
protocol = protocol_dict[key]
|
|
# hasshServer fields
|
|
skex = seastc = smastc = scastc = ""
|
|
if 'kex_algorithms' in packet.ssh.field_names:
|
|
skex = packet.ssh.kex_algorithms
|
|
if 'encryption_algorithms_server_to_client' in packet.ssh.field_names:
|
|
seastc = packet.ssh.encryption_algorithms_server_to_client
|
|
if 'mac_algorithms_server_to_client' in packet.ssh.field_names:
|
|
smastc = packet.ssh.mac_algorithms_server_to_client
|
|
if 'compression_algorithms_server_to_client' in packet.ssh.field_names:
|
|
scastc = packet.ssh.compression_algorithms_server_to_client
|
|
# Log other kexinit fields (only in JSON)
|
|
slcts = slstc = seacts = smacts = scacts = sshka = ""
|
|
if 'languages_client_to_server' in packet.ssh.field_names:
|
|
slcts = packet.ssh.languages_client_to_server
|
|
if 'languages_server_to_client' in packet.ssh.field_names:
|
|
slstc = packet.ssh.languages_server_to_client
|
|
if 'encryption_algorithms_client_to_server' in packet.ssh.field_names:
|
|
seacts = packet.ssh.encryption_algorithms_client_to_server
|
|
if 'mac_algorithms_client_to_server' in packet.ssh.field_names:
|
|
smacts = packet.ssh.mac_algorithms_client_to_server
|
|
if 'compression_algorithms_client_to_server' in packet.ssh.field_names:
|
|
scacts = packet.ssh.compression_algorithms_client_to_server
|
|
if 'server_host_key_algorithms' in packet.ssh.field_names:
|
|
sshka = packet.ssh.server_host_key_algorithms
|
|
# Create hasshServer
|
|
hasshs_str = ';'.join([skex, seastc, smastc, scastc])
|
|
hasshs = md5(hasshs_str.encode()).hexdigest()
|
|
record = {"timestamp": packet.sniff_time.isoformat(),
|
|
"sourceIp": packet.ip.src,
|
|
"destinationIp": packet.ip.dst,
|
|
"sourcePort": packet.tcp.srcport,
|
|
"destinationPort": packet.tcp.dstport,
|
|
"server": protocol,
|
|
"hasshServer": hasshs,
|
|
"hasshServerAlgorithms": hasshs_str,
|
|
"hasshVersion": HASSH_VERSION,
|
|
"skex": skex,
|
|
"seastc": seastc,
|
|
"smastc": smastc,
|
|
"scastc": scastc,
|
|
"slcts": slcts,
|
|
"slstc": slstc,
|
|
"seacts": seacts,
|
|
"smacts": smacts,
|
|
"scacts": scacts,
|
|
"sshka": sshka}
|
|
return record
|
|
|
|
|
|
def csv_logging(record):
|
|
"""generate output in csv format"""
|
|
csv_record = ('{ts},{si},{di},{sp},{dp},{t},"{p}",{h},{v},"{ha}",'
|
|
'"{k}","{e}","{m}","{c}"')
|
|
if 'hassh' in record:
|
|
hasshType = 'client'
|
|
kexAlgs = record['ckex']
|
|
encAlgs = record['ceacts']
|
|
macAlgs = record['cmacts']
|
|
cmpAlgs = record['ccacts']
|
|
hassh = record['hassh']
|
|
hasshAlgorithms = record['hasshAlgorithms']
|
|
identificationString = record['client']
|
|
elif 'hasshServer' in record:
|
|
hasshType = 'server'
|
|
kexAlgs = record['skex']
|
|
encAlgs = record['seastc']
|
|
macAlgs = record['smastc']
|
|
cmpAlgs = record['scastc']
|
|
hassh = record['hasshServer']
|
|
hasshAlgorithms = record['hasshServerAlgorithms']
|
|
identificationString = record['server']
|
|
csv_record = csv_record.format(
|
|
ts=record['timestamp'], si=record['sourceIp'],
|
|
di=record['destinationIp'], sp=record['sourcePort'],
|
|
dp=record['destinationPort'], t=hasshType, p=identificationString,
|
|
h=hassh, v=HASSH_VERSION, ha=hasshAlgorithms, k=kexAlgs, e=encAlgs,
|
|
m=macAlgs, c=cmpAlgs)
|
|
return csv_record
|
|
|
|
|
|
def parse_cmd_args():
|
|
"""parse command line arguments"""
|
|
desc = """A python script for extracting HASSH fingerprints"""
|
|
parser = argparse.ArgumentParser(description=(desc))
|
|
helptxt = "pcap file to process"
|
|
parser.add_argument('-r', '--read_file', type=str, help=helptxt)
|
|
helptxt = "directory of pcap files to process"
|
|
parser.add_argument('-d', '--read_directory', type=str, help=helptxt)
|
|
helptxt = "listen on interface"
|
|
parser.add_argument('-i', '--interface', type=str, help=helptxt)
|
|
helptxt = "client or server fingerprint. Default: all"
|
|
parser.add_argument(
|
|
'-fp',
|
|
'--fingerprint',
|
|
default='all',
|
|
choices=['client', 'server'],
|
|
help=helptxt)
|
|
helptxt = "a dictionary of {decode_criterion_string: decode_as_protocol} \
|
|
that are used to tell tshark to decode protocols in situations it \
|
|
wouldn't usually. Default: {'tcp.port==2222': 'ssh'}."
|
|
parser.add_argument(
|
|
'-da', '--decode_as', type=dict, default=DECODE_AS, help=helptxt)
|
|
helptxt = "BPF capture filter to use (for live capture only).\
|
|
Default: 'tcp port 22 or tcp port 2222'"
|
|
parser.add_argument(
|
|
'-f', '--bpf_filter', type=str, default=CAP_BPF_FILTER, help=helptxt)
|
|
helptxt = "specify the output log format (json/csv)"
|
|
parser.add_argument(
|
|
'-l',
|
|
'--log_format',
|
|
choices=['json', 'csv'],
|
|
help=helptxt)
|
|
helptxt = "specify the output log file. Default: hassh.log"
|
|
parser.add_argument(
|
|
'-o', '--output_file', default='hassh.log', type=str, help=helptxt)
|
|
helptxt = "save the live captured packets to this file"
|
|
parser.add_argument(
|
|
'-w', '--write_pcap', default=None, type=str, help=helptxt)
|
|
helptxt = "print the output"
|
|
parser.add_argument(
|
|
'-p', '--print_output', action="store_true", help=helptxt)
|
|
return parser.parse_args()
|
|
|
|
|
|
def setup_logging(logfile):
|
|
"""setup logging"""
|
|
logger = logging.getLogger()
|
|
handler = logging.FileHandler(logfile)
|
|
formatter = logging.Formatter('%(message)s')
|
|
handler.setFormatter(formatter)
|
|
logger.addHandler(handler)
|
|
logger.setLevel(logging.INFO)
|
|
return logger
|
|
|
|
|
|
def main():
|
|
"""intake arguments from the user and extract HASSH fingerprints."""
|
|
args = parse_cmd_args()
|
|
setup_logging(args.output_file)
|
|
logger = logging.getLogger()
|
|
|
|
csv_header = ("timestamp,sourceIp,destinationIp,sourcePort,"
|
|
"destinationPort,hasshType,identificationString,"
|
|
"hassh,hasshVersion,hasshAlgorithms,kexAlgs,encAlgs,"
|
|
"macAlgs,cmpAlgs")
|
|
if args.log_format == 'csv':
|
|
logger.info(csv_header)
|
|
|
|
# Process PCAP file
|
|
if args.read_file:
|
|
cap = pyshark.FileCapture(args.read_file, decode_as=args.decode_as)
|
|
try:
|
|
for packet in cap:
|
|
process_packet(
|
|
packet,
|
|
logf=args.log_format,
|
|
fingerprint=args.fingerprint,
|
|
pout=args.print_output)
|
|
cap.close()
|
|
cap.eventloop.stop()
|
|
except Exception as e:
|
|
print('Error: {}'.format(e))
|
|
pass
|
|
|
|
# Process directory of PCAP files
|
|
elif args.read_directory:
|
|
files = [f.path for f in os.scandir(args.read_directory)
|
|
if not f.name.startswith('.') and not f.is_dir()
|
|
and (f.name.endswith(".pcap") or f.name.endswith(".pcapng")
|
|
or f.name.endswith(".cap"))]
|
|
for file in files:
|
|
cap = pyshark.FileCapture(file, decode_as=args.decode_as)
|
|
try:
|
|
for packet in cap:
|
|
process_packet(
|
|
packet,
|
|
logf=args.log_format,
|
|
fingerprint=args.fingerprint,
|
|
pout=args.print_output)
|
|
cap.close()
|
|
cap.eventloop.stop()
|
|
except Exception as e:
|
|
print('Error: {}'.format(e))
|
|
pass
|
|
|
|
# Capture live network traffic
|
|
elif args.interface:
|
|
# TODO: Use a Ring Buffer (LiveRingCapture), when the issue is fixed:
|
|
# https://github.com/KimiNewt/pyshark/issues/299
|
|
cap = pyshark.LiveCapture(
|
|
interface=args.interface,
|
|
decode_as=args.decode_as,
|
|
bpf_filter=args.bpf_filter,
|
|
output_file=args.write_pcap)
|
|
try:
|
|
for packet in cap.sniff_continuously(packet_count=0):
|
|
# if len(protocol_dict) > 10000:
|
|
# protocol_dict.clear()
|
|
process_packet(
|
|
packet,
|
|
logf=args.log_format,
|
|
fingerprint=args.fingerprint,
|
|
pout=args.print_output)
|
|
except (KeyboardInterrupt, SystemExit):
|
|
print("Exiting..\nBYE o/\n")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|