cookieJar

a fun aspect of pentesting is finding interesting ways to get authenticated sessions running. usually, a session authentication relies on the user entering some kind of key (password) that corresponds to their user ID (username). however, in some cases an attacker can “revive” sessions using just the session cookies.

i wrote cookieJar for exactly this purpose: extracting cookies from various browsers (and factor in the OS) and then use them to create authenticated sessions. the script handles different formats and encryption methods (see: Cryptodome) and can extract cookies related to all websites or a specific domain.

some limitiations of the script:

if browsers update their cookie storage mechanisms (very likely), the script will stop working.

accounts with 2FA will stop the script from working.

sessions are managed server-side, so if specific parameters like user-agent, IP address, etc. don’t line up, the script will stop working.

consent + security risks…

cookieJar uses several libraries and modules to retrieve, decrypt, and manage browser cookies across multiple browsers and operating systems. arguably, the most important module i’ve used is Cryptodome, which is included in the source repo.

Cryptodome

the Cryptodome module contains the pycryptodome library, which is used for secure hashing and encryption services. in the context of cookieJar, it’s used for decrypting cookies.

AES.py: used for decrypting cookies from browsers like chrome, opera, edge.

KDF.py: a key derivation function that’s used to generate a cryptographic key from a password. used in the PBKDF2 function in cookieJar.

padding.py: used in block cipher algorithms to ensure that the last block of data is the correct size. used in unpad to remove padding from the decrypted data.

lz4.block: part of the lz4 library, which provides bindings for the LZ4 compression algorithm. used specifically for handling cookies from firefox, which uses the compression algorithm to store its cookies.

cookieJar

flowchart

cookieJar is a comprehensive python script for security research and penetration testing, as it allows researchers to analyze the cookies stored by a browser, which can contain sensitive information such as session identifiers and login tokens.

<http.cookiejar.Cookie version=0 name='sessionid' value='1234567890abcdef' port=None port_specified=False domain='instagram.com' domain_specified=True domain_initial_dot=False path='/' path_specified=True secure=True expires=1672444800 discard=False comment=None comment_url=None rest={'HttpOnly': None}, rfc2109=False>

this cookie has the following properties:

name: the name of the cookie (sessionid).

value: the value of the cookie, usually a unique identifier that the server uses to recognize the client.

domain: the domain that set the cookie (instagram.com).

path: path on the domain where the cookie is valid. in this case, it’s /, meaning the cookie is valid for the entire domain.

secure: a boolean value indicating whether the cookie should only be sent over secure (HTTPS) connections.

expires: expiration date of the cookie as a timestamp (1672444800 corresponds to january 1st, 2024).

HttpOnly: a flag indicating whether the cookie is inaccessible to javascript’s Document.cookie API to mitigate XSS attacks.

the actual data stored in a cookie can vary greatly depending on the website and the purpose of the cookie. for example, a session cookie might contain a unique identifier that the server uses to keep track of your session, while a preference cookie might contain information about your preferred language or other settings.

when cookieJar collects cookies, it stores them in a http.cookiejar.CookieJar object. this object behaves like a list of http.cookiejar.Cookie objects, and you can iterate over it to access individual cookies.

cookies = load()
for cookie in cookies:
  print(cookie)

this will print out all the cookies stored in the CookieJar, one per line.

importing necessary libraries

the script first imports some standard and 3rd-party libraries. they provide the necessary functionalities for file handling, database operations, encryption and more.

import base64
import configparser
import contextlib
import glob
import http.cookiejar
import json
import os
import struct
import subprocess
import sys
import tempfile
from io import BytesIO
from typing import Union

it also imports the sqlite3 library, which is used to interact with SQLite databases that many browsers use to store cookies. if the pysqlite2.dbapi2 module is available, it’s used instead of the standard sqlite3 module (improved performance + additional features).

try:
    from pysqlite2 import dbapi2 as sqlite3
except ImportError:
    import sqlite3

for linux/BSD, the script imports either dbus or jeepney. these are used to interact with the d-bus message system, a mechanism that allows different parts of the system to communicate with each other. in this case, it’s used to retrieve the encryption key for cookies from the system’s keyring.

if sys.platform.startswith('linux') or 'bsd' in sys.platform.lower():
    try:
        import dbus
        USE_DBUS_LINUX = True
    except ImportError:
        import jeepney
        from jeepney.io.blocking import open_dbus_connection
        USE_DBUS_LINUX = False

the lz4.block is imported for handling LZ4-compressed cookies from firefox. LZ4 is a fast, lossless compression algorithm that firefox uses to store its cookies.

import lz4.block

finally, the script imports the aforementioned modules from the Cryptodome library.

from Cryptodome.Cipher import AES
from Cryptodome.Protocol.KDF import PBKDF2
from Cryptodome.Util.Padding import unpad

defining constants + helper functions

the script defines a constant for the default chromium password, used as the key for encrypting cookies in chromium-based browsers. it also defines a custom exception class for browser cookie errors.

class ChromiumBasedBrowser:
    ...
    def load(self):
        ...
        # decrypt the cookie value
        value = self._decrypt(value, enc_value)
        ...
    def _decrypt(self, value, encrypted_value):
        ...
        data = aes.decrypt_and_verify(encrypted_value[12:-16], tag)
        ...
        return data.decode()

