Commit e94e735a0cfb66f0117b5a2ae15913c9964318e2

Authored by Philippe Lagadec
Committed by GitHub
2 parents c211c3dd 5034a62d

Merge pull request #842 from federicofantini/master

password discovery and 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_dir)
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_dir=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_dir: folder to store the decrypted file in case you want
  336 + to 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_dir:
  413 + if os.path.isdir(decrypted_dir) and os.access(decrypted_dir, os.W_OK):
  414 + log.info(f"Saving decrypted file in: {decrypted_dir}")
  415 + shutil.copy(decrypt_file, decrypted_dir)
  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_dir", dest='decrypted_dir', type=str,
  275 + default=None,
  276 + help='store the decrypted file to this folder.')
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 '
@@ -914,7 +917,7 @@ def process_file(filepath, field_filter_mode=None): @@ -914,7 +917,7 @@ def process_file(filepath, field_filter_mode=None):
914 # === MAIN ================================================================= 917 # === MAIN =================================================================
915 918
916 919
917 -def process_maybe_encrypted(filepath, passwords=None, crypto_nesting=0, 920 +def process_maybe_encrypted(filepath, passwords=None, decrypted_dir=None, crypto_nesting=0,
918 **kwargs): 921 **kwargs):
919 """ 922 """
920 Process a file that might be encrypted. 923 Process a file that might be encrypted.
@@ -925,6 +928,8 @@ def process_maybe_encrypted(filepath, passwords=None, crypto_nesting=0, @@ -925,6 +928,8 @@ def process_maybe_encrypted(filepath, passwords=None, crypto_nesting=0,
925 928
926 :param str filepath: path to file on disc. 929 :param str filepath: path to file on disc.
927 :param passwords: list of passwords (str) to try for decryption or None 930 :param passwords: list of passwords (str) to try for decryption or None
  931 + :param decrypted_dir: folder to store the decrypted file in case you want
  932 + to preserve it
928 :param int crypto_nesting: How many decryption layers were already used to 933 :param int crypto_nesting: How many decryption layers were already used to
929 get the given file. 934 get the given file.
930 :param kwargs: same as :py:func:`process_file` 935 :param kwargs: same as :py:func:`process_file`
@@ -953,12 +958,14 @@ def process_maybe_encrypted(filepath, passwords=None, crypto_nesting=0, @@ -953,12 +958,14 @@ def process_maybe_encrypted(filepath, passwords=None, crypto_nesting=0,
953 passwords = list(passwords) + crypto.DEFAULT_PASSWORDS 958 passwords = list(passwords) + crypto.DEFAULT_PASSWORDS
954 try: 959 try:
955 logger.debug('Trying to decrypt file') 960 logger.debug('Trying to decrypt file')
956 - decrypted_file = crypto.decrypt(filepath, passwords) 961 + decrypted_file, correct_password = crypto.decrypt(filepath, passwords, decrypted_dir)
  962 + if correct_password:
  963 + logger.info(f"The correct password is: {correct_password}")
957 if not decrypted_file: 964 if not decrypted_file:
958 logger.error('Decrypt failed, run with debug output to get details') 965 logger.error('Decrypt failed, run with debug output to get details')
959 raise crypto.WrongEncryptionPassword(filepath) 966 raise crypto.WrongEncryptionPassword(filepath)
960 logger.info('Analyze decrypted file') 967 logger.info('Analyze decrypted file')
961 - result = process_maybe_encrypted(decrypted_file, passwords, 968 + result = process_maybe_encrypted(decrypted_file, passwords, decrypted_dir,
962 crypto_nesting+1, **kwargs) 969 crypto_nesting+1, **kwargs)
963 finally: # clean up 970 finally: # clean up
964 try: # (maybe file was not yet created) 971 try: # (maybe file was not yet created)
@@ -994,7 +1001,7 @@ def main(cmd_line_args=None): @@ -994,7 +1001,7 @@ def main(cmd_line_args=None):
994 return_code = 1 1001 return_code = 1
995 try: 1002 try:
996 text = process_maybe_encrypted( 1003 text = process_maybe_encrypted(
997 - args.filepath, args.password, 1004 + args.filepath, args.password, args.decrypted_dir,
998 field_filter_mode=args.field_filter_mode) 1005 field_filter_mode=args.field_filter_mode)
999 return_code = 0 1006 return_code = 0
1000 except Exception as exc: 1007 except Exception as exc:
oletools/olevba.py
@@ -3477,15 +3477,18 @@ class VBA_Parser(object): @@ -3477,15 +3477,18 @@ class VBA_Parser(object):
3477 self.is_encrypted = crypto.is_encrypted(self.ole_file) 3477 self.is_encrypted = crypto.is_encrypted(self.ole_file)
3478 return self.is_encrypted 3478 return self.is_encrypted
3479 3479
3480 - def decrypt_file(self, passwords_list=None): 3480 + def decrypt_file(self, passwords_list=None, decrypted_dir=None):
3481 decrypted_file = None 3481 decrypted_file = None
  3482 + correct_password = None
3482 if self.detect_is_encrypted(): 3483 if self.detect_is_encrypted():
3483 passwords = crypto.DEFAULT_PASSWORDS 3484 passwords = crypto.DEFAULT_PASSWORDS
3484 if passwords_list and isinstance(passwords_list, list): 3485 if passwords_list and isinstance(passwords_list, list):
3485 passwords.extend(passwords_list) 3486 passwords.extend(passwords_list)
3486 - decrypted_file = crypto.decrypt(self.filename, passwords) 3487 + decrypted_file, correct_password = crypto.decrypt(self.filename, passwords, decrypted_dir)
  3488 + if correct_password:
  3489 + log.info(f"The correct password is: {correct_password}")
3487 3490
3488 - return decrypted_file 3491 + return decrypted_file, correct_password
3489 3492
3490 def encode_string(self, unicode_str): 3493 def encode_string(self, unicode_str):
3491 """ 3494 """
@@ -4378,6 +4381,9 @@ def parse_args(cmd_line_args=None): @@ -4378,6 +4381,9 @@ def parse_args(cmd_line_args=None):
4378 default=None, 4381 default=None,
4379 help='if the file is a zip archive, open all files ' 4382 help='if the file is a zip archive, open all files '
4380 'from it, using the provided password.') 4383 'from it, using the provided password.')
  4384 + parser.add_argument("--decrypted_dir", dest='decrypted_dir', type=str,
  4385 + default=None,
  4386 + help='store the decrypted file to this folder.')
4381 parser.add_argument("-p", "--password", type=str, action='append', 4387 parser.add_argument("-p", "--password", type=str, action='append',
4382 default=[], 4388 default=[],
4383 help='if encrypted office files are encountered, try ' 4389 help='if encrypted office files are encountered, try '
@@ -4559,10 +4565,13 @@ def process_file(filename, data, container, options, crypto_nesting=0): @@ -4559,10 +4565,13 @@ def process_file(filename, data, container, options, crypto_nesting=0):
4559 try: 4565 try:
4560 log.debug('Checking encryption passwords {}'.format(options.password)) 4566 log.debug('Checking encryption passwords {}'.format(options.password))
4561 passwords = options.password + crypto.DEFAULT_PASSWORDS 4567 passwords = options.password + crypto.DEFAULT_PASSWORDS
4562 - decrypted_file = crypto.decrypt(filename, passwords) 4568 + log.debug('Checking decrypted filepath {}'.format(options.decrypted_dir))
  4569 +
  4570 + decrypted_file, correct_password = crypto.decrypt(filename, passwords, options.decrypted_dir)
4563 if not decrypted_file: 4571 if not decrypted_file:
4564 log.error('Decrypt failed, run with debug output to get details') 4572 log.error('Decrypt failed, run with debug output to get details')
4565 raise crypto.WrongEncryptionPassword(filename) 4573 raise crypto.WrongEncryptionPassword(filename)
  4574 + log.info(f'The correct password is: {correct_password}')
4566 log.info('Working on decrypted file') 4575 log.info('Working on decrypted file')
4567 return process_file(decrypted_file, data, container or filename, 4576 return process_file(decrypted_file, data, container or filename,
4568 options, crypto_nesting+1) 4577 options, crypto_nesting+1)