diff --git a/README.md b/README.md index 8291949..87fe69f 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ ### Import modules * [OCR](misp_modules/modules/import_mod/ocr.py) Optical Character Recognition (OCR) module for MISP to import attributes from images, scan or faxes. +* [stiximport](misp_modules/modules/expansion/stiximport.py) - An import module to process STIX xml/json ## How to install and start MISP modules? diff --git a/REQUIREMENTS b/REQUIREMENTS index 482ad1c..f138191 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -1,3 +1,5 @@ +stix +cybox tornado dnspython3 requests diff --git a/misp_modules/modules/import_mod/__init__.py b/misp_modules/modules/import_mod/__init__.py index 5716751..b53259e 100644 --- a/misp_modules/modules/import_mod/__init__.py +++ b/misp_modules/modules/import_mod/__init__.py @@ -1 +1 @@ -__all__ = ['testimport', 'ocr'] +__all__ = ['testimport', 'ocr', 'stiximport'] diff --git a/misp_modules/modules/import_mod/stiximport.py b/misp_modules/modules/import_mod/stiximport.py new file mode 100755 index 0000000..a701ba5 --- /dev/null +++ b/misp_modules/modules/import_mod/stiximport.py @@ -0,0 +1,214 @@ +import json +from stix.core import STIXPackage +import re +import base64 +import hashlib + +misperrors = {'error': 'Error'} +userConfig = {} +inputSource = ['file'] + +moduleinfo = {'version': '0.1', 'author': 'Hannah Ward', + 'description': 'Import some stix stuff', + 'module-type': ['import']} + +moduleconfig = [] + + +def handler(q=False): + # Just in case we have no data + if q is False: + return False + + # The return value + r = {'results': []} + + # Load up that JSON + q = json.loads(q) + + # It's b64 encoded, so decode that stuff + package = str(base64.b64decode(q.get("data", None)), 'utf-8') + + # If something really weird happened + if not package: + return json.dumps({"success": 0}) + + # Load up the package into STIX + package = loadPackage(package) + + # Build all the observables + if package.observables: + for obs in package.observables: + r["results"].append(buildObservable(obs)) + + if package.threat_actors: + for ta in package.threat_actors: + r["results"].append(buildActor(ta)) + + if package.indicators: + for ind in package.indicators: + r["results"].append(buildIndicator(ind)) + + if package.exploit_targets: + for et in package.exploit_targets: + r["results"].append(buildExploitTarget(et)) + + if package.campaigns: + for cpn in package.campaigns: + r["results"].append(buildCampaign(cpn)) + # Clean up results + # Don't send on anything that didn't have a value + r["results"] = [x for x in r["results"] if len(x["values"]) != 0] + return r + +# Quick and dirty regex for IP addresses +ipre = re.compile("([0-9]{1,3}.){3}[0-9]{1,3}") + + +def buildCampaign(cpn): + """ + Extract a campaign name + """ + return {"values": [cpn.title], "types": ["campaign-name"]} + + +def buildExploitTarget(et): + """ + Extract CVEs from exploit targets + """ + + r = {"values": [], "types": ["vulnerability"]} + + if et.vulnerabilities: + for v in et.vulnerabilities: + if v.cve_id: + r["values"].append(v.cve_id) + return r + + +def identifyHash(hsh): + """ + What's that hash!? + """ + + possible_hashes = [] + + hashes = [x for x in hashlib.algorithms_guaranteed] + + for h in hashes: + if len(str(hsh)) == len(hashlib.new(h).hexdigest()): + possible_hashes.append(h) + possible_hashes.append("filename|{}".format(h)) + return possible_hashes + + +def buildIndicator(ind): + """ + Extract hashes + and other fun things + like that + """ + r = {"values": [], "types": []} + + # Try to get hashes. I hate stix + if ind.observable: + return buildObservable(ind.observable) + return r + + +def buildActor(ta): + """ + Extract the name + and comment of a + threat actor + """ + + r = {"values": [ta.title], "types": ["threat-actor"]} + + return r + + +def buildObservable(o): + """ + Take a STIX observable + and extract the value + and category + """ + + # Life is easier with json + if not isinstance(o, dict): + o = json.loads(o.to_json()) + # Make a new record to store values in + r = {"values": []} + + # Get the object properties. This contains all the + # fun stuff like values + if "observable_composition" in o: + # May as well be useless + return r + + props = o["object"]["properties"] + + # If it has an address_value field, it's gonna be an address + # print(props) + # Kinda obvious really + if "address_value" in props: + + # We've got ourselves a nice little address + value = props["address_value"] + + if isinstance(value, dict): + # Sometimes it's embedded in a dictionary + value = value["value"] + + # Is it an IP? + if ipre.match(str(value)): + # Yes! + r["values"].append(value) + r["types"] = ["ip-src", "ip-dst"] + else: + # Probably a domain yo + r["values"].append(value) + r["types"] = ["domain", "hostname"] + + if "hashes" in props: + for hsh in props["hashes"]: + r["values"].append(hsh["simple_hash_value"]["value"]) + r["types"] = identifyHash(hsh["simple_hash_value"]["value"]) + return r + + +def loadPackage(data): + # Write the stix package to a tmp file + with open("/tmp/stixdump", "w") as f: + f.write(data) + try: + # Try loading it into every format we know of + try: + package = STIXPackage().from_xml(open("/tmp/stixdump", "r")) + except: + package = STIXPackage().from_json(open("/tmp/stixdump", "r")) + except Exception: + print("Failed to load package") + raise ValueError("COULD NOT LOAD STIX PACKAGE!") + return package + + +def introspection(): + modulesetup = {} + try: + userConfig + modulesetup['userConfig'] = userConfig + except NameError: + pass + try: + inputSource + modulesetup['inputSource'] = inputSource + except NameError: + pass + return modulesetup + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo diff --git a/setup.py b/setup.py index ed92f56..d48e13e 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,8 @@ setup( 'pyeupi', 'ipasn-redis', 'asnhistory', + 'stix', + 'cybox', 'pillow', 'pytesseract', ] diff --git a/tests/stix.xml b/tests/stix.xml new file mode 100644 index 0000000..a4a60d8 --- /dev/null +++ b/tests/stix.xml @@ -0,0 +1,331 @@ + + + + CNC Server 1 + + + 82.146.166.56 + + + + + CNC Server 2 + + + 209.239.79.47 + + + + + CNC Server 3 + + + 41.213.121.180 + + + + + Watering Hole Wordpress + + + eu-society.com + + + + + Watering Hole Wordpress + + + aromatravel.org + + + + + Watering Hole Wordpress + + + bss.servebbs.com + + + + + + + Watering Hole Detected + URL Watchlist + + + + C2 List + + + C2 List + + + C2 List + + + + + + CnC Beaconing Detected + C2 + + + + + + + + + + + + + + + Malware CnC Channels + + Advantage + + + + Hosting + + + + + + + + + + + + + Fingerprinting and whitelisting during watering-hole operations + + Theft - Credential Theft + + + + Domain Registration + + + C2 List + + + C2 List + + + C2 List + + + + + + + + + + Spear-phishing in tandem with 0-day exploits + + Unauthorized Access + + + + + + + Infiltration of organisations via third party supplier/partner + + Unauthorized Access + + + + + + + Custom recon tool to compromise and identify credentials of the network + + Theft - Credential Theft + + + + + + + Multiple means of C2 communications given the diversity of the attacker toolset + + Advantage + + + + + + + rootkit communicates during the same time as network activity, encoded with an XOR key + + Advantage + + + + + + + Kernel-centric rootkit waits for network trigger before launching + + Advantage + + + + + + + Kernel centric exfiltration over TCP/UDP/DNS/ICMP/HTTP + + Theft + + + + + + + Exfiltration over HTTP/HTTPS + + Theft + + + + + + + Use of previously undocumented functions in their Kernel centric attacks + + Advantage + + + + + + + + + + + + + + + + + Privilage Escalation Vulnerability + + CVE-2013-5065 + + + + + + The Epic Turla Campaign + The Epic Turla Campaign + + Advantage - Political + + + + + + + + + + SNAKE Campaign + The SNAKE Campaign + + Advantage - Political + + + + + + + + + + + + SNAKE + +The group behind the SNAKE campaign are a top tier nation-state threat. Their capabilities extend from subtle watering-hole attacks to sophisticated server rootkits – virtually undetectable by conventional security products. +This threat actor group has been operating continuously for over a decade, infiltrating governments and strategic private sector networks in that time. The most notorious of their early campaigns led to a breach of classified US military systems, an extensive clean-up called ‘Operation Buckshot Yankee’, and led to the creation of the US Cyber Command. +Whilst the sophisticated rootkit is used for persistent access to networks, the group also leverage more straight-forward capabilities for gaining an initial toe-hold on targets. This includes the use of watering-hole attacks and basic remote access tools. + + +The group behind the SNAKE campaign are a top tier nation-state threat. Their capabilities extend from subtle watering-hole attacks to sophisticated server rootkits – virtually undetectable by conventional security products. + + + + + + SNAKE + + + Turla + + + WRAITH + + + + + + Russia + + + Moscow + + + + + snake@gmail.com + twitter.com/snake + + + Russian + + + + + Political + + + Expert + + + Advantage - Political + + + Theft - Intellectual Property + + + + diff --git a/tests/test.py b/tests/test.py index 4732358..c2f2fb0 100755 --- a/tests/test.py +++ b/tests/test.py @@ -3,24 +3,37 @@ import unittest import requests - +import base64 +import json class TestModules(unittest.TestCase): def setUp(self): self.maxDiff = None self.headers = {'Content-Type': 'application/json'} + self.url = "http://127.0.0.1:6666/" def test_introspection(self): - response = requests.get('http://127.0.0.1:6666/modules') + response = requests.get(self.url + "modules") print(response.json()) def test_cve(self): with open('tests/bodycve.json', 'r') as f: - response = requests.post('http://127.0.0.1:6666/query', data=f.read()) + response = requests.post(self.url + "query", data=f.read()) print(response.json()) def test_dns(self): with open('tests/body.json', 'r') as f: - response = requests.post('http://127.0.0.1:6666/query', data=f.read()) + response = requests.post(self.url + "query", data=f.read()) print(response.json()) + + def test_stix(self): + with open("tests/stix.xml", "r") as f: + data = json.dumps({"module":"stiximport", + "data":str(base64.b64encode(bytes(f.read(), 'utf-8')), 'utf-8') + }) + response = requests.post(self.url + "query", data=data) + print(response.json()) + +if __name__ == '__main__': + unittest.main()