From 792f908082dce1f2839ebf0c12e897f21d1f6057 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?V=C3=A1clav=20Barto=C5=A1?= <bartos@cesnet.cz>
Date: Fri, 16 Aug 2019 15:12:54 +0200
Subject: [PATCH] shodan2warden connector added

---
 shodan/shodan2warden.py | 264 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 264 insertions(+)
 create mode 100644 shodan/shodan2warden.py

diff --git a/shodan/shodan2warden.py b/shodan/shodan2warden.py
new file mode 100644
index 0000000..a8af304
--- /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()
-- 
GitLab