-
Pavel Valach authoredPavel Valach authored
wardenfiler.py 16.83 KiB
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Cesnet z.s.p.o
# Use of this source is governed by a 3-clause BSD-style license, see LICENSE file.
"""
Wardenfiler output connector. Writes audit logs to Wardenfiler spool directory in IDEA format
"""
import os
import errno
import socket
import json
import string
from urllib.parse import urlparse
from time import time, gmtime, strftime
from datetime import datetime
from uuid import uuid4
from hashlib import sha1
from base64 import b64encode
from ipaddress import ip_address
from ipaddress import IPv4Network
from ipaddress import IPv6Network
from cowrie.core.config import CowrieConfig
import cowrie.core.output
class Filer(object):
"""
IDEA files creator
"""
def __init__(self, directory):
self.basedir = self._ensure_path(directory)
self.tmp = self._ensure_path(os.path.join(self.basedir, "tmp"))
self.incoming = self._ensure_path(os.path.join(self.basedir, "incoming"))
self.hostname = socket.gethostname()
self.pid = os.getpid()
def _ensure_path(self, p):
try:
os.mkdir(p)
except OSError:
if not os.path.isdir(p):
raise
return p
def _get_new_name(self, fd=None):
(inode, device) = os.fstat(fd)[1:3] if fd else (0, 0)
return "%s.%d.%f.%d.%d" % (
self.hostname, self.pid, time(), device, inode)
def create_unique_file(self):
tmpname = None
while not tmpname:
tmpname = self._get_new_name()
try:
fd = os.open(os.path.join(self.tmp, tmpname), os.O_CREAT | os.O_RDWR | os.O_EXCL)
except OSError as e:
if e.errno != errno.EEXIST:
raise
tmpname = None
newname = self._get_new_name(fd)
os.rename(os.path.join(self.tmp, tmpname), os.path.join(self.tmp, newname))
nf = os.fdopen(fd, "w")
return nf, newname
def publish_file(self, short_name):
os.rename(os.path.join(self.tmp, short_name), os.path.join(self.incoming, short_name))
class Output(cowrie.core.output.Output):
"""
Wardenfiler Output
"""
detector_name = None
resolve_nat = False
reported_public_ipv4 = None
reported_public_ipv6 = None
reported_ssh_port = None
nat_host = "gateway"
nat_port = 1456
anon_mask_4 = 32
anon_mask_6 = 128
aggr_win = 5 * 60
test_mode = True
output_dir = "var/spool/warden"
drop_malware = True
win_start = None
attackers = {}
sessions = {}
port_xlat = {}
def save_event(self, event):
f, name = self.filer.create_unique_file()
with f:
f.write(json.dumps(event, ensure_ascii=True))
self.filer.publish_file(name)
def start(self):
if CowrieConfig.has_option('output_wardenfiler', 'detector_name'):
self.detector_name = CowrieConfig.get('output_wardenfiler', 'detector_name')
if CowrieConfig.has_option('output_wardenfiler', 'resolve_nat'):
self.resolve_nat = CowrieConfig.getboolean('output_wardenfiler', 'resolve_nat')
if CowrieConfig.has_option('output_wardenfiler', 'reported_public_ipv4'):
self.reported_public_ipv4 = CowrieConfig.get('output_wardenfiler', 'reported_public_ipv4')
if CowrieConfig.has_option('output_wardenfiler', 'reported_public_ipv6'):
self.reported_public_ipv6 = CowrieConfig.get('output_wardenfiler', 'reported_public_ipv6')
if CowrieConfig.has_option('output_wardenfiler', 'reported_ssh_port'):
self.reported_ssh_port = CowrieConfig.getint('output_wardenfiler', 'reported_ssh_port')
if CowrieConfig.has_option('output_wardenfiler', 'nat_host'):
self.nat_host = CowrieConfig.get('output_wardenfiler', 'nat_host')
if CowrieConfig.has_option('output_wardenfiler', 'nat_port'):
self.nat_port = CowrieConfig.getint('output_wardenfiler', 'nat_port')
if CowrieConfig.has_option('output_wardenfiler', 'anon_mask_4'):
self.anon_mask_4 = CowrieConfig.getint('output_wardenfiler', 'anon_mask_4')
if CowrieConfig.has_option('output_wardenfiler', 'anon_mask_6'):
self.anon_mask_6 = CowrieConfig.getint('output_wardenfiler', 'anon_mask_6')
if CowrieConfig.has_option('output_wardenfiler', 'aggr_win'):
self.aggr_win = CowrieConfig.getint('output_wardenfiler', 'aggr_win')
if CowrieConfig.has_option('output_wardenfiler', 'test_mode'):
self.test_mode = CowrieConfig.getboolean('output_wardenfiler', 'test_mode')
if CowrieConfig.has_option('output_wardenfiler', 'output_dir'):
self.output_dir = CowrieConfig.get('output_wardenfiler', 'output_dir')
if CowrieConfig.has_option('output_wardenfiler', 'port_xlat'):
self.port_xlat = dict((int(x), int(y)) for x, y in (e.split(':') for e in CowrieConfig.get('output_wardenfiler', 'port_xlat').split()))
if CowrieConfig.has_option('output_wardenfiler', 'drop_malware'):
self.drop_malware = CowrieConfig.getboolean('output_wardenfiler', 'drop_malware')
self.filer = Filer(self.output_dir)
def stop(self):
"""
No actions needed on honeypot shutdown
"""
def write(self, entry):
event = {
"Format": "IDEA0",
"ID": str(uuid4()),
"DetectTime": entry['timestamp'],
"Category": [],
"Source": [{"Proto": ["tcp", "ssh"]}],
"Target": [{ "Proto": ["tcp", "ssh"]}],
"Node": [
{
"Name": self.detector_name,
"Type": ["Connection", "Auth", "Honeypot"],
"SW": ["Cowrie with Warden Filer output module"],
}
]
}
if self.test_mode:
event["Category"].append("Test")
if entry["src_ip"].startswith("::ffff:"):
entry["src_ip"] = entry["src_ip"][7:]
if entry.get("dst_ip") and entry["dst_ip"].startswith("::ffff:"):
entry["dst_ip"] = entry["dst_ip"][7:]
# detect IPv4 or IPv6
src_af = "IP4" if not ':' in entry["src_ip"] else "IP6"
# If configured, override destination IP and port
if entry.get("dst_ip"):
if src_af == "IP4" and self.reported_public_ipv4:
entry["dst_ip"] = self.reported_public_ipv4
elif src_af == "IP6" and self.reported_public_ipv6:
entry["dst_ip"] = self.reported_public_ipv6
if entry.get("dst_port") and self.reported_ssh_port:
entry["dst_port"] = self.reported_ssh_port
if entry["eventid"] == 'cowrie.session.connect':
# Do not track a session for a source
# which is not globally routable
if not ip_address(entry["src_ip"]).is_global:
return()
if self.resolve_nat:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((self.nat_host, self.nat_port))
s.sendall((','.join((entry["src_ip"], str(entry["src_port"]))).encode("utf-8")))
dst = s.recv(50).decode("utf-8")
s.close()
if dst != "NE":
entry["dst_ip"] = dst
else:
return()
entry["dst_ip"] = (
str(IPv4Network("/".join((entry["dst_ip"], str(self.anon_mask_4))), False).network_address) if not ':' in entry["dst_ip"] else
str(IPv6Network("/".join((entry["dst_ip"], str(self.anon_mask_6))), False).network_address)
)
entry["loggedin"] = False
# AID - aggregation ID
entry["aid"] = aid = ','.join((entry["src_ip"], entry["dst_ip"]))
self.sessions[entry["session"]] = entry
ws = self.win_start or time()
cnt = self.attackers.get(aid, 0)
if (time() - ws < self.aggr_win):
self.attackers[aid] = cnt + 1
else:
event["Node"][0]["AggrWin"] = strftime("%H:%M:%S", gmtime(float(self.aggr_win)))
event["WinStartTime"] = datetime.utcfromtimestamp(ws).isoformat() + 'Z'
event["WinEndTime"] = datetime.utcfromtimestamp(ws + self.aggr_win).isoformat() + 'Z'
event["Category"].append("Attempt.Login")
event["Note"] = "SSH login attempt"
for i, c in self.attackers.items():
a_src_ip, a_dst_ip = i.split(',')
a_af = "IP4" if not ':' in a_src_ip else "IP6"
event["ID"] = str(uuid4())
event["DetectTime"] = event["WinEndTime"]
event["ConnCount"] = c
event["Source"] = [{"Proto": ["tcp", "ssh"], a_af: [a_src_ip]}]
event["Target"] = [{"Proto": ["tcp", "ssh"], a_af: [a_dst_ip]}]
if (self.anon_mask_4 < 32 and a_af == "IP4") or (self.anon_mask_6 < 128):
event["Target"][0]["Anonymised"] = True
self.save_event(event)
self.attackers = {}
ws = time()
self.attackers[aid] = 1
self.win_start = ws
elif entry["session"] not in self.sessions:
# We do not save sessions
# that were created during previous Cowrie runs
# and we should not care about them.
return()
elif entry["eventid"] == 'cowrie.login.success':
s = entry["session"]
if s in self.sessions:
self.sessions[s]["input"] = []
self.sessions[s]["loggedin"] = True
elif entry["eventid"] == 'cowrie.command.input':
s = entry["session"]
if s in self.sessions:
self.sessions[s]["input"].append(entry["input"])
elif entry["eventid"] == 'cowrie.session.file_download':
s = entry["session"]
if s in self.sessions:
sch = { "http": 80, "https": 443, "ftp": 21 }
# deal with the file first (drop even if not reported)
mware = None
fname = None
if "outfile" in entry and os.path.exists(entry["outfile"]):
fp = open(entry["outfile"], "rb")
mware = fp.read()
fp.close()
if self.drop_malware:
os.remove(entry["outfile"])
if mware:
# TODO: Classify everything as Malware?
event["Category"].append("Malware")
event["Note"] = "Malware download during honeypot session"
if "url" in entry and entry["url"].startswith(tuple(sch.keys())):
url = urlparse(entry["url"])
url_host = url.hostname
url_ai = socket.getaddrinfo(url_host, None)[0]
url_af = "IP6" if url_ai[0] == socket.AddressFamily.AF_INET6 else "IP4"
url_ip = url_ai[4][0]
proto = [ "tcp", url.scheme ]
port = url.port or sch[url.scheme]
fname = os.path.basename(entry["url"])
if not fname and 'destfile' in entry:
fname = os.path.basename(entry['destfile'])
elif not "url" in entry:
if "destfile" in entry:
event["Note"] = "Redirected content during honeypot session"
fname = os.path.basename(entry["destfile"])
else:
event["Note"] = "Stdin contents during honeypot session"
else:
# TODO: Some exotic protocol? Let's not worry with that now
return()
event["DetectTime"] = entry["timestamp"]
if "url" in entry:
del event["Target"]
event["Source"][0] = { "Type": ["Malware"] }
event["Source"][0]["URL"] = [entry["url"]]
event["Source"][0][url_af] = [url_ip]
event["Source"][0]["Proto"] = proto
event["Source"][0]["Port"] = [port]
if url_ip != url_host:
event["Source"][0]["Hostname"] = [url_host]
else:
event["Source"][0] = { "Type": ["Botnet"] }
# the source of the malicious activity is the host, we don't have further details to that
event["Source"][0][src_af] = [entry["src_ip"]]
event["Source"][0]["Port"] = [self.sessions[s]["src_port"]]
event["Attach"] = [{
"Type": ["ShellCode"],
"Hash": ["sha256:" + entry["shasum"]],
"Size": len(mware),
"Note": "Some probably malicious code downloaded during honeypot SSH session",
"ContentEncoding": "base64",
"Content": b64encode(mware).decode(),
}]
if fname:
event["Attach"][0]["FileName"] = [fname]
if "url" in entry:
event["Attach"][0]["ExternalURI"] = [entry["url"]]
self.save_event(event)
elif entry["eventid"] == 'cowrie.session.file_upload':
# Upload through SCP or SFTP to the honeypot
s = entry["session"]
if s in self.sessions:
# deal with the file first (drop even if not reported)
mware = None
fname = None
if "outfile" in entry and os.path.exists(entry["outfile"]):
fp = open(entry["outfile"], "rb")
mware = fp.read()
fp.close()
if self.drop_malware:
os.remove(entry["outfile"])
fname = entry["filename"]
if mware:
event["Category"].append("Malware")
event["Note"] = "Malware download during honeypot session"
event["DetectTime"] = entry["timestamp"]
event["Source"][0] = { "Type": ["Botnet"] }
# the source of the malicious activity is the host, we don't have further details to that
event["Source"][0][src_af] = [entry["src_ip"]]
event["Source"][0]["Port"] = [self.sessions[s]["src_port"]]
event["Attach"] = [{
"Type": ["ShellCode"],
"FileName": [fname],
"Hash": ["sha256:" + entry["shasum"]],
"Size": len(mware),
"Note": "Some probably malicious code downloaded during honeypot SSH session",
"ContentEncoding": "base64",
"Content": b64encode(mware).decode(),
}]
self.save_event(event)
elif entry["eventid"] == 'cowrie.session.closed':
s = entry["session"]
if s in self.sessions and self.sessions[s]["loggedin"]:
idata = '\n'.join(self.sessions[s]["input"])
plain = all(c in string.printable for c in idata)
event["Category"].append("Intrusion.UserCompromise")
event["Note"] = "SSH successful login" + (" with unauthorized command input" if len(idata) else "")
event["Source"][0][src_af] = [entry["src_ip"]]
event["Target"][0][src_af] = [self.sessions[s]["dst_ip"]]
event["Source"][0]["Port"] = [self.sessions[s]["src_port"]]
dst_port = self.sessions[s]["dst_port"]
if dst_port in self.port_xlat:
dst_port = self.port_xlat[dst_port]
event["Target"][0]["Port"] = [dst_port]
if len(idata):
eidata = idata if plain else b64encode(idata.encode()).decode()
attach = {
"Type": ["Exploit"],
"Hash": ["sha1:" + sha1(idata.encode("utf-8")).hexdigest()],
"Size": len(idata),
"Note": "Commands entered by attacker during honeypot SSH session",
"Content": eidata
}
if not plain:
attach["ContentEncoding"] = "base64"
event["Attach"] = [attach]
self.save_event(event)
self.sessions.pop(s, None)