如何读取 key3.db 文件?

信息安全 加密 密码 网页浏览器 数据库 火狐
2021-08-30 10:58:34

该文件key3.db包含用于加密存储在logins.json文件中的 Firefox 密码的密钥。

我不在 Firefox 中使用主密码,但根据这篇文章,我的密码是使用存储在key3.db文件中的随机密钥加密的。

我尝试使用 SqliteBrowser 打开文件,但出现错误:“文件格式无效”。谁能知道如何阅读此文件,以便我能够获取 Firefox 密码的密钥?

3个回答

如果没有设置主密码,Firefox 使用空密码。

有关用于解密和转储数据(以及更多)的脏 Python 脚本,请参见以下代码。这是我的一个旧项目,我刚刚更新了signons.sqlite之前(现在logins.json)保存的密码和用户名的读取。我不能保证(一切)都正常工作,但在我的测试用例中确实如此。

#!/usr/bin/env python

from ctypes import *
import struct
import base64
import platform
import os
import sqlite3
import json

# TODO: if python 3. use
#from configParser import ConfigParser
# else
from ConfigParser import ConfigParser

#
# Some little firefox forensic script delivering following data:
# - saved passwords (decrypted if no master pw is set)
# - visited pages
# - download history
# - form history
# - cookies 
#
# TODO:
# - flash cookies and stuff
# - finish comments
# - add little brute forcer for master pw
# - dump master pw ?
# - add some more compatibility stuff (mac if requested, else fuck it)
# - use some user defined exceptions instead plain exception
# - cache
# - think about which data from the db to return ;)
#
# ATTENTION: On my win7 system i needed a newer sqlite version.
# see http://code.activestate.com/lists/python-tutor/96183/
# - easy (and dirty) solution: get new sqlite.dll, replace in ...python/DLLS/
#
# CREDITS:
# - Decrypt saved passwords: https://github.com/pradeep1288/ffpasscracker
# - Myself for doing a .schema on every db to get the structure *yay for me*
#


#Password structures for firefox >= 3.5 ?!
class SECItem(Structure):
    _fields_ = [('type',c_uint),('data',c_void_p),('len',c_uint)]
class secuPWData(Structure):
    _fields_ = [('source',c_ubyte),('data',c_char_p)]
(SECWouldBlock,SECFailure,SECSuccess)=(-2,-1,0)
(PW_NONE,PW_FROMFILE,PW_PLAINTEXT,PW_EXTERNAL)=(0,1,2,3)


class FileNotFoundException(Exception): pass