the firefox class handles cookies from firefox, using the lz4.block module to decompress LZ4-compressed cookies.

class Firefox:
    ...
    def __add_session_cookies_lz4(self, cj):
        ...
        json_data = json.loads(lz4.block.decompress(file_obj.read()))
        ...

loading cookies from all browsers

the load function loads cookies from all supported browsers and returns them in a combined http.cookiejar.CookieJar object. this function iterates over a list of functions that load cookies from each supported browser, and adds all the cookies they return to a single CookieJar.

def load(domain_name=""):
    # function to load cookies from all supported browsers and return combined cookie jar
    cj = http.cookiejar.CookieJar()
    for cookie_fn in [chrome, chromium, opera, opera_gx, brave, edge, vivaldi, firefox, safari]:
        try:
            for cookie in cookie_fn(domain_name=domain_name):
                cj.set_cookie(cookie)
        except BrowserCookieError:
            pass
    return cj

the script includes a separate class for each browser, as each browser stores its cookies differently. these classes inherit from the Browser base class, which provides common functionality, and override the load method to implement specific cookie extraction and decryption.

for example, the Chrome class handles cookies from google chrome.

class Chrome(ChromiumBasedBrowser):
    def __init__(self, cookie_file=None, domain_name="", key_file=None):
        super().__init__(cookie_file, domain_name, key_file)

the firefox class handles cookies from firefox, but overrides the load method to handle firefox unique cookie storage format.

class Firefox(Browser):
    def __init__(self, cookie_file=None, domain_name=""):
        super().__init__(cookie_file, domain_name)
    def load(self):
        ...

decrypting cookies

the ability to decrypt encrypted cookies is the most important part of cookieJar. it’s done using the Cryptodome library, which provides a variety of cryptographic recipes and primitives.

the decryption process varies depending on the browser and the OS. for example, chromium-based browsers on windows use the windows data protection API (DPAPI) to encrypt cookies, while on linux/macOS, they use AES encryption with a key derived from a predefined password.

the _decrypt method in the ChromiumBasedBrowser handles this decryption process. it first checks the OS, then uses the appropriate decryption method.

def _decrypt(self, value, encrypted_value):
    # method to decrypt encoded cookies
    ...
    if sys.platform == 'win32':
        try:
            # try to decrypt using the Windows Chromium method
            decrypted_value = _crypt_unprotect_data(encrypted_value)
        except Exception:
            return value
    else:
        # for Linux & Mac, we have to remove the 'v10' or 'v11' prefix
        encrypted_value = encrypted_value[3:]
        nonce, tag = encrypted_value[:12], encrypted_value[-16:]
        cipher = AES.new(self.v10_key, AES.MODE_GCM, nonce=nonce)
        decrypted_value = cipher.decrypt_and_verify(encrypted_value[12:-16], tag)
    return decrypted_value.decode('utf-8')

in the case of windows, the _crypt_unprotect_data function is used to decrypt the cookie using the DPAPI. this function uses the CryptUnprotectData function from the crypt32.dll library, which is a part of the windows API.

def _crypt_unprotect_data(
        cipher_text=b'', entropy=b'', reserved=None, prompt_struct=None, is_key=False
):
    ...
    if not ctypes.windll.crypt32.CryptUnprotectData(
            ctypes.byref(blob_in), ctypes.byref(desc), ctypes.byref(blob_entropy),
            reserved, prompt_struct, CRYPTPROTECT_UI_FORBIDDEN, ctypes.byref(blob_out)
    ):
        # if the function fails, raise a RuntimeError
        raise RuntimeError('Failed to decrypt the cipher text with DPAPI')  # raise an error

on linux/macOS, the script uses the AES algorithm from the Cryptodome.Cipher module to decrypt the cookies. the key for the AES encryption is derived from the predefined password using the PBKDF2 algorithm from the Cryptodome.Protocol.KDF module.

aes = AES.new(self.v10_key, AES.MODE_GCM, nonce=nonce)
data = aes.decrypt_and_verify(encrypted_value[12:-16], tag)

error handling

issues can arise when dealing with cookies. for example, if a browser’s cookies are stored in a SQLite database, and that database is locked because the browser is currently using it, the script creates a temporary copy of the database to avoid a locking error.

def _create_local_copy(cookie_file):
    ...
    with contextlib.closing(sqlite3.connect('file:{}?mode=ro'.format(pathname2url(cookie_file)), uri=True)) as conn:
        with tempfile.TemporaryFile() as tmp_file:
            for line in conn.iterdump():
                tmp_file.write('{}\n'.format(line).encode('utf-8'))
            tmp_file.seek(0)
            conn_temp = sqlite3.connect('file:{}?mode=rw'.format(pathname2url(tmp_file.name)), uri=True)
            return conn_temp

the script also includes a custom exception class: BrowserCookieError. it’s raised when there’s an error retrieving or decrypting cookies, this allows the script to fail gracefully and provide usefull error messages.

class BrowserCookieError(Exception): pass

conclusion

i wrote this for the same reason i wrote everything else: education. cookieJar illustrates key security concepts, specifically those related to web security, encryption, and data storage.