XSSveil — an open-source blind XSS tool built on interact.sh. Sprays per-field tokenized payloads whose out-of-band callbacks go to interact.sh, then correlates each callback back to the exact injection point and the vulnerable URL where it executed.
interact.sh correlates a session by the first 20 chars of the subdomain. Everything after that is ours, so each input field gets a unique token embedded right after the prefix:
<20-char-session-id><field-token>.oast.fun
- Injection point — the field token is in the hostname, so it survives even DNS-only callbacks (script/img blocked). We strip the 20-char prefix → token → DB lookup → "ticket_subject on /support/new-ticket".
- Vulnerable URL — the JS beacon exfils
location.hrefin the HTTP path, so the UI shows where it actually rendered (e.g.admin.target.com/tickets/8823).
The dashboard joins both: where you injected ↔ where it executed.
15 context-specific templates (HTML body, quoted/unquoted attributes, JS
strings, template literals, <script> breakout, href javascript:, SVG,
iframe srcdoc, markdown, DNS-only, and a polyglot). Spraying multiple contexts
per field raises true positives. Each context gets its own token.
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python server.py| Surface | Default bind | Auth | Purpose |
|---|---|---|---|
| Operator | 127.0.0.1:5050 |
login required | dashboard + /api/* |
| Collector | 0.0.0.0:5051 |
public (by necessity) | /collect, /agent/*, /probe.js |
Operator routes are refused (404) on the collector port and vice-versa, so
exposing the collector to the internet never exposes the console. On startup a
login password is generated and printed (or set BX_PASSWORD). Set
BX_SECRET to keep sessions valid across restarts.
Environment:
BX_PASSWORD=... # operator login password (else generated + printed)
BX_SECRET=... # Flask session secret (else random per start)
BX_OPERATOR_PORT=5050 # dashboard (bind loopback)
BX_COLLECTOR_PORT=5051 # victim-facing (bind 0.0.0.0)
Put TLS in front of the collector (Caddy/nginx). Payloads beacon over the collector's scheme; on an HTTPS target an
http://collector is mixed-content blocked. Terminate HTTPS at a reverse proxy → the collector port.
(Operator port 5050 — macOS AirPlay squats on 5000.)
An enterprise-style single-page console with a sidebar nav, KPI tiles, and light/dark themes (toggle top-right; choice is remembered). Views:
- Overview — KPI tiles (fires, fields, vulnerable URLs, captures, payloads), the live Interactions table, search, and Burp/CSV export.
- Session & Payloads — start the interact.sh session, set the collector, and generate per-field payloads.
- Live Sessions — the reverse-proxy / JS-shell console (online agents show a live count badge in the nav).
- Alerts & Filters — notifications, block/whitelist filters, custom JS.
- Start session — interact.sh server (default
oast.fun) + optional Collector URL. The collector must be a host the victim browser can reach (a public VPS in a real engagement), since that's where the screenshot and DOM are POSTed. Leave blank to run OOB-only (no capture). - Generate payloads for a target URL + field name → copy → plant them. With a collector set, a Full capture payload (screenshot + DOM + cookies) is added on top of the 15 context payloads.
- Watch the Interactions table light up live (SSE). Rows with a 📷/📄 in the Evidence column open a viewer with the screenshot, DOM snapshot, cookies and storage of the vulnerable page.
The Flask template is cached in memory, so restart the server after editing
templates/index.html.
interact.sh confirms that a payload fired and which field (token in the
hostname). The collector grabs the heavy evidence: the capture payload loads
/probe.js, which pulls html2canvas from the collector, screenshots
document.body, and POSTs the PNG + outerHTML + cookies + local/session
storage to /collect. If html2canvas is blocked it still sends DOM + cookies.
Deploy the collector on a public host. Modern browsers' Private Network Access blocks a public page (e.g. a real target) from calling
127.0.0.1, so a localhost collector only works when the victim page is itself on localhost. In a real engagement the collector is a public VPS, which the victim reaches normally.
- Notifications — on every new fire, alerts go to Slack, Discord, Telegram, and/or a generic webhook (raw JSON). So you hear about a blind XSS that fires days later without watching the dashboard.
- Block / whitelist filtering — drop noisy reports by domain or source IP (e.g. your own testing host), or whitelist so only in-scope domains are kept. Filtering applies to both interact.sh callbacks and collector captures.
- Custom JavaScript — inject your own JS into the victim context (runs inside the probe), e.g. to exfil a CSRF token or hit an internal admin endpoint.
- Stats overview — header counters: fires, distinct fired fields, distinct vulnerable URLs, captures, payloads generated.
- Report search — live filter the interactions table by field / URL / IP / UA.
- Fire deduplication — repeated fires of the same payload on the same page (an admin reloading a ticket 50×) collapse into one row with a ×N hit count and first/last-seen, so a genuinely new sink is never buried in noise.
- Internal-IP flagging — callbacks from RFC1918 / loopback / reserved
addresses get an INTERNAL badge. An internal source firing your payload is
the strongest signal it reached a real internal tool (admin panel, log viewer),
not your own external testing box. (
netutil.is_internal) - Burp / ZAP / ffuf export — download all generated payloads as a newline
wordlist (
?fmt=burp, for Burp Intruder / ffuf) or CSV with token↔context↔ target mapping (?fmt=csv, for ZAP / spreadsheets) to spray them at scale.
Configured in the dashboard's "Notifications & filters" panel (persisted in the
settings table). Notifications and filters need no restart.
The "Persistent session" payload injects an agent that keeps polling your collector. Each fired agent becomes a live session in the dashboard's "Live sessions" panel (green dot = online, INTERNAL badge for RFC1918 sources). Select one to drive the victim's browser in real time:
- Reverse proxy — enter a target URL; the agent runs
fetch(url, {credentials:'include'})in the victim's own origin, so the request carries their authenticated session — HttpOnly cookies included — and the response is streamed back and rendered in a sandboxed iframe (or raw). You effectively browse the target as the victim. - JS console — run arbitrary JavaScript in the victim context, see the return value.
- Screenshot / cookies — on-demand live html2canvas snapshot and cookie/storage dump.
- Command history — every command run against a session, with its type,
timestamp, argument, and result (or a pending indicator), shown below the
console (
/api/sessions/<sid>/history).
Flow: agent.js → /agent/poll (heartbeat + command pickup) → command runs in
victim → /agent/result. Operators enqueue via /api/sessions/<sid>/cmd and
read answers via /api/cmd/<id>. Sessions are in-memory (live connections).
This is powerful and intrusive — only use it against systems you are explicitly authorized to test.
interactsh_client.py— register/poll/deregister + RSA-OAEP/AES-CFB decryptpayloads.py— context templates + per-field token generationdb.py— SQLite correlation store (injections ↔ interactions)server.py— Flask API + background poller + SSE feed + collector endpointscollector.py— probe.js generator (screenshot + DOM exfil + custom JS)notifications.py— Slack / Discord / Telegram / webhook alertsnetutil.py— internal / RFC1918 source-IP classificationagent.py— persistent agent.js (reverse proxy / JS shell)sessions.py— in-memory live-session + command/result storestatic/html2canvas.js— served to victims for screenshotstemplates/index.html— dashboard UI + capture viewer
XSSveil is for authorized security testing only — penetration tests, bug bounty programs within scope, and your own lab. It captures session cookies, DOM, and can drive a victim's browser via the reverse proxy; using it against systems you do not have explicit written permission to test is illegal in most jurisdictions. You are solely responsible for how you use it.
MIT — see LICENSE. Bundles
html2canvas (MIT) in static/.