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