Commit ccf99d1a8f85e552f5cc130fbaa504cfe5725a92

Authored by Federico Fantini
1 parent 82b49eb6

added support to password discovery during decryption

added support to decrypted filepath dstfile
oletools/crypto.py
... ... @@ -33,7 +33,7 @@ potentially encrypted files::
33 33 raise crypto.MaxCryptoNestingReached(crypto_nesting, filename)
34 34 decrypted_file = None
35 35 try:
36   - decrypted_file = crypto.decrypt(input_file, passwords)
  36 + decrypted_file, correct_password = crypto.decrypt(input_file, passwords, decrypted_filepath)
37 37 if decrypted_file is None:
38 38 raise crypto.WrongEncryptionPassword(input_file)
39 39 # might still be encrypted, so call this again recursively
... ... @@ -102,6 +102,7 @@ __version__ = '0.60'
102 102 import sys
103 103 import struct
104 104 import os
  105 +import shutil
105 106 from os.path import splitext, isfile
106 107 from tempfile import mkstemp
107 108 import zipfile
... ... @@ -314,7 +315,7 @@ def check_msoffcrypto():
314 315 return msoffcrypto is not None
315 316  
316 317  
317   -def decrypt(filename, passwords=None, **temp_file_args):
  318 +def decrypt(filename, passwords=None, decrypted_filepath=None, **temp_file_args):
318 319 """
319 320 Try to decrypt an encrypted file
320 321  
... ... @@ -331,7 +332,10 @@ def decrypt(filename, passwords=None, **temp_file_args):
331 332 `dirname` or `prefix`. `suffix` will default to
332 333 suffix of input `filename`, `prefix` defaults to
333 334 `oletools-decrypt-`; `text` will be ignored
334   - :returns: name of the decrypted temporary file (type str) or `None`
  335 + :param decrypted_filepath: filepath of the decrypted file in case you want to
  336 + preserve it
  337 + :returns: a tuple with the name of the decrypted temporary file (type str) or `None`
  338 + and the correct password or 'None'
335 339 :raises: :py:class:`ImportError` if :py:mod:`msoffcrypto-tools` not found
336 340 :raises: :py:class:`ValueError` if the given file is not encrypted
337 341 """
... ... @@ -370,6 +374,7 @@ def decrypt(filename, passwords=None, **temp_file_args):
370 374 raise ValueError('Given input file {} is not encrypted!'
371 375 .format(filename))
372 376  
  377 + correct_password = None
373 378 for password in passwords:
374 379 log.debug('Trying to decrypt with password {!r}'.format(password))
375 380 write_descriptor = None
... ... @@ -387,6 +392,7 @@ def decrypt(filename, passwords=None, **temp_file_args):
387 392 # decryption was successfull; clean up and return
388 393 write_handle.close()
389 394 write_handle = None
  395 + correct_password = password
390 396 break
391 397 except Exception:
392 398 log.debug('Failed to decrypt', exc_info=True)
... ... @@ -399,6 +405,15 @@ def decrypt(filename, passwords=None, **temp_file_args):
399 405 if decrypt_file and isfile(decrypt_file):
400 406 os.unlink(decrypt_file)
401 407 decrypt_file = None
402   - # if we reach this, all passwords were tried without success
403   - log.debug('All passwords failed')
404   - return decrypt_file
  408 + correct_password = None
  409 +
  410 + if decrypt_file and correct_password:
  411 + log.debug(f'Successfully decrypted the file with password: {correct_password}')
  412 + if decrypted_filepath:
  413 + if os.path.isdir(decrypted_filepath) and os.access(decrypted_filepath, os.W_OK):
  414 + log.info(f"Saving decrypted file in: {decrypted_filepath}")
  415 + shutil.copy(decrypt_file, decrypted_filepath)
  416 + else:
  417 + log.info('All passwords failed')
  418 +
  419 + return decrypt_file, correct_password
... ...
oletools/msodde.py
... ... @@ -271,6 +271,9 @@ def process_args(cmd_line_args=None):
271 271 parser.add_argument("-p", "--password", type=str, action='append',
272 272 help='if encrypted office files are encountered, try '
273 273 'decryption with this password. May be repeated.')
  274 + parser.add_argument("--decrypted_filepath", dest='decrypted_filepath', type=str,
  275 + default=None,
  276 + help='save the decrypted file to this location.')
