Skip to content

Commit

Permalink
added support to password discovery during decryption
Browse files Browse the repository at this point in the history
added support to decrypted filepath dstfile
  • Loading branch information
federicofantini committed Feb 13, 2024
1 parent 82b49eb commit ccf99d1
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 14 deletions.
27 changes: 21 additions & 6 deletions oletools/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def script_main_function(input_file, passwords, crypto_nesting=0, args):
raise crypto.MaxCryptoNestingReached(crypto_nesting, filename)
decrypted_file = None
try:
decrypted_file = crypto.decrypt(input_file, passwords)
decrypted_file, correct_password = crypto.decrypt(input_file, passwords, decrypted_filepath)
if decrypted_file is None:
raise crypto.WrongEncryptionPassword(input_file)
# might still be encrypted, so call this again recursively
Expand Down Expand Up @@ -102,6 +102,7 @@ def script_main_function(input_file, passwords, crypto_nesting=0, args):
import sys
import struct
import os
import shutil
from os.path import splitext, isfile
from tempfile import mkstemp
import zipfile
Expand Down Expand Up @@ -314,7 +315,7 @@ def check_msoffcrypto():
return msoffcrypto is not None


def decrypt(filename, passwords=None, **temp_file_args):
def decrypt(filename, passwords=None, decrypted_filepath=None, **temp_file_args):
"""
Try to decrypt an encrypted file
Expand All @@ -331,7 +332,10 @@ def decrypt(filename, passwords=None, **temp_file_args):
`dirname` or `prefix`. `suffix` will default to
suffix of input `filename`, `prefix` defaults to
`oletools-decrypt-`; `text` will be ignored
:returns: name of the decrypted temporary file (type str) or `None`
:param decrypted_filepath: filepath of the decrypted file in case you want to
preserve it
:returns: a tuple with the name of the decrypted temporary file (type str) or `None`
and the correct password or 'None'
:raises: :py:class:`ImportError` if :py:mod:`msoffcrypto-tools` not found
:raises: :py:class:`ValueError` if the given file is not encrypted
"""
Expand Down Expand Up @@ -370,6 +374,7 @@ def decrypt(filename, passwords=None, **temp_file_args):
raise ValueError('Given input file {} is not encrypted!'
.format(filename))

correct_password = None
for password in passwords:
log.debug('Trying to decrypt with password {!r}'.format(password))
write_descriptor = None
Expand All @@ -387,6 +392,7 @@ def decrypt(filename, passwords=None, **temp_file_args):
# decryption was successfull; clean up and return
write_handle.close()
write_handle = None
correct_password = password
break
except Exception:
log.debug('Failed to decrypt', exc_info=True)
Expand All @@ -399,6 +405,15 @@ def decrypt(filename, passwords=None, **temp_file_args):
if decrypt_file and isfile(decrypt_file):
os.unlink(decrypt_file)
decrypt_file = None
# if we reach this, all passwords were tried without success
log.debug('All passwords failed')
return decrypt_file
correct_password = None

if decrypt_file and correct_password:
log.debug(f'Successfully decrypted the file with password: {correct_password}')
if decrypted_filepath:
if os.path.isdir(decrypted_filepath) and os.access(decrypted_filepath, os.W_OK):
log.info(f"Saving decrypted file in: {decrypted_filepath}")
shutil.copy(decrypt_file, decrypted_filepath)
else:
log.info('All passwords failed')

