From 4116f7d88cd68ef46191369745153df63e115087 Mon Sep 17 00:00:00 2001 From: AlessandroZ Date: Tue, 12 May 2020 14:39:43 +0200 Subject: [PATCH] support last firefox version (thks to @lclevy) --- Linux/lazagne/softwares/browsers/mozilla.py | 165 +++++++++----- Mac/lazagne/softwares/browsers/mozilla.py | 168 ++++++++------ Windows/lazagne/softwares/browsers/mozilla.py | 207 +++++++++++------- 3 files changed, 342 insertions(+), 198 deletions(-) diff --git a/Linux/lazagne/softwares/browsers/mozilla.py b/Linux/lazagne/softwares/browsers/mozilla.py index e13406f4..6d4e29c3 100755 --- a/Linux/lazagne/softwares/browsers/mozilla.py +++ b/Linux/lazagne/softwares/browsers/mozilla.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- # portable decryption functions and BSD DB parsing by Laurent Clevy (@lorenzo2472) # from https://github.com/lclevy/firepwd/blob/master/firepwd.py @@ -13,13 +13,14 @@ from lazagne.config.module_info import ModuleInfo from lazagne.config.crypto.pyDes import triple_des, CBC +from lazagne.config.crypto.pyaes import AESModeOfOperationCBC from lazagne.config.dico import get_dic from lazagne.config.constant import constant from pyasn1.codec.der import decoder from lazagne.config import homes from binascii import unhexlify from base64 import b64decode -from hashlib import sha1 +from hashlib import sha1, pbkdf2_hmac try: from ConfigParser import RawConfigParser # Python 2.7 @@ -29,6 +30,16 @@ if sys.version_info[0]: python_version = sys.version_info[0] +def l(n): + try: + return long(n) + except NameError: + return int(n) + + +CKA_ID = unhexlify('f8000000000000000000000000000001') +AES_BLOCK_SIZE = 16 + def convert_to_byte(s): if python_version == 2: @@ -53,6 +64,7 @@ def long_to_bytes(n, blocksize=0): """ # after much testing, this algorithm was deemed to be the fastest s = convert_to_byte('') + n = l(n) while n > 0: s = struct.pack('>I', n & 0xffffffff) + s n = n >> 32 @@ -101,7 +113,7 @@ def get_firefox_profiles(self, directory): else: # No "IsRelative" in profiles.ini profile_path = os.path.join(directory, cp.get(section, 'Path').strip()) - + if profile_path: profile_list.append(profile_path) @@ -117,9 +129,9 @@ def get_key(self, profile): try: row = None # Remove error when file is empty - with open(os.path.join(profile, 'key4.db'), 'rb') as f: + with open(os.path.join(profile, 'key4.db'), 'rb') as f: content = f.read() - + if content: conn = sqlite3.connect(os.path.join(profile, 'key4.db')) # Firefox 58.0.2 / NSS 3.35 with key4.db in SQLite c = conn.cursor() @@ -135,7 +147,7 @@ def get_key(self, profile): else: if row: - (global_salt, master_password, entry_salt) = self.manage_masterpassword(master_password=u'', key_data=row) + (global_salt, master_password, entry_salt) = self.manage_masterpassword(master_password=b'', key_data=row) if global_salt: try: @@ -144,29 +156,34 @@ def get_key(self, profile): for row in c: if row[0]: break + a11 = row[0] # CKA_VALUE a102 = row[1] # f8000000000000000000000000000001, CKA_ID - # a11 : CKA_VALUE - # a102 : f8000000000000000000000000000001, CKA_ID - # self.print_asn1(a11, len(a11), 0) - # SEQUENCE { - # SEQUENCE { - # OBJECTIDENTIFIER 1.2.840.113549.1.12.5.1.3 - # SEQUENCE { - # OCTETSTRING entry_salt_for_3des_key - # INTEGER 01 - # } - # } - # OCTETSTRING encrypted_3des_key (with 8 bytes of PKCS#7 padding) - # } - decoded_a11 = decoder.decode(a11) - entry_salt = decoded_a11[0][0][1][0].asOctets() - cipher_t = decoded_a11[0][1].asOctets() - key = self.decrypt_3des(global_salt, master_password, entry_salt, cipher_t) - if key: - self.debug(u'key: {key}'.format(key=repr(key))) - yield key[:24] + if python_version == 2: + a102 = str(a102) + + if a102 == CKA_ID: + # a11 : CKA_VALUE + # a102 : f8000000000000000000000000000001, CKA_ID + # self.print_asn1(a11, len(a11), 0) + # SEQUENCE { + # SEQUENCE { + # OBJECTIDENTIFIER 1.2.840.113549.1.12.5.1.3 + # SEQUENCE { + # OCTETSTRING entry_salt_for_3des_key + # INTEGER 01 + # } + # } + # OCTETSTRING encrypted_3des_key (with 8 bytes of PKCS#7 padding) + # } + decoded_a11 = decoder.decode(a11) + key = self.decrypt_3des(decoded_a11, master_password, global_salt) + if key: + self.debug(u'key: {key}'.format(key=repr(key))) + yield key[:24] + # else: + # Nothing saved except Exception: self.debug(traceback.format_exc()) @@ -212,6 +229,7 @@ def print_asn1(self, d, l, rl): skip = 1 else: skip = 0 + if type_ == 0x30: seq_len = length read_len = 0 @@ -284,21 +302,52 @@ def read_bsddb(self, name): return db @staticmethod - def decrypt_3des(global_salt, master_password, entry_salt, encrypted_data): + def decrypt_3des(decoded_item, master_password, global_salt): """ User master key is also encrypted (if provided, the master_password could be used to encrypt it) """ # See http://www.drh-consultancy.demon.co.uk/key3.html - hp = sha1(global_salt + convert_to_byte(master_password)).digest() - pes = entry_salt + convert_to_byte('\x00') * (20 - len(entry_salt)) - chp = sha1(hp + entry_salt).digest() - k1 = hmac.new(chp, pes + entry_salt, sha1).digest() - tk = hmac.new(chp, pes, sha1).digest() - k2 = hmac.new(chp, tk + entry_salt, sha1).digest() - k = k1 + k2 - iv = k[-8:] - key = k[:24] - return triple_des(key, CBC, iv).decrypt(encrypted_data) + pbeAlgo = str(decoded_item[0][0][0]) + if pbeAlgo == '1.2.840.113549.1.12.5.1.3': # pbeWithSha1AndTripleDES-CBC + entry_salt = decoded_item[0][0][1][0].asOctets() + cipher_t = decoded_item[0][1].asOctets() + + # See http://www.drh-consultancy.demon.co.uk/key3.html + hp = sha1(global_salt + convert_to_byte(master_password)).digest() + pes = entry_salt + convert_to_byte('\x00') * (20 - len(entry_salt)) + chp = sha1(hp + entry_salt).digest() + k1 = hmac.new(chp, pes + entry_salt, sha1).digest() + tk = hmac.new(chp, pes, sha1).digest() + k2 = hmac.new(chp, tk + entry_salt, sha1).digest() + k = k1 + k2 + iv = k[-8:] + key = k[:24] + return triple_des(key, CBC, iv).decrypt(cipher_t) + + # New version + elif pbeAlgo == '1.2.840.113549.1.5.13': # pkcs5 pbes2 + + assert str(decoded_item[0][0][1][0][0]) == '1.2.840.113549.1.5.12' + assert str(decoded_item[0][0][1][0][1][3][0]) == '1.2.840.113549.2.9' + assert str(decoded_item[0][0][1][1][0]) == '2.16.840.1.101.3.4.1.42' + # https://tools.ietf.org/html/rfc8018#page-23 + entry_salt = decoded_item[0][0][1][0][1][0].asOctets() + iteration_count = int(decoded_item[0][0][1][0][1][1]) + key_length = int(decoded_item[0][0][1][0][1][2]) + assert key_length == 32 + + k = sha1(global_salt + master_password).digest() + key = pbkdf2_hmac('sha256', k, entry_salt, iteration_count, dklen=key_length) + + # https://hg.mozilla.org/projects/nss/rev/fc636973ad06392d11597620b602779b4af312f6#l6.49 + iv = b'\x04\x0e' + decoded_item[0][0][1][1][1].asOctets() + # 04 is OCTETSTRING, 0x0e is length == 14 + encrypted_value = decoded_item[0][1].asOctets() + aes = AESModeOfOperationCBC(key, iv=iv) + cleartxt = b"".join([aes.decrypt(encrypted_value[i:i + AES_BLOCK_SIZE]) + for i in range(0, len(encrypted_value), AES_BLOCK_SIZE)]) + + return cleartxt def extract_secret_key(self, key_data, global_salt, master_password, entry_salt): @@ -309,13 +358,11 @@ def extract_secret_key(self, key_data, global_salt, master_password, entry_salt) salt_len = o(priv_key_entry[1]) name_len = o(priv_key_entry[2]) priv_key_entry_asn1 = decoder.decode(priv_key_entry[3 + salt_len + name_len:]) - data = priv_key_entry[3 + salt_len + name_len:] + # data = priv_key_entry[3 + salt_len + name_len:] # self.print_asn1(data, len(data), 0) # See https://github.com/philsmd/pswRecovery4Moz/blob/master/pswRecovery4Moz.txt - entry_salt = priv_key_entry_asn1[0][0][1][0].asOctets() - priv_key_data = priv_key_entry_asn1[0][1].asOctets() - priv_key = self.decrypt_3des(global_salt, master_password, entry_salt, priv_key_data) + priv_key = self.decrypt_3des(priv_key_entry_asn1, master_password, global_salt) # self.print_asn1(priv_key, len(priv_key), 0) priv_key_asn1 = decoder.decode(priv_key) pr_key = priv_key_asn1[0][2].asOctets() @@ -360,6 +407,7 @@ def get_login_data(self, profile): except Exception: self.debug(traceback.format_exc()) return [] + # Using sqlite3 database for row in c: enc_username = row[6] @@ -367,7 +415,7 @@ def get_login_data(self, profile): logins.append((self.decode_login_data(enc_username), self.decode_login_data(enc_password), row[1])) return logins - def manage_masterpassword(self, master_password=u'', key_data=None, new_version=True): + def manage_masterpassword(self, master_password=b'', key_data=None, new_version=True): """ Check if a master password is set. If so, try to find it using a dictionary attack @@ -375,25 +423,28 @@ def manage_masterpassword(self, master_password=u'', key_data=None, new_version= (global_salt, master_password, entry_salt) = self.is_master_password_correct(master_password=master_password, key_data=key_data, new_version=new_version) + if not global_salt: - self.warning(u'Master Password is used !') + self.info(u'Master Password is used !') (global_salt, master_password, entry_salt) = self.brute_master_password(key_data=key_data, new_version=new_version) if not master_password: - return u'', u'', u'' + return '', '', '' return global_salt, master_password, entry_salt - def is_master_password_correct(self, key_data, master_password=u'', new_version=True): + def is_master_password_correct(self, key_data, master_password=b'', new_version=True): try: + entry_salt = b"" if not new_version: # See http://www.drh-consultancy.demon.co.uk/key3.html pwd_check = key_data.get(b'password-check') if not pwd_check: - return u'', u'', u'' - entry_salt_len = o(pwd_check[1]) - entry_salt = pwd_check[3: 3 + entry_salt_len] - encrypted_passwd = pwd_check[-16:] + return '', '', '' + # Hope not breaking something (not tested for old version) + # entry_salt_len = o(pwd_check[1]) + # entry_salt = pwd_check[3: 3 + entry_salt_len] + # encrypted_passwd = pwd_check[-16:] global_salt = key_data[b'global-salt'] else: @@ -411,23 +462,21 @@ def is_master_password_correct(self, key_data, master_password=u'', new_version= # OCTETSTRING encrypted_password_check # } decoded_item2 = decoder.decode(item2) - entry_salt = decoded_item2[0][0][1][0].asOctets() - encrypted_passwd = decoded_item2[0][1].asOctets() - cleartext_data = self.decrypt_3des(global_salt, master_password, entry_salt, encrypted_passwd) + cleartext_data = self.decrypt_3des(decoded_item2, master_password, global_salt) if cleartext_data != convert_to_byte('password-check\x02\x02'): - return u'', u'', u'' + return '', '', '' return global_salt, master_password, entry_salt except Exception: self.debug(traceback.format_exc()) - return u'', u'', u'' + return '', '', '' def brute_master_password(self, key_data, new_version=True): """ Try to find master_password doing a dictionary attack using the 500 most used passwords """ - wordlist = constant.passwordFound + get_dic() + wordlist = constant.password_found + get_dic() num_lines = (len(wordlist) - 1) self.info(u'%d most used passwords !!! ' % num_lines) @@ -440,7 +489,7 @@ def brute_master_password(self, key_data, new_version=True): return global_salt, master_password, entry_salt self.warning(u'No password has been found using the default list') - return u'', u'', u'' + return '', '', '' def remove_padding(self, data): """ @@ -486,8 +535,8 @@ def run(self): pwd_found.append( { 'URL': url, - 'Login': self.decrypt(key=key, iv=user[1], ciphertext=user[2]).decode('utf8'), - 'Password': self.decrypt(key=key, iv=password[1], ciphertext=password[2]).decode('utf8'), + 'Login': self.decrypt(key=key, iv=user[1], ciphertext=user[2]).decode('utf-8'), + 'Password': self.decrypt(key=key, iv=password[1], ciphertext=password[2]).decode('utf-8'), } ) except Exception: diff --git a/Mac/lazagne/softwares/browsers/mozilla.py b/Mac/lazagne/softwares/browsers/mozilla.py index 2aef3f5b..6e0b1752 100755 --- a/Mac/lazagne/softwares/browsers/mozilla.py +++ b/Mac/lazagne/softwares/browsers/mozilla.py @@ -9,26 +9,36 @@ import struct import sys import traceback -from base64 import b64decode -from binascii import unhexlify -from hashlib import sha1 - -from pyasn1.codec.der import decoder +import os -from lazagne.config.constant import constant +from lazagne.config.module_info import ModuleInfo from lazagne.config.crypto.pyDes import triple_des, CBC +from lazagne.config.crypto.pyaes import AESModeOfOperationCBC from lazagne.config.dico import get_dic -from lazagne.config.module_info import ModuleInfo +from lazagne.config.constant import constant +from pyasn1.codec.der import decoder +from binascii import unhexlify +from base64 import b64decode +from hashlib import sha1, pbkdf2_hmac try: from ConfigParser import RawConfigParser # Python 2.7 except ImportError: from configparser import RawConfigParser # Python 3 -import os if sys.version_info[0]: python_version = sys.version_info[0] +def l(n): + try: + return long(n) + except NameError: + return int(n) + + +CKA_ID = unhexlify('f8000000000000000000000000000001') +AES_BLOCK_SIZE = 16 + def convert_to_byte(s): if python_version == 2: @@ -37,13 +47,6 @@ def convert_to_byte(s): return s.encode() -def l(n): - try: - return long(n) - except NameError: - return int(n) - - def o(c): if python_version == 2: return ord(c) @@ -94,6 +97,7 @@ def get_firefox_profiles(self, directory): """ cp = RawConfigParser() profile_list = [] + try: cp.read(os.path.join(directory, 'profiles.ini')) for section in cp.sections(): @@ -142,7 +146,8 @@ def get_key(self, profile): else: if row: - (global_salt, master_password, entrySalt) = self.manage_masterpassword(master_password=u'', key_data=row) + (global_salt, master_password, entry_salt) = self.manage_masterpassword(master_password=b'', key_data=row) + if global_salt: try: # Decrypt 3DES key to decrypt "logins.json" content @@ -150,25 +155,35 @@ def get_key(self, profile): for row in c: if row[0]: break + a11 = row[0] # CKA_VALUE a102 = row[1] # f8000000000000000000000000000001, CKA_ID - # SEQUENCE { - # SEQUENCE { - # OBJECTIDENTIFIER 1.2.840.113549.1.12.5.1.3 - # SEQUENCE { - # OCTETSTRING entry_salt_for_3des_key - # INTEGER 01 - # } - # } - # OCTETSTRING encrypted_3des_key (with 8 bytes of PKCS#7 padding) - # } - decoded_a11 = decoder.decode(a11) - entry_salt = decoded_a11[0][0][1][0].asOctets() - cipher_t = decoded_a11[0][1].asOctets() - key = self.decrypt_3des(global_salt, master_password, entry_salt, cipher_t) - if key: - self.debug(u'key: {key}'.format(key=repr(key))) - yield key[:24] + + if python_version == 2: + a102 = str(a102) + + if a102 == CKA_ID: + # a11 : CKA_VALUE + # a102 : f8000000000000000000000000000001, CKA_ID + # self.print_asn1(a11, len(a11), 0) + # SEQUENCE { + # SEQUENCE { + # OBJECTIDENTIFIER 1.2.840.113549.1.12.5.1.3 + # SEQUENCE { + # OCTETSTRING entry_salt_for_3des_key + # INTEGER 01 + # } + # } + # OCTETSTRING encrypted_3des_key (with 8 bytes of PKCS#7 padding) + # } + decoded_a11 = decoder.decode(a11) + key = self.decrypt_3des(decoded_a11, master_password, global_salt) + if key: + self.debug(u'key: {key}'.format(key=repr(key))) + yield key[:24] + # else: + # Nothing saved + except Exception: self.debug(traceback.format_exc()) @@ -286,21 +301,52 @@ def read_bsddb(self, name): return db @staticmethod - def decrypt_3des(global_salt, master_password, entry_salt, encrypted_data): + def decrypt_3des(decoded_item, master_password, global_salt): """ User master key is also encrypted (if provided, the master_password could be used to encrypt it) """ # See http://www.drh-consultancy.demon.co.uk/key3.html - hp = sha1(global_salt + convert_to_byte(master_password)).digest() - pes = entry_salt + convert_to_byte('\x00') * (20 - len(entry_salt)) - chp = sha1(hp + entry_salt).digest() - k1 = hmac.new(chp, pes + entry_salt, sha1).digest() - tk = hmac.new(chp, pes, sha1).digest() - k2 = hmac.new(chp, tk + entry_salt, sha1).digest() - k = k1 + k2 - iv = k[-8:] - key = k[:24] - return triple_des(key, CBC, iv).decrypt(encrypted_data) + pbeAlgo = str(decoded_item[0][0][0]) + if pbeAlgo == '1.2.840.113549.1.12.5.1.3': # pbeWithSha1AndTripleDES-CBC + entry_salt = decoded_item[0][0][1][0].asOctets() + cipher_t = decoded_item[0][1].asOctets() + + # See http://www.drh-consultancy.demon.co.uk/key3.html + hp = sha1(global_salt + convert_to_byte(master_password)).digest() + pes = entry_salt + convert_to_byte('\x00') * (20 - len(entry_salt)) + chp = sha1(hp + entry_salt).digest() + k1 = hmac.new(chp, pes + entry_salt, sha1).digest() + tk = hmac.new(chp, pes, sha1).digest() + k2 = hmac.new(chp, tk + entry_salt, sha1).digest() + k = k1 + k2 + iv = k[-8:] + key = k[:24] + return triple_des(key, CBC, iv).decrypt(cipher_t) + + # New version + elif pbeAlgo == '1.2.840.113549.1.5.13': # pkcs5 pbes2 + + assert str(decoded_item[0][0][1][0][0]) == '1.2.840.113549.1.5.12' + assert str(decoded_item[0][0][1][0][1][3][0]) == '1.2.840.113549.2.9' + assert str(decoded_item[0][0][1][1][0]) == '2.16.840.1.101.3.4.1.42' + # https://tools.ietf.org/html/rfc8018#page-23 + entry_salt = decoded_item[0][0][1][0][1][0].asOctets() + iteration_count = int(decoded_item[0][0][1][0][1][1]) + key_length = int(decoded_item[0][0][1][0][1][2]) + assert key_length == 32 + + k = sha1(global_salt + master_password).digest() + key = pbkdf2_hmac('sha256', k, entry_salt, iteration_count, dklen=key_length) + + # https://hg.mozilla.org/projects/nss/rev/fc636973ad06392d11597620b602779b4af312f6#l6.49 + iv = b'\x04\x0e' + decoded_item[0][0][1][1][1].asOctets() + # 04 is OCTETSTRING, 0x0e is length == 14 + encrypted_value = decoded_item[0][1].asOctets() + aes = AESModeOfOperationCBC(key, iv=iv) + cleartxt = b"".join([aes.decrypt(encrypted_value[i:i + AES_BLOCK_SIZE]) + for i in range(0, len(encrypted_value), AES_BLOCK_SIZE)]) + + return cleartxt def extract_secret_key(self, key_data, global_salt, master_password, entry_salt): @@ -311,13 +357,11 @@ def extract_secret_key(self, key_data, global_salt, master_password, entry_salt) salt_len = o(priv_key_entry[1]) name_len = o(priv_key_entry[2]) priv_key_entry_asn1 = decoder.decode(priv_key_entry[3 + salt_len + name_len:]) - data = priv_key_entry[3 + salt_len + name_len:] + # data = priv_key_entry[3 + salt_len + name_len:] # self.print_asn1(data, len(data), 0) # See https://github.com/philsmd/pswRecovery4Moz/blob/master/pswRecovery4Moz.txt - entry_salt = priv_key_entry_asn1[0][0][1][0].asOctets() - priv_key_data = priv_key_entry_asn1[0][1].asOctets() - priv_key = self.decrypt_3des(global_salt, master_password, entry_salt, priv_key_data) + priv_key = self.decrypt_3des(priv_key_entry_asn1, master_password, global_salt) # self.print_asn1(priv_key, len(priv_key), 0) priv_key_asn1 = decoder.decode(priv_key) pr_key = priv_key_asn1[0][2].asOctets() @@ -370,7 +414,7 @@ def get_login_data(self, profile): logins.append((self.decode_login_data(enc_username), self.decode_login_data(enc_password), row[1])) return logins - def manage_masterpassword(self, master_password=u'', key_data=None, new_version=True): + def manage_masterpassword(self, master_password=b'', key_data=None, new_version=True): """ Check if a master password is set. If so, try to find it using a dictionary attack @@ -380,24 +424,26 @@ def manage_masterpassword(self, master_password=u'', key_data=None, new_version= new_version=new_version) if not global_salt: - self.warning(u'Master Password is used !') + self.info(u'Master Password is used !') (global_salt, master_password, entry_salt) = self.brute_master_password(key_data=key_data, new_version=new_version) if not master_password: - return u'', u'', u'' + return '', '', '' return global_salt, master_password, entry_salt - def is_master_password_correct(self, key_data, master_password=u'', new_version=True): + def is_master_password_correct(self, key_data, master_password=b'', new_version=True): try: + entry_salt = b"" if not new_version: # See http://www.drh-consultancy.demon.co.uk/key3.html pwd_check = key_data.get(b'password-check') if not pwd_check: - return u'', u'', u'' - entry_salt_len = o(pwd_check[1]) - entry_salt = pwd_check[3: 3 + entry_salt_len] - encrypted_passwd = pwd_check[-16:] + return '', '', '' + # Hope not breaking something (not tested for old version) + # entry_salt_len = o(pwd_check[1]) + # entry_salt = pwd_check[3: 3 + entry_salt_len] + # encrypted_passwd = pwd_check[-16:] global_salt = key_data[b'global-salt'] else: @@ -415,17 +461,15 @@ def is_master_password_correct(self, key_data, master_password=u'', new_version= # OCTETSTRING encrypted_password_check # } decoded_item2 = decoder.decode(item2) - entry_salt = decoded_item2[0][0][1][0].asOctets() - encrypted_passwd = decoded_item2[0][1].asOctets() - cleartext_data = self.decrypt_3des(global_salt, master_password, entry_salt, encrypted_passwd) + cleartext_data = self.decrypt_3des(decoded_item2, master_password, global_salt) if cleartext_data != convert_to_byte('password-check\x02\x02'): - return u'', u'', u'' + return '', '', '' return global_salt, master_password, entry_salt except Exception: self.debug(traceback.format_exc()) - return u'', u'', u'' + return '', '', '' def brute_master_password(self, key_data, new_version=True): """ @@ -444,7 +488,7 @@ def brute_master_password(self, key_data, new_version=True): return global_salt, master_password, entry_salt self.warning(u'No password has been found using the default list') - return u'', u'', u'' + return '', '', '' @staticmethod def remove_padding(self, data): diff --git a/Windows/lazagne/softwares/browsers/mozilla.py b/Windows/lazagne/softwares/browsers/mozilla.py index 58fa158a..d7e69605 100755 --- a/Windows/lazagne/softwares/browsers/mozilla.py +++ b/Windows/lazagne/softwares/browsers/mozilla.py @@ -7,25 +7,28 @@ import json import sqlite3 import struct +import sys import traceback -from base64 import b64decode -from binascii import unhexlify -from hashlib import sha1 - -from pyasn1.codec.der import decoder +import os -from lazagne.config.constant import constant +from lazagne.config.module_info import ModuleInfo from lazagne.config.crypto.pyDes import triple_des, CBC +from lazagne.config.crypto.pyaes import AESModeOfOperationCBC from lazagne.config.dico import get_dic -from lazagne.config.module_info import ModuleInfo +from lazagne.config.constant import constant +from pyasn1.codec.der import decoder +from binascii import unhexlify +from base64 import b64decode from lazagne.config.winstructure import char_to_int, convert_to_byte +from hashlib import sha1, pbkdf2_hmac try: from ConfigParser import RawConfigParser # Python 2.7 except ImportError: from configparser import RawConfigParser # Python 3 -import os +if sys.version_info[0]: + python_version = sys.version_info[0] def l(n): try: @@ -34,6 +37,10 @@ def l(n): return int(n) +CKA_ID = unhexlify('f8000000000000000000000000000001') +AES_BLOCK_SIZE = 16 + + def long_to_bytes(n, blocksize=0): """long_to_bytes(n:long, blocksize:int) : string Convert a long integer to a byte string. @@ -69,7 +76,7 @@ class Mozilla(ModuleInfo): def __init__(self, browser_name, path): self.path = path - ModuleInfo.__init__(self, browser_name, 'browsers') + ModuleInfo.__init__(self, browser_name, category='browsers') def get_firefox_profiles(self, directory): """ @@ -77,6 +84,7 @@ def get_firefox_profiles(self, directory): """ cp = RawConfigParser() profile_list = [] + try: cp.read(os.path.join(directory, 'profiles.ini')) for section in cp.sections(): @@ -123,51 +131,66 @@ def get_key(self, profile): except Exception: self.debug(traceback.format_exc()) + else: if row: - (global_salt, master_password, entry_salt) = self.manage_masterpassword(master_password='', key_data=row) + (global_salt, master_password, entry_salt) = self.manage_masterpassword(master_password=b'', key_data=row) if global_salt: - # Decrypt 3DES key to decrypt "logins.json" content - c.execute("SELECT a11,a102 FROM nssPrivate;") - for row in c: - if row[0]: - break - a11 = row[0] # CKA_VALUE - a102 = row[1] # f8000000000000000000000000000001, CKA_ID - # self.print_asn1(a11, len(a11), 0) - # SEQUENCE { - # SEQUENCE { - # OBJECTIDENTIFIER 1.2.840.113549.1.12.5.1.3 - # SEQUENCE { - # OCTETSTRING entry_salt_for_3des_key - # INTEGER 01 - # } - # } - # OCTETSTRING encrypted_3des_key (with 8 bytes of PKCS#7 padding) - # } - decoded_a11 = decoder.decode(a11) - entry_salt = decoded_a11[0][0][1][0].asOctets() - cipher_t = decoded_a11[0][1].asOctets() - key = self.decrypt_3des(global_salt, master_password, entry_salt, cipher_t) + try: + # Decrypt 3DES key to decrypt "logins.json" content + c.execute("SELECT a11,a102 FROM nssPrivate;") + for row in c: + if row[0]: + break + + a11 = row[0] # CKA_VALUE + a102 = row[1] # f8000000000000000000000000000001, CKA_ID + + if python_version == 2: + a102 = str(a102) + + if a102 == CKA_ID: + # a11 : CKA_VALUE + # a102 : f8000000000000000000000000000001, CKA_ID + # self.print_asn1(a11, len(a11), 0) + # SEQUENCE { + # SEQUENCE { + # OBJECTIDENTIFIER 1.2.840.113549.1.12.5.1.3 + # SEQUENCE { + # OCTETSTRING entry_salt_for_3des_key + # INTEGER 01 + # } + # } + # OCTETSTRING encrypted_3des_key (with 8 bytes of PKCS#7 padding) + # } + decoded_a11 = decoder.decode(a11) + key = self.decrypt_3des(decoded_a11, master_password, global_salt) + if key: + self.debug(u'key: {key}'.format(key=repr(key))) + yield key[:24] + # else: + # Nothing saved + + except Exception: + self.debug(traceback.format_exc()) + + try: + key3_file = os.path.join(profile, 'key3.db') + if os.path.exists(key3_file): + key_data = self.read_bsddb(key3_file) + # Check masterpassword + (global_salt, master_password, entry_salt) = self.manage_masterpassword(master_password=u'', + key_data=key_data, + new_version=False) + if global_salt: + key = self.extract_secret_key(key_data=key_data, + global_salt=global_salt, + master_password=master_password, + entry_salt=entry_salt) if key: self.debug(u'key: {key}'.format(key=repr(key))) yield key[:24] - - try: - key_data = self.read_bsddb(os.path.join(profile, 'key3.db')) - # Check masterpassword - (global_salt, master_password, entry_salt) = self.manage_masterpassword(master_password='', - key_data=key_data, - new_version=False) - if global_salt: - key = self.extract_secret_key(key_data=key_data, - global_salt=global_salt, - master_password=master_password, - entry_salt=entry_salt) - if key: - self.debug(u'key: {key}'.format(key=repr(key))) - yield key[:24] except Exception: self.debug(traceback.format_exc()) @@ -218,12 +241,12 @@ def read_bsddb(self, name): header = f.read(4 * 15) magic = self.get_long_be(header, 0) if magic != 0x61561: - self.error(u'Bad magic number') + self.warning(u'Bad magic number') return False version = self.get_long_be(header, 4) if version != 2: - self.error(u'Bad version !=2 (1.85)') + self.warning(u'Bad version !=2 (1.85)') return False pagesize = self.get_long_be(header, 12) @@ -266,21 +289,52 @@ def read_bsddb(self, name): return db @staticmethod - def decrypt_3des(global_salt, master_password, entry_salt, encrypted_data): + def decrypt_3des(decoded_item, master_password, global_salt): """ User master key is also encrypted (if provided, the master_password could be used to encrypt it) """ # See http://www.drh-consultancy.demon.co.uk/key3.html - hp = sha1(global_salt + master_password.encode()).digest() - pes = entry_salt + convert_to_byte('\x00') * (20 - len(entry_salt)) - chp = sha1(hp + entry_salt).digest() - k1 = hmac.new(chp, pes + entry_salt, sha1).digest() - tk = hmac.new(chp, pes, sha1).digest() - k2 = hmac.new(chp, tk + entry_salt, sha1).digest() - k = k1 + k2 - iv = k[-8:] - key = k[:24] - return triple_des(key, CBC, iv).decrypt(encrypted_data) + pbeAlgo = str(decoded_item[0][0][0]) + if pbeAlgo == '1.2.840.113549.1.12.5.1.3': # pbeWithSha1AndTripleDES-CBC + entry_salt = decoded_item[0][0][1][0].asOctets() + cipher_t = decoded_item[0][1].asOctets() + + # See http://www.drh-consultancy.demon.co.uk/key3.html + hp = sha1(global_salt + convert_to_byte(master_password)).digest() + pes = entry_salt + convert_to_byte('\x00') * (20 - len(entry_salt)) + chp = sha1(hp + entry_salt).digest() + k1 = hmac.new(chp, pes + entry_salt, sha1).digest() + tk = hmac.new(chp, pes, sha1).digest() + k2 = hmac.new(chp, tk + entry_salt, sha1).digest() + k = k1 + k2 + iv = k[-8:] + key = k[:24] + return triple_des(key, CBC, iv).decrypt(cipher_t) + + # New version + elif pbeAlgo == '1.2.840.113549.1.5.13': # pkcs5 pbes2 + + assert str(decoded_item[0][0][1][0][0]) == '1.2.840.113549.1.5.12' + assert str(decoded_item[0][0][1][0][1][3][0]) == '1.2.840.113549.2.9' + assert str(decoded_item[0][0][1][1][0]) == '2.16.840.1.101.3.4.1.42' + # https://tools.ietf.org/html/rfc8018#page-23 + entry_salt = decoded_item[0][0][1][0][1][0].asOctets() + iteration_count = int(decoded_item[0][0][1][0][1][1]) + key_length = int(decoded_item[0][0][1][0][1][2]) + assert key_length == 32 + + k = sha1(global_salt + master_password).digest() + key = pbkdf2_hmac('sha256', k, entry_salt, iteration_count, dklen=key_length) + + # https://hg.mozilla.org/projects/nss/rev/fc636973ad06392d11597620b602779b4af312f6#l6.49 + iv = b'\x04\x0e' + decoded_item[0][0][1][1][1].asOctets() + # 04 is OCTETSTRING, 0x0e is length == 14 + encrypted_value = decoded_item[0][1].asOctets() + aes = AESModeOfOperationCBC(key, iv=iv) + cleartxt = b"".join([aes.decrypt(encrypted_value[i:i + AES_BLOCK_SIZE]) + for i in range(0, len(encrypted_value), AES_BLOCK_SIZE)]) + + return cleartxt def extract_secret_key(self, key_data, global_salt, master_password, entry_salt): @@ -291,13 +345,11 @@ def extract_secret_key(self, key_data, global_salt, master_password, entry_salt) salt_len = char_to_int(priv_key_entry[1]) name_len = char_to_int(priv_key_entry[2]) priv_key_entry_asn1 = decoder.decode(priv_key_entry[3 + salt_len + name_len:]) - data = priv_key_entry[3 + salt_len + name_len:] + # data = priv_key_entry[3 + salt_len + name_len:] # self.print_asn1(data, len(data), 0) # See https://github.com/philsmd/pswRecovery4Moz/blob/master/pswRecovery4Moz.txt - entry_salt = priv_key_entry_asn1[0][0][1][0].asOctets() - priv_key_data = priv_key_entry_asn1[0][1].asOctets() - priv_key = self.decrypt_3des(global_salt, master_password, entry_salt, priv_key_data) + priv_key = self.decrypt_3des(priv_key_entry_asn1, master_password, global_salt) # self.print_asn1(priv_key, len(priv_key), 0) priv_key_asn1 = decoder.decode(priv_key) pr_key = priv_key_asn1[0][2].asOctets() @@ -350,7 +402,7 @@ def get_login_data(self, profile): logins.append((self.decode_login_data(enc_username), self.decode_login_data(enc_password), row[1])) return logins - def manage_masterpassword(self, master_password='', key_data=None, new_version=True): + def manage_masterpassword(self, master_password=b'', key_data=None, new_version=True): """ Check if a master password is set. If so, try to find it using a dictionary attack @@ -368,16 +420,18 @@ def manage_masterpassword(self, master_password='', key_data=None, new_version=T return global_salt, master_password, entry_salt - def is_master_password_correct(self, key_data, master_password='', new_version=True): + def is_master_password_correct(self, key_data, master_password=b'', new_version=True): try: + entry_salt = b"" if not new_version: # See http://www.drh-consultancy.demon.co.uk/key3.html pwd_check = key_data.get(b'password-check') if not pwd_check: return '', '', '' - entry_salt_len = char_to_int(pwd_check[1]) - entry_salt = pwd_check[3: 3 + entry_salt_len] - encrypted_passwd = pwd_check[-16:] + # Hope not breaking something (not tested for old version) + # entry_salt_len = char_to_int(pwd_check[1]) + # entry_salt = pwd_check[3: 3 + entry_salt_len] + # encrypted_passwd = pwd_check[-16:] global_salt = key_data[b'global-salt'] else: @@ -395,10 +449,8 @@ def is_master_password_correct(self, key_data, master_password='', new_version=T # OCTETSTRING encrypted_password_check # } decoded_item2 = decoder.decode(item2) - entry_salt = decoded_item2[0][0][1][0].asOctets() - encrypted_passwd = decoded_item2[0][1].asOctets() - cleartext_data = self.decrypt_3des(global_salt, master_password, entry_salt, encrypted_passwd) + cleartext_data = self.decrypt_3des(decoded_item2, master_password, global_salt) if cleartext_data != convert_to_byte('password-check\x02\x02'): return '', '', '' @@ -420,7 +472,7 @@ def brute_master_password(self, key_data, new_version=True): master_password=word.strip(), new_version=new_version) if master_password: - self.debug(u'Master password found: {}'.format(master_password)) + self.info(u'Master password found: {}'.format(master_password)) return global_salt, master_password, entry_salt self.warning(u'No password has been found using the default list') @@ -452,7 +504,6 @@ def run(self): """ Main function """ - # path = self.get_path(software_name) pwd_found = [] self.path = self.path.format(**constant.profile) if os.path.exists(self.path): @@ -466,11 +517,11 @@ def run(self): try: pwd_found.append({ 'URL': url, - 'Login': self.decrypt(key=key, iv=user[1], ciphertext=user[2]).decode("utf-8"), - 'Password': self.decrypt(key=key, iv=passw[1], ciphertext=passw[2]).decode("utf-8"), + 'Login': self.decrypt(key=key, iv=user[1], ciphertext=user[2]).decode('utf-8'), + 'Password': self.decrypt(key=key, iv=passw[1], ciphertext=passw[2]).decode('utf-8'), }) - except Exception as e: - self.debug(u'An error occurred decrypting the password: {error}'.format(error=e)) + except Exception: + self.debug(u'An error occured decrypting the password: {error}'.format(error=traceback.format_exc())) else: self.info(u'Database empty')