diff --git a/cowrie.cfg.dist b/cowrie.cfg.dist index 2d0fe79..ba5cc45 100644 --- a/cowrie.cfg.dist +++ b/cowrie.cfg.dist @@ -236,7 +236,17 @@ logfile = log/cowrie.json #index = cowrie #type = cowrie -# Local Syslog output module +# Send statics to SANS +# https://isc.sans.edu/ssh.html +# You must signup for an api key. + +#[output_dshield] +#auth_key = auth_key_here +#userid = userid_here +#batch_size = 100 + + +## Local Syslog output module # # This sends log messages to the local syslog daemon. # Facility can be: diff --git a/cowrie/output/dshield.py b/cowrie/output/dshield.py new file mode 100644 index 0000000..cb9bd4e --- /dev/null +++ b/cowrie/output/dshield.py @@ -0,0 +1,123 @@ +# Simple elasticsearch logger + +import os +import json +import dateutil.parser +import time +import base64 +import hmac +import hashlib +import requests +import re + +from twisted.python import log +from twisted.internet import threads, reactor + +import cowrie.core.output + + +class Output(cowrie.core.output.Output): + + def __init__(self, cfg): + self.auth_key = cfg.get('output_dshield', 'auth_key') + self.userid = cfg.get('output_dshield', 'userid') + self.batch_size = int(cfg.get('output_dshield', 'batch_size')) + cowrie.core.output.Output.__init__(self, cfg) + + def start(self): + self.batch = [] # this is used to store login attempts in batches + + def stop(self): + pass + + def write(self, entry): + for i in entry.keys(): + # remove twisted 15 legacy keys + if i.startswith('log_'): + del entry[i] + + # we only want login events + if entry["eventid"] == 'KIPP0002' or entry["eventid"] == 'KIPP0003': + date = dateutil.parser.parse(entry["timestamp"]) + self.batch.append({ + 'date' : date.date().__str__(), + 'time' : date.time().strftime("%H:%M:%S"), + 'timezone' : time.strftime("%z"), + 'source_ip' : entry['src_ip'], + 'user' : entry['username'], + 'password' : entry['password'], + }) + + if len(self.batch) >= self.batch_size: + batch_to_send = self.batch + self.submit_entries(batch_to_send) + self.batch = [] + + def transmission_error(self, batch): + self.batch.extend(batch) # adds all the events we just tried to send back in again + if len(self.batch) > self.batch_size * 2: + self.batch = self.batch[-self.batch_size:] + + def submit_entries(self, batch): + ''' + Large parts of this method are adapted from kippo-pyshield by jkakavas + Many thanks to their efforts. + source: https://github.com/jkakavas/kippo-pyshield + ''' + log_output = '' + # Format login attempts as tab seperated log entries + for attempt in self.batch: + log_output += '{0}\t{1}\t{2}\t{3}\t{4}\t{5}\n'.format(attempt['date'],attempt['time'],attempt['timezone'],attempt['source_ip'],attempt['user'],attempt['password']) + # The nonce is predefined as explained in the original script : + # trying to avoid sending the authentication key in the "clear" but not wanting to + # deal with a full digest like exchange. Using a fixed nonce to mix up the limited + # userid. + nonce = base64.b64decode('ElWO1arph+Jifqme6eXD8Uj+QTAmijAWxX1msbJzXDM=') + digest = base64.b64encode(hmac.new('{0}{1}'.format(nonce,self.userid),base64.b64decode(self.auth_key), hashlib.sha256).digest()) + auth_header = 'credentials={0} nonce=ElWO1arph+Jifqme6eXD8Uj+QTAmijAWxX1msbJzXDM= userid={1}'.format(digest, self.userid) + headers = {'X-ISC-Authorization': auth_header, + 'Content-Type':'text/plain', + 'Content-Length': len(log_output)} + log.msg(headers) + req = threads.deferToThread(requests.request, + method ='PUT', + url = 'https://secure.dshield.org/api/file/sshlog', + headers = headers, + timeout = 10, + data = log_output) + + def check_response(resp): + failed = False + response = resp.content + if resp.status_code == requests.codes.ok: + sha1_regex = re.compile(ur'([^<]+)<\/sha1checksum>') + sha1_match = sha1_regex.search(response) + if sha1_match is None: + log.err('dshield ERROR: Could not find sha1checksum in response') + failed = True + sha1_local = hashlib.sha1() + sha1_local.update(log_output) + if sha1_match.group(1) != sha1_local.hexdigest(): + log.err('\ndshield ERROR: SHA1 Mismatch {0} {1} .\n'.format(sha1_match.group(1), sha1_local.hexdigest())) + failed = True + md5_regex = re.compile(ur'([^<]+)<\/md5checksum>') + md5_match = md5_regex.search(response) + if md5_match is None: + log.err('dshield ERROR: Could not find md5checksum in response') + failed = True + md5_local = hashlib.md5() + md5_local.update(log_output) + if md5_match.group(1) != md5_local.hexdigest(): + log.err('\ndshield ERROR: MD5 Mismatch {0} {1} .\n'.format(md5_match.group(1), md5_local.hexdigest())) + failed = True + log.msg('\ndshield SUCCESS: Sent {0} bytes worth of data to secure.dshield.org\n'.format(len(log_output))) + else: + log.err('\ndshield ERROR: error {0} .\n'.format(resp.status_code)) + log.err('Response was {0}'.format(response)) + failed = True + + if failed: + # something went wrong, we need to add them to batch. + reactor.callFromThread(self.transmission_error, batch) + + req.addCallback(check_response)