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/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