From 5b8ba53c544951055922a60c952801c17711c951 Mon Sep 17 00:00:00 2001 From: Robert Scheck Date: Fri, 11 May 2018 00:51:18 +0200 Subject: [PATCH] New option to allow tagging rather rejecting (or replacing) (#41) --- macromilter/config.ini | 4 +-- macromilter/macromilter.py | 69 +++++++++++++++++++++++++++++--------- 2 files changed, 56 insertions(+), 17 deletions(-) mode change 100644 => 100755 macromilter/macromilter.py diff --git a/macromilter/config.ini b/macromilter/config.ini index 3d93009..a549253 100644 --- a/macromilter/config.ini +++ b/macromilter/config.ini @@ -10,8 +10,8 @@ TIMEOUT = 30 MAX_FILESIZE = 50000000 # Reject error message MESSAGE = ERROR - Attachment contains unallowed office macros! -# Reject the mail if a malware macro is detected (yes/no) -REJECT_MESSAGE = yes +# Action if mail contains a malware macro (reject, replace, tag) +ACTION = reject # Max nested archive depth - recommendation = 5 MAX_ZIP = 5 diff --git a/macromilter/macromilter.py b/macromilter/macromilter.py old mode 100644 new mode 100755 index 5e1bd94..801bf61 --- a/macromilter/macromilter.py +++ b/macromilter/macromilter.py @@ -107,7 +107,17 @@ MAX_FILESIZE = config.getint('Milter', 'MAX_FILESIZE') MESSAGE = config.get('Milter', 'MESSAGE') MAX_ZIP = config.getint('Milter', 'MAX_ZIP') - REJECT_MESSAGE = config.getboolean('Milter', 'REJECT_MESSAGE') + try: + if config.getboolean('Milter', 'REJECT_MESSAGE'): + ACTION = 'reject' + else: + ACTION = 'replace' + except: + pass + try: + ACTION = config.get('Milter', 'ACTION').lower() + except: + ACTION = 'reject' LOGFILE_DIR = config.get('Logging', 'LOGFILE_DIR') LOGFILE_NAME = config.get('Logging', 'LOGFILE_NAME') LOGLEVEL = config.getint('Logging', 'LOGLEVEL') @@ -212,6 +222,8 @@ def eom(self): self.setreply('550', '5.7.1', MESSAGE) if self.sender_is_in_whitelist(msg): + if ACTION == 'tag': + self.addheader('X-MacroMilter-Status', 'Whitelisted') return Milter.ACCEPT else: return self.checkforVBA(msg) @@ -220,6 +232,8 @@ def eom(self): exc_type, exc_obj, exc_tb = sys.exc_info() fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] log.error("Unexpected error - fall back to ACCEPT: %s %s %s" % (exc_type, fname, exc_tb.tb_lineno)) + if ACTION == 'tag': + self.addheader('X-MacroMilter-Status', 'Unchecked') return Milter.ACCEPT ## ==== Data processing ==== @@ -229,7 +243,10 @@ def fileHasAlreadyBeenParsed(self, data): hash_data = hashlib.md5(data).hexdigest() # check if file is already parsed if hash_data in hashtable: - log.warning("[%d] Attachment %s already parsed: REJECT" % (self.id, hash_data)) + if ACTION == 'tag': + log.warning("[%d] Attachment %s already parsed: TAG" % (self.id, hash_data)) + else: + log.warning("[%d] Attachment %s already parsed: REJECT" % (self.id, hash_data)) return True else: return False @@ -247,6 +264,8 @@ def checkforVBA(self, msg): Checks if it contains a vba macro and checks if user is whitelisted or file already parsed ''' # Accept all messages with no attachment + if ACTION == 'tag': + self.addheader('X-MacroMilter-Status', 'Clean') result = Milter.ACCEPT try: for part in msg.walk(): @@ -265,7 +284,14 @@ def checkforVBA(self, msg): attachment_fileobj = StringIO.StringIO(attachment) # check if file was already parsed if self.fileHasAlreadyBeenParsed(attachment): - return Milter.REJECT + if ACTION == 'tag': + self.chgheader('X-MacroMilter-Status', 1, 'Suspicious Macro') + return Milter.ACCEPT + elif ACTION == 'replace': + # Known issue: https://github.com/sbidy/MacroMilter/issues/37 + return Milter.REJECT + else: + return Milter.REJECT # check if this is a supported file type (if not, just skip it) # TODO: this function should be provided by olevba if olefile.isOleFile(attachment_fileobj) or is_zipfile(attachment_fileobj) or 'http://schemas.microsoft.com/office/word/2003/wordml' in attachment \ @@ -282,10 +308,15 @@ def checkforVBA(self, msg): zipvba = self.getZipFiles(attachment, filename) vba_code_all_modules += zipvba + '\n' except ToManyZipException: - log.warning("[%d] Attachment %s is reached the max. nested zip count! ZipBomb?: REJECT" % (self.id, filename)) - # rewrite the reject message - self.setreply('550', '5.7.2', "The message contains a suspicious archive and was rejected!") - return Milter.REJECT + if ACTION == 'tag': + log.warning("[%d] Attachment %s is reached the max. nested zip count! ZipBomb?: TAG" % (self.id, filename)) + self.chgheader('X-MacroMilter-Status', 1, 'Unknown') + return Milter.ACCEPT + else: + log.warning("[%d] Attachment %s is reached the max. nested zip count! ZipBomb?: REJECT" % (self.id, filename)) + # rewrite the reject message + self.setreply('550', '5.7.2', "The message contains a suspicious archive and was rejected!") + return MILTER.REJECT # check the rest of the message vba_parser = olevba.VBA_Parser(filename='message', data=attachment) for (subfilename, stream_path, vba_filename, vba_code) in vba_parser.extract_all_macros(): @@ -297,16 +328,22 @@ def checkforVBA(self, msg): # Add MD5 to the database self.addHashtoDB(attachment) # Replace the attachment or reject it - if REJECT_MESSAGE: - log.warning('[%d] The attachment %r contains a suspicious macro: REJECT' % (self.id, filename)) - result = Milter.REJECT - else: + if ACTION == 'tag': + log.warning('[%d] The attachment %r contains a suspicious macro: TAG' % (self.id, filename)) + self.chgheader('X-MacroMilter-Status', 1, 'Suspicious Macro') + return Milter.ACCEPT + elif ACTION == 'replace': log.warning('[%d] The attachment %r contains a suspicious macro: replace it with a text file' % (self.id, filename)) part.set_payload('This attachment has been removed because it contains a suspicious macro.') part.set_type('text/plain') part.replace_header('Content-Transfer-Encoding', '7bit') + else: + log.warning('[%d] The attachment %r contains a suspicious macro: REJECT' % (self.id, filename)) + return Milter.REJECT else: log.debug('[%d] The attachment %r is clean.' % (self.id, filename)) + if ACTION == 'tag': + self.chgheader('X-MacroMilter-Status', 1, 'Macro') except Exception: log.error('[%d] Error while processing the message' % self.id) @@ -315,10 +352,12 @@ def checkforVBA(self, msg): exep = ''.join('!! ' + line for line in lines) log.debug("[%d] Exeption code: [%s]" % (self.id, exep)) - if REJECT_MESSAGE is False: - body = str(msg) - self.message = io.BytesIO(body) - self.replacebody(body) + if ACTION != 'reject': + if ACTION == 'replace': + # Known issue: https://github.com/sbidy/MacroMilter/issues/38 + body = str(msg) + self.message = io.BytesIO(body) + self.replacebody(body) log.info('[%d] Message relayed' % self.id) return result