diff --git a/censys/README.txt b/censys/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..abc09184694de38870744b32b26a73230d8547b8 --- /dev/null +++ b/censys/README.txt @@ -0,0 +1,68 @@ +========================= +Censys2warden connector +========================= + +This connector uses the internet-wide scanning service Censys.io to search for +potentially vulnerable open services in given network (ASN) and reports them to +Warden (via Warden filer daemon). + +The script sends a set of preconfigured queries to Censys API to search for all +matching IPs in given ASN. The queries correspond to various potentially +vulnerable hosts or other problems with open services. + +Since Censys' free account has quite strict limits on number of queries and +results obtained, it is recommended to have at least the lowest tier of a paid +account. + +Currently implemented queries: + +- Publicly accessible IPMI protocol +- Publicly accessible SCADA (BACnet) system +- Publicly accessible printer via IPP protocol +- Open MongoDB database +- Publicly accessible PCAnywhere (an unsupported vulnerable remote-access SW) +- Open Elastic database +- Web page or other service with a "hacked by" message +- Web running on an old (unsupported) PHP version + +Important: These rules may need to be changed according to needs, interests and +security policies of each organization. + +For each host in given ASN found to be matching a query, a corresponding IDEA +message is created and put to an output directory. Messages from this directory +should be sent to Warden by a Warden filer daemon. + +The script is assumed to be run periodically (e.g. once a day) by cron. + + +------------------------- +Installation: + +1. create a directory for the script and resulting IDEA files, e.g. + `/data/censys2warden/` and `/data/censys2warden/warden_sender` +2. put the censys2warden.py script into the first directory +3. set up warden_filer daemon to read IDEA messages from + `/data/censys2warden/warden_sender` and send them to Warden + (see README of Warden filer) +4. set up cron to run the scirpt every day (use censys2warden.cron as an + example) + + +------------------------- +Configuration: + +The script takes the following arguments: + + -h, --help show this help message and exit + -i APIID, --apiid APIID + Censys API ID + -s APISECRET, --apisecret APISECRET + Censys API secret + -a ASN, --asn ASN ASN to query + -n NODE, --node NODE Node name to fill into IDEA messages + -d PATH, --destdir PATH + Path to destination directory (with 'incoming' and + 'temp' subdirectories) (default: CWD) + -t, --test Add 'Test' category to IDEA messages. + -v, --verbose Print information about progress and results + diff --git a/censys/censys2warden.cron b/censys/censys2warden.cron new file mode 100644 index 0000000000000000000000000000000000000000..df55515ba31be5928226212e370ce9d6f6e1dd18 --- /dev/null +++ b/censys/censys2warden.cron @@ -0,0 +1,2 @@ +# Run every day at 9:00 +0 9 * * * shodan2warden python3 /data/censys2warden/censys2warden.py $(cat /data/censys2warden/api_params) -a 2852 -n cz.cesnet.ext.censys -d /data/censys2warden/warden_sender --test -v >>/data/censys2warden/censys2warden.log 2>&1 \ No newline at end of file diff --git a/censys/censys2warden.py b/censys/censys2warden.py new file mode 100644 index 0000000000000000000000000000000000000000..cab2bc6c826950590ec4fe3e4a0c46693d12c456 --- /dev/null +++ b/censys/censys2warden.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Author: Pavla Hlučková +# Václav Bartoš <bartos@cesnet.cz> + +import censys.ipv4 +import json +import os +from datetime import datetime +from uuid import uuid4 +import argparse + +def vprint(*args, **kwargs): + # Verbose print + if VERBOSE: + print("[{}] ".format(datetime.now().strftime("%Y-%m-%d %H:%M:%S")), end="") + print(*args, **kwargs) + +IPV4_FIELDS = ['ip', 'updated_at', 'ports', 'protocols','tags', 'metadata.description','metadata.device_type', 'metadata.manufacturer', 'location.city', 'location.country_code'] + +MAX_RESULTS_PER_QUERY = 1000 + +def generateIdeaEvent(detect_time, category, description,ip_string,ports,proto,content_type,content,note): + create_time = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + # if there's no timezone in detect_time, assume UTC (which Shodan normally uses) and append 'Z' + if 'Z' not in detect_time and '+' not in detect_time and '-' not in detect_time: + detect_time += 'Z' + event = { + "Format": "IDEA0", + "ID": str(uuid4()), + "Category": [category], + "CreateTime": create_time, + "DetectTime": detect_time, + "Description": description, + "Ref": ["https://www.censys.io/ipv4/" + ip_string], + "Source": [ + { + "IP4": [ip_string], + "Port": ports, + "Proto": proto + } + ], + "Node": [ + { + "Name": node_name, + "SW": ["censys2warden"], + "Type": ["External", "Recon"] + } + ], + "Attach": [ + { + "ContentType": content_type, + "Content": json.dumps(content) if content_type == 'application/json' else content, + "Note": note + } + ] + } + + if test_category: + event["Category"].append("Test") + + filename = "{}_{}_{}_{}.json".format( + datetime.utcnow().strftime('%Y%m%d%H%M%S%f'), + category.replace('.','').lower(), + ip_string, + event['ID'][:8]) + tmp_destination = os.path.join(default_directory, 'tmp', filename) + inc_destination = os.path.join(default_directory, 'incoming', filename) + + with open(tmp_destination, 'w') as json_file: + json.dump(event, json_file) + + os.rename(tmp_destination, inc_destination) + +def IPMI(): + query = asnString + "protocols: \"623/ipmi\" tags: ipmi" + category = "Vulnerable.Config" + description = "Publicly accessible insecure protocol: IPMI" + proto = ["udp", "ipmi"] + content_type = "application/json" + note = "Original Censys data (subset)" + vprint("Querying for IMPI:", query) + for banner in c.search(query, IPV4_FIELDS, max_records=MAX_RESULTS_PER_QUERY): + vprint("Found problematic IP:", banner.get('ip')) + generateIdeaEvent(banner.get('updated_at'),category,description,banner.get('ip'),[623],proto,content_type,banner,note) + +def SCADA(): + query = asnString + "scada" + category = "Vulnerable.Config" + description = "Publicly accessible SCADA (BACnet) system" + proto = ["udp", "bacnet"] + content_type = "application/json" + note = "Original Censys data (subset)" + vprint("Querying for SCADA:", query) + for banner in c.search(query, IPV4_FIELDS, max_records=MAX_RESULTS_PER_QUERY): + # TODO: find out the port of the scada protocol(s) + # (sometimes there are multiple services running on the IP) + vprint("Found problematic IP:", banner.get('ip')) + generateIdeaEvent(banner.get('updated_at'),category,description,banner.get('ip'),banner.get('ports'),proto,content_type,banner,note) + +def printerIPP(): + query = asnString + "protocols: \"631/ipp\"" + category = "Vulnerable.Config" + description = "Potentially vulnerable IPP printer" + proto = ["tcp", "ipp"] + content_type = "application/json" + note = "Original Censys data (subset)" + vprint("Querying for printer IPP:", query) + for banner in c.search(query, IPV4_FIELDS, max_records=MAX_RESULTS_PER_QUERY): + vprint("Found problematic IP:", banner.get('ip')) + generateIdeaEvent(banner.get('updated_at'),category,description,banner.get('ip'),[631],proto,content_type,banner,note) + +def mongoDB(): + query = asnString + "protocols: \"27017/mongodb\"" + category = "Vulnerable.Config" + description = "Potentially vulnerable MongoDB database" + proto = ["tcp"] + content_type = "application/json" + note = "Original Censys data (subset)" + vprint("Querying for MongoDB:", query) + for banner in c.search(query, IPV4_FIELDS,max_records=MAX_RESULTS_PER_QUERY): + # TODO: try to find a field with DB size, but it seems there's none + vprint("Found problematic IP:", banner.get('ip')) + generateIdeaEvent(banner.get('updated_at'),category,description,banner.get('ip'),[27017],proto,content_type,banner,note) + +def PCA(): + query = asnString + "tags: pca" + category = "Vulnerable.Config" + description = "An old unsupported service 'PCAnywhere' open to internet" + proto = ["tcp","pca"] + content_type = "application/json" + note = "Original Censys data (subset)" + vprint("Querying for PCAnywhere:", query) + for banner in c.search(query, IPV4_FIELDS,max_records=MAX_RESULTS_PER_QUERY): + vprint("Found problematic IP:", banner.get('ip')) + generateIdeaEvent(banner.get('updated_at'), category, description, banner.get('ip'), banner.get('ports'), proto,content_type, banner, note) + +def elasticSearch(): + query = asnString + "protocols: \"9200/elasticsearch\"" + category = "Vulnerable.Config" + description = "Possibly vulnerable data displayed - Elastic Search" + proto = ["tcp"] + content_type = "application/json" + note = "Original Censys data (subset)" + vprint("Querying for Elastic Indices:", query) + for banner in c.search(query,IPV4_FIELDS,max_records=MAX_RESULTS_PER_QUERY): + vprint("Found problematic IP:", banner.get('ip')) + generateIdeaEvent(banner.get('updated_at'), category, description, banner.get('ip'), banner.get('ports'), proto,content_type, banner, note) + + +def hacked(): + IPV4_FIELDS_HACKED = ['80.http.get.body','443.https.get.body','ip', 'updated_at', 'ports', 'protocols','tags', 'metadata.description','metadata.device_type', 'metadata.manufacturer', 'location.city', 'location.country_code'] + query = asnString + "\"hacked\"" + category = "Information.UnauthorizedModification" + description = "Service probably hacked (\"hacked\" string found in banner)" + proto = [] + content_type = "application/json" + note = "Original Censys data (subset)" + vprint("Querying for \"hacked\" string:", query) + for banner in c.search(query, IPV4_FIELDS_HACKED,max_records=MAX_RESULTS_PER_QUERY): + if "Test Page" in str(banner.get('80.http.get.body')) or "Test Page" in str(banner.get('443.https.get.body')): + continue + if "Cyber Security" in str(banner.get('80.http.get.body')) or "Cyber Security" in str(banner.get('443.https.get.body')): + # this was added to filter a false positive on a cybersecurity page mentioning hacking, see http://158.196.109.174/ + continue + vprint("Found problematic IP:", banner.get('ip')) + generateIdeaEvent(banner.get('updated_at'), category, description, banner.get('ip'), banner.get('ports'), proto,content_type, banner, note) + +def unsupportedPHP(): + query = asnString + "(80.http.get.headers.x_powered_by: PHP\\/5.* OR 8080.http.get.headers.x_powered_by: PHP\\/5.* OR 443.https.get.headers.x_powered_by: PHP\\/5.*)" + category = "Vulnerable.Open" + description = "Web running on old (unsupported) PHP version" + proto = ["tcp", "http"] + content_type = "application/json" + note = "Original Censys data (subset)" + vprint("Querying for unsupported PHP:", query) + for banner in c.search(query,IPV4_FIELDS,max_records=MAX_RESULTS_PER_QUERY): + vprint("Found problematic IP:", banner.get('ip')) + generateIdeaEvent(banner.get('updated_at'), category, description, banner.get('ip'), banner.get('ports'), proto,content_type, banner, note) + + +def parse_args(): + # command line argument parser + parser = argparse.ArgumentParser( + description="Searches Censys for potential problems with open services in given ASN. For each such problem generates an IDEA message into gievn directory (to be sent to Warden by warden_filer). This script is assumed to be run daily by cron.") + parser.add_argument('-i', '--apiid', required=True, + help="Censys API ID") + parser.add_argument('-s', '--apisecret', required=True, + help="Censys API secret") + parser.add_argument('-a', '--asn', type=int, required=True, + help="ASN to query") + parser.add_argument('-n', '--node', required=True, + help="Node name to fill into IDEA messages") + parser.add_argument('-d', '--destdir', dest="path", default=os.getcwd(), + help="Path to destination directory (with 'incoming' and 'temp' subdirectories) (default: CWD)") + parser.add_argument('-t', '--test', action="store_true", + help="Add 'Test' category to IDEA messages.") + parser.add_argument('-v', '--verbose', action="store_true", + help="Print information about progress and results") + return parser.parse_args() + + +def main(): + IPMI() + SCADA() + printerIPP() + mongoDB() + PCA() + elasticSearch() + hacked() + unsupportedPHP() + + +if __name__ == "__main__": + # getting arguments from argparse + args = parse_args() + VERBOSE = args.verbose + default_directory = args.path + node_name = args.node + test_category = args.test + asnString = "autonomous_system.asn:" + str(args.asn) + " AND " + + c = censys.ipv4.CensysIPv4(api_id=str(args.apiid), + api_secret=str(args.apisecret)) + + # incoming directory creation + directory = "incoming" + path = os.path.join(default_directory, directory) + os.makedirs(path, exist_ok=True) + + # tmp directory creation + directory = "tmp" + path = os.path.join(default_directory, directory) + os.makedirs(path, exist_ok=True) + + main() + diff --git a/cowrie/warden_client_cowrie.cfg b/cowrie/warden_client_cowrie.cfg new file mode 100644 index 0000000000000000000000000000000000000000..5668548946e964bee92ae0705d758418ee639ab9 --- /dev/null +++ b/cowrie/warden_client_cowrie.cfg @@ -0,0 +1,15 @@ +{ + "warden": "warden_client.cfg", + "name": "cz.cesnet.server.kippo", + "secret": "", + + "anonymised": "no", + "target_net": "195.113.0.0/16", + + "dbhost": "localhost", + "dbuser": "kippo", + "dbpass": "kippopass", + "dbname": "kippo", + "dbport": 3306, + "awin": 5 +} diff --git a/cowrie/warden_sender_cowrie.py b/cowrie/warden_sender_cowrie.py new file mode 100755 index 0000000000000000000000000000000000000000..db056aaa18cc2a1e75477e24fffc6c6e950e2872 --- /dev/null +++ b/cowrie/warden_sender_cowrie.py @@ -0,0 +1,305 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# + +from warden_client import Client, Error, read_cfg, format_timestamp +from time import time, gmtime, strftime +from math import trunc +from uuid import uuid4 +import MySQLdb as my +import MySQLdb.cursors as mycursors +import tempfile, subprocess, base64 +import json +import string +import os +import sys + +#warden client startup +aconfig = read_cfg('warden_client_cowrie.cfg') +wconfig = read_cfg('warden_client.cfg') +aclient_name = aconfig['name'] +wconfig['name'] = aclient_name +aanonymised = aconfig['anonymised'] +aanonymised_net = aconfig['target_net'] +aanonymised = aanonymised if (aanonymised_net != '0.0.0.0/0') or (aanonymised_net == 'omit') else '0.0.0.0/0' +awin = aconfig['awin'] * 60 +atest = aconfig['test_mode'] + +wclient = Client(**wconfig) + +def idea_fill_addresses(event, source_ip, destination_ip, anonymised, anonymised_net): + af = "IP4" if not ':' in source_ip else "IP6" + event['Source'][0][af] = [source_ip] + if anonymised != 'omit': + if anonymised == 'yes': + event['Target'][0]['Anonymised'] = True + event['Target'][0][af] = [anonymised_net] + else: + event['Target'][0][af] = [destination_ip] + return event + +def gen_event_idea_cowrie_info(detect_time, src_ip, dst_ip, win_start_time, win_end_time, aggr_win, conn_count): + event = { + "Format": "IDEA0", + "ID": str(uuid4()), + "DetectTime": detect_time, + "WinStartTime": win_start_time, + "WinEndTime": win_end_time, + "Category": ["Attempt.Login"], + "Note": "SSH login attempt", + "ConnCount": conn_count, + "Source": [{}], + "Target": [{ "Proto": ["tcp", "ssh"], "Port": [22]}], + "Node": [ + { + "Name": aclient_name, + "Type": ["Connection","Honeypot","Recon"], + "SW": ["Cowrie"], + "AggrWin": strftime("%H:%M:%S", gmtime(aggr_win)) + } + ] + } + # Test if we're testing + if atest == "true": + event["Category"].append('Test') + + event = idea_fill_addresses(event, src_ip, dst_ip, aanonymised, aanonymised_net) + return event + + +def gen_event_idea_cowrie_auth(detect_time, src_ip, dst_ip, username, password, sessionid): + + event = { + "Format": "IDEA0", + "ID": str(uuid4()), + "DetectTime": detect_time, + "Category": ["Information.UnauthorizedAccess"], + "Note": "SSH successfull attempt", + "ConnCount": 1, + "Source": [{}], + "Target": [{ "Proto": ["tcp", "ssh"], "Port" : [22] }], + "Node": [ + { + "Name": aclient_name, + "Type": ["Honeypot", "Connection", "Auth"], + "SW": ["Cowrie"], + } + ], + "Attach": [{ "sessionid": sessionid, "username": username, "password": password }] + } + # Test if we're testing + if atest == "true": + event["Category"].append('Test') + + event = idea_fill_addresses(event, src_ip, dst_ip, aanonymised, aanonymised_net) + + return event + + +def gen_event_idea_cowrie_ttylog(detect_time, src_ip, dst_ip, sessionid, ttylog, iinput): + + event = { + "Format": "IDEA0", + "ID": str(uuid4()), + "DetectTime": detect_time, + "Category": ["Information.UnauthorizedAccess"], + "Note": "Cowrie ttylog", + "ConnCount": 1, + "Source": [{}], + "Target": [{ "Proto": ["tcp", "ssh"], "Port" : [22] }], + "Node": [ + { + "Name": aclient_name, + "Type": ["Honeypot", "Data"], + "SW": ["Cowrie"], + } + ], + "Attach": [ { "sessionid": sessionid, "ttylog": ttylog, "iinput": iinput, "smart": iinput } ] + } + # Test if we're testing + if atest == "true": + event["Category"].append('Test') + + event = idea_fill_addresses(event, src_ip, dst_ip, aanonymised, aanonymised_net) + + return event + + +def gen_event_idea_cowrie_download(detect_time, src_ip, dst_ip, sessionid, url, outfile): + + event = { + "Format": "IDEA0", + "ID": str(uuid4()), + "DetectTime": detect_time, + "Category": ["Malware"], + "Note": "Cowrie download", + "ConnCount": 1, + "Source": [{}], + "Target": [{ "Proto": ["tcp", "ssh"], "Port" : [22]}], + "Node": [ + { + "Name": aclient_name, + "Type": ["Honeypot", "Data"], + "SW": ["Cowrie"], + } + ], + "Attach": [{ "sessionid": sessionid, "url": url, "outfile": outfile, "smart": url }] + } + # Test if we're testing + if atest == "true": + event["Category"].append('Test') + + event = idea_fill_addresses(event, src_ip, dst_ip, aanonymised, aanonymised_net) + + return event + + +def get_iinput(sessionid): + ret = [] + query = "SELECT GROUP_CONCAT(input SEPARATOR '--SEP--') as i FROM input WHERE session=%s GROUP BY session;" + crs.execute(query, (sessionid,)) + rows = crs.fetchall() + for row in rows: + ret.append(row["i"]) + return ''.join(ret) + + +def get_ttylog(sessionid): + ret = "" + query = "SELECT id, session, ttylog FROM ttylog WHERE session=%s;" + crs.execute(query, (sessionid,)) + rows = crs.fetchall() + for row in rows: + try: + tf = tempfile.NamedTemporaryFile(delete=False) + with open(tf.name, 'w') as f: + f.write(row['ttylog']) + ret = subprocess.check_output(["/opt/cowrie/bin/playlog", "-m0", tf.name]) + finally: + os.remove(tf.name) + + #try to dumpit to json to see if there are some binary input and perhaps wrap it to base64 + try: + a = json.dumps(ret) + except UnicodeDecodeError as e: + wclient.logger.warning("wraping binary content") + ret = base64.b64encode(ret) + + return ret + + + +con = my.connect( host=aconfig['dbhost'], user=aconfig['dbuser'], passwd=aconfig['dbpass'], + db=aconfig['dbname'], port=aconfig['dbport'], cursorclass=mycursors.DictCursor) +crs = con.cursor() +events = [] + +#kippo vs cowrie +#cowrie/core/dblog.py: def nowUnix(self): +#cowrie/core/dblog.py- """return the current UTC time as an UNIX timestamp""" +#cowrie/core/dblog.py- return int(time.time()) +#kippo/core/dblog.py: def nowUnix(self): +#kippo/core/dblog.py- """return the current UTC time as an UNIX timestamp""" +#kippo/core/dblog.py- return int(time.mktime(time.gmtime()[:-1] + (-1,))) +# k sozalenju +# >>> int(time.mktime(time.gmtime()[:-1] + (-1,)))-int(time.time()) != 0 + + +#old senders data +query = "SELECT UNIX_TIMESTAMP(s.starttime) as starttime, s.ip, COUNT(s.id) as attack_scale, sn.ip as sensor \ + FROM sessions s \ + LEFT JOIN sensors sn ON s.sensor=sn.id \ + WHERE s.starttime > DATE_SUB(UTC_TIMESTAMP(), INTERVAL + %s SECOND) \ + GROUP BY s.ip ORDER BY s.starttime ASC;" + +crs.execute(query, (awin,)) +etime = format_timestamp(time()) +stime = format_timestamp(time() - awin) +rows = crs.fetchall() +for row in rows: + a = gen_event_idea_cowrie_info( + detect_time = format_timestamp(row['starttime']), + src_ip = row['ip'], + dst_ip = row['sensor'], + + win_start_time = stime, + win_end_time = etime, + aggr_win = awin, + conn_count = row['attack_scale'] + ) + events.append(a) + + + +#success login +query = "SELECT UNIX_TIMESTAMP(a.timestamp) as timestamp, s.ip as sourceip, sn.ip as sensor, a.session as sessionid, a.username as username, a.password as password \ + FROM auth a JOIN sessions s ON s.id=a.session JOIN sensors sn ON s.sensor=sn.id \ + WHERE a.success=1 AND a.timestamp > DATE_SUB(UTC_TIMESTAMP(), INTERVAL + %s SECOND) \ + ORDER BY a.timestamp ASC;" + +crs.execute(query, (awin,)) +rows = crs.fetchall() +for row in rows: + a = gen_event_idea_cowrie_auth( + detect_time = format_timestamp(row['timestamp']), + src_ip = row['sourceip'], + dst_ip = row['sensor'], + + username = row['username'], + password = row['password'], + sessionid = row['sessionid'] + ) + events.append(a) + +#ttylog+iinput reporter +query = "SELECT UNIX_TIMESTAMP(s.starttime) as starttime, s.ip as sourceip, sn.ip as sensor, t.session as sessionid \ + FROM ttylog t JOIN sessions s ON s.id=t.session JOIN sensors sn ON s.sensor=sn.id \ + WHERE s.starttime > DATE_SUB(UTC_TIMESTAMP(), INTERVAL + %s SECOND) \ + ORDER BY s.starttime ASC;" + +crs.execute(query, (awin,)) +rows = crs.fetchall() +for row in rows: + a = gen_event_idea_cowrie_ttylog( + detect_time = format_timestamp(row['starttime']), + src_ip = row['sourceip'], + dst_ip = row['sensor'], + + sessionid = row['sessionid'], + ttylog = get_ttylog(row['sessionid']), + iinput = get_iinput(row['sessionid']) + ) + events.append(a) + + +#download +query = "SELECT UNIX_TIMESTAMP(s.starttime) as starttime, s.ip as sourceip, sn.ip as sensor, d.session as sessionid, d.url as url, d.outfile as ofile \ + FROM downloads d JOIN sessions s ON s.id=d.session JOIN sensors sn ON s.sensor=sn.id \ + WHERE s.starttime > DATE_SUB(UTC_TIMESTAMP(), INTERVAL + %s SECOND) \ + ORDER BY s.starttime ASC;" + +crs.execute(query, (awin,)) +rows = crs.fetchall() +for row in rows: + a = gen_event_idea_cowrie_download( + detect_time = format_timestamp(row['starttime']), + src_ip = row['sourceip'], + dst_ip = row['sensor'], + + sessionid = row['sessionid'], + url = row['url'], + outfile = row['ofile'] + ) + events.append(a) + + +print "=== Sending ===" +start = time() +ret = wclient.sendEvents(events) + +if 'saved' in ret: + wclient.logger.info("%d event(s) successfully delivered." % ret['saved']) + +print "Time: %f" % (time() - start) + + diff --git a/dionaea/warden3-dio-sender.py b/dionaea/warden3-dio-sender.py new file mode 100755 index 0000000000000000000000000000000000000000..b992ad495c2bad5a742968758b206231799c017f --- /dev/null +++ b/dionaea/warden3-dio-sender.py @@ -0,0 +1,246 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2011-2018 Cesnet z.s.p.o +# Use of this source is governed by a 3-clause BSD-style license, see LICENSE file. + +from warden_client import Client, Error, read_cfg, format_timestamp +import json +import string +import urllib +from time import time, gmtime, strftime, sleep +from math import trunc +from uuid import uuid4 +from os import path, remove +import base64 +import sqlite3 +import sys +import re + +aconfig = read_cfg('warden_client_dio.cfg') +wconfig = read_cfg('warden_client.cfg') +aclient_name = aconfig['name'] +wconfig['name'] = aclient_name +aanonymised = aconfig['anonymised'] +aanonymised_net = aconfig['target_net'] +aanonymised = aanonymised if (aanonymised_net != '0.0.0.0/0') or (aanonymised_net == 'omit') else '0.0.0.0/0' +atest = aconfig['test_mode'] + +awin = aconfig['awin'] * 60 +abinpath = aconfig['binaries_path'] +adbfile = aconfig['dbfile'] +aconattempts = aconfig['con_attempts'] +aretryinterval = aconfig['con_retry_interval'] +areportbinaries = aconfig['report_binaries'] +apurgebinaries = aconfig['purge_binaries'] + +wconfig['secret'] = aconfig.get('secret', '') +wclient = Client(**wconfig) + +def idea_fill_addresses(event, source_ip, destination_ip, anonymised, anonymised_net): + af = "IP4" if not ':' in source_ip else "IP6" + event['Source'][0][af] = [source_ip] + if anonymised != 'omit': + if anonymised == 'yes': + event['Target'][0]['Anonymised'] = True + event['Target'][0][af] = [anonymised_net] + else: + event['Target'][0][af] = [destination_ip] + + return event + +def gen_attach_idea_smb(logger, report_binaries, purge_binaries, binaries_path, filename, hashtype, hashdigest, vtpermalink, avref): + + refs = [] + attach = { + "Handle": 'att1', + "FileName": [filename], + "Type": ["Malware"], + "Hash": ["%s:%s" % (hashtype, hashdigest)], + } + + if vtpermalink is not None: + refs.append('url:' + vtpermalink) + + if avref is not None: + refs.extend(avref.split(';')) + + if refs: + refs = [urllib.quote(ref, safe=':') for ref in refs] + refs = list(set(refs)) + attach['Ref'] = refs + + if report_binaries == 'true': + try: + fpath = path.join(binaries_path, hashdigest) + with open(fpath, "r") as f: + fdata = f.read() + attach['ContentType'] = 'application/octet-stream' + attach['ContentEncoding'] = 'base64' + attach['Size'] = len(fdata) + attach['Content'] = base64.b64encode(fdata) + except (IOError) as e: + logger.info("Reading id file \"%s\" with malware failed, information will not be attached." % (fpath)) + if purge_binaries == 'true': + try: + remove(filename) + except OSError: + pass + + return attach + +def gen_attach_idea_db(logger, data): + + attach = {} + attach["Handle"] = 'att1' + attach["Type"] = ["Malware"] + attach['ContentType'] = 'application/octet-stream' + attach['ContentEncoding'] = 'base64' + attach['Size'] = len(data) + attach['Content'] = base64.b64encode(data) + + return attach + +def gen_event_idea_dio(logger, binaries_path, report_binaries, purge_binaries, client_name, anonymised, target_net, detect_time, win_start_time, win_end_time, aggr_win, data): + + category = [] + event = { + "Format": "IDEA0", + "ID": str(uuid4()), + "DetectTime": detect_time, + "WinStartTime": win_start_time, + "WinEndTime": win_end_time, + "ConnCount": data['attack_scale'], + "Source": [{}], + "Target": [{}], + "Node": [ + { + "Name": client_name, + "Type": ["Connection", "Protocol", "Honeypot"], + "SW": ["Dionaea"], + "AggrWin": strftime("%H:%M:%S", gmtime(aggr_win)) + } + ] + } + + # Save TCP/UDP proto + proto = [data['proto']] + + # smbd allows save malware + if data['service'] == 'smbd' and data['download_md5_hash'] is not None: + category.append('Attempt.Exploit') + category.append('Malware') + proto.append('smb') + + if data['download_url'] != '': + event['Source'][0]['URL'] = [data['download_url']] + filename = data['download_url'].split('/')[-1] + + if filename != '' and data['download_md5_hash'] != '': + # Generate "SMB Attach" part of IDEA + a = gen_attach_idea_smb(logger, report_binaries, binaries_path, filename, "md5", data['download_md5_hash'], data['virustotal_permalink'], data['scan_result']) + + event['Source'][0]['AttachHand'] = ['att1'] + event['Attach'] = [a] + + if data['service'] == 'mysqld': + #Clean exported data + if data['mysql_query'] is not None: + mysql_data = re.sub("select @@version_comment limit 1,?", "", data['mysql_query']) + if mysql_data != "": + # Generate "MySQL Attach" part of IDEA + a = gen_attach_idea_db(logger, mysql_data) + + category.append('Attempt.Exploit') + proto.append('mysql') + event['Source'][0]['AttachHand'] = ['att1'] + event['Attach'] = [a] + + if data['service'] == 'mssqld': + #Clean exported data + if data['mssql_query'] is not None: + mssql_data = data['mssql_query'] + if mssql_data != "": + # Generate "MSSQL Attach" part of IDEA + a = gen_attach_idea_db(logger, mssql_data) + + category.append('Attempt.Exploit') + proto.append('mssql') + event['Source'][0]['AttachHand'] = ['att1'] + event['Attach'] = [a] + + event['Source'][0]['Port'] = [data['src_port']] + event['Target'][0]['Port'] = [data['dst_port']] + event['Source'][0]['Proto'] = proto + event['Target'][0]['Proto'] = proto + + idea_fill_addresses(event, data['src_ip'], data['dst_ip'], aanonymised, aanonymised_net) + + # Add default category + if not category: + category.append('Recon.Scanning') + + # Test if we're testing + if atest == "true": + category.append('Test') + + event['Category'] = category + + return event + +def main(): + + con = sqlite3.connect(adbfile) + con.row_factory = sqlite3.Row + con.text_factory = str + crs = con.cursor() + + events = [] + + query = "SELECT c.connection_timestamp AS timestamp, c.remote_host AS src_ip, c.remote_port AS src_port, c.connection_transport AS proto, \ + c.local_host AS dst_ip, c.local_port AS dst_port, COUNT(c.connection) as attack_scale, c.connection_protocol AS service, d.download_url, d.download_md5_hash, \ + v.virustotal_permalink, GROUP_CONCAT('urn:' || vt.virustotalscan_scanner || ':' || vt.virustotalscan_result,';') AS scan_result, \ + group_concat(mca.mysql_command_arg_data) as mysql_query, \ + group_concat(msc.mssql_command_cmd) as mssql_query \ + FROM connections AS c LEFT JOIN downloads AS d ON c.connection = d.connection \ + LEFT JOIN virustotals AS v ON d.download_md5_hash = v.virustotal_md5_hash \ + LEFT JOIN virustotalscans vt ON v.virustotal = vt.virustotal \ + LEFT JOIN mysql_commands mc ON c.connection = mc.connection \ + LEFT JOIN mysql_command_args mca ON mc.mysql_command = mca.mysql_command \ + LEFT JOIN mssql_commands msc ON c.connection = msc.connection \ + WHERE datetime(connection_timestamp,'unixepoch') > datetime('now','-%d seconds') AND c.remote_host != '' \ + GROUP BY c.remote_host, c.local_port ORDER BY c.connection_timestamp ASC;" % (awin) + + attempts = 0 + while attempts < aconattempts: + try: + crs.execute(query) + break + except sqlite3.Error, e: + attempts += 1 + wclient.logger.info("Info: %s - attempt %d/%d." % (e.args[0], attempts, aconattempts)) + if attempts == aconattempts: + wclient.logger.error("Error: %s (dbfile: %s)" % (e.args[0], adbfile)) + + sleep(aretryinterval) + + rows = crs.fetchall() + + if con: + con.close + + etime = format_timestamp(time()) + stime = format_timestamp(time() - awin) + + for row in rows: + dtime = format_timestamp(row['timestamp']) + events.append(gen_event_idea_dio(logger = wclient.logger, binaries_path = abinpath, report_binaries = areportbinaries, purge_binaries = apurgebinaries, client_name = aclient_name, anonymised = aanonymised, target_net = aanonymised_net, detect_time = dtime, win_start_time = stime, win_end_time = etime, aggr_win = awin, data = row)) + + start = time() + ret = wclient.sendEvents(events) + if 'saved' in ret: + wclient.logger.info("%d event(s) successfully delivered in %d seconds" % (ret['saved'], (time() - start))) + + +if __name__ == "__main__": + main() diff --git a/dionaea/warden_client-dio.cfg b/dionaea/warden_client-dio.cfg new file mode 100644 index 0000000000000000000000000000000000000000..537d96ab12e92c58163b9c2c1a34e8962bc6f714 --- /dev/null +++ b/dionaea/warden_client-dio.cfg @@ -0,0 +1,15 @@ +{ + "warden": "warden_client.cfg", + "name": "cz.cesnet.server.dionaea", + "secret": "", + + "anonymised": "no", + "target_net": "195.113.0.0/16", + + "dbfile": "/opt/dionaea/var/dionaea/logsql.sqlite", + "binaries_path" : "/opt/dionaea/var/dionaea/binaries", + "report_binaries" : "true", + "con_attempts" : 3, + "con_retry_interval" : 5, + "awin": 5 +} diff --git a/shodan/README.txt b/shodan/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..247893ca2e1aaab9a7836254faafe5eddb584700 --- /dev/null +++ b/shodan/README.txt @@ -0,0 +1,63 @@ +========================= +Shodan2warden connector +========================= + +This connector uses the internet-wide scanning service Shodan to search for +potentially vulnerable open services in given network (ASN) and reports them to +Warden (via Warden filer daemon). + +The script sends a set of preconfigured queries to Shodan API to search for all +matching IPs in given ASN. The queries correspond to various potentially +vulnerable hosts or other problems with open services. + +Currently implemented queries: + +- Publicly accessible IPMI protocol +- Publicly accessible SCADA (BACnet) system +- Publicly accessible printer via incesure PJL protocol +- Publicly accessible printer via IPP protocol +- Open MongoDB database +- Open Elastic database +- Open anonymous FTP +- Web page or other service with a "hacked by" message +- Web running on an old (unsupported) PHP version + +Important: These rules may need to be changed according to needs, interests and +security policies of each organization. + +For each host in given ASN found to be matching a query, a corresponding IDEA +message is created and put to an output directory. Messages from this directory +should be sent to Warden by a Warden filer daemon. + +The script is assumed to be run periodically (e.g. once a day) by cron. + + +------------------------- +Installation: + +1. create a directory for the script and resulting IDEA files, e.g. + `/data/shodan2warden/` and `/data/shodan2warden/warden_sender` +2. put the shodan2warden.py script into the first directory +3. set up warden_filer daemon to read IDEA messages from + `/data/shodan2warden/warden_sender` and send them to Warden + (see README of Warden filer) +4. set up cron to run the scirpt every day (use shodan2warden.cron as an + example) + + +------------------------- +Configuration: + +The script takes the following arguments: + + -h, --help show help message and exit + -k APIKEY, --apikey APIKEY + Shodan API key + -a ASN, --asn ASN ASN to query + -n NODE, --node NODE Node name to fill into IDEA messages + -d PATH, --destdir PATH + Path to destination directory (with 'incoming' and + 'temp' subdirectories) (default: CWD) + -t, --test Add 'Test' category to IDEA messages. + -v, --verbose Print information about progress and results + diff --git a/shodan/shodan2warden.cron b/shodan/shodan2warden.cron new file mode 100644 index 0000000000000000000000000000000000000000..ea09bb7e2b21e1b6ec94404c1df048c3cec24b9c --- /dev/null +++ b/shodan/shodan2warden.cron @@ -0,0 +1,2 @@ +# Run every day at 9:00 +0 9 * * * shodan2warden python3 /data/shodan2warden/shodan2warden.py -k $(cat /data/shodan2warden/shodan_key) -a 2852 -n cz.cesnet.ext.shodan -d /data/shodan2warden/warden_sender --test -v >>/data/shodan2warden/shodan2warden.log 2>&1 \ No newline at end of file diff --git a/shodan/shodan2warden.py b/shodan/shodan2warden.py new file mode 100644 index 0000000000000000000000000000000000000000..a8af304ee386dbad3c54c24ba5d58e04ff821dab --- /dev/null +++ b/shodan/shodan2warden.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Author: Pavla HluÄŤková +# Václav Bartoš <bartos@cesnet.cz> + +from shodan import Shodan +from datetime import datetime +from uuid import uuid4 +import json +import os +import argparse + +# Global variables +VERBOSE = False +test_category = False +node_name = 'undefined' + +def vprint(*args, **kwargs): + # Verbose print + if VERBOSE: + print("[{}] ".format(datetime.now().strftime("%Y-%m-%d %H:%M:%S")), end="") + print(*args, **kwargs) + + +def generateIdeaEvent(detect_time, category, description,ip_string,port_num,proto,content_type,content,note): + create_time = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + # if there's no timezone in detect_time, assume UTC (which Shodan normally uses) and append 'Z' + if 'Z' not in detect_time and '+' not in detect_time and '-' not in detect_time: + detect_time += 'Z' + event = { + "Format": "IDEA0", + "ID": str(uuid4()), + "Category": [category], + "CreateTime": create_time, + "DetectTime": detect_time, + "Description": description, + "Ref": ["https://www.shodan.io/host/" + ip_string], + "Source": [ + { + "IP4": [ip_string], + "Port": [port_num], + "Proto": proto + } + ], + "Node": [ + { + "Name": node_name, + "SW": ["shodan2warden"], + "Type": ["External", "Recon"] + } + ], + "Attach": [ + { + "ContentType": content_type, + "Content": content, + "Note": note + } + ] + } + + if test_category: + event["Category"].append("Test") + + filename = "{}_{}_{}.json".format( + datetime.utcnow().strftime('%Y%m%d%H%M%S%f'), + ip_string, + event['ID'][:8]) + tmp_destination = os.path.join(default_directory, 'tmp', filename) + inc_destination = os.path.join(default_directory, 'incoming', filename) + + with open(tmp_destination, 'w') as json_file: + json.dump(event, json_file) + + os.rename(tmp_destination, inc_destination) + + +def IPMI(): + query = asnString+" port:623" + category = "Vulnerable.Config" + description = "Publicly accessible insecure protocol: IPMI" + proto = ["udp", "ipmi"] + content_type = "text/plain" + note = "Original service banner from Shodan" + vprint("Querying for IMPI:", query) + for banner in api.search_cursor(query): + vprint("Found problematic IP:", banner.get('ip_str')) + generateIdeaEvent(banner.get('timestamp'), category, description, + banner.get('ip_str'), banner.get('port'), + proto, content_type, banner.get('data'), note) + +def SCADA(): + query = asnString+" port:47808" + category = "Vulnerable.Config" + description = "Publicly accessible SCADA (BACnet) system" + proto = ["udp", "bacnet"] + content_type = "text/plain" + note = "Original service banner from Shodan" + vprint("Querying for SCADA:", query) + for banner in api.search_cursor(query): + vprint("Found problematic IP:", banner.get('ip_str')) + generateIdeaEvent(banner.get('timestamp'), category, description, + banner.get('ip_str'), banner.get('port'), + proto, content_type, banner.get('data'), note) + +def printerPJL(): + query = asnString + " port:9100 PJL INFO STATUS" + category = "Vulnerable.Config" + description = "Vulnerable PJL printer" + proto = [] + content_type = "text/plain" + note = "Original service banner from Shodan" + vprint("Querying for printer PLJ:", query) + for banner in api.search_cursor(query): + vprint("Found problematic IP:", banner.get('ip_str')) + generateIdeaEvent(banner.get('timestamp'), category, description, + banner.get('ip_str'), banner.get('port'), + proto, content_type, banner.get('data'), note) + +def printerIPP(): + query = asnString+" port:631" + category = "Vulnerable.Config" + description = "Potentially vulnerable IPP printer" + proto = ["ipp"] + content_type = "text/plain" + note = "Original service banner from Shodan" + vprint("Querying for printer IPP:", query) + for banner in api.search_cursor(query): + if "close" in banner.get('data'): + continue + vprint("Found problematic IP:", banner.get('ip_str')) + generateIdeaEvent(banner.get('timestamp'), category, description, + banner.get('ip_str'), banner.get('port'), + proto, content_type, banner.get('data'), note) + +def mongoDB(): + query = asnString+" \"mongodb metrics\"" + category = "Vulnerable.Config" + description = "Potentially vulnerable MongoDB database" + proto = ["tcp"] + content_type = "text/plain" + note = "Original service banner from Shodan" + vprint("Querying for MongoDB:", query) + for banner in api.search_cursor(query): + if "\"totalSize\": 0.0" in banner.get('data'): + continue # skip if database is empty + if "hacked" in banner.get('data'): + category = "Information.UnauthorizedModification" + description = "Potentially exploited mongoDB database" + vprint("Found problematic IP:", banner.get('ip_str')) + generateIdeaEvent(banner.get('timestamp'), category, description, + banner.get('ip_str'), banner.get('port'), + proto, content_type, banner.get('data'), note) + +def elasticIndices(): + query = asnString+" \"Elastic Indices\"" + category = "Vulnerable.Config" + description = "Possibly vulnerable data displayed - Elastic Indices" + proto = ["tcp"] + content_type = "text/plain" + note = "Original service banner from Shodan" + vprint("Querying for Elastic Indices:", query) + for banner in api.search_cursor(query): + vprint("Found problematic IP:", banner.get('ip_str')) + generateIdeaEvent(banner.get('timestamp'), category, description, + banner.get('ip_str'), banner.get('port'), + proto, content_type, banner.get('data'), note) + +def anonFTP(): + query = asnString+" port:21 \"anonymous logged in\"" + category = "Vulnerable.Config" + description = "Open anonymous FTP" + proto = ["tcp", "ftp"] + content_type = "text/plain" + note = "Original service banner from Shodan" + vprint("Querying for anonymous FTP:", query) + for banner in api.search_cursor(query): + vprint("Found problematic IP:", banner.get('ip_str')) + generateIdeaEvent(banner.get('timestamp'), category, description, + banner.get('ip_str'), banner.get('port'), + proto, content_type, banner.get('data'), note) + +def hacked(): + query = asnString+" \"hacked\"" + category = "Information.UnauthorizedModification" + description = "Service probably hacked (\"hacked\" string found in banner)" + proto = [] + content_type = "text/plain" + note = "Original service banner from Shodan" + vprint("Querying for \"hacked\" string:", query) + for banner in api.search_cursor(query): + vprint("Found problematic IP:", banner.get('ip_str')) + generateIdeaEvent(banner.get('timestamp'), category, description, + banner.get('ip_str'), banner.get('port'), + proto, content_type, banner.get('data'), note) + +def unsupportedPHP(): + query = asnString+" PHP" + category = "Vulnerable.Open" + description = "Web running on old (unsupported) PHP version" + proto = ["tcp", "http"] + content_type = "text/plain" + note = "Original service banner from Shodan" + vprint("Querying for unsupported PHP:", query) + for banner in api.search_cursor(query): + if "test page" in banner.get('data'): + continue + if "PHP/5" in banner.get('data') or "PHP/4" in banner.get('data'): + vprint("Found problematic IP:", banner.get('ip_str')) + generateIdeaEvent(banner.get('timestamp'), category, description, + banner.get('ip_str'), banner.get('port'), + proto, content_type, banner.get('data'), note) + + +def parse_args(): + # command line argument parser + parser = argparse.ArgumentParser(description="Searches Shodan for potential problems with open services in given ASN. For each such problem generates an IDEA message into gievn directory (to be sent to Warden by warden_filer). This script is assumed to be run daily by cron.") + parser.add_argument('-k', '--apikey', required=True, + help="Shodan API key") + parser.add_argument('-a', '--asn', type=int, required=True, + help="ASN to query") + parser.add_argument('-n', '--node', required=True, + help="Node name to fill into IDEA messages") + parser.add_argument('-d', '--destdir', dest="path", default=os.getcwd(), + help="Path to destination directory (with 'incoming' and 'temp' subdirectories) (default: CWD)") + parser.add_argument('-t', '--test', action="store_true", + help="Add 'Test' category to IDEA messages.") + parser.add_argument('-v', '--verbose', action="store_true", + help="Print information about progress and results") + return parser.parse_args() + +def main(): + IPMI() + SCADA() + printerPJL() + printerIPP() + mongoDB() + elasticIndices() + anonFTP() + hacked() + unsupportedPHP() + +if __name__ == "__main__": + #getting arguments from argparse + args = parse_args() + VERBOSE = args.verbose + default_directory = args.path + node_name = args.node + test_category = args.test + asnString = "asn:as" + str(args.asn) + + api = Shodan(args.apikey) + + # incoming directory creation + directory = "incoming" + path = os.path.join(default_directory, directory) + os.makedirs(path, exist_ok=True) + + # tmp directory creation + directory = "tmp" + path = os.path.join(default_directory, directory) + os.makedirs(path, exist_ok=True) + + main() diff --git a/tippingpoint/tpToIdea.py b/tippingpoint/tpToIdea.py new file mode 100644 index 0000000000000000000000000000000000000000..52b9bacb46e477a368945a52650772d2d15bc961 --- /dev/null +++ b/tippingpoint/tpToIdea.py @@ -0,0 +1,642 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017-2018 Cesnet z.s.p.o +# Use of this source is governed by a 3-clause BSD-style license, see LICENSE file. + +import json +from uuid import uuid4 +import re +import socket +import optparse +import sys +import os +import signal +import resource +import os.path as pth +import atexit +import time +from datetime import datetime +import logging + + +class FileWatcher(object): + + def __init__(self, filename, tail=True): + self.filename = filename + self.open() + self.line_buffer = "" + if tail and self.f: + self.f.seek(0, os.SEEK_END) + + def open(self): + try: + self.f = open(self.filename, "r") + st = os.fstat(self.f.fileno()) + self.inode, self.size = st.st_ino, st.st_size + except IOError: + self.f = None + self.inode = -1 + self.size = -1 + + def _check_reopen(self): + try: + st = os.stat(self.filename) + cur_inode, cur_size = st.st_ino, st.st_size + except OSError as e: + cur_inode = -1 + cur_size = -1 + if cur_inode != self.inode or cur_size < self.size: + self.close() + self.open() + + def readline(self): + if not self.f: + self.open() + if not self.f: + return self.line_buffer + res = self.f.readline() + if not res: + self._check_reopen() + if not self.f: + return self.line_buffer + res = self.f.readline() + if not res.endswith("\n"): + self.line_buffer += res + else: + res = self.line_buffer + res + self.line_buffer = "" + return res + + def close(self): + try: + if self.f: + self.f.close() + except IOError: + pass + self.inode = -1 + self.size = -1 + + def __repr__(self): + return '%s("%s")' % (type(self).__name__, self.filename) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + return False + + def __iter__(self): + return self + + def next(self): + return self.readline() + + +class Filer(object): + + def __init__(self, directory): + self.basedir = self._ensure_path(directory) + self.tmp = self._ensure_path(pth.join(self.basedir, "tmp")) + self.incoming = self._ensure_path(pth.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 pth.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.time(), device, inode) + + def create_unique_file(self): + # First find and open name unique within tmp + tmpname = None + while not tmpname: + tmpname = self._get_new_name() + try: + fd = os.open(pth.join(self.tmp, tmpname), os.O_CREAT | os.O_RDWR | os.O_EXCL) + except OSError as e: + if e.errno != errno.EEXIST: + raise # other errors than duplicates should get noticed + tmpname = None + # Now we know the device/inode, rename to make unique within system + newname = self._get_new_name(fd) + os.rename(pth.join(self.tmp, tmpname), pth.join(self.tmp, newname)) + nf = os.fdopen(fd, "w") + return nf, newname + + def publish_file(self, short_name): + os.rename(pth.join(self.tmp, short_name), pth.join(self.incoming, short_name)) + + +class IdeaGen(object): + + tp_tax_to_idea = { + 1: { + 1: ["Attempt.Exploit"], + 2: ["Attempt.Exploit"], + 3: ["Attempt.Exploit"], + 4: ["Attempt.Exploit"], + 5: ["Attempt.Exploit"], + 6: ["Attempt.Exploit"], + 255: ["Attempt.Exploit"] + }, + + 2: { + 1: ["Malware.Worm"], + 2: ["Malware.Virus"], + 3: ["Malware.Trojan"], + 4: ["Intrusion.Botnet"], + 5: ["Fraud.Phishing"], + 255: ["Malware"] + }, + + 3: { + 1: ["Availability.DDoS"], + 2: ["Availability.DDoS"], + 3: ["Availability.DDoS"], + 255: ["Availability.DDoS"] + }, + + 4: { + 1: ["Other"], + 2: ["Other"], + 3: ["Other"], + 4: ["Other"], + 5: ["Other"], + 6: ["Attempt.Login"], + 7: ["Malware.Spyware"], + 255: ["Other"] + }, + + 5: { + 1: ["Recon.Scanning"], + 2: ["Attempt.Exploit"], + 3: ["Attempt.Exploit"], + 4: ["Recon.Scanning", "Attempt.Exploit"], + 255: ["Attempt.Exploit"] + }, + + 6: { + 1: ["Anomaly.Protocol"], + 2: ["Anomaly.Traffic"], + 3: ["Anomaly.Application"], + 255: ["Anomaly"] + }, + + 7: { + 1: ["Anomaly.Traffic"], + 2: ["Anomaly.Application"], + 255: ["Anomaly.Traffic"] + }, + + 8: { + 1: ["Other"], + 2: ["Other"], + 255: ["Other"] + }, + } + + tp_prot_to_idea = { + 1: "appletalk", + 2: "auth", + 3: "bgp", + 4: "cdp", + 5: "clns", + 6: "dhcp", + 7: "domain", + 8: "finger", + 9: "ftp", + 10: "hsrp", + 11: "http", + 12: "icmp", + 13: "igmp", + 14: "eigrp", + 15: "ipv6", + 16: "ipx", + 17: "irc", + 18: "isis", + 19: "isakmp", + 20: "ldap", + 21: "mpls", + 22: "ms-rpc", + 23: "ms-sql", + 24: "nat", + 25: "netbios", + 26: "nntp", + 27: "ntp", + 28: "oracle", + 29: "ospf", + 30: "pop-imap", + 31: "sunrpc", + 32: "qos", + 33: "rip", + 34: "sunrpc", + 35: "smb", + 36: "smtp", + 37: "snmp", + 38: "sql", + 39: "ssh", + 40: "ssl-tls", + 41: "tacacs", + 42: "tcp", + 43: "telnet", + 45: "udp", + 46: "uucp", + 48: "x11", + 49: "tftp", + 50: "ip", + 51: "nfs", + 52: "wins", + 80: "h323", + 81: "megaco", + 82: "mgcp", + 83: "sip", + 84: "rtp", + 99: "voip", + 100: "aim", + 101: "msn", + 102: "yahoo", + 103: "icq", + 119: "im", + 120: "musicmatch", + 121: "winamp", + 122: "shoutcast", + 123: "windows", + 124: "quicktime", + 125: "rtsp", + 149: "streaming", + 150: "bittorrent", + 151: "manolito", + 152: "directconnect", + 153: "earthstation5", + 154: "p2p", + 155: "fasttrack", + 156: "gnutella", + 157: "twister", + 158: "winmx", + 180: "p2p", + 190: "scada-dnp3", + 191: "scada-iccp", + 192: "scada-iec", + 193: "scada-modbus", + 194: "scada-opc", + 199: "scada", + 254: "multi-protocol", + } + + def __init__(self, name, test=False, other=False): + self.name = name + self.test = test + self.other = other + + def convert_category_and_protocol(self, category, id_taxonomy): + ''' + converts category from record to IDEA category + :param category: TippingPoint category description + :param id_taxonomy: TippingPoint taxonomy id + :return: if category or incident is empty or is not important for saving it return None, otherwise return + converted category + ''' + if not (category and id_taxonomy): + return None + tp_cat_maj = id_taxonomy >> 24 + tp_cat_min = id_taxonomy >> 16 & 0b11111111 + tp_prot = id_taxonomy >> 8 & 0b11111111 + tp_platf = id_taxonomy & 0b11111111 + try: + category = IdeaGen.tp_tax_to_idea[tp_cat_maj][tp_cat_min] + except KeyError: + category = ["Other"] + + try: + protocol = IdeaGen.tp_prot_to_idea[tp_prot] + except KeyError: + protocol = None + + return { 'category': category, 'protocol': protocol } + + def gen_event_idea(self, timestamp, category, id_taxonomy, cve, filter_name, proto, src_ip, src_port, + dest_ip, dest_port, conn_count, url, severity, orig_data): + ''' + put every piece of record together into IDEA message + :return: new IDEA message + ''' + + if (category == ["Other"]) and not self.other: + return None + + event = { + 'Format': "IDEA0", + 'ID': str(uuid4()), + 'DetectTime': datetime.utcfromtimestamp(timestamp / 1000).isoformat() + 'Z', + 'Category': category + (["Test"] if self.test else []) + } + if cve: + event['Ref'] = ['urn:cve:'.format(i) for i in cve] + if conn_count and int(conn_count): + event['ConnCount'] = int(conn_count) + source = {} + target = {} + if src_ip: + # TippingPoint vSMS bugfix: Remove excessive spaces occasionally included inside the IPv6 address + src_ip = src_ip.replace(" ", "") + af = "IP4" if not ':' in src_ip else "IP6" + source[af] = [src_ip] + if src_port and int(src_port): + source['Port'] = [int(src_port)] + if proto: + source['Proto'] = proto + if dest_ip and (dest_ip != "0.0.0.0"): + # TippingPoint vSMS bugfix: Remove excessive spaces occasionally included inside the IPv6 address + dest_ip = dest_ip.replace(" ", "") + af = "IP4" if not ':' in dest_ip else "IP6" + target[af] = [dest_ip] + if dest_port and int(dest_port): + target['Port'] = [int(dest_port)] + if proto: + target['Proto'] = proto + if url: + target['URL'] = url + + if source: + event['Source'] = [source] + if target: + if category and category == "Reputation": + event['Source'].append(target) + else: + event['Target'] = [target] + if orig_data: + event['Attach'] = [{ + 'Type': ["OrigData"], + 'Content': orig_data.strip() + }] + event['Node'] = [{ + 'Name': self.name, + 'Type': ["Datagram", "Content", "Protocol", "Signature", "Policy", "Heuristic"], + 'SW': ["TippingPoint_NX_NGIPS"] + }] + return event + + +def daemonize( + work_dir=None, chroot_dir=None, + umask=None, uid=None, gid=None, + pidfile=None, files_preserve=[], signals={}): + # Dirs, limits, users + if chroot_dir is not None: + os.chdir(chroot_dir) + os.chroot(chroot_dir) + if umask is not None: + os.umask(umask) + if work_dir is not None: + os.chdir(work_dir) + if gid is not None: + os.setgid(gid) + if uid is not None: + os.setuid(uid) + # Doublefork, split session + if os.fork() > 0: + os._exit(0) + try: + os.setsid() + except OSError: + pass + if os.fork() > 0: + os._exit(0) + # Setup signal handlers + for (signum, handler) in signals.items(): + signal.signal(signum, handler) + # Close descriptors + descr_preserve = set(f.fileno() for f in files_preserve) + maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] + if maxfd == resource.RLIM_INFINITY: + maxfd = 65535 + for fd in range(maxfd, 3, -1): # 3 means omit stdin, stdout, stderr + if fd not in descr_preserve: + try: + os.close(fd) + except Exception: + pass + # Redirect stdin, stdout, stderr to /dev/null + devnull = os.open(os.devnull, os.O_RDWR) + for fd in range(3): + os.dup2(devnull, fd) + # PID file + if pidfile is not None: + pidd = os.open(pidfile, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_TRUNC) + os.write(pidd, str(os.getpid())+"\n") + os.close(pidd) + # Define and setup atexit closure + + @atexit.register + def unlink_pid(): + try: + os.unlink(pidfile) + except Exception: + pass + + +def get_args(): + optp = optparse.OptionParser( + usage="usage: %prog [options] logfile ...", + description="Watch TippingPoint logfiles and generate Idea events into directory") + optp.add_option( + "-n", "--name", + default=None, + dest="name", + type="string", + action="store", + help="Warden client name") + optp.add_option( + "--test", + default=False, + dest="test", + action="store_true", + help="Add \"Test\" category") + optp.add_option( + "--other", + default=False, + dest="other", + action="store_true", + help="Send events having \"Other\" category (usually nonmalicious)") + optp.add_option( + "-o", "--oneshot", + default=False, + dest="oneshot", + action="store_true", + help="process files and quit (do not daemonize)") + optp.add_option( + "--poll", + default=1, + dest="poll", + type="int", + action="store", + help="log file polling interval") + optp.add_option( + "-d", "--dir", + default=None, + dest="dir", + type="string", + action="store", + help="Target directory (mandatory)") + optp.add_option( + "-p", "--pid", + default=pth.join("/var/run", pth.splitext(pth.basename(sys.argv[0]))[0] + ".pid"), + dest="pid", + type="string", + action="store", + help="create PID file with this name (default: %default)") + optp.add_option( + "-u", "--uid", + default=None, + dest="uid", + type="int", + action="store", + help="user id to run under") + optp.add_option( + "-g", "--gid", + default=None, + dest="gid", + type="int", + action="store", + help="group id to run under") + optp.add_option( + "--origdata", + default=False, + dest="origdata", + action="store_true", + help="Store original report to IDEA message") + return optp + + +def not_empty(test_string): + # tests if string is not empty + return None if test_string.strip() in ["", "null"] else test_string + + +def save_events(event, filer): + f, name = filer.create_unique_file() + with f: + f.write(json.dumps(event, ensure_ascii=True)) + filer.publish_file(name) + + +def process_data(line, filer, origdata, idea_gen): + ''' + takes one record, parse it to parameters, give it to Ideagen and writes to file + :param line: one record + :param idea_file: where output goes + :param origdata: if true, write original record to IDEA message + ''' + + ll_prot_str = [" ip:", " ipv6:", " udp:", " tcp:", "(ip)", "(ipv6)", "(udp)", "(tcp)", " ip ", " ipv6 ", " udp ", " tcp "] + + row = line.split("|") + + catprot = idea_gen.convert_category_and_protocol(category=row[1], id_taxonomy=int(row[2])) + proto = [] + if not_empty(row[5]).lower() in ['ip', 'ip6', 'udp', 'tcp']: + proto.append(row[5].lower()) + + flt_proto = [pstr for pstr in ll_prot_str if(pstr in not_empty(row[4]).lower())] + if flt_proto: + proto.append(flt_proto[0].translate(None, " ():v")) + + if catprot['protocol']: + proto.append(catprot['protocol']) + if set(proto).intersection({"ip", "ip6"}): + try: + proto.remove("ip") + proto.remove("ip6") + except ValueError: + pass + if not "udp" in proto: + proto.append("tcp") + proto = list(set(proto)) + + timestamp = row[0].split(" ")[-1] + cve = [i for i in row[3].split(",") if i not in ("null", "")] + odata = "|".join([timestamp] + row[1:]) + if catprot['category'] and not_empty(row[0][-14:-3]): + idea_event = idea_gen.gen_event_idea(timestamp=int(timestamp), category=catprot['category'], id_taxonomy=int(row[2]), + cve=cve, filter_name=not_empty(row[4]), proto=proto, + src_ip=not_empty(row[6]), src_port=not_empty(row[7]), dest_ip=not_empty(row[8]), dest_port=not_empty(row[9]), + conn_count=not_empty(row[10]), severity=not_empty(row[11]), url=not_empty(row[12]), + orig_data=odata if origdata else False) + if idea_event: + save_events(idea_event, filer) + + +running_flag = True +reload_flag = False + + +def terminate_me(signum, frame): + global running_flag + running_flag = False + + +def reload_me(signum, frame): + global reload_flag + reload_flag = True + + +def main(): + global running_flag + global reload_flag + logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG, filename='tipping_point_log.log', filemode='w') + optp = get_args() + opts, args = optp.parse_args() + if not args or opts.name is None or opts.dir is None: + optp.print_help() + sys.exit() + if opts.oneshot: + signal.signal(signal.SIGINT, terminate_me) + signal.signal(signal.SIGTERM, terminate_me) + files = [open(arg) for arg in args] + else: + daemonize( + pidfile=opts.pid, + uid=opts.uid, + gid=opts.gid, + signals={ + signal.SIGINT: terminate_me, + signal.SIGTERM: terminate_me, + signal.SIGHUP: reload_me + }) + files = [FileWatcher(arg) for arg in args] + filer = Filer(opts.dir) + idea_gen = IdeaGen(opts.name, opts.test, opts.other) + while running_flag: + for log_file in files: + while True: + line = log_file.readline() + if line is None or not line.strip(): + logging.info("no line") + break + logging.info("readline") + process_data(line, filer, opts.origdata, idea_gen) + if not running_flag: + break + if reload_flag: + for f in files: + f.close() + f.open() + reload_flag = False + if opts.oneshot: + break + else: + time.sleep(opts.poll) + + +if __name__ == "__main__": + main() \ No newline at end of file