Skip to content
Snippets Groups Projects
Select Git revision
  • 87f7b98057de07c0e229d230a40035a97149853f
  • master default protected
  • dionaea-creds
  • dionaea-smb
  • 4-cowrie-dio-node-sw-field
5 results

log_wardenfiler.py

Blame
  • log_wardenfiler.py 18.44 KiB
    #!/usr/bin/python
    # -*- coding: utf-8 -*-
    #
    # Copyright (C) 2020 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 hashlib
    import logging
    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 dionaea import IHandlerLoader
    from dionaea.core import ihandler, connection
    from dionaea.exception import LoaderError
    
    logger = logging.getLogger("log_wardenfiler")
    logger.setLevel(logging.DEBUG)
    
    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 LogWardenfilerHandlerLoader(IHandlerLoader):
        name = "log_wardenfiler"
    
        @classmethod
        def start(cls, config=None):
            try:
                return LogWardenfilerHandler("*", config=config)
            except LoaderError as e:
                logger.error(e.msg, *e.args)
                return None
    
    class LogWardenfilerHandler(ihandler):
        detector_name = None
        resolve_nat = False
        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 = {}
    
        def __init__(self, path, config = None):
            logger.debug("%s ready!", self.__class__.__name__)
            ihandler.__init__(self, path)
            self.path = path
            self._config = config
    
        def _bytes_to_str(self, s):
            if isinstance(s, str):
                return s
            return str(s, "utf-8", "backslashreplace")
    
        def _fixup_event(self, event):
            if 'database' in event and isinstance(event['database'], bytes):
                event['database'] = self._bytes_to_str(event['database'])
            return event
    
        def _save_event(self, event):
            event = self._fixup_event(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 'detector_name' in self._config:
                self.detector_name = self._config.get('detector_name')
            if 'resolve_nat' in self._config:
                self.resolve_nat = self._config.get('resolve_nat')
            if 'nat_host' in self._config:
                self.nat_host = self._config.get('nat_host')
            if 'nat_port' in self._config:
                self.nat_port = self._config.get('nat_port')
            if 'reported_ipv4' in self._config:
                self.reported_ipv4 = self._config.get('reported_ipv4')
            if 'reported_ipv6' in self._config:
                self.reported_ipv6 = self._config.get('reported_ipv6')
            if 'anon_mask_4' in self._config:
                self.anon_mask_4 = self._config.get('anon_mask_4')
            if 'anon_mask_6' in self._config:
                self.anon_mask_6 = self._config.get('anon_mask_6')
            if 'aggr_win' in self._config:
                self.aggr_win = self._config.get('aggr_win')
            if 'test_mode' in self._config:
                self.test_mode = self._config.get('test_mode')
            if 'output_dir' in self._config:
                self.output_dir = self._config.get('output_dir')
            if 'drop_malware' in self._config:
                self.drop_malware = self._config.get('drop_malware')
    
            self.filer = Filer(self.output_dir)
    
        def _aggregate(self):
            ws = self.win_start or time()
            if (time() - ws >= self.aggr_win):
                logger.info("Counting attacks: %s" % json.dumps(self.attackers, ensure_ascii = True))
                we = datetime.utcfromtimestamp(ws + self.aggr_win).isoformat() + 'Z'
                sevent = {
                    "Format": "IDEA0",
                    "WinStartTime": datetime.utcfromtimestamp(ws).isoformat() + 'Z',
                    "WinEndTime": we,
                    "DetectTime": we,
                    "Category": [],
                    "Node": [
                        {
                            "Name": self.detector_name,
                            "Type": ["Connection", "Auth", "Honeypot"],
                            "SW": ["Dionaea with Warden Filer output module"],
                            "AggrWin": strftime("%H:%M:%S", gmtime(float(self.aggr_win)))
                        }
                    ]
                }
                if self.test_mode:
                    sevent["Category"].append("Test")
    
                for i, a in self.attackers.items():
                    c = a["count"]
                    if c > 1:
                        src_ip, dst_ip, dst_port, proto = i.split(',')
                        sevent["ID"] = str(uuid4())
                        if len(a["creds"]):
                            sevent["Category"] = ["Recon.Scanning"]
                            sevent["Note"] = "Successful logins to honeypoted service."
                        else:
                            sevent["Category"] = ["Attempt.Login"]
                            sevent["Note"] = "Connection attempts to IPs assigned to honeypot."
                        sevent["ConnCount"] = c
                        af = "IP4" if not ':' in src_ip else "IP6"
                        proto = [proto]
                        if a["proto"]:
                            proto.append(a["proto"])
                        sevent["Source"] = [{"Proto": proto, af: [src_ip], "Port": a["sports"]}]
                        sevent["Target"] = [{"Proto": proto, af: [dst_ip], "Port": [int(dst_port)]}]
                        if (self.anon_mask_4 < 32) and (not ':' in  dst_ip) or (self.anon_mask_6 < 128):
                            sevent["Target"][0]["Anonymised"] = "true"
                        if len(a["creds"]):
                            attach = {
                                "Type": ["Credentials"],
                                "Note": "Credentials used by attacker used for simulated honeypot login",
                                "Credentials": a["creds"]
                            }
                            sevent["Attach"] = attach
                        self._save_event(sevent)
                        logger.info("sending scanning event for %s probing %s (%i times)" % (src_ip, dst_ip, c))
                self.attackers = {}
                self.win_start = time()
    
        def _make_idea(self, con):
            s = self.sessions[con]
            proto = [s["trans"]]
            if s["proto"]:
                proto.append(s["proto"])
            event = {
                "Format": "IDEA0",
                "ID": s["id"],
                "DetectTime": s["dt"],
                "Category": s["cat"],
                "Source": [{"Proto": proto, s["af"]: [s["src_ip"]], "Port": [s["src_port"]]}],
                "Target": [{ "Proto": proto, s["af"]: [s["dst_ip"]], "Port": [s["dst_port"]]}],
                "Node": [
                    {
                        "Name": self.detector_name,
                        "Type": ["Connection", "Auth", "Honeypot"],
                        "SW": ["Dionaea with Warden Filer output module"],
                    }
                ]
            }
    
            if s["anon"]:
                event["Target"][0]["Anonymised"] = "true"
    
            if len(s["creds"]):
                p = {
                    "ftp": "FTP",
                    "mysql": "MySQL",
                    "ms-sql-s": "MSSQL",
                }
                event["Category"].append("Intrusion.UserCompromise")
                if s["proto"]:
                    event["Note"] = p[s["proto"]] + " successful login"
                else:
                    event["Note"] = "Successful login attempt"
                attach = {
                    "Type": ["Credentials"],
                    "Note": "Credentials used by attacker used for simulated honeypot login",
                    "Credentials": s["creds"]
                }
                if "Attach" not in event:
                    event["Attach"] = []
                event["Attach"].append(attach)
            else:
                # login without password or similar thing
                event["Category"].append("Recon.Scanning")
                event["Note"] = "Connection"
    
            if len(s["cmds"]):
                # consider this an exploit only if there was a login attempt
                if len(s["creds"]):
                    event["Category"].append("Attempt.Exploit")
                event["Note"] += " with command input"
                idata = "\n".join(str(c) for c in s["cmds"])
                plain = all(c in string.printable for c in 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 session",
                    "Content": eidata
                }
                if not plain:
                    attach["ContentEncoding"] = "base64"
                if "Attach" not in event:
                    event["Attach"] = []
                event["Attach"].append(attach)
    
            return(event)
    
        def _register_connection(self, con, proto = None, cred = None, cmd = None):
            if not con in self.sessions:
                self.sessions[con] = {}
    
                src_ip = con.remote.host
                dst_ip = con.local.host
                if src_ip.startswith("::ffff:"):
                    src_ip = src_ip[7:]
                if dst_ip.startswith("::ffff:"):
                    dst_ip = dst_ip[7:]
    
                af = "IP4" if not ':' in src_ip else "IP6"
    
                # Test for static IP to report as attack target
                if af == "IP4" and self.reported_ipv4:
                    dst_ip = self.reported_ipv4
                # Resolve NAT if instructed
                elif af == "IP4" and self.resolve_nat:
                    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                    s.connect((self.nat_host, self.nat_port))
                    s.sendall((','.join((src_ip, str(con.remote.port))).encode("utf-8")))
                    dst = s.recv(50).decode("utf-8")
                    s.close()
                    if dst != "NE":
                        dst_ip = dst
                    else:
                        logger.warn("no translation for %s:%s" % (src_ip, con.remote.port))
                        return()
                elif af == "IP6" and self.reported_ipv6:
                    dst_ip = self.reported_ipv6
    
                
                anon = (self.anon_mask_4 < 32) and (not ':' in  dst_ip) or (self.anon_mask_6 < 128)
                if anon:
                    dst_ip = [(
                        str(IPv4Network("/".join((dst_ip, str(self.anon_mask_4))), False).network_address) if not ':' in dst_ip else
                        str(IPv6Network("/".join((dst_ip, str(self.anon_mask_6))), False).network_address)
                    )]
    
                self.sessions[con]["id"] = str(uuid4())
                self.sessions[con]["dt"] = datetime.utcnow().isoformat() + "Z"
                self.sessions[con]["cat"] = ["Test"] if self.test_mode else []
                self.sessions[con]["af"] = af
                self.sessions[con]["anon"] = anon
                self.sessions[con]["src_ip"] = src_ip
                self.sessions[con]["dst_ip"] = dst_ip
                self.sessions[con]["src_port"] = con.remote.port
                self.sessions[con]["dst_port"] = con.local.port
                self.sessions[con]["trans"] = con.transport
                self.sessions[con]["proto"] = None
                self.sessions[con]["creds"] = []
                self.sessions[con]["cmds"] = []
    
            aid = ','.join((self.sessions[con]["src_ip"], self.sessions[con]["dst_ip"], str(con.local.port), con.transport))
    
            if not aid in self.attackers:
                self.attackers[aid] = {
                    "count": 0,
                    "sports": [],
                    "creds": [],
                    "proto": None
                }
    
            self.attackers[aid]["count"] += 1
            if not con.remote.port in self.attackers[aid]["sports"]:
                self.attackers[aid]["sports"].append(con.remote.port)
            if proto:
                self.sessions[con]["proto"] = proto
                self.attackers[aid]["proto"] = proto
            if cred:
                self.sessions[con]["creds"].append(cred)
                self.attackers[aid]["creds"].append(cred)
            if cmd:
                self.sessions[con]["cmds"].append(cmd)
    
        def handle_incident(self, icd):
            pass
    
        def handle_incident_dionaea_connection_tcp_listen(self, icd):
            pass;
    
        def handle_incident_dionaea_connection_tls_listen(self, icd):
            pass
    
        def handle_incident_dionaea_connection_tcp_connect(self, icd):
            con = icd.con
            self._register_connection(con)
            logger.info("connect connection to %s/%s:%i from %s:%i" % (con.remote.host, con.remote.hostname, con.remote.port, con.local.host, con.local.port))
    
        def handle_incident_dionaea_connection_tls_connect(self, icd):
            con = icd.con
            self._register_connection(con, "ssl-tls")
            logger.info("connect connection to %s/%s:%i from %s:%i" % (con.remote.host, con.remote.hostname, con.remote.port, con.local.host, con.local.port))
    
        def handle_incident_dionaea_connection_udp_connect(self, icd):
            con = icd.con
            self._register_connection(con)
            logger.info("connect connection to %s/%s:%i from %s:%i" % (con.remote.host, con.remote.hostname, con.remote.port, con.local.host, con.local.port))
    
        def handle_incident_dionaea_connection_tcp_accept(self, icd):
            con = icd.con
            self._register_connection(con)
            logger.info("accepted connection from %s:%i to %s:%i" % (con.remote.host, con.remote.port, con.local.host, con.local.port))
    
        def handle_incident_dionaea_connection_tls_accept(self, icd):
            con = icd.con
            self._register_connection(con, "ssl-tls")
            logger.info("accepted connection from %s:%i to %s:%i" % (con.remote.host, con.remote.port, con.local.host, con.local.port))
    
        def handle_incident_dionaea_connection_tcp_reject(self, icd):
            con = icd.con
            self._register_connection(con)
            logger.info("reject connection from %s:%i to %s:%i" % (con.remote.host, con.remote.port, con.local.host, con.local.port))
    
        def handle_incident_dionaea_modules_python_ftp_command(self, icd):
            con = icd.con
            cmd = icd.command.decode()
            if hasattr(icd, 'arguments'):
                cmd += " " + " ".join(icd.arguments)
            self._register_connection(con, "ftp", cmd = cmd)
            logger.info("new FTP command within connection from %s:%i to %s:%i" % (con.remote.host, con.remote.port, con.local.host, con.local.port))
    
        def handle_incident_dionaea_modules_python_mssql_cmd(self, icd):
            con = icd.con
            self._register_connection(con, "ms-sql-s", cmd = icd.cmd)
            logger.info("new MSSQL command within connection from %s:%i to %s:%i" % (con.remote.host, con.remote.port, con.local.host, con.local.port))
    
        def handle_incident_dionaea_modules_python_mysql_command(self, icd):
            con = icd.con
            cmd = str(icd.command)
            if hasattr(icd, 'args'):
                cmd += "\n" + "\n".join(icd.args)
            self._register_connection(con, "mysql", cmd = cmd)
            logger.info("new MYSQL command within connection from %s:%i to %s:%i" % (con.remote.host, con.remote.port, con.local.host, con.local.port))
    
        def handle_incident_dionaea_modules_python_ftp_login(self, icd):
            con = icd.con
            self._register_connection(con, "ftp",  cred = {"User": self._bytes_to_str(icd.username), "Password": self._bytes_to_str(icd.password)})
            logger.info("new FTP login within connection from %s:%i to %s:%i" % (con.remote.host, con.remote.port, con.local.host, con.local.port))
    
        def handle_incident_dionaea_modules_python_mssql_login(self, icd):
            con = icd.con
            self._register_connection(con, "ms-sql-s",  cred = {"User": self._bytes_to_str(icd.username), "Password": self._bytes_to_str(icd.password)})
            logger.info("new MSSQL login within connection from %s:%i to %s:%i" % (con.remote.host, con.remote.port, con.local.host, con.local.port))
    
        def handle_incident_dionaea_modules_python_mysql_login(self, icd):
            con = icd.con
            self._register_connection(con, "mysql",  cred = {"User": self._bytes_to_str(icd.username), "Password": self._bytes_to_str(icd.password)})
            logger.info("new MySQL login within connection from %s:%i to %s:%i" % (con.remote.host, con.remote.port, con.local.host, con.local.port))
    
        def handle_incident_dionaea_modules_python_p0f(self, icd):
            pass;
    
        def handle_incident_dionaea_connection_free(self, icd):
            con = icd.con
    
            self._aggregate()
    
            if con in self.sessions:
                s = self.sessions[con]
    
                # Do not generate IDEA event for a source
                # which is not globally routable
                if not ip_address(s["src_ip"]).is_global:
                    logger.info("not generating an event for connection from non-global IP %s:%s" % (con.remote.host, con.remote.port))
    
                elif s.get("cmds"):
                    event = self._make_idea(con)
                    self._save_event(event)
                    logger.info("sending connection event from %s:%i to %s:%i" % (con.remote.host, con.remote.port, con.local.host, con.local.port))
                self.sessions.pop(con, None)
                logger.info("closing connection from %s:%i to %s:%i" % (con.remote.host, con.remote.port, con.local.host, con.local.port))
            else:
                logger.warn("no attack data for %s:%s" % (con.remote.host, con.remote.port))