From a6c21e7ffc1cead3553d111dac9367837829d6e6 Mon Sep 17 00:00:00 2001 From: philsmd Date: Sat, 21 May 2022 11:45:48 +0200 Subject: [PATCH] make our hash extraction scripts executable --- tools/bitwarden2hashcat.py | 1 + tools/metamask2hashcat.py | 0 tools/mozilla2hashcat.py | 547 ++++++++++++++++++------------------ tools/virtualbox2hashcat.py | 0 tools/vmwarevmx2hashcat.py | 0 5 files changed, 275 insertions(+), 273 deletions(-) mode change 100644 => 100755 tools/bitwarden2hashcat.py mode change 100644 => 100755 tools/metamask2hashcat.py mode change 100644 => 100755 tools/mozilla2hashcat.py mode change 100644 => 100755 tools/virtualbox2hashcat.py mode change 100644 => 100755 tools/vmwarevmx2hashcat.py diff --git a/tools/bitwarden2hashcat.py b/tools/bitwarden2hashcat.py old mode 100644 new mode 100755 index 07b56cb83..6015bb8ef --- a/tools/bitwarden2hashcat.py +++ b/tools/bitwarden2hashcat.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python """Utility to extract Bitwarden hash for hashcat from Google Chrome / Firefox / Desktop local data""" # diff --git a/tools/metamask2hashcat.py b/tools/metamask2hashcat.py old mode 100644 new mode 100755 diff --git a/tools/mozilla2hashcat.py b/tools/mozilla2hashcat.py old mode 100644 new mode 100755 index 577bf7836..8e7f152e3 --- a/tools/mozilla2hashcat.py +++ b/tools/mozilla2hashcat.py @@ -1,273 +1,274 @@ -# -# Script to extract the "hash" from a password protected key3.db or key4.db file. -# -# This code is based on the tool "firepwd" (https://github.com/lclevy/firepwd (GPL-license) -# Although the code has been changed a bit, all credit goes to @lclevy for his initial work. -# -# Tested with Python 3.8.5 and the following libraries: PyCryptodome 3.10.1 and pyasn1 0.4.8 -# -# Author: -# https://github.com/Banaanhangwagen -# https://github.com/mneitsabes -# License: MIT -# - -import argparse -from collections import namedtuple -import enum -import binascii -import hashlib -import hmac -import os -import sqlite3 -import struct -import sys - -from Crypto.Cipher import AES, DES3 -from pyasn1.codec.der import decoder - - -class MasterPasswordInfos: - def __init__(self, mode, global_salt, entry_salt, cipher_text, no_master_password, iteration=None, iv=None): - if mode not in ['aes', '3des']: - raise ValueError('Bad mode') - - self.mode = mode - self.global_salt = global_salt - self.entry_salt = entry_salt - self.cipher_text = cipher_text - self.no_master_password = no_master_password - self.iteration = iteration - self.iv = iv - - -def read_bsd_db(db_filepath: str) -> {}: - """ - Read the key3.db. - - :param db_filepath: the database filepath - :type db_filepath: str - :return: the dict - :rtype: dict - """ - with open(db_filepath, 'rb') as f: - header = f.read(4 * 15) - - magic = struct.unpack('>L', header[0:4])[0] - if magic != 0x61561: - raise ValueError('Bad magic number') - - version = struct.unpack('>L', header[4:8])[0] - if version != 2: - raise ValueError('Bad version') - - pagesize = struct.unpack('>L', header[12:16])[0] - nkeys = struct.unpack('>L', header[56:60])[0] - - readkeys = 0 - page = 1 - db1 = [] - - while readkeys < nkeys: - f.seek(pagesize*page) - - offsets = f.read((nkeys+1) * 4 + 2) - offset_vals = [] - i = 0 - nval = 0 - val = 1 - keys = 0 - - while nval != val: - keys += 1 - key = struct.unpack(' MasterPasswordInfos: - """ - Extract the master password information from the database. - - :param db_filepath: the db filepath - :type db_filepath: str - :param db_version: the db_type, 3 or 4 - :type db_version: int - :return: the infos - :rtype: MasterPasswordInfos - """ - if db_version not in [3, 4]: - raise ValueError('db_version not supported') - - if db_version == 3: - db_values = read_bsd_db(db_filepath) - - global_salt = db_values[b'global-salt'] - pwd_check = db_values[b'password-check'] - entry_salt_len = pwd_check[1] - entry_salt = pwd_check[3: 3 + entry_salt_len] - cipher_text = pwd_check[-16:] - - no_master_password = is_decrypting_mozilla_3des_without_master_password(global_salt, entry_salt, cipher_text) - - return MasterPasswordInfos('3des', global_salt, entry_salt, cipher_text, no_master_password) - else: - db = sqlite3.connect(db_filepath) - c = db.cursor() - c.execute('SELECT item1,item2 FROM metadata WHERE id = "password"') - global_salt, encoded_item2 = c.fetchone() - decoded_item2 = decoder.decode(encoded_item2) - - pbe_algo = str(decoded_item2[0][0][0]) - if pbe_algo == '1.2.840.113549.1.12.5.1.3': # pbeWithSha1AndTripleDES-CBC - entry_salt = decoded_item2[0][0][1][0].asOctets() - cipher_text = decoded_item2[0][1].asOctets() - - no_master_password = is_decrypting_mozilla_3des_without_master_password(global_salt, entry_salt, - cipher_text) - return MasterPasswordInfos('3des', global_salt, entry_salt, cipher_text, no_master_password) - elif pbe_algo == '1.2.840.113549.1.5.13': # pkcs5 pbes2 - assert str(decoded_item2[0][0][1][0][0]) == '1.2.840.113549.1.5.12' - assert str(decoded_item2[0][0][1][0][1][3][0]) == '1.2.840.113549.2.9' - assert str(decoded_item2[0][0][1][1][0]) == '2.16.840.1.101.3.4.1.42' - assert int(decoded_item2[0][0][1][0][1][2]) == 32 # key length - - entry_salt = decoded_item2[0][0][1][0][1][0].asOctets() - iteration = int(decoded_item2[0][0][1][0][1][1]) - iv = b'\x04\x0e' + decoded_item2[0][0][1][1][1].asOctets() - cipher_text = decoded_item2[0][1].asOctets() - - no_master_password = is_decrypting_pbe_aes_without_password(global_salt, entry_salt, iteration, iv, - cipher_text) - return MasterPasswordInfos('aes', global_salt, entry_salt, cipher_text, no_master_password, iteration, iv) - -def hex(b) -> str: - """ - Returns the hexily version of the binary datas. - - :param b: binary datas - :return: the string - """ - return binascii.hexlify(b).decode('utf8') - - -def get_hashcat_string(mpinfos: MasterPasswordInfos) -> str: - """ - Print the hashchat format string. - - :param mpinfos: the infos - :type mpinfos: MasterPasswordInfos - :return: the string - :rtype: str - """ - - if mpinfos.no_master_password: - return 'No Primary Password is set.' - else: - s = '$mozilla$*' - - if mpinfos.mode == '3des': - s += f'3DES*{hex(mpinfos.global_salt)}*{hex(mpinfos.entry_salt)}*{hex(mpinfos.cipher_text)}' - else: - s += f'AES*{hex(mpinfos.global_salt)}*{hex(mpinfos.entry_salt)}*{mpinfos.iteration}*' \ - f'{hex(mpinfos.iv)}*{hex(mpinfos.cipher_text)}' - - return s - - -if __name__ == '__main__': - usage = 'python3 mozilla2hashcat.py \n' - parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, usage=usage) - parser.add_argument('dir_or_db', help='The directory containing the key3/4.db-file or the key3/4.db-file itself') - args = parser.parse_args() - - db_filepath = None - db_type = None - - if os.path.isdir(args.dir_or_db): - db3_filepath = os.path.join(args.dir_or_db, 'key3.db') - db4_filepath = os.path.join(args.dir_or_db, 'key4.db') - - if os.path.exists(db3_filepath): - db_filepath = db3_filepath - db_type = 3 - elif os.path.exists(db4_filepath): - db_filepath = db4_filepath - db_type = 4 - elif os.path.isfile(args.dir_or_db): - filename = os.path.basename(args.dir_or_db) - if filename == 'key3.db': - db_filepath = args.dir_or_db - db_type = 3 - elif filename == 'key4.db': - db_filepath = args.dir_or_db - db_type = 4 - - if not db_filepath: - sys.stderr.write('key3.db or key4.db file not found\n') - exit(-1) - - infos = extract_master_password_infos(db_filepath, db_type) - print(get_hashcat_string(infos)) +#!/usr/bin/env python +# +# Script to extract the "hash" from a password protected key3.db or key4.db file. +# +# This code is based on the tool "firepwd" (https://github.com/lclevy/firepwd (GPL-license) +# Although the code has been changed a bit, all credit goes to @lclevy for his initial work. +# +# Tested with Python 3.8.5 and the following libraries: PyCryptodome 3.10.1 and pyasn1 0.4.8 +# +# Author: +# https://github.com/Banaanhangwagen +# https://github.com/mneitsabes +# License: MIT +# + +import argparse +from collections import namedtuple +import enum +import binascii +import hashlib +import hmac +import os +import sqlite3 +import struct +import sys + +from Crypto.Cipher import AES, DES3 +from pyasn1.codec.der import decoder + + +class MasterPasswordInfos: + def __init__(self, mode, global_salt, entry_salt, cipher_text, no_master_password, iteration=None, iv=None): + if mode not in ['aes', '3des']: + raise ValueError('Bad mode') + + self.mode = mode + self.global_salt = global_salt + self.entry_salt = entry_salt + self.cipher_text = cipher_text + self.no_master_password = no_master_password + self.iteration = iteration + self.iv = iv + + +def read_bsd_db(db_filepath: str) -> {}: + """ + Read the key3.db. + + :param db_filepath: the database filepath + :type db_filepath: str + :return: the dict + :rtype: dict + """ + with open(db_filepath, 'rb') as f: + header = f.read(4 * 15) + + magic = struct.unpack('>L', header[0:4])[0] + if magic != 0x61561: + raise ValueError('Bad magic number') + + version = struct.unpack('>L', header[4:8])[0] + if version != 2: + raise ValueError('Bad version') + + pagesize = struct.unpack('>L', header[12:16])[0] + nkeys = struct.unpack('>L', header[56:60])[0] + + readkeys = 0 + page = 1 + db1 = [] + + while readkeys < nkeys: + f.seek(pagesize*page) + + offsets = f.read((nkeys+1) * 4 + 2) + offset_vals = [] + i = 0 + nval = 0 + val = 1 + keys = 0 + + while nval != val: + keys += 1 + key = struct.unpack(' MasterPasswordInfos: + """ + Extract the master password information from the database. + + :param db_filepath: the db filepath + :type db_filepath: str + :param db_version: the db_type, 3 or 4 + :type db_version: int + :return: the infos + :rtype: MasterPasswordInfos + """ + if db_version not in [3, 4]: + raise ValueError('db_version not supported') + + if db_version == 3: + db_values = read_bsd_db(db_filepath) + + global_salt = db_values[b'global-salt'] + pwd_check = db_values[b'password-check'] + entry_salt_len = pwd_check[1] + entry_salt = pwd_check[3: 3 + entry_salt_len] + cipher_text = pwd_check[-16:] + + no_master_password = is_decrypting_mozilla_3des_without_master_password(global_salt, entry_salt, cipher_text) + + return MasterPasswordInfos('3des', global_salt, entry_salt, cipher_text, no_master_password) + else: + db = sqlite3.connect(db_filepath) + c = db.cursor() + c.execute('SELECT item1,item2 FROM metadata WHERE id = "password"') + global_salt, encoded_item2 = c.fetchone() + decoded_item2 = decoder.decode(encoded_item2) + + pbe_algo = str(decoded_item2[0][0][0]) + if pbe_algo == '1.2.840.113549.1.12.5.1.3': # pbeWithSha1AndTripleDES-CBC + entry_salt = decoded_item2[0][0][1][0].asOctets() + cipher_text = decoded_item2[0][1].asOctets() + + no_master_password = is_decrypting_mozilla_3des_without_master_password(global_salt, entry_salt, + cipher_text) + return MasterPasswordInfos('3des', global_salt, entry_salt, cipher_text, no_master_password) + elif pbe_algo == '1.2.840.113549.1.5.13': # pkcs5 pbes2 + assert str(decoded_item2[0][0][1][0][0]) == '1.2.840.113549.1.5.12' + assert str(decoded_item2[0][0][1][0][1][3][0]) == '1.2.840.113549.2.9' + assert str(decoded_item2[0][0][1][1][0]) == '2.16.840.1.101.3.4.1.42' + assert int(decoded_item2[0][0][1][0][1][2]) == 32 # key length + + entry_salt = decoded_item2[0][0][1][0][1][0].asOctets() + iteration = int(decoded_item2[0][0][1][0][1][1]) + iv = b'\x04\x0e' + decoded_item2[0][0][1][1][1].asOctets() + cipher_text = decoded_item2[0][1].asOctets() + + no_master_password = is_decrypting_pbe_aes_without_password(global_salt, entry_salt, iteration, iv, + cipher_text) + return MasterPasswordInfos('aes', global_salt, entry_salt, cipher_text, no_master_password, iteration, iv) + +def hex(b) -> str: + """ + Returns the hexily version of the binary datas. + + :param b: binary datas + :return: the string + """ + return binascii.hexlify(b).decode('utf8') + + +def get_hashcat_string(mpinfos: MasterPasswordInfos) -> str: + """ + Print the hashchat format string. + + :param mpinfos: the infos + :type mpinfos: MasterPasswordInfos + :return: the string + :rtype: str + """ + + if mpinfos.no_master_password: + return 'No Primary Password is set.' + else: + s = '$mozilla$*' + + if mpinfos.mode == '3des': + s += f'3DES*{hex(mpinfos.global_salt)}*{hex(mpinfos.entry_salt)}*{hex(mpinfos.cipher_text)}' + else: + s += f'AES*{hex(mpinfos.global_salt)}*{hex(mpinfos.entry_salt)}*{mpinfos.iteration}*' \ + f'{hex(mpinfos.iv)}*{hex(mpinfos.cipher_text)}' + + return s + + +if __name__ == '__main__': + usage = 'python3 mozilla2hashcat.py \n' + parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, usage=usage) + parser.add_argument('dir_or_db', help='The directory containing the key3/4.db-file or the key3/4.db-file itself') + args = parser.parse_args() + + db_filepath = None + db_type = None + + if os.path.isdir(args.dir_or_db): + db3_filepath = os.path.join(args.dir_or_db, 'key3.db') + db4_filepath = os.path.join(args.dir_or_db, 'key4.db') + + if os.path.exists(db3_filepath): + db_filepath = db3_filepath + db_type = 3 + elif os.path.exists(db4_filepath): + db_filepath = db4_filepath + db_type = 4 + elif os.path.isfile(args.dir_or_db): + filename = os.path.basename(args.dir_or_db) + if filename == 'key3.db': + db_filepath = args.dir_or_db + db_type = 3 + elif filename == 'key4.db': + db_filepath = args.dir_or_db + db_type = 4 + + if not db_filepath: + sys.stderr.write('key3.db or key4.db file not found\n') + exit(-1) + + infos = extract_master_password_infos(db_filepath, db_type) + print(get_hashcat_string(infos)) diff --git a/tools/virtualbox2hashcat.py b/tools/virtualbox2hashcat.py old mode 100644 new mode 100755 diff --git a/tools/vmwarevmx2hashcat.py b/tools/vmwarevmx2hashcat.py old mode 100644 new mode 100755