Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

password discovery and decrypted filepath dstfile #842

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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