HTTP Request Smuggling,
made approachable.
An HTTP desync / request-smuggling testing tool in pure Python 3.
Classic TE.CL and CL.TE detection corroborated by three independent signals -
a per-target dynamic gadget oracle, structural
response fingerprinting, and a statistical RTT baseline -
that combine into a tiered STRONG / CONFIRMED /
Potential confidence label. Same stack of advanced scanners
(CL.0, TE.0, pause-based, parser discrepancy, header removal, Expect,
hop-by-hop, bare-LF chunked, connection-state, HTTP/2 downgrade), now with
fingerprint-only detection paths that catch desyncs the old status-only
oracle silently missed. Optional NiceGUI web frontend with a structured
findings panel and Markdown report export.
______ _
/ _____) | |
( (____ ____ _ _ ____ ____| | _____ ____
\____ \| \| | | |/ _ |/ _ | || ___ |/ ___)
_____) ) | | | |_| ( (_| ( (_| | || ____| |
(______/|_|_|_|____/ \___ |\___ |\_)_____)_|
(_____(_____|
@l0lsec v2.1
Built for finding real desyncs.
Every scanner in Smuggler pairs a timing/disconnect oracle with a positive confirmation probe (gadget surfacing, canary divergence, matched-pair comparison) so a finding has multiple signals behind it - not just a slow response.
Pure Python 3
No build step, no native deps for the core scanners. git clone and run. HTTP/2 support is a single optional pip install h2.
11 scanner classes
Classic TE.CL / CL.TE plus CL.0, TE.0, bare-LF chunked, pause-based, parser-discrepancy, header-removal, Expect, hop-by-hop, connection-state, and HTTP/2 downgrade - each with its own oracle.
Replay mode
Continuously replay a captured request, optionally interleaved with a baseline request for differential analysis. Live RPS, success/timeout/error counters, and request-ID tagging.
Tiered confidence
Findings vote across timing + gadget + fingerprint + RTT-baseline signals. STRONG (timing + 2-3 corroborators), CONFIRMED (timing + 1), Potential (timing only). No more guessing whether a timing blip is real.
Dynamic gadget oracle
Per-target gadget selection (OPTIONS *, random 404, robots, favicon, query reflection) with auto-derived look_for signatures and a per-run canary. Replaces the hard-coded /robots.txt + "llow:" pair that silently broke on Akamai / Cloudflare edges.
Fingerprint diff
Six-axis structural diff (status, framing, headers, body length, body head/tail hashes) with a 3-sample noisy-axis baseline. Catches desyncs that flip Set-Cookie or Content-Length without changing status - HOPBYHOP_FP_*, HDRREMOVAL_FP, CONNSTATE_FP payload tags.
Web GUI
Optional NiceGUI frontend exposes every CLI flag, streams colorized output to your browser, and renders live replay stats with a graceful SIGINT โ SIGKILL stop ladder.
Findings triage
Real-time panel splits results into Confirmed (oracle fired) and Needs manual confirmation (single-signal heuristic). Each finding ships with a per-class confirmation / escalation playbook and a one-click Copy as Markdown report.
Per-payload tools
Inline hex + annotated-text viewer (the invisible 0x09/0x0a byte that makes the mutation work is highlighted), one-click Replay this, and copy a byte-exact openssl s_client / ncat reproduction command - no -crlf, so bare-LF mutations survive.
Tooling-friendly
Burp-style raw request files, proxy support, custom cookies, persistent TCP connections, mutation configs (default.py, exhaustive.py, doubles.py, http10.py, chunkext.py), and a tested pytest harness.
Installation & quickstart
Clone the repo and you have the CLI. Install the requirements file to unlock the optional HTTP/2 scanner and the web GUI.
CLI core
$ git clone https://github.com/l0lsec/smuggler.git
$ cd smuggler
$ python3 smuggler.py -h
Web GUI optional
$ pip install -r requirements.txt
$ python3 webgui.py
# open http://127.0.0.1:8765
Common invocations
# Default scan: TE.CL + CL.TE against a single URL
$ python3 smuggler.py -u https://target.com/
# Run every advanced scanner
$ python3 smuggler.py -u https://target.com/ --scan-type all
# Use a captured Burp request as a template (extracts host/method/cookies)
$ python3 smuggler.py -r request.txt
# Replay a smuggling POC continuously with a baseline diff
$ python3 smuggler.py -r poc.txt --baseline-request normal.txt --replay
# Pipe a list of hosts
$ cat hosts.txt | python3 smuggler.py
# Route everything through Burp
$ python3 smuggler.py -u https://target.com/ --proxy http://127.0.0.1:8080
A web GUI for every CLI flag.
The optional webgui.py wraps the CLI as a subprocess, streams
its colorized output into your browser, and gives you a stop button -
without hiding any options or behavior.
- Three target modes: single URL, list of hosts (piped via stdin), or request file (upload, path, or inline edit).
- Full CLI parity - every flag from
--scan-typedown to--pause-timeoutis a form control. - Structured Findings panel parses smuggler's stdout in real time and splits results into Confirmed vs Needs manual confirmation.
- Each finding ships with a per-class confirmation / escalation playbook tailored to the scan that fired (CLTE, CL.0, ParserDisc, HopByHop, โฆ).
- Markdown report export โ copy the whole triage report (or a single finding) ready to paste into a bounty / pentest ticket.
- Per-payload actions: hex + annotated text viewer (highlights the invisible
0x09/0x0abyte that makes the mutation work), one-click Replay, and byte-exactopenssl s_clientrepro. - Live payload discovery โ new files dropped into
payloads/mid-run get a NEW badge and a toast notification. - Replay live stats: total / success / failed / timeout / error / RPS / baseline-ratio / latest request ID.
- Colorized output with smuggler's ANSI rendered as styled HTML.
- Stop button sends SIGINT โ SIGTERM โ SIGKILL in a graceful ladder.
- "Copy command" surfaces the exact
python3 smuggler.py โฆinvocation.
127.0.0.1 by default.
Smuggler is an offensive scanner - do not expose webgui.py on a
public interface. The --public flag exists but prints a loud warning.
Three signals, one tiered verdict.
Every scanner now consults three independent oracle signals on top of the original timing check. Findings combine them into a confidence tier so you know whether you're looking at a high-signal lead or a CDN blip.
Dynamic gadget oracle lib/Oracle.py
Per-target candidate probe (OPTIONS *, random-404, robots, favicon, query-reflect) with auto-derived look_for - canary reflection > status divergence > distinctive header > body n-gram. Replaces the hard-coded /robots.txt + "llow:" pair that silently broke on Akamai, sharded backends, and any target that doesn't serve robots.
Response fingerprint lib/Fingerprint.py
Six-axis structural snapshot diffed against a 3-sample baseline. The baseline records which axes are noisy on this target (Date, request IDs, cache tags) and excludes them from later diffs. A probe is "structurally different" when it diverges on the status axis OR on >=2 non-noisy axes.
statusframingheader_setbody_lenbody_headbody_tail
Statistical RTT baseline lib/Timing.py
N=5 RTT samples on fresh connections produce median + MAD. A response is flagged when |rtt - median| > k * MAD (k=3). MAD is robust to the very outliers we're detecting; a 50ms MAD floor stops localhost-flat baselines from classifying every wobble as anomalous. Augments - does not replace - the binary timeout deadline.
Confidence tiers
Reproducible timing anomaly is the entry ticket; the three corroborators decide the tier. Annotation in square brackets shows which fired.
| Tier | Required signals |
|---|---|
| STRONG | Reproducible timeout + 2 or 3 of { gadget hit, fingerprint divergence, RTT anomaly } |
| CONFIRMED | Reproducible timeout + exactly 1 corroborator |
| Potential | Reproducible timeout only, no corroborator |
Worked example - Akamai edge with Disallow: stripped
The old detector used /robots.txt + "llow:" as its only
positive oracle. On a fronting edge that strips the Disallow: line,
that oracle silently returned False - findings landed as Potential
(timing-only) and got buried. Under the new pipeline:
GadgetOraclewalks the catalogue and picksOPTIONS /- baseline returns 405, gadget returns 200. Auto-derivedlook_forbecomes"Allow:", matched header-only.- CLTE timing oracle fires reproducibly.
- Smuggled
OPTIONS /hits the backend; victim leg comes back withContent-Lengthflipped 4287 โ 0 andLast-Modifiedmissing fromheader_set. TimingBaseline.is_anomalous(rtt, k=3)flags the response RTT well outside median + MAD.
The old code would have called this Potential and you'd have ignored it. The annotation makes the upgrade auditable.
What each scanner actually does.
Every scanner pairs an anomaly oracle with one or more positive confirmation steps. Confidence reflects the strength of the corroboration, not severity.
| Attack class | --scan-type |
Oracle | Confidence |
|---|---|---|---|
| TE.CL / CL.TE | tecl, clte |
Timing anomaly + dynamic gadget probe + victim-leg fingerprint diff + statistical RTT baseline | STRONG / CONFIRMED / Potential |
| CL.0 / 0.CL | cl0 |
Pipelined victim observes the smuggled gadget or victim-leg fingerprint diverges from a clean baseline; tries user method + GET + POST | High (3-of-5) |
| TE.0 | te0 |
Victim observes the smuggled gadget or fingerprint diverges, after a zero-chunk terminator | High (3-of-5) |
| Bare-LF / Bare-CR chunked | bare-lf |
Pipelined victim observes the gadget or fingerprint diverges when framing uses bare LF/CR | High (3-of-5) |
| Pause-based desync | pause |
Send headers, pause N s, send body; pipelined victim observes gadget or fingerprint diverges | Medium-high (2-of-3) |
| Connection-state attack | connection-state |
Status flip vs fresh connection (CONNSTATE) or >=2-axis fingerprint divergence with confirmation (CONNSTATE_FP) |
Medium |
| Parser discrepancy | parser-discrepancy |
Per-technique control + canary probe; findings annotate diff axes, HIDDEN downgrades to PARTIAL-HIDE when non-status axes also flip |
Medium-high |
| Header removal (Keep-Alive) | header-removal |
Status / canary flip (HDRREMOVAL) or 3-of-5 reproducible fingerprint-only divergences (HDRREMOVAL_FP) |
Medium-high |
| Expect-based desync | expect |
Multiple Expect variants pipelined with a victim; same gadget + fingerprint oracle as CL.0 |
High when confirmed |
| Hop-by-hop auth bypass | hop-by-hop |
Status flip (HOPBYHOP_*) or reproducible non-status fingerprint divergence (HOPBYHOP_FP_*) |
High when reproducible |
| HTTP/2 downgrade | h2 |
Send H2 attack stream, then open a parallel H1 connection; flag if victim sees gadget | High |
Request file modes - they aren't interchangeable.
A surprising fraction of "Smuggler says OK but Burp shows desync" reports come from
pasting a smuggling POC into -r without --replay. Here's the
mental model.
Scan mode (default -r)
The request file is a template. Smuggler extracts method, endpoint, host, and cookies, then synthesizes its own smuggling payloads from your chosen config. Body bytes and embedded request lines are ignored. Use req_clean.txt.
Replay mode (-r --replay)
The request file is sent verbatim, looping until you stop. Pair with --baseline-request to interleave a clean request for differential analysis. This is what you want for POC-shaped files like req_poc.txt.
Notice: line when a
-r file (without --replay) contains body bytes or embedded request lines.
Pick the depth of your TE/CL fuzzing.
Configs control which header mutations the classic TE.CL / CL.TE scanners
try. They are Python files in configs/ that you can read and edit.
# Faster, smaller mutation set (default)
python3 smuggler.py -u https://target.com/ -c default.py
# Wider mutation surface, slower
python3 smuggler.py -u https://target.com/ -c exhaustive.py
# Niche: doubled headers, chunk-extensions, HTTP/1.0 quirks
python3 smuggler.py -u https://target.com/ -c doubles.py
python3 smuggler.py -u https://target.com/ -c chunkext.py
python3 smuggler.py -u https://target.com/ -c http10.py
exec()'d in the same process as Smuggler.
Only load configs you trust - they have full process privileges.