class Firefox_data_generic(object):
    """
    Some general informations:
    - Every timestamp is a unix timestamp in millisec (/1000 to use with pythons time stuff)
    - 
    """

    def __init__(self, profilpath = None, compatibility_infos = None ):
        if platform.system().lower() == 'windows':
            find_profil_path =  self.__find_profile_path_windows
            load_libraries = self.__load_libraries_win
        else: # TODO. care about mac (which i don't atm)
            find_profil_path = self.__find_profile_path_linux
            load_libraries = self.__load_libraries_linux

        if profilpath is None:
            self.profilpath = find_profil_path()
        else:
            self.profilpath = profilpath
        self.default_profil = self.__get_default_profile()

        if compatibility_infos is None:
            self.ff_version, self.platform_dir, self.app_dir = self._get_info_from__compatibility_ini()
        else:
            self.ff_version, self.platform_dir, self.app_dir = compatibility_infos

        load_libraries(*self.ff_version)

        # set up for the specified ff version
        major, minor = self.ff_version
        # We don' care about stone age ff versions
        if self.ff_version < (3, 5):
            raise Exception('Nobody got time for implementing the features for this fucking old ff version')
        # download history moved from downloads.sqlite to places.sqlite
        if major >= 26:
            self.get_all_downloads = self.__get_all_downloads_places
            print 'get downloads from places.sqlite'
        else:
            self.get_all_downloads = self.__get_all_downloads_downloads
            print 'get downloads from downloads.sqlite'


        #print 'Profilepath is:', self.profilpath

    #############################################################
    # Placeholder
    #############################################################
    def get_all_downloads(self, profile = None):
        """ returns [(url, fileName, saveTo, size, endTime, done), ...]
        monkey patched to __get_all_downloads_downloads for version < 26 (TODO: ?)
        or else to __get_all_downloads_places
        """
        raise NotImplementedError('get_all_downloads not implemented')


    #############################################################
    # General working functions
    #############################################################  
    def _get_info_from__compatibility_ini(self, profile = None):
        if profile is None:
            profile = self.default_profil
        config_file = '/'.join((profile, 'compatibility.ini'))
        if not os.path.isfile(config_file):
            raise FileNotFoundException('compatibility.ini not found at "%s"' % config_file)
        config = ConfigParser()
        config.read(config_file) # TODO: catch return (list of succesfully read configs)
        major, minor = config.get('Compatibility', 'LastVersion').split('_')[0].split('.')[:2]
        # TODO: check if value exists
        return ( (int(major), int(minor)), config.get('Compatibility', 'LastPlatformDir'), config.get('Compatibility', 'LastAppDir'))

    def get_all_visited(self, profile = None):
        """ Return all visited urls in the form of [(url, visit_count, last_visit_date), ...]
        Ordered by date asc.
        If no profile path is suplied use the default one.
        Should work from v. 3.0 and above (TODO: ?)
        """
        if profile is None:
            profile = self.default_profil
        db_path = '/'.join((profile, 'places.sqlite'))
        if not os.path.isfile(db_path):
            return None # TODO: raise exception
        con = sqlite3.connect(db_path)
        ret = con.execute('SELECT url, visit_count, last_visit_date FROM moz_places where visit_count > 0 order by last_visit_date asc;').fetchall()
        con.close()
        return ret

    def search_history(self, custom_filter, profile = None):
        for elem in self.get_all_visited(profile):
            if custom_filter(elem):
                yield elem      


    def test_master_pw(self, password, profile = None):
        if profile is None:
            profile = self.default_profil
        self.libnss.NSS_Init(profile) # TODO: check for errors here ? Init it once for all functions ?
        keySlot = self.libnss.PK11_GetInternalKeySlot()
        ret = self.libnss.PK11_CheckUserPassword( c_int(keySlot), c_char_p(password)) == SECSuccess
        self.libnss.PK11_FreeSlot(c_int(keySlot))
        self.libnss.NSS_Shutdown()
        return ret

    def is_master_password_set(self, profile = None):
        return not self.test_master_pw('', profile)




    def get_saved_passes(self, profilpath = None, decrypt = True, masterpass = ''):
        """ Try to get and decrypt saved passwords.
        The result (if successfuly) is in the form:[(hostname, httpRealm, formSubmitURL, usernameField, passwordField, username, password, encType, timeCreated, timeLastUsed, timePasswordChanged, timesUsed), ...]
        username and password are decrypted if no master password is set or they are not encrypted (doh) or the decrypt flag is set to False.
        In this cases the unaltered data is returned.
        """
        # decrypting part from https://github.com/pradeep1288/ffpasscracker 
        # for db schema: http://www.securityxploded.com/firepasswordviewer.php#for_firefox_3_5_and_above
        #  or just do a .schema moz_logins in the signons.sqlite /* signons.sqlite is gone and replaced by logins.json */
        if profilpath is None:
            profilpath = self.default_profil


        db_path = '/'.join((profilpath, 'logins.json'))
        if not os.path.isfile(db_path):
            raise FileNotFoundException('signons.sqlite not found at "%s"' % db_path)
        with open(db_path, 'rb') as fd:
            login_data = json.load(fd)

        # care about 'would_block' (-2) ?
        if self.libnss.NSS_Init(profilpath) != SECSuccess:
            # TODO: react to this
            print """Error Initalizing NSS_Init,\n
            propably no usefull results"""
            raise Exception('NSS_Init failed') # TODO: error code ?

        # TODO: check errors
        slot = self.libnss.PK11_GetInternalKeySlot()
        self.libnss.PK11_CheckUserPassword(slot, masterpass)
        self.libnss.PK11_Authenticate(slot, True, 0)

        pwdata = secuPWData()
        pwdata.source = PW_NONE
        pwdata.data=0

        uname = SECItem()
        passwd = SECItem()
        dectext = SECItem()
        con = sqlite3.connect(db_path)
        ret = list()
        for row in login_data['logins']:        
            if not decrypt: # not encrypted
                row["encryptedUsername"] = base64.b64decode(row["encryptedUsername"])
                row["encryptedPassword"] = base64.b64decode(row["encryptedPassword"])
                ret.append(row)
                continue
            # decrypt it                 
            uname.data  = cast(c_char_p(base64.b64decode(row["encryptedUsername"])),c_void_p)
            uname.len = len(base64.b64decode(row["encryptedUsername"]))
            passwd.data = cast(c_char_p(base64.b64decode(row["encryptedPassword"])),c_void_p)
            passwd.len=len(base64.b64decode(row["encryptedPassword"]))
            if self.libnss.PK11SDR_Decrypt(byref(uname),byref(dectext), 0)==-1:
                raise Exception('Username decrypt exception with:' + str(row))
            row["encryptedUsername"] = string_at(dectext.data,dectext.len)
            if self.libnss.PK11SDR_Decrypt(byref(passwd),byref(dectext), 0)==-1:
                raise Exception('Password decrypt exception with:' + str(row))
            row["encryptedPassword"] = string_at(dectext.data, dectext.len)
            ret.append(row)
        self.libnss.NSS_Shutdown()
        con.close()
        return ret

    def get_all_cookies(self, profile = None): # yummy
        """ Returns all cookies in the form of [(baseDomain, appId, inBrowserElement, name, value, host, path, expiry, lastAccessed, creationTime, isSecure, isHttpOnly), ...]
        Should work for versions >= 3.0 (TODO: ?)
        """
        if profile is None:
            profile = self.default_profil
        db_path = '/'.join((profile, 'cookies.sqlite'))
        con = sqlite3.connect(db_path)
        ret = con.execute('SELECT baseDomain, appId, inBrowserElement, name, value, host, path, expiry, lastAccessed, creationTime, isSecure, isHttpOnly FROM moz_cookies;').fetchall()
        con.close()
        return ret

    def get_all_form_history(self, profile = None, orderBy = 'firstUsed', orderASC = False):
        """ Returns all form history in the for of: [(fieldname, value, timesUsed, firstUsed, lastUsed), ...]
        Should work for versions >= 3.0 (TODO: ?)
        """
        if profile is None:
            profile = self.default_profil
        db_path = '/'.join((profile, 'formhistory.sqlite'))
        con = sqlite3.connect(db_path)
        ret = con.execute('SELECT fieldname, value, timesUsed, firstUsed, lastUsed from moz_formhistory order by %s %s;' % (orderBy, ('ASC' if orderASC else 'DESC'))).fetchall()
        con.close()
        return ret

    def search_form_history(self, custom_filter, profile = None, orderBy = 'firstUsed', orderASC = False):
        for elem in self.get_all_form_history(profile, orderBy, orderASC):
            if custom_filter(elem):
                yield elem 

    #############################################################
    # Version and platform specific functions
    #############################################################
    def __find_profile_path_windows(self):
        """ Should work for win >= 2000 (TODO: ?)
        """
        # TODO: should work for Windows 2000, XP, Vista, and Windows 7, (win 8 TODO: ?)
        path = '/'.join((os.environ['APPDATA'], 'Mozilla/Firefox/Profiles'))
        if not os.path.isdir(path):
            raise FileNotFoundException('Profile folder "%s" not found.' % path)
        return path

    def __find_profile_path_linux(self):
        # TODO: check where the default profile path on different distros are
        path = '/'.join((os.environ['HOME'], '.mozilla/firefox'))
        if not os.path.isdir(path):
            raise FileNotFoundException('Profile folder "%s" not found.' % path)
        return path

    def __get_default_profile(self):
        try:
            profile = ( p for p in os.listdir(self.profilpath) if p.endswith('.default') and os.path.isdir('/'.join((self.profilpath, p)))  ).next()
            return '/'.join((self.profilpath, profile))
        except StopIteration:
            return None

    def __load_libraries_win(self, major, minor):
        #TODO: check relevant version changes
        os.environ['PATH'] = ';'.join([self.platform_dir, os.environ['PATH']])
        self.libnss = CDLL('/'.join((self.platform_dir, "nss3.dll")))

    def __load_libraries_linux(self, major, minor):
        #TODO: check relevant version changes
        #TODO: could that lib be somewhere else (not in path) ? (i.e: is this safe ?)
        self.libnss = CDLL("libnss3.so")

    def __get_all_downloads_downloads(self, profile = None):       
        #TODO: untested but should work
        if profile is None:
            profile = self.default_profil
        db_path = '/'.join((profile, 'downloads.sqlite'))
        con = sqlite3.connect(db_path)
        ret = con.execute('SELECT source, name, target, maxBytes, endTime, state FROM moz_downloads;').fetchall()
        con.close()
        return ret

    def __get_all_downloads_places(self, profile = None):
        if profile is None:
            profile = self.default_profil
        db_path = '/'.join((profile, 'places.sqlite'))
        con = sqlite3.connect(db_path)
        ret = list()
        for download in con.execute("""SELECT url, a.content, b.content, c.content from moz_annos a, moz_annos b, moz_annos c, moz_places p
            where a.place_id = b.place_id and b.place_id = c.place_id and p.id = a.place_id
            and a.anno_attribute_id = (select id from moz_anno_attributes where name = 'downloads/destinationFileName')
            and b.anno_attribute_id = (select id from moz_anno_attributes where name = 'downloads/destinationFileURI')
            and c.anno_attribute_id = (select id from moz_anno_attributes where name = 'downloads/metaData');
        """).fetchall():
            values = json.loads(download[3])
            ret.append( (download[0], download[1], download[2], values['fileSize'], values['endTime'], values['state']) )
        con.close()       
        return ret







