Commit e94e735a0cfb66f0117b5a2ae15913c9964318e2
Committed by
GitHub
Merge pull request #842 from federicofantini/master
password discovery and decrypted filepath dstfile
Showing
3 changed files
with
45 additions
and
14 deletions
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) | ... | ... |