mail_to_misp/mail_to_misp.py

331 lines
11 KiB
Python
Raw Normal View History

2017-06-01 17:00:32 +02:00
#!/usr/bin/env python3
2017-05-24 11:02:28 +02:00
# -*- coding: utf-8 -*-
2017-04-27 13:58:49 +02:00
2017-05-31 16:47:08 +02:00
import os
2017-06-01 09:02:11 +02:00
import sys
try:
configfile = os.path.basename(sys.argv[0]).split(".py")[0] + "_config"
except Exception as e:
print("Couldn't locate config file {0}".format(configfile))
sys.exit(-1)
try:
import urlmarker
import hashmarker
import re
from pyfaup.faup import Faup
2017-12-14 11:27:48 +01:00
from pymisp import PyMISP, MISPEvent
2017-06-01 09:02:11 +02:00
from defang import refang
import dns.resolver
import email
from email.generator import Generator
import tempfile
import socket
import syslog
import ftfy
2017-12-20 16:08:27 +01:00
import hashlib
2017-06-01 09:02:11 +02:00
config = __import__(configfile)
except ImportError as e:
print("(!) Problem loading module:")
print(e)
sys.exit(-1)
2017-05-30 11:24:30 +02:00
syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_USER)
2017-05-24 15:44:14 +02:00
def is_valid_ipv4_address(address):
try:
socket.inet_pton(socket.AF_INET, address)
except AttributeError: # no inet_pton here, sorry
try:
socket.inet_aton(address)
except socket.error:
return False
return address.count('.') == 3
except socket.error: # not a valid address
return False
return True
def is_valid_ipv6_address(address):
try:
socket.inet_pton(socket.AF_INET6, address)
except socket.error: # not a valid address
return False
return True
2017-04-27 13:58:49 +02:00
debug = config.debug
2017-05-23 15:09:25 +02:00
stdin_used = False
2017-05-30 11:24:30 +02:00
email_subject = config.email_subject_prefix
2017-06-02 06:58:05 +02:00
mail_subject = ""
2017-06-01 18:39:39 +02:00
#try:
#if not sys.stdin.isatty():
if len(sys.argv) == 1:
mailcontent = sys.stdin.buffer.read().decode("utf-8", "ignore")
else:
# read from tempfile
if sys.argv[1] == "-r":
tempfilename = sys.argv[2]
tf = open(tempfilename, 'r')
mailcontent = tf.read()
tf.close()
# receive data and subject through arguments
else:
mailcontent = sys.argv[1]
syslog.syslog(mailcontent)
if len(sys.argv) >= 3:
mail_subject = sys.argv[2].encode("utf-8", "ignore")
2017-06-01 18:39:39 +02:00
email_data = b''
msg = email.message_from_string(mailcontent)
if not mail_subject:
try:
mail_subject = msg.get('Subject').encode("utf-8", "ignore")
except:
pass
for part in msg.walk():
if part.get_content_charset() is None:
# This could probably be detected
charset = 'utf-8'
else:
charset = part.get_content_charset()
if part.get_content_maintype() == 'multipart':
continue
if part.get_content_maintype() == 'text':
part.set_charset(charset)
2017-06-01 18:45:31 +02:00
syslog.syslog(str(part.get_payload(decode=True)))
2017-06-01 18:39:39 +02:00
email_data += part.get_payload(decode=True)
2017-06-02 07:32:51 +02:00
try:
email_subject += mail_subject
except Exception as e:
syslog.syslog(str(e))
2017-06-01 18:39:39 +02:00
stdin_used = True
2017-04-27 13:58:49 +02:00
2017-06-01 12:16:32 +02:00
#if debug:
# syslog.syslog("Encoding of subject: {0}".format(ftfy.guess_bytes(email_subject)[1]))
# syslog.syslog("Encoding of body: {0}".format(ftfy.guess_bytes(email_data)[1]))
2017-06-01 12:16:32 +02:00
try:
email_data = ftfy.fix_text(email_data.decode("utf-8", "ignore"))
except:
email_data = ftfy.fix_text(email_data)
try:
email_subject = ftfy.fix_text(email_subject.decode("utf-8", "ignore"))
except:
email_subject = ftfy.fix_text(email_subject)
2017-04-27 13:58:49 +02:00
if debug:
syslog.syslog(email_subject)
syslog.syslog(email_data)
2017-04-27 13:58:49 +02:00
misp_url = config.misp_url
misp_key = config.misp_key
misp_verifycert = config.misp_verifycert
resolver = dns.resolver.Resolver(configure=False)
resolver.nameservers = config.nameservers
excludelist = config.excludelist
externallist = config.externallist
2017-05-29 17:06:46 +02:00
internallist = config.internallist
2017-04-28 10:00:58 +02:00
noidsflaglist = config.noidsflaglist
ignorelist = config.ignorelist
enforcewarninglist = config.enforcewarninglist
2017-12-20 16:08:27 +01:00
sighting = config.sighting
removelist = config.removelist
2017-04-27 13:58:49 +02:00
malwaretags = config.malwaretags
dependingtags = config.dependingtags
tlptag_default = config.tlptag_default
2017-05-17 09:51:47 +02:00
stopword = config.stopword
hash_only_tags = config.hash_only_tags
2017-05-29 15:35:56 +02:00
forward_identifiers = config.forward_identifiers
2017-05-31 15:45:53 +02:00
attach_original_mail = config.attach_original_mail
2017-04-27 13:58:49 +02:00
original_email_data = email_data
2017-04-27 13:58:49 +02:00
# Ignore lines in body of message
for ignoreline in ignorelist:
email_data = re.sub(ignoreline, "", email_data)
2017-04-27 13:58:49 +02:00
# Remove words from subject
for removeword in removelist:
email_subject = re.sub(removeword, "", email_subject)
2017-12-14 11:27:48 +01:00
if debug:
import logging
logger = logging.getLogger('pymisp').setLevel(logging.DEBUG)
2017-12-14 11:27:48 +01:00
logging.basicConfig(level=logging.DEBUG, filename="/tmp/mail_to_misp_debug.log", filemode='w')
2017-04-27 13:58:49 +02:00
def init(url, key):
return PyMISP(url, key, misp_verifycert, 'json', debug=None)
2017-12-14 11:27:48 +01:00
2017-12-20 16:08:27 +01:00
def sight(sighting, value):
if sighting:
d = {'value': value}
misp.set_sightings(d)
2017-04-27 13:58:49 +02:00
# Evaluate classification
tlp_tag = tlptag_default
2017-04-27 13:58:49 +02:00
tlptags = config.tlptags
for tag in tlptags:
for alternativetag in tlptags[tag]:
if alternativetag in email_data.lower():
2017-04-27 13:58:49 +02:00
tlp_tag = tag
# Create the MISP event
misp = init(misp_url, misp_key)
new_event = misp.new_event(info=email_subject, distribution=0, threat_level_id=3, analysis=1)
2017-12-14 11:27:48 +01:00
misp_event = MISPEvent()
misp_event.load(new_event)
misp.tag(misp_event.uuid, tlp_tag)
2017-04-27 13:58:49 +02:00
2017-05-31 15:45:53 +02:00
if attach_original_mail and original_email_data:
2017-12-20 14:38:11 +01:00
misp.add_named_attribute(new_event, 'email-body', original_email_data, category='Payload delivery',
to_ids=False, enforceWarninglist=enforcewarninglist)
2017-04-27 13:58:49 +02:00
# Add additional tags depending on others
for tag in dependingtags:
if tag in tlp_tag:
for dependingtag in dependingtags[tag]:
2017-12-14 11:27:48 +01:00
misp.tag(misp_event.uuid, dependingtag)
2017-04-27 13:58:49 +02:00
2017-05-29 15:35:56 +02:00
## Prepare extraction of IOCs
# Limit the input if the stopword is found
2017-05-17 09:51:47 +02:00
email_data = email_data.split(stopword, 1)[0]
2017-05-29 15:35:56 +02:00
# Find the first forwarding message and use that content
position = 99999
t_email_data = email_data
for identifier in forward_identifiers:
new_position = email_data.find(identifier)
2017-05-29 17:06:46 +02:00
if new_position == -1:
new_position = position
2017-05-29 15:35:56 +02:00
if new_position < position:
t_before, t_split, t_email_data = email_data.partition(identifier)
position = new_position
email_data = t_email_data
# Refang email data
email_data = refang(email_data)
2017-05-29 15:35:56 +02:00
## Extract various IOCs
2017-05-24 15:44:14 +02:00
urllist = list()
urllist += re.findall(urlmarker.WEB_URL_REGEX, email_data)
2017-04-28 10:00:58 +02:00
urllist += re.findall(urlmarker.IP_REGEX, email_data)
2017-04-27 13:58:49 +02:00
if debug:
2017-05-30 11:24:30 +02:00
syslog.syslog(str(urllist))
2017-04-27 13:58:49 +02:00
# Init Faup
f = Faup()
2017-04-27 17:50:50 +02:00
# Add tags according to configuration
2017-04-27 13:58:49 +02:00
for malware in malwaretags:
if malware in email_subject.lower():
2017-04-27 13:58:49 +02:00
for tag in malwaretags[malware]:
misp.tag(misp_event.uuid, tag)
2017-04-27 13:58:49 +02:00
2017-04-28 10:00:58 +02:00
# Extract and add hashes
hashlist_md5 = re.findall(hashmarker.MD5_REGEX, email_data)
hashlist_sha1 = re.findall(hashmarker.SHA1_REGEX, email_data)
hashlist_sha256 = re.findall(hashmarker.SHA256_REGEX, email_data)
for h in hashlist_md5:
misp.add_named_attribute(new_event, 'md5', h, to_ids=True, enforceWarninglist=enforcewarninglist)
2017-12-20 16:08:27 +01:00
sight(sighting, h)
2017-04-28 10:00:58 +02:00
for h in hashlist_sha1:
misp.add_named_attribute(new_event, 'sha1', h, to_ids=True, enforceWarninglist=enforcewarninglist)
2017-12-20 16:08:27 +01:00
sight(sighting, h)
2017-04-28 10:00:58 +02:00
for h in hashlist_sha256:
misp.add_named_attribute(new_event, 'sha256', h, to_ids=True, enforceWarninglist=enforcewarninglist)
2017-12-20 16:08:27 +01:00
sight(sighting, h)
2017-04-28 10:00:58 +02:00
if (len(hashlist_md5) > 0) or (len(hashlist_sha1) > 0) or (len(hashlist_sha256) > 0):
for tag in hash_only_tags:
misp.tag(misp_event.uuid, tag)
2017-04-27 13:58:49 +02:00
# Add IOCs and expanded information to MISP
for entry in urllist:
2017-04-28 10:00:58 +02:00
ids_flag = True
2017-04-27 13:58:49 +02:00
f.decode(entry)
domainname = f.get_domain().decode('utf-8', 'ignore')
hostname = f.get_host().decode('utf-8', 'ignore')
2017-06-16 09:59:46 +02:00
try:
schema = f.get_scheme().decode('utf-8', 'ignore')
except:
schema = False
2017-04-27 13:58:49 +02:00
if debug:
syslog.syslog(domainname)
2017-04-27 13:58:49 +02:00
if domainname not in excludelist:
2017-05-29 17:06:46 +02:00
if domainname in internallist:
misp.add_named_attribute(new_event, 'link', entry, category='Internal reference',
to_ids=False, distribution=0, enforceWarninglist=enforcewarninglist)
2017-12-20 16:08:27 +01:00
sight(sighting, entry)
2017-05-29 17:06:46 +02:00
elif domainname in externallist:
misp.add_named_attribute(new_event, 'link', entry, category='External analysis',
to_ids=False, enforceWarninglist=enforcewarninglist)
2017-12-20 16:08:27 +01:00
sight(sighting, entry)
2017-04-27 13:58:49 +02:00
else:
2017-12-14 11:27:48 +01:00
comment = ""
2017-04-28 10:00:58 +02:00
if (domainname in noidsflaglist) or (hostname in noidsflaglist):
ids_flag = False
2017-12-14 11:27:48 +01:00
comment = "Known host (mostly for connectivity test or IP lookup)"
2017-04-27 13:58:49 +02:00
if debug:
2017-05-30 11:24:30 +02:00
syslog.syslog(str(entry))
2017-05-24 15:44:14 +02:00
if hostname:
2017-06-16 09:59:46 +02:00
if schema:
if is_valid_ipv4_address(hostname):
misp.add_named_attribute(new_event, 'url', entry, category='Network activity',
to_ids=False, enforceWarninglist=enforcewarninglist)
2017-06-16 09:59:46 +02:00
else:
misp.add_named_attribute(new_event, 'url', entry, category='Network activity',
to_ids=ids_flag, enforceWarninglist=enforcewarninglist)
2017-12-20 16:08:27 +01:00
sight(sighting, entry)
2017-04-27 13:58:49 +02:00
if debug:
syslog.syslog(hostname)
2017-12-14 11:27:48 +01:00
try:
port = f.get_port().decode('utf-8', 'ignore')
except:
port = None
2017-05-24 15:44:14 +02:00
comment = ""
if port:
2017-12-14 11:27:48 +01:00
comment = "on port: " + port
if is_valid_ipv4_address(hostname):
misp.add_named_attribute(new_event, 'ip-dst', hostname, comment=comment, category='Network activity',
to_ids=False, enforceWarninglist=enforcewarninglist)
2017-12-20 16:08:27 +01:00
sight(sighting, hostname)
2017-05-24 15:44:14 +02:00
else:
misp.add_named_attribute(new_event, 'hostname', hostname, comment=comment, category='Network activity',
to_ids=ids_flag, enforceWarninglist=enforcewarninglist)
2017-12-20 16:08:27 +01:00
sight(sighting, hostname)
2017-05-24 15:44:14 +02:00
try:
for rdata in dns.resolver.query(hostname, 'A'):
2017-05-24 15:44:14 +02:00
if debug:
2017-05-30 11:24:30 +02:00
syslog.syslog(str(rdata))
misp.add_named_attribute(new_event, 'ip-dst', rdata.to_text(), comment=hostname,
category='Network activity', to_ids=False,
enforceWarninglist=enforcewarninglist)
2017-12-20 16:08:27 +01:00
sight(sighting, rdata.to_text())
2017-05-24 15:44:14 +02:00
except Exception as e:
if debug:
2017-05-31 16:47:08 +02:00
syslog.syslog(str(e))
2017-05-24 11:02:28 +02:00
# Try to add attachments
if stdin_used:
for part in msg.walk():
if part.get_content_maintype() == 'multipart':
continue
if part.get_content_maintype() != 'text' and part.get_payload(decode=True) is not None:
2017-05-24 11:02:28 +02:00
filename = part.get_filename()
_, output_path = tempfile.mkstemp()
output = open(output_path, 'wb')
output.write(part.get_payload(decode=True))
2017-12-14 11:27:48 +01:00
attachment = part.get_payload(decode=True)
event_id = misp_event.id
misp.upload_sample(filename, output_path, event_id, distribution=None, to_ids=True)
2017-12-20 16:08:27 +01:00
file_hash = hashlib.sha256(open(output_path, 'rb').read()).hexdigest()
sight(sighting, file_hash)
2017-05-24 11:02:28 +02:00
output.close()
2017-12-14 11:27:48 +01:00
if debug:
syslog.syslog("Job finished.")