return decrypt_file, correct_password
15 changes: 11 additions & 4 deletions oletools/msodde.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ def process_args(cmd_line_args=None):
parser.add_argument("-p", "--password", type=str, action='append',
help='if encrypted office files are encountered, try '
'decryption with this password. May be repeated.')
parser.add_argument("--decrypted_filepath", dest='decrypted_filepath', type=str,
default=None,
help='save the decrypted file to this location.')
filter_group = parser.add_argument_group(
title='Filter which OpenXML field commands are returned',
description='Only applies to OpenXML (e.g. docx) and rtf, not to OLE '
Expand Down Expand Up @@ -910,7 +913,7 @@ def process_file(filepath, field_filter_mode=None):
# === MAIN =================================================================


def process_maybe_encrypted(filepath, passwords=None, crypto_nesting=0,
def process_maybe_encrypted(filepath, passwords=None, decrypted_filepath=None, crypto_nesting=0,
**kwargs):
"""
Process a file that might be encrypted.
Expand All @@ -921,6 +924,8 @@ def process_maybe_encrypted(filepath, passwords=None, crypto_nesting=0,
:param str filepath: path to file on disc.
:param passwords: list of passwords (str) to try for decryption or None
:param decrypted_filepath: filepath of the decrypted file in case you want to
preserve it
:param int crypto_nesting: How many decryption layers were already used to
get the given file.
:param kwargs: same as :py:func:`process_file`
Expand Down Expand Up @@ -949,12 +954,14 @@ def process_maybe_encrypted(filepath, passwords=None, crypto_nesting=0,
passwords = list(passwords) + crypto.DEFAULT_PASSWORDS
try:
logger.debug('Trying to decrypt file')
decrypted_file = crypto.decrypt(filepath, passwords)
decrypted_file, correct_password = crypto.decrypt(filepath, passwords, decrypted_filepath)
if correct_password:
logger.info(f"The correct password is: {correct_password}")
if not decrypted_file:
logger.error('Decrypt failed, run with debug output to get details')
raise crypto.WrongEncryptionPassword(filepath)
logger.info('Analyze decrypted file')
result = process_maybe_encrypted(decrypted_file, passwords,
result = process_maybe_encrypted(decrypted_file, passwords, decrypted_filepath,
crypto_nesting+1, **kwargs)
finally: # clean up
try: # (maybe file was not yet created)
Expand Down Expand Up @@ -990,7 +997,7 @@ def main(cmd_line_args=None):
return_code = 1
try:
text = process_maybe_encrypted(
args.filepath, args.password,
args.filepath, args.password, args.decrypted_filepath,
field_filter_mode=args.field_filter_mode)
return_code = 0
except Exception as exc:
Expand Down
17 changes: 13 additions & 4 deletions oletools/olevba.py
Original file line number Diff line number Diff line change
Expand Up @@ -3473,15 +3473,18 @@ def detect_is_encrypted(self):
self.is_encrypted = crypto.is_encrypted(self.ole_file)
return self.is_encrypted

def decrypt_file(self, passwords_list=None):
def decrypt_file(self, passwords_list=None, decrypted_filepath=None):
decrypted_file = None
correct_password = None
if self.detect_is_encrypted():
passwords = crypto.DEFAULT_PASSWORDS
if passwords_list and isinstance(passwords_list, list):
passwords.extend(passwords_list)
decrypted_file = crypto.decrypt(self.filename, passwords)
decrypted_file, correct_password = crypto.decrypt(self.filename, passwords, decrypted_filepath)
if correct_password:
log.info(f"The correct password is: {correct_password}")

return decrypted_file
return decrypted_file, correct_password

def encode_string(self, unicode_str):
"""
Expand Down Expand Up @@ -4370,6 +4373,9 @@ def parse_args(cmd_line_args=None):
default=None,
help='if the file is a zip archive, open all files '
'from it, using the provided password.')
parser.add_argument("--decrypted_filepath", dest='decrypted_filepath', type=str,
default=None,
help='save the decrypted file to this location.')
parser.add_argument("-p", "--password", type=str, action='append',
default=[],
help='if encrypted office files are encountered, try '
Expand Down Expand Up @@ -4551,10 +4557,13 @@ def process_file(filename, data, container, options, crypto_nesting=0):
try:
log.debug('Checking encryption passwords {}'.format(options.password))
passwords = options.password + crypto.DEFAULT_PASSWORDS
decrypted_file = crypto.decrypt(filename, passwords)
log.debug('Checking decrypted filepath {}'.format(options.decrypted_filepath))

decrypted_file, correct_password = crypto.decrypt(filename, passwords, options.decrypted_filepath)
if not decrypted_file:
log.error('Decrypt failed, run with debug output to get details')
raise crypto.WrongEncryptionPassword(filename)
log.info(f'The correct password is: {correct_password}')
log.info('Working on decrypted file')
return process_file(decrypted_file, data, container or filename,
options, crypto_nesting+1)
Expand Down

0 comments on commit ccf99d1

Please sign in to comment.