Commit ccf99d1a8f85e552f5cc130fbaa504cfe5725a92
1 parent
82b49eb6
added support to password discovery during decryption
added support to decrypted filepath dstfile
Showing
3 changed files
with
45 additions
and
14 deletions
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) |