def check_mraptor(self): ''' Check the attachments of a message using mraptor. If an attachment is identified as suspicious, it is replaced by a simple text file. :return: Milter.ACCEPT or Milter.DISCARD if processing error ''' msg = email.message_from_string(self.message.getvalue()) result = Milter.ACCEPT try: for part in msg.walk(): # for name, value in part.items(): # log.debug(' - %s: %r' % (name, value)) content_type = part.get_content_type() log.debug('[%d] Content-type: %r' % (self.id, content_type)) # TODO: handle any content-type, but check the file magic? if not content_type.startswith('multipart'): filename = part.get_filename(None) log.debug('[%d] Analyzing attachment %r' % (self.id, filename)) attachment = part.get_payload(decode=True) attachment_lowercase = attachment.lower() # check if this is a supported file type (if not, just skip it) # TODO: this function should be provided by olevba if attachment.startswith(olevba.olefile.MAGIC) \ or is_zipfile(StringIO.StringIO(attachment)) \ or 'http://schemas.microsoft.com/office/word/2003/wordml' in attachment \ or ('mime' in attachment_lowercase and 'version' in attachment_lowercase and 'multipart' in attachment_lowercase): vba_parser = olevba.VBA_Parser(filename='message', data=attachment) vba_code_all_modules = '' for (subfilename, stream_path, vba_filename, vba_code) in vba_parser.extract_all_macros(): vba_code_all_modules += vba_code + '\n' m = mraptor.MacroRaptor(vba_code_all_modules) m.scan() if m.suspicious: 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') # TODO: handle case when CTE is absent part.replace_header('Content-Transfer-Encoding', '7bit') # for name, value in part.items(): # log.debug(' - %s: %r' % (name, value)) # TODO: archive filtered e-mail to a file else: log.debug('The attachment %r is clean.' % filename) except Exception: log.exception('[%d] Error while processing the message' % self.id) # TODO: depending on error, decide to forward the e-mail as-is or not result = Milter.DISCARD # TODO: only do this if the body has actually changed body = str(msg) self.message = io.BytesIO(body) self.replacebody(body) log.info('[%d] Message relayed' % self.id) return result
def check_mraptor(self): ''' Check the attachments of a message using mraptor. If an attachment is identified as suspicious, it is replaced by a simple text file. :return: Milter.ACCEPT or Milter.DISCARD if processing error ''' msg = email.message_from_string(self.message.getvalue()) result = Milter.ACCEPT try: for part in msg.walk(): # for name, value in part.items(): # log.debug(' - %s: %r' % (name, value)) content_type = part.get_content_type() log.debug('[%d] Content-type: %r' % (self.id, content_type)) # TODO: handle any content-type, but check the file magic? if not content_type.startswith('multipart'): filename = part.get_filename(None) log.debug('[%d] Analyzing attachment %r' % (self.id, filename)) attachment = part.get_payload(decode=True) attachment_lowercase = attachment.lower() # check if this is a supported file type (if not, just skip it) # TODO: this function should be provided by olevba if attachment.startswith(olevba.olefile.MAGIC) \ or is_zipfile(StringIO.StringIO(attachment)) \ or 'http://schemas.microsoft.com/office/word/2003/wordml' in attachment \ or ('mime' in attachment_lowercase and 'version' in attachment_lowercase and 'multipart' in attachment_lowercase): vba_parser = olevba.VBA_Parser(filename='message', data=attachment) vba_code_all_modules = '' for (subfilename, stream_path, vba_filename, vba_code) in vba_parser.extract_all_macros(): vba_code_all_modules += vba_code + '\n' m = mraptor.MacroRaptor(vba_code_all_modules) m.scan() if m.suspicious: 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') # TODO: handle case when CTE is absent part.replace_header('Content-Transfer-Encoding', '7bit') # for name, value in part.items(): # log.debug(' - %s: %r' % (name, value)) # TODO: archive filtered e-mail to a file else: log.debug('The attachment %r is clean.' % filename) except Exception: log.exception('[%d] Error while processing the message' % self.id) # TODO: depending on error, decide to forward the e-mail as-is or not result = Milter.DISCARD # TODO: only do this if the body has actually changed body = str(msg) self.message = io.BytesIO(body) self.replacebody(body) log.info('[%d] Message relayed' % self.id) return result
def zipwalk(self, zfilename, count, tmpfiles): z = ZipFile(zfilename, 'r') # start walk for info in z.infolist(): fname = info.filename data = z.read(fname) extn = (os.path.splitext(fname)[1]).lower() # create a random secure temp file tmp_fs, tmpfpath = tempfile.mkstemp(suffix=extn) # add tmp filename to list tmpfiles.append(tmpfpath) if extn == '.zip' or extn == '.7z': checkz = False # use a context manager to open the file with open(tmpfpath, 'w') as f: f.write(data) if is_zipfile(tmpfpath): checkz = True count = count + 1 # check each round if count > MAX_ZIP: self.deleteFileRecursive(tmpfiles) tmpfiles = [] raise ToManyZipException( "[%d] Too many nested zips found - possible zipbomb!" % self.id) if checkz and not olefile.isOleFile(data): try: # recursive call if nested for x in self.zipwalk(tmpfpath, count, tmpfiles): yield x except Exception: self.deleteFileRecursive(tmpfiles) tmpfiles = [] raise else: # return the generator yield (info, data) # cleanup tmp self.deleteFileRecursive(tmpfiles) tmpfiles = []
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 result = Milter.ACCEPT try: for part in msg.walk(): # for name, value in part.items(): # log.debug(' - %s: %r' % (name, value)) content_type = part.get_content_type() log.debug('[%d] Content-Type: %r' % (self.id, content_type)) # TODO: handle any content-type, but check the file magic? if not content_type.startswith('multipart'): filename = part.get_filename(None) attachment = part.get_payload(decode=True) if attachment is None: return Milter.CONTINUE log.debug('[%d] Analyzing attachment: %r' % (self.id, filename)) attachment_lowercase = attachment.lower() attachment_fileobj = StringIO.StringIO(attachment) # check if file was already parsed if self.fileHasAlreadyBeenParsed(attachment): 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 \ or ('mime' in attachment_lowercase and 'version' in attachment_lowercase \ and 'multipart' in attachment_lowercase): vba_code_all_modules = '' # check if the attachment is a zip if not olefile.isOleFile(attachment_fileobj): extn = (os.path.splitext(filename)[1]).lower() # skip non archives if is_zipfile(attachment_fileobj) and not ( ".docx" in extn or ".xlsx" in extn or ".pptx" in extn): # extract all file in zip and add try: 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 # 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(): vba_code_all_modules += vba_code + '\n' # run the mraptor m = mraptor.MacroRaptor(vba_code_all_modules) m.scan() if m.suspicious: # 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: 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.debug('[%d] The attachment %r is clean.' % (self.id, filename)) except Exception: log.error('[%d] Error while processing the message' % self.id) exc_type, exc_value, exc_traceback = sys.exc_info() lines = traceback.format_exception(exc_type, exc_value, exc_traceback) 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) log.info('[%d] Message relayed' % self.id) return result