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