if __name__ == '__main__':
    profile = "C:/Users/USERNAME/AppData/Roaming/Mozilla/Firefox/Profiles/FF_PROFILE"
    ff = Firefox_data_generic()
    print 'version and paths:'
    print ff._get_info_from__compatibility_ini()

    print 'Master password is set:', ff.is_master_password_set(profile=profile) 

    print "Test some password", ff.test_master_pw('test1234567')

    print 'passwords:'
    print ff.get_saved_passes(profilpath=profile, decrypt=True, masterpass="")

    print 'cookies:'
    print ff.get_all_cookies()

    #print 'form history:'    
    #print ff.get_all_form_history(orderBy='name', True)
    print 'form history from search bar:'
    print list(ff.search_form_history(lambda x: 'searchbar' in x[0]))

    #print 'visited sites:'    
    #print ff.get_all_visited()
    print 'visited sites with login in url:'
    print list(ff.search_history(lambda x: 'login' in x[0]))

    print 'downloads:'
    print ff.get_all_downloads()

它应该可以在 Windows 和 Linux 上运行,但我只是在 Windows 系统上测试了这些更改。

编辑:我将破解密码的脚本替换为一个仅转储不同信息的脚本(包括具有给定密码的解密形式的密码和用户名)

解密 Firefox 保存的密码数据库的指南

  1. git clone https://github.com/unode/firefox_decrypt.git
  2. cd firefox_decrypt
  3. python firefox_decrypt.py
  4. 如果您有主密码,请输入主密码,或者直接按回车键
  5. 享受

或者你也可以使用https://www.dumpzilla.org/ 👍

使用 libdb-utils(基于 rhel)或 libdb1-compat(基于 debian)包中的 db_dump185 命令

我知道这是一个老问题,但我自己一直在研究这个问题,想要一个问题的答案,而不是解决方法。

之所以如此困难,是因为这个 key3.db 文件实际上是 berkeley 数据库的旧版本,常规 db_dump 命令无法读取。为了阅读这个 1.85 版本的包,我需要使用几乎相同的 db_dump185 命令,这并不是很明显。我绕着圈子试图用 --enable-dump185 选项重新编译,而整个操作系统包都包含该选项,但需要一个单独的命令才能利用。

现在我打开了那个数据库,我仍然不确定如何处理里面的数据,这似乎与命令行工具使用 key3.db 和 logins.json 解密我的 Firefox 45.7.0 密码的问题相同?也有。我认为我目前的解决方案是不使用 (cert8|key3).db 而是改为使用 (cert9|key4).db,因为 sqlite3 似乎能够打开 key4.db 而无需大惊小怪。似乎 key4.db 中的元数据表包含 item1 和 item2 记录,我假设它们确实是加密/解密值进/出 logins.json 所需的密钥和 IV,但我仍然不确定如何在这一点上把所有这些放在一起。

希望如果我自己无法弄清楚这一点,则此信息可以帮助其他人找到我现在正在关注的当前兔子洞之后的答案。