2015-11-02 18:00:33 +01:00
|
|
|
#!/usr/bin/env python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import os
|
|
|
|
import mimetypes
|
|
|
|
import shlex
|
|
|
|
import subprocess
|
2015-11-03 15:30:59 +01:00
|
|
|
import zipfile
|
2015-11-02 18:00:33 +01:00
|
|
|
|
|
|
|
import oletools.oleid
|
|
|
|
import olefile
|
|
|
|
import officedissector
|
2015-12-10 02:26:26 +01:00
|
|
|
import warnings
|
2015-12-15 23:40:25 +01:00
|
|
|
import exifread
|
2015-12-10 02:26:26 +01:00
|
|
|
from PIL import Image
|
2016-05-16 12:25:52 +02:00
|
|
|
# from PIL import PngImagePlugin
|
2015-11-02 18:00:33 +01:00
|
|
|
from pdfid import PDFiD, cPDFiD
|
|
|
|
|
|
|
|
from kittengroomer import FileBase, KittenGroomerBase, main
|
|
|
|
|
2017-02-22 17:29:20 +01:00
|
|
|
|
2016-12-16 23:18:53 +01:00
|
|
|
SEVENZ_PATH = '/usr/bin/7z'
|
2015-11-02 18:00:33 +01:00
|
|
|
|
|
|
|
|
2017-02-22 17:29:20 +01:00
|
|
|
class Config:
|
|
|
|
# Application subtypes (mimetype: 'application/<subtype>')
|
|
|
|
mimes_ooxml = ['vnd.openxmlformats-officedocument.']
|
|
|
|
mimes_office = ['msword', 'vnd.ms-']
|
|
|
|
mimes_libreoffice = ['vnd.oasis.opendocument']
|
|
|
|
mimes_rtf = ['rtf', 'richtext']
|
|
|
|
mimes_pdf = ['pdf', 'postscript']
|
|
|
|
mimes_xml = ['xml']
|
|
|
|
mimes_ms = ['dosexec']
|
|
|
|
mimes_compressed = ['zip', 'rar', 'bzip2', 'lzip', 'lzma', 'lzop',
|
|
|
|
'xz', 'compress', 'gzip', 'tar']
|
|
|
|
mimes_data = ['octet-stream']
|
|
|
|
|
|
|
|
# Image subtypes
|
|
|
|
mimes_exif = ['image/jpeg', 'image/tiff']
|
|
|
|
mimes_png = ['image/png']
|
|
|
|
|
|
|
|
# Mimetypes with metadata
|
|
|
|
mimes_metadata = ['image/jpeg', 'image/tiff', 'image/png']
|
|
|
|
|
|
|
|
# Commonly used malicious extensions
|
|
|
|
# Sources: http://www.howtogeek.com/137270/50-file-extensions-that-are-potentially-dangerous-on-windows/
|
|
|
|
# https://github.com/wiregit/wirecode/blob/master/components/core-settings/src/main/java/org/limewire/core/settings/FilterSettings.java
|
|
|
|
malicious_exts = (
|
|
|
|
# Applications
|
|
|
|
".exe", ".pif", ".application", ".gadget", ".msi", ".msp", ".com", ".scr",
|
|
|
|
".hta", ".cpl", ".msc", ".jar",
|
|
|
|
# Scripts
|
|
|
|
".bat", ".cmd", ".vb", ".vbs", ".vbe", ".js", ".jse", ".ws", ".wsf",
|
|
|
|
".wsc", ".wsh", ".ps1", ".ps1xml", ".ps2", ".ps2xml", ".psc1", ".psc2",
|
|
|
|
".msh", ".msh1", ".msh2", ".mshxml", ".msh1xml", ".msh2xml",
|
|
|
|
# Shortcuts
|
|
|
|
".scf", ".lnk", ".inf",
|
|
|
|
# Other
|
|
|
|
".reg", ".dll",
|
|
|
|
# Office macro (OOXML with macro enabled)
|
|
|
|
".docm", ".dotm", ".xlsm", ".xltm", ".xlam", ".pptm", ".potm", ".ppam",
|
|
|
|
".ppsm", ".sldm",
|
|
|
|
# banned from wirecode
|
|
|
|
".asf", ".asx", ".au", ".htm", ".html", ".mht", ".vbs",
|
|
|
|
".wax", ".wm", ".wma", ".wmd", ".wmv", ".wmx", ".wmz", ".wvx",
|
|
|
|
)
|
|
|
|
|
|
|
|
# Aliases
|
|
|
|
aliases = {
|
|
|
|
# Win executables
|
|
|
|
'application/x-msdos-program': 'application/x-dosexec',
|
|
|
|
'application/x-dosexec': 'application/x-msdos-program',
|
|
|
|
# Other apps with confusing mimetypes
|
|
|
|
'application/rtf': 'text/rtf',
|
|
|
|
}
|
|
|
|
|
|
|
|
# Sometimes, mimetypes.guess_type gives unexpected results, such as for .tar.gz files:
|
|
|
|
# In [12]: mimetypes.guess_type('toot.tar.gz', strict=False)
|
|
|
|
# Out[12]: ('application/x-tar', 'gzip')
|
|
|
|
# It works as expected if you do mimetypes.guess_type('application/gzip', strict=False)
|
|
|
|
override_ext = {'.gz': 'application/gzip'}
|
2015-11-04 11:06:57 +01:00
|
|
|
|
2015-11-02 18:00:33 +01:00
|
|
|
|
|
|
|
class File(FileBase):
|
|
|
|
|
2017-02-21 04:38:41 +01:00
|
|
|
def __init__(self, src_path, dst_path, logger):
|
|
|
|
super(File, self).__init__(src_path, dst_path, logger)
|
2015-11-04 11:06:57 +01:00
|
|
|
self.is_recursive = False
|
2016-12-16 23:18:53 +01:00
|
|
|
self._check_dangerous()
|
|
|
|
if self.is_dangerous():
|
|
|
|
return
|
|
|
|
|
|
|
|
self.log_details.update({'maintype': self.main_type,
|
|
|
|
'subtype': self.sub_type,
|
|
|
|
'extension': self.extension})
|
|
|
|
self._check_extension()
|
|
|
|
self._check_mime()
|
|
|
|
|
|
|
|
def _check_dangerous(self):
|
2015-11-05 14:43:54 +01:00
|
|
|
if not self.has_mimetype():
|
|
|
|
# No mimetype, should not happen.
|
|
|
|
self.make_dangerous()
|
|
|
|
if not self.has_extension():
|
2015-11-04 11:06:57 +01:00
|
|
|
self.make_dangerous()
|
2017-02-22 17:29:20 +01:00
|
|
|
if self.extension in Config.malicious_exts:
|
2015-11-04 11:06:57 +01:00
|
|
|
self.log_details.update({'malicious_extension': self.extension})
|
|
|
|
self.make_dangerous()
|
2015-11-05 14:43:54 +01:00
|
|
|
|
2016-12-16 23:18:53 +01:00
|
|
|
def _check_extension(self):
|
|
|
|
"""Guesses the file's mimetype based on its extension. If the file's
|
|
|
|
mimetype (as determined by libmagic) is contained in the mimetype
|
|
|
|
module's list of valid mimetypes and the expected mimetype based on its
|
|
|
|
extension differs from the mimetype determined by libmagic, then it
|
|
|
|
marks the file as dangerous."""
|
2017-02-22 17:29:20 +01:00
|
|
|
if self.extension in Config.override_ext:
|
|
|
|
expected_mimetype = Config.override_ext[self.extension]
|
2015-11-02 18:00:33 +01:00
|
|
|
else:
|
|
|
|
expected_mimetype, encoding = mimetypes.guess_type(self.src_path, strict=False)
|
2017-02-22 17:29:20 +01:00
|
|
|
if expected_mimetype in Config.aliases:
|
|
|
|
expected_mimetype = Config.aliases[expected_mimetype]
|
2015-11-02 18:00:33 +01:00
|
|
|
is_known_extension = self.extension in mimetypes.types_map.keys()
|
2015-11-05 14:43:54 +01:00
|
|
|
if is_known_extension and expected_mimetype != self.mimetype:
|
2015-11-02 18:00:33 +01:00
|
|
|
self.log_details.update({'expected_mimetype': expected_mimetype})
|
|
|
|
self.make_dangerous()
|
|
|
|
|
2016-12-16 23:18:53 +01:00
|
|
|
def _check_mime(self):
|
|
|
|
"""Takes the mimetype (as determined by libmagic) and determines
|
|
|
|
whether the list of extensions that are normally associated with
|
|
|
|
that extension contains the file's actual extension."""
|
2017-02-22 17:29:20 +01:00
|
|
|
if self.mimetype in Config.aliases:
|
|
|
|
mimetype = Config.aliases[self.mimetype]
|
2015-11-05 14:43:54 +01:00
|
|
|
else:
|
|
|
|
mimetype = self.mimetype
|
2015-11-02 18:00:33 +01:00
|
|
|
expected_extensions = mimetypes.guess_all_extensions(mimetype, strict=False)
|
2015-11-05 10:34:03 +01:00
|
|
|
if expected_extensions:
|
2015-11-02 18:00:33 +01:00
|
|
|
if len(self.extension) > 0 and self.extension not in expected_extensions:
|
|
|
|
self.log_details.update({'expected_extensions': expected_extensions})
|
|
|
|
self.make_dangerous()
|
|
|
|
|
2015-12-15 23:13:26 +01:00
|
|
|
def has_metadata(self):
|
2017-02-22 17:29:20 +01:00
|
|
|
if self.mimetype in Config.mimes_metadata:
|
2015-12-10 02:26:26 +01:00
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2015-11-02 18:00:33 +01:00
|
|
|
|
2015-11-03 11:12:29 +01:00
|
|
|
class KittenGroomerFileCheck(KittenGroomerBase):
|
2015-11-02 18:00:33 +01:00
|
|
|
|
2017-02-11 01:27:53 +01:00
|
|
|
def __init__(self, root_src, root_dst, max_recursive_depth=2, debug=False):
|
2015-11-04 11:06:57 +01:00
|
|
|
super(KittenGroomerFileCheck, self).__init__(root_src, root_dst, debug)
|
2016-12-16 23:18:53 +01:00
|
|
|
self.recursive_archive_depth = 0
|
|
|
|
self.max_recursive_depth = max_recursive_depth
|
2017-02-21 04:38:41 +01:00
|
|
|
self.log_name = self.logger.log
|
2015-11-02 18:00:33 +01:00
|
|
|
|
|
|
|
subtypes_apps = [
|
2017-02-22 17:29:20 +01:00
|
|
|
(Config.mimes_office, self._winoffice),
|
|
|
|
(Config.mimes_ooxml, self._ooxml),
|
|
|
|
(Config.mimes_rtf, self.text),
|
|
|
|
(Config.mimes_libreoffice, self._libreoffice),
|
|
|
|
(Config.mimes_pdf, self._pdf),
|
|
|
|
(Config.mimes_xml, self.text),
|
|
|
|
(Config.mimes_ms, self._executables),
|
|
|
|
(Config.mimes_compressed, self._archive),
|
|
|
|
(Config.mimes_data, self._binary_app),
|
2015-11-02 18:00:33 +01:00
|
|
|
]
|
2017-02-22 17:29:20 +01:00
|
|
|
self.app_subtype_methods = self._make_method_dict(subtypes_apps)
|
2015-11-02 18:00:33 +01:00
|
|
|
|
2015-12-15 23:13:26 +01:00
|
|
|
types_metadata = [
|
2017-02-22 17:29:20 +01:00
|
|
|
(Config.mimes_exif, self._metadata_exif),
|
|
|
|
(Config.mimes_png, self._metadata_png),
|
2015-12-15 23:13:26 +01:00
|
|
|
]
|
2017-02-22 17:29:20 +01:00
|
|
|
self.metadata_mimetype_methods = self._make_method_dict(types_metadata)
|
2016-02-01 12:34:47 +01:00
|
|
|
|
2015-11-02 18:00:33 +01:00
|
|
|
self.mime_processing_options = {
|
|
|
|
'text': self.text,
|
|
|
|
'audio': self.audio,
|
|
|
|
'image': self.image,
|
|
|
|
'video': self.video,
|
|
|
|
'application': self.application,
|
|
|
|
'example': self.example,
|
|
|
|
'message': self.message,
|
|
|
|
'model': self.model,
|
|
|
|
'multipart': self.multipart,
|
|
|
|
'inode': self.inode,
|
|
|
|
}
|
|
|
|
|
2016-12-16 23:18:53 +01:00
|
|
|
# ##### Helper functions #####
|
2017-02-22 17:29:20 +01:00
|
|
|
def _make_method_dict(self, list_of_tuples):
|
|
|
|
"""Returns a dictionary with mimetype: method pairs."""
|
|
|
|
dict_to_return = {}
|
|
|
|
for list_of_subtypes, method in list_of_tuples:
|
|
|
|
for subtype in list_of_subtypes:
|
|
|
|
dict_to_return[subtype] = method
|
|
|
|
return dict_to_return
|
2015-11-02 18:00:33 +01:00
|
|
|
|
|
|
|
def _print_log(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Print the logs related to the current file being processed."""
|
2017-02-22 17:29:20 +01:00
|
|
|
# TODO: change name to _write_log, move to helpers
|
2017-02-21 04:38:41 +01:00
|
|
|
tmp_log = self.logger.log.fields(**self.cur_file.log_details)
|
2015-11-05 14:43:54 +01:00
|
|
|
if self.cur_file.is_dangerous():
|
2015-11-02 18:00:33 +01:00
|
|
|
tmp_log.warning(self.cur_file.log_string)
|
|
|
|
elif self.cur_file.log_details.get('unknown') or self.cur_file.log_details.get('binary'):
|
|
|
|
tmp_log.info(self.cur_file.log_string)
|
|
|
|
else:
|
|
|
|
tmp_log.debug(self.cur_file.log_string)
|
|
|
|
|
2016-12-21 20:48:07 +01:00
|
|
|
def _run_process(self, command_string, timeout=None):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Run command_string in a subprocess, wait until it finishes."""
|
|
|
|
args = shlex.split(command_string)
|
2017-02-22 17:29:20 +01:00
|
|
|
# TODO: log_debug_err and log_debug are now broken, fix, move to helpers
|
2017-02-21 04:38:41 +01:00
|
|
|
with open(self.logger.log_debug_err, 'ab') as stderr, open(self.logger.log_debug_out, 'ab') as stdout:
|
2016-12-21 20:48:07 +01:00
|
|
|
try:
|
|
|
|
subprocess.check_call(args, stdout=stdout, stderr=stderr, timeout=timeout)
|
|
|
|
except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
|
|
|
|
return
|
2015-11-02 18:00:33 +01:00
|
|
|
return True
|
|
|
|
|
|
|
|
#######################
|
2016-12-16 23:18:53 +01:00
|
|
|
# ##### Discarded mimetypes, reason in the docstring ######
|
2015-11-02 18:00:33 +01:00
|
|
|
def inode(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Empty file or symlink."""
|
2015-11-24 17:45:06 +01:00
|
|
|
if self.cur_file.is_symlink():
|
2016-12-16 19:55:15 +01:00
|
|
|
self.cur_file.log_string += 'Symlink to {}'.format(self.cur_file.log_details['symlink'])
|
2015-11-24 17:45:06 +01:00
|
|
|
else:
|
|
|
|
self.cur_file.log_string += 'Inode file'
|
2015-11-02 18:00:33 +01:00
|
|
|
|
|
|
|
def unknown(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Main type should never be unknown."""
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.log_string += 'Unknown file'
|
|
|
|
|
|
|
|
def example(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Used in examples, should never be returned by libmagic."""
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.log_string += 'Example file'
|
|
|
|
|
2016-05-17 11:50:34 +02:00
|
|
|
def multipart(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Used in web apps, should never be returned by libmagic"""
|
2016-05-17 11:50:34 +02:00
|
|
|
self.cur_file.log_string += 'Multipart file'
|
|
|
|
|
2016-12-16 23:18:53 +01:00
|
|
|
# ##### Treated as malicious, no reason to have it on a USB key ######
|
2015-11-02 18:00:33 +01:00
|
|
|
def message(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Process a message file."""
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.log_string += 'Message file'
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
self._safe_copy()
|
|
|
|
|
|
|
|
def model(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Process a model file."""
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.log_string += 'Model file'
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
self._safe_copy()
|
|
|
|
|
2016-12-16 23:18:53 +01:00
|
|
|
# ##### Files that will be converted ######
|
2015-11-02 18:00:33 +01:00
|
|
|
def text(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Process an rtf, ooxml, or plaintext file."""
|
2017-02-22 17:29:20 +01:00
|
|
|
for mt in Config.mimes_rtf:
|
|
|
|
if mt in self.cur_file.sub_type:
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.log_string += 'Rich Text file'
|
|
|
|
# TODO: need a way to convert it to plain text
|
|
|
|
self.cur_file.force_ext('.txt')
|
|
|
|
self._safe_copy()
|
2016-02-01 12:34:47 +01:00
|
|
|
return
|
2017-02-22 17:29:20 +01:00
|
|
|
for mt in Config.mimes_ooxml:
|
|
|
|
if mt in self.cur_file.sub_type:
|
2016-02-01 12:34:47 +01:00
|
|
|
self.cur_file.log_string += 'OOXML File'
|
|
|
|
self._ooxml()
|
|
|
|
return
|
|
|
|
self.cur_file.log_string += 'Text file'
|
|
|
|
self.cur_file.force_ext('.txt')
|
|
|
|
self._safe_copy()
|
2015-11-02 18:00:33 +01:00
|
|
|
|
|
|
|
def application(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Processes an application specific file according to its subtype."""
|
2017-02-22 17:29:20 +01:00
|
|
|
for subtype, method in self.app_subtype_methods.items():
|
2015-11-02 18:00:33 +01:00
|
|
|
if subtype in self.cur_file.sub_type:
|
2017-02-22 17:29:20 +01:00
|
|
|
method()
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.log_string += 'Application file'
|
|
|
|
return
|
|
|
|
self.cur_file.log_string += 'Unknown Application file'
|
|
|
|
self._unknown_app()
|
|
|
|
|
|
|
|
def _executables(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Processes an executable file."""
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.add_log_details('processing_type', 'executable')
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
self._safe_copy()
|
|
|
|
|
|
|
|
def _winoffice(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Processes a winoffice file using olefile/oletools."""
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.add_log_details('processing_type', 'WinOffice')
|
|
|
|
# Try as if it is a valid document
|
|
|
|
oid = oletools.oleid.OleID(self.cur_file.src_path)
|
|
|
|
if not olefile.isOleFile(self.cur_file.src_path):
|
|
|
|
# Manual processing, may already count as suspicious
|
|
|
|
try:
|
|
|
|
ole = olefile.OleFileIO(self.cur_file.src_path, raise_defects=olefile.DEFECT_INCORRECT)
|
|
|
|
except:
|
|
|
|
self.cur_file.add_log_details('not_parsable', True)
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
if ole.parsing_issues:
|
|
|
|
self.cur_file.add_log_details('parsing_issues', True)
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
else:
|
|
|
|
if ole.exists('macros/vba') or ole.exists('Macros') \
|
|
|
|
or ole.exists('_VBA_PROJECT_CUR') or ole.exists('VBA'):
|
|
|
|
self.cur_file.add_log_details('macro', True)
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
else:
|
|
|
|
indicators = oid.check()
|
|
|
|
# Encrypted ban be set by multiple checks on the script
|
|
|
|
if oid.encrypted.value:
|
|
|
|
self.cur_file.add_log_details('encrypted', True)
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
if oid.macros.value or oid.ole.exists('macros/vba') or oid.ole.exists('Macros') \
|
|
|
|
or oid.ole.exists('_VBA_PROJECT_CUR') or oid.ole.exists('VBA'):
|
|
|
|
self.cur_file.add_log_details('macro', True)
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
for i in indicators:
|
2016-05-14 20:44:16 +02:00
|
|
|
if i.id == 'ObjectPool' and i.value:
|
2015-11-02 18:00:33 +01:00
|
|
|
# FIXME: Is it suspicious?
|
|
|
|
self.cur_file.add_log_details('objpool', True)
|
2016-05-14 20:44:16 +02:00
|
|
|
elif i.id == 'flash' and i.value:
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.add_log_details('flash', True)
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
self._safe_copy()
|
|
|
|
|
|
|
|
def _ooxml(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Processes an ooxml file."""
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.add_log_details('processing_type', 'ooxml')
|
2016-02-01 14:19:51 +01:00
|
|
|
try:
|
|
|
|
doc = officedissector.doc.Document(self.cur_file.src_path)
|
|
|
|
except Exception:
|
|
|
|
# Invalid file
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
self._safe_copy()
|
|
|
|
return
|
2015-11-02 18:00:33 +01:00
|
|
|
# There are probably other potentially malicious features:
|
|
|
|
# fonts, custom props, custom XML
|
|
|
|
if doc.is_macro_enabled or len(doc.features.macros) > 0:
|
|
|
|
self.cur_file.add_log_details('macro', True)
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
if len(doc.features.embedded_controls) > 0:
|
|
|
|
self.cur_file.add_log_details('activex', True)
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
if len(doc.features.embedded_objects) > 0:
|
|
|
|
# Exploited by CVE-2014-4114 (OLE)
|
|
|
|
self.cur_file.add_log_details('embedded_obj', True)
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
if len(doc.features.embedded_packages) > 0:
|
|
|
|
self.cur_file.add_log_details('embedded_pack', True)
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
self._safe_copy()
|
|
|
|
|
|
|
|
def _libreoffice(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Processes a libreoffice file."""
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.add_log_details('processing_type', 'libreoffice')
|
|
|
|
# As long as there ar no way to do a sanity check on the files => dangerous
|
2015-11-03 15:30:59 +01:00
|
|
|
try:
|
|
|
|
lodoc = zipfile.ZipFile(self.cur_file.src_path, 'r')
|
|
|
|
except:
|
|
|
|
self.cur_file.add_log_details('invalid', True)
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
for f in lodoc.infolist():
|
|
|
|
fname = f.filename.lower()
|
|
|
|
if fname.startswith('script') or fname.startswith('basic') or \
|
|
|
|
fname.startswith('object') or fname.endswith('.bin'):
|
|
|
|
self.cur_file.add_log_details('macro', True)
|
|
|
|
self.cur_file.make_dangerous()
|
2015-11-02 18:00:33 +01:00
|
|
|
self._safe_copy()
|
|
|
|
|
|
|
|
def _pdf(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Processes a PDF file."""
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.add_log_details('processing_type', 'pdf')
|
|
|
|
xmlDoc = PDFiD(self.cur_file.src_path)
|
2015-11-03 13:04:14 +01:00
|
|
|
oPDFiD = cPDFiD(xmlDoc, True)
|
2015-11-02 18:00:33 +01:00
|
|
|
# TODO: other keywords?
|
2016-12-16 19:55:15 +01:00
|
|
|
if oPDFiD.encrypt.count > 0:
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.add_log_details('encrypted', True)
|
|
|
|
self.cur_file.make_dangerous()
|
2016-12-16 19:55:15 +01:00
|
|
|
if oPDFiD.js.count > 0 or oPDFiD.javascript.count > 0:
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.add_log_details('javascript', True)
|
|
|
|
self.cur_file.make_dangerous()
|
2016-12-16 19:55:15 +01:00
|
|
|
if oPDFiD.aa.count > 0 or oPDFiD.openaction.count > 0:
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.add_log_details('openaction', True)
|
|
|
|
self.cur_file.make_dangerous()
|
2016-12-16 19:55:15 +01:00
|
|
|
if oPDFiD.richmedia.count > 0:
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.add_log_details('flash', True)
|
|
|
|
self.cur_file.make_dangerous()
|
2016-12-16 19:55:15 +01:00
|
|
|
if oPDFiD.launch.count > 0:
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.add_log_details('launch', True)
|
|
|
|
self.cur_file.make_dangerous()
|
2017-03-14 10:47:20 +01:00
|
|
|
self._safe_copy()
|
2015-11-02 18:00:33 +01:00
|
|
|
|
|
|
|
def _archive(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Processes an archive using 7zip. The archive is extracted to a
|
|
|
|
temporary directory and self.processdir is called on that directory.
|
|
|
|
The recursive archive depth is increased to protect against archive
|
|
|
|
bombs."""
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.add_log_details('processing_type', 'archive')
|
|
|
|
self.cur_file.is_recursive = True
|
|
|
|
self.cur_file.log_string += 'Archive extracted, processing content.'
|
|
|
|
tmpdir = self.cur_file.dst_path + '_temp'
|
|
|
|
self._safe_mkdir(tmpdir)
|
2016-12-16 23:18:53 +01:00
|
|
|
extract_command = '{} -p1 x "{}" -o"{}" -bd -aoa'.format(SEVENZ_PATH, self.cur_file.src_path, tmpdir)
|
2015-11-02 18:00:33 +01:00
|
|
|
self._run_process(extract_command)
|
2016-12-16 23:18:53 +01:00
|
|
|
self.recursive_archive_depth += 1
|
2017-02-22 17:29:20 +01:00
|
|
|
self.logger.tree(tmpdir)
|
2015-11-02 18:00:33 +01:00
|
|
|
self.processdir(tmpdir, self.cur_file.dst_path)
|
2016-12-16 23:18:53 +01:00
|
|
|
self.recursive_archive_depth -= 1
|
2015-11-02 18:00:33 +01:00
|
|
|
self._safe_rmtree(tmpdir)
|
|
|
|
|
2016-12-16 23:18:53 +01:00
|
|
|
def _handle_archivebomb(self, src_dir):
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
self.cur_file.add_log_details('Archive Bomb', True)
|
|
|
|
self.log_name.warning('ARCHIVE BOMB.')
|
|
|
|
self.log_name.warning('The content of the archive contains recursively other archives.')
|
|
|
|
self.log_name.warning('This is a bad sign so the archive is not extracted to the destination key.')
|
|
|
|
self._safe_rmtree(src_dir)
|
|
|
|
if src_dir.endswith('_temp'):
|
|
|
|
bomb_path = src_dir[:-len('_temp')]
|
|
|
|
self._safe_remove(bomb_path)
|
|
|
|
|
2015-11-02 18:00:33 +01:00
|
|
|
def _unknown_app(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Processes an unknown file."""
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.make_unknown()
|
|
|
|
self._safe_copy()
|
|
|
|
|
|
|
|
def _binary_app(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Processses an unknown binary file."""
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.make_binary()
|
|
|
|
self._safe_copy()
|
|
|
|
|
|
|
|
#######################
|
2015-12-15 23:13:26 +01:00
|
|
|
# Metadata extractors
|
2017-02-16 23:27:00 +01:00
|
|
|
def _metadata_exif(self, metadata_file_path):
|
2015-12-15 23:40:25 +01:00
|
|
|
img = open(self.cur_file.src_path, 'rb')
|
|
|
|
tags = None
|
2016-02-01 12:34:47 +01:00
|
|
|
|
2015-12-15 23:40:25 +01:00
|
|
|
try:
|
|
|
|
tags = exifread.process_file(img, debug=True)
|
|
|
|
except Exception as e:
|
|
|
|
print("Error while trying to grab full metadata for file {}; retrying for partial data.".format(self.cur_file.src_path))
|
|
|
|
print(e)
|
2016-05-16 12:25:52 +02:00
|
|
|
if tags is None:
|
2015-12-15 23:40:25 +01:00
|
|
|
try:
|
|
|
|
tags = exifread.process_file(img, debug=True)
|
|
|
|
except Exception as e:
|
|
|
|
print("Failed to get any metadata for file {}.".format(self.cur_file.src_path))
|
|
|
|
print(e)
|
|
|
|
img.close()
|
|
|
|
return False
|
2016-02-01 12:34:47 +01:00
|
|
|
|
2015-12-15 23:40:25 +01:00
|
|
|
for tag in sorted(tags.keys()):
|
|
|
|
# These are long and obnoxious/binary
|
|
|
|
if tag not in ('JPEGThumbnail', 'TIFFThumbnail'):
|
|
|
|
printable = str(tags[tag])
|
|
|
|
|
2016-05-16 12:25:52 +02:00
|
|
|
# Exifreader truncates data.
|
2015-12-15 23:40:25 +01:00
|
|
|
if len(printable) > 25 and printable.endswith(", ... ]"):
|
|
|
|
value = tags[tag].values
|
2016-12-16 19:55:15 +01:00
|
|
|
if isinstance(value, str):
|
2015-12-15 23:40:25 +01:00
|
|
|
printable = value
|
|
|
|
else:
|
|
|
|
printable = str(value)
|
2017-02-16 23:27:00 +01:00
|
|
|
|
|
|
|
with open(metadata_file_path, 'w+') as metadata_file:
|
|
|
|
metadata_file.write("Key: {}\tValue: {}\n".format(tag, printable))
|
2015-12-15 23:13:26 +01:00
|
|
|
self.cur_file.add_log_details('metadata', 'exif')
|
|
|
|
img.close()
|
2015-12-15 23:40:25 +01:00
|
|
|
return True
|
2015-12-15 23:13:26 +01:00
|
|
|
|
2017-02-16 23:27:00 +01:00
|
|
|
def _metadata_png(self, metadata_file_path):
|
2015-12-15 23:40:25 +01:00
|
|
|
warnings.simplefilter('error', Image.DecompressionBombWarning)
|
|
|
|
try:
|
|
|
|
img = Image.open(self.cur_file.src_path)
|
|
|
|
for tag in sorted(img.info.keys()):
|
|
|
|
# These are long and obnoxious/binary
|
|
|
|
if tag not in ('icc_profile'):
|
2017-02-16 23:27:00 +01:00
|
|
|
with open(metadata_file_path, 'w+') as metadata_file:
|
|
|
|
metadata_file.write("Key: {}\tValue: {}\n".format(tag, img.info[tag]))
|
2015-12-15 23:40:25 +01:00
|
|
|
self.cur_file.add_log_details('metadata', 'png')
|
|
|
|
img.close()
|
|
|
|
# Catch decompression bombs
|
|
|
|
except Exception as e:
|
|
|
|
print("Caught exception processing metadata for {}".format(self.cur_file.src_path))
|
|
|
|
print(e)
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
self._safe_copy()
|
|
|
|
return False
|
2015-12-15 23:13:26 +01:00
|
|
|
|
|
|
|
def extract_metadata(self):
|
2017-02-16 23:27:00 +01:00
|
|
|
metadata_file_path = self.cur_file.create_metadata_file(".metadata.txt")
|
2017-02-22 17:29:20 +01:00
|
|
|
mt = self.cur_file.mimetype
|
|
|
|
metadata_processing_method = self.metadata_mimetype_methods.get(mt)
|
2017-02-16 23:27:00 +01:00
|
|
|
if metadata_processing_method:
|
2017-02-22 17:29:20 +01:00
|
|
|
# TODO: should we return metadata and write it here instead of in processing method?
|
2017-02-16 23:27:00 +01:00
|
|
|
metadata_processing_method(metadata_file_path)
|
2015-11-02 18:00:33 +01:00
|
|
|
|
2015-12-15 23:13:26 +01:00
|
|
|
#######################
|
2016-12-16 23:18:53 +01:00
|
|
|
# ##### Media - audio and video aren't converted ######
|
2015-11-02 18:00:33 +01:00
|
|
|
def audio(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Processes an audio file."""
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.log_string += 'Audio file'
|
|
|
|
self._media_processing()
|
|
|
|
|
2016-12-16 23:18:53 +01:00
|
|
|
def video(self):
|
|
|
|
"""Processes a video."""
|
|
|
|
self.cur_file.log_string += 'Video file'
|
|
|
|
self._media_processing()
|
|
|
|
|
|
|
|
def _media_processing(self):
|
|
|
|
"""Generic way to process all media files."""
|
|
|
|
self.cur_file.add_log_details('processing_type', 'media')
|
|
|
|
self._safe_copy()
|
|
|
|
|
2015-11-02 18:00:33 +01:00
|
|
|
def image(self):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Processes an image.
|
|
|
|
|
|
|
|
Extracts metadata if metadata is present. Creates a temporary
|
|
|
|
directory, opens the using PIL.Image, saves it to the temporary
|
|
|
|
directory, and copies it to the destination."""
|
2015-12-15 23:13:26 +01:00
|
|
|
if self.cur_file.has_metadata():
|
|
|
|
self.extract_metadata()
|
|
|
|
|
2016-05-16 12:25:52 +02:00
|
|
|
# FIXME make sure this works for png, gif, tiff
|
2015-12-10 02:26:26 +01:00
|
|
|
# Create a temp directory
|
|
|
|
dst_dir, filename = os.path.split(self.cur_file.dst_path)
|
|
|
|
tmpdir = os.path.join(dst_dir, 'temp')
|
|
|
|
tmppath = os.path.join(tmpdir, filename)
|
|
|
|
self._safe_mkdir(tmpdir)
|
|
|
|
|
|
|
|
# Do our image conversions
|
|
|
|
warnings.simplefilter('error', Image.DecompressionBombWarning)
|
2015-12-15 23:13:26 +01:00
|
|
|
try:
|
|
|
|
imIn = Image.open(self.cur_file.src_path)
|
|
|
|
imOut = Image.frombytes(imIn.mode, imIn.size, imIn.tobytes())
|
|
|
|
imOut.save(tmppath)
|
2015-12-10 02:26:26 +01:00
|
|
|
|
2016-05-16 12:25:52 +02:00
|
|
|
# Copy the file back out and cleanup
|
2015-12-15 23:13:26 +01:00
|
|
|
self._safe_copy(tmppath)
|
|
|
|
self._safe_rmtree(tmpdir)
|
2016-02-01 12:34:47 +01:00
|
|
|
|
2015-12-15 23:13:26 +01:00
|
|
|
# Catch decompression bombs
|
|
|
|
except Exception as e:
|
2015-12-15 23:40:25 +01:00
|
|
|
print("Caught exception (possible decompression bomb?) while translating file {}.".format(self.cur_file.src_path))
|
2015-12-15 23:13:26 +01:00
|
|
|
print(e)
|
|
|
|
self.cur_file.make_dangerous()
|
|
|
|
self._safe_copy()
|
2015-12-10 02:26:26 +01:00
|
|
|
|
2015-11-02 18:00:33 +01:00
|
|
|
self.cur_file.log_string += 'Image file'
|
2015-12-10 02:26:26 +01:00
|
|
|
self.cur_file.add_log_details('processing_type', 'image')
|
|
|
|
|
2015-11-02 18:00:33 +01:00
|
|
|
#######################
|
|
|
|
|
2016-12-22 03:24:29 +01:00
|
|
|
def process_file(self, srcpath, dstpath, relative_path):
|
2017-02-21 04:38:41 +01:00
|
|
|
self.cur_file = File(srcpath, dstpath, self.logger)
|
2016-12-22 03:24:29 +01:00
|
|
|
self.log_name.info('Processing {} ({}/{})',
|
|
|
|
relative_path,
|
|
|
|
self.cur_file.main_type,
|
|
|
|
self.cur_file.sub_type)
|
|
|
|
if not self.cur_file.is_dangerous():
|
|
|
|
self.mime_processing_options.get(self.cur_file.main_type, self.unknown)()
|
|
|
|
else:
|
|
|
|
self._safe_copy()
|
|
|
|
if not self.cur_file.is_recursive:
|
|
|
|
self._print_log()
|
|
|
|
|
2015-11-02 18:00:33 +01:00
|
|
|
def processdir(self, src_dir=None, dst_dir=None):
|
2016-12-16 23:18:53 +01:00
|
|
|
"""Main function coordinating file processing."""
|
2017-02-22 17:29:20 +01:00
|
|
|
# TODO: do we need defaults here?
|
2015-11-02 18:00:33 +01:00
|
|
|
if src_dir is None:
|
|
|
|
src_dir = self.src_root_dir
|
|
|
|
if dst_dir is None:
|
|
|
|
dst_dir = self.dst_root_dir
|
|
|
|
|
2016-12-16 23:18:53 +01:00
|
|
|
if self.recursive_archive_depth > 0:
|
2015-11-02 18:00:33 +01:00
|
|
|
self._print_log()
|
|
|
|
|
2016-12-16 23:18:53 +01:00
|
|
|
if self.recursive_archive_depth >= self.max_recursive_depth:
|
|
|
|
self._handle_archivebomb(src_dir)
|
2015-11-02 18:00:33 +01:00
|
|
|
|
|
|
|
for srcpath in self._list_all_files(src_dir):
|
2016-12-22 03:24:29 +01:00
|
|
|
dstpath = srcpath.replace(src_dir, dst_dir)
|
|
|
|
relative_path = srcpath.replace(src_dir + '/', '')
|
|
|
|
# which path do we want in the log?
|
|
|
|
self.process_file(srcpath, dstpath, relative_path)
|
2015-11-02 18:00:33 +01:00
|
|
|
|
2016-12-16 23:18:53 +01:00
|
|
|
|
2015-11-02 18:00:33 +01:00
|
|
|
if __name__ == '__main__':
|
2016-12-16 23:18:53 +01:00
|
|
|
main(KittenGroomerFileCheck, 'File sanitizer used in CIRCLean. Renames potentially dangerous files.')
|