diff --git a/oletools/crypto.py b/oletools/crypto.py index 9079ae9..df49d7e 100644 --- a/oletools/crypto.py +++ b/oletools/crypto.py @@ -33,7 +33,7 @@ potentially encrypted files:: raise crypto.MaxCryptoNestingReached(crypto_nesting, filename) decrypted_file = None try: - decrypted_file = crypto.decrypt(input_file, passwords) + decrypted_file, correct_password = crypto.decrypt(input_file, passwords, decrypted_filepath) if decrypted_file is None: raise crypto.WrongEncryptionPassword(input_file) # might still be encrypted, so call this again recursively @@ -102,6 +102,7 @@ __version__ = '0.60' import sys import struct import os +import shutil from os.path import splitext, isfile from tempfile import mkstemp import zipfile @@ -314,7 +315,7 @@ def check_msoffcrypto(): return msoffcrypto is not None -def decrypt(filename, passwords=None, **temp_file_args): +def decrypt(filename, passwords=None, decrypted_filepath=None, **temp_file_args): """ Try to decrypt an encrypted file @@ -331,7 +332,10 @@ def decrypt(filename, passwords=None, **temp_file_args): `dirname` or `prefix`. `suffix` will default to suffix of input `filename`, `prefix` defaults to `oletools-decrypt-`; `text` will be ignored - :returns: name of the decrypted temporary file (type str) or `None` + :param decrypted_filepath: filepath of the decrypted file in case you want to + preserve it + :returns: a tuple with the name of the decrypted temporary file (type str) or `None` + and the correct password or 'None' :raises: :py:class:`ImportError` if :py:mod:`msoffcrypto-tools` not found :raises: :py:class:`ValueError` if the given file is not encrypted """ @@ -370,6 +374,7 @@ def decrypt(filename, passwords=None, **temp_file_args): raise ValueError('Given input file {} is not encrypted!' .format(filename)) + correct_password = None for password in passwords: log.debug('Trying to decrypt with password {!r}'.format(password)) write_descriptor = None @@ -387,6 +392,7 @@ def decrypt(filename, passwords=None, **temp_file_args): # decryption was successfull; clean up and return write_handle.close() write_handle = None + correct_password = password break except Exception: log.debug('Failed to decrypt', exc_info=True) @@ -399,6 +405,15 @@ def decrypt(filename, passwords=None, **temp_file_args): if decrypt_file and isfile(decrypt_file): os.unlink(decrypt_file) decrypt_file = None - # if we reach this, all passwords were tried without success - log.debug('All passwords failed') - return decrypt_file + correct_password = None + + if decrypt_file and correct_password: + log.debug(f'Successfully decrypted the file with password: {correct_password}') + if decrypted_filepath: + if os.path.isdir(decrypted_filepath) and os.access(decrypted_filepath, os.W_OK): + log.info(f"Saving decrypted file in: {decrypted_filepath}") + shutil.copy(decrypt_file, decrypted_filepath) + else: + log.info('All passwords failed') + + return decrypt_file, correct_password diff --git a/oletools/msodde.py b/oletools/msodde.py index 303d974..2047fb9 100644 --- a/oletools/msodde.py +++ b/oletools/msodde.py @@ -271,6 +271,9 @@ def process_args(cmd_line_args=None): parser.add_argument("-p", "--password", type=str, action='append', help='if encrypted office files are encountered, try ' 'decryption with this password. May be repeated.') + parser.add_argument("--decrypted_filepath", dest='decrypted_filepath', type=str, + default=None, + help='save the decrypted file to this location.') filter_group = parser.add_argument_group( title='Filter which OpenXML field commands are returned', description='Only applies to OpenXML (e.g. docx) and rtf, not to OLE ' @@ -910,7 +913,7 @@ def process_file(filepath, field_filter_mode=None): # === MAIN ================================================================= -def process_maybe_encrypted(filepath, passwords=None, crypto_nesting=0, +def process_maybe_encrypted(filepath, passwords=None, decrypted_filepath=None, crypto_nesting=0, **kwargs): """ Process a file that might be encrypted. @@ -921,6 +924,8 @@ def process_maybe_encrypted(filepath, passwords=None, crypto_nesting=0, :param str filepath: path to file on disc. :param passwords: list of passwords (str) to try for decryption or None + :param decrypted_filepath: filepath of the decrypted file in case you want to + preserve it :param int crypto_nesting: How many decryption layers were already used to get the given file. :param kwargs: same as :py:func:`process_file` @@ -949,12 +954,14 @@ def process_maybe_encrypted(filepath, passwords=None, crypto_nesting=0, passwords = list(passwords) + crypto.DEFAULT_PASSWORDS try: logger.debug('Trying to decrypt file') - decrypted_file = crypto.decrypt(filepath, passwords) + decrypted_file, correct_password = crypto.decrypt(filepath, passwords, decrypted_filepath) + if correct_password: + logger.info(f"The correct password is: {correct_password}") if not decrypted_file: logger.error('Decrypt failed, run with debug output to get details') raise crypto.WrongEncryptionPassword(filepath) logger.info('Analyze decrypted file') - result = process_maybe_encrypted(decrypted_file, passwords, + result = process_maybe_encrypted(decrypted_file, passwords, decrypted_filepath, crypto_nesting+1, **kwargs) finally: # clean up try: # (maybe file was not yet created) @@ -990,7 +997,7 @@ def main(cmd_line_args=None): return_code = 1 try: text = process_maybe_encrypted( - args.filepath, args.password, + args.filepath, args.password, args.decrypted_filepath, field_filter_mode=args.field_filter_mode) return_code = 0 except Exception as exc: diff --git a/oletools/olevba.py b/oletools/olevba.py index 13c7402..4f1d9ba 100644 --- a/oletools/olevba.py +++ b/oletools/olevba.py @@ -3473,15 +3473,18 @@ class VBA_Parser(object): self.is_encrypted = crypto.is_encrypted(self.ole_file) return self.is_encrypted - def decrypt_file(self, passwords_list=None): + def decrypt_file(self, passwords_list=None, decrypted_filepath=None): decrypted_file = None + correct_password = None if self.detect_is_encrypted(): passwords = crypto.DEFAULT_PASSWORDS if passwords_list and isinstance(passwords_list, list): passwords.extend(passwords_list) - decrypted_file = crypto.decrypt(self.filename, passwords) + decrypted_file, correct_password = crypto.decrypt(self.filename, passwords, decrypted_filepath) + if correct_password: + log.info(f"The correct password is: {correct_password}") - return decrypted_file + return decrypted_file, correct_password def encode_string(self, unicode_str): """ @@ -4370,6 +4373,9 @@ def parse_args(cmd_line_args=None): default=None, help='if the file is a zip archive, open all files ' 'from it, using the provided password.') + parser.add_argument("--decrypted_filepath", dest='decrypted_filepath', type=str, + default=None, + help='save the decrypted file to this location.') parser.add_argument("-p", "--password", type=str, action='append', default=[], help='if encrypted office files are encountered, try ' @@ -4551,10 +4557,13 @@ def process_file(filename, data, container, options, crypto_nesting=0): try: log.debug('Checking encryption passwords {}'.format(options.password)) passwords = options.password + crypto.DEFAULT_PASSWORDS - decrypted_file = crypto.decrypt(filename, passwords) + log.debug('Checking decrypted filepath {}'.format(options.decrypted_filepath)) + + decrypted_file, correct_password = crypto.decrypt(filename, passwords, options.decrypted_filepath) if not decrypted_file: log.error('Decrypt failed, run with debug output to get details') raise crypto.WrongEncryptionPassword(filename) + log.info(f'The correct password is: {correct_password}') log.info('Working on decrypted file') return process_file(decrypted_file, data, container or filename, options, crypto_nesting+1)