274 277 filter_group = parser.add_argument_group(
275 278 title='Filter which OpenXML field commands are returned',
276 279 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):
910 913 # === MAIN =================================================================
911 914  
912 915  
913   -def process_maybe_encrypted(filepath, passwords=None, crypto_nesting=0,
  916 +def process_maybe_encrypted(filepath, passwords=None, decrypted_filepath=None, crypto_nesting=0,
914 917 **kwargs):
915 918 """
916 919 Process a file that might be encrypted.
... ... @@ -921,6 +924,8 @@ def process_maybe_encrypted(filepath, passwords=None, crypto_nesting=0,
921 924  
922 925 :param str filepath: path to file on disc.
923 926 :param passwords: list of passwords (str) to try for decryption or None
  927 + :param decrypted_filepath: filepath of the decrypted file in case you want to
  928 + preserve it
924 929 :param int crypto_nesting: How many decryption layers were already used to
925 930 get the given file.
926 931 :param kwargs: same as :py:func:`process_file`
... ... @@ -949,12 +954,14 @@ def process_maybe_encrypted(filepath, passwords=None, crypto_nesting=0,
949 954 passwords = list(passwords) + crypto.DEFAULT_PASSWORDS
950 955 try:
951 956 logger.debug('Trying to decrypt file')
952   - decrypted_file = crypto.decrypt(filepath, passwords)
  957 + decrypted_file, correct_password = crypto.decrypt(filepath, passwords, decrypted_filepath)
  958 + if correct_password:
  959 + logger.info(f"The correct password is: {correct_password}")
953 960 if not decrypted_file:
954 961 logger.error('Decrypt failed, run with debug output to get details')
955 962 raise crypto.WrongEncryptionPassword(filepath)
956 963 logger.info('Analyze decrypted file')
957   - result = process_maybe_encrypted(decrypted_file, passwords,
  964 + result = process_maybe_encrypted(decrypted_file, passwords, decrypted_filepath,
958 965 crypto_nesting+1, **kwargs)
959 966 finally: # clean up
960 967 try: # (maybe file was not yet created)
... ... @@ -990,7 +997,7 @@ def main(cmd_line_args=None):
990 997 return_code = 1
991 998 try:
992 999 text = process_maybe_encrypted(
993   - args.filepath, args.password,
  1000 + args.filepath, args.password, args.decrypted_filepath,
994 1001 field_filter_mode=args.field_filter_mode)
995 1002 return_code = 0
996 1003 except Exception as exc:
... ...
oletools/olevba.py
... ... @@ -3473,15 +3473,18 @@ class VBA_Parser(object):
3473 3473 self.is_encrypted = crypto.is_encrypted(self.ole_file)
3474 3474 return self.is_encrypted
3475 3475  
3476   - def decrypt_file(self, passwords_list=None):
  3476 + def decrypt_file(self, passwords_list=None, decrypted_filepath=None):
3477 3477 decrypted_file = None
  3478 + correct_password = None
3478 3479 if self.detect_is_encrypted():
3479 3480 passwords = crypto.DEFAULT_PASSWORDS
3480 3481 if passwords_list and isinstance(passwords_list, list):
3481 3482 passwords.extend(passwords_list)
3482   - decrypted_file = crypto.decrypt(self.filename, passwords)
  3483 + decrypted_file, correct_password = crypto.decrypt(self.filename, passwords, decrypted_filepath)
  3484 + if correct_password:
  3485 + log.info(f"The correct password is: {correct_password}")
3483 3486  
3484   - return decrypted_file
  3487 + return decrypted_file, correct_password
3485 3488  
3486 3489 def encode_string(self, unicode_str):
3487 3490 """
... ... @@ -4370,6 +4373,9 @@ def parse_args(cmd_line_args=None):
4370 4373 default=None,
4371 4374 help='if the file is a zip archive, open all files '
4372 4375 'from it, using the provided password.')
  4376 + parser.add_argument("--decrypted_filepath", dest='decrypted_filepath', type=str,
  4377 + default=None,
  4378 + help='save the decrypted file to this location.')
4373 4379 parser.add_argument("-p", "--password", type=str, action='append',
4374 4380 default=[],
4375 4381 help='if encrypted office files are encountered, try '
... ... @@ -4551,10 +4557,13 @@ def process_file(filename, data, container, options, crypto_nesting=0):
4551 4557 try:
4552 4558 log.debug('Checking encryption passwords {}'.format(options.password))
4553 4559 passwords = options.password + crypto.DEFAULT_PASSWORDS
4554   - decrypted_file = crypto.decrypt(filename, passwords)
  4560 + log.debug('Checking decrypted filepath {}'.format(options.decrypted_filepath))
  4561 +
  4562 + decrypted_file, correct_password = crypto.decrypt(filename, passwords, options.decrypted_filepath)
4555 4563 if not decrypted_file:
4556 4564 log.error('Decrypt failed, run with debug output to get details')
4557 4565 raise crypto.WrongEncryptionPassword(filename)
  4566 + log.info(f'The correct password is: {correct_password}')
4558 4567 log.info('Working on decrypted file')
4559 4568 return process_file(decrypted_file, data, container or filename,
4560 4569 options, crypto_nesting+1)
... ...