0) Threat Model & Mindset
You’re not hacking the app. You’re hacking the seam between machines.
When a request leaves the client, it walks a hop-chain. If two parsers disagree about where the request ends, you desync the queue and your bytes become the victim’s next request.
[ Client ]
│ (TLS/ALPN: h2 or h1)
▼
[ Edge / WAF / CDN ] (Parser A: normalizes, buffers, re-serializes)
│
▼
[ Reverse Proxy ] (Parser B: may strip TE, recalc CL)
│
▼
[ Origin / App ] (Parser C: framework/server; keeps keep-alive queue)
Operator brain (use these lenses)
Delimiters: Which hop trusts
Content-Length
(CL)? Which trustsTransfer-Encoding: chunked
(TE)?Normalizers: Who rewrites, strips, or merges headers (duplicates, spaces, commas)?
Queues: Where do keep-alive pipelines exist? That’s the desync stage.
Impact vector map (desync → profit)
Cache poisoning (edge or shared CDN)
Credential/session hijack (victim request inherits attacker-controlled prefix)
WAF bypass (edge blocks, origin accepts)
Internal SSRF / alternate routing (smuggled Host/path hits internal handlers)
Admin action forgery (privileged endpoints queued into the victim’s turn)
Pre-exploit hygiene
Force HTTP/1.1 end-to-end first (model the simplest bridge).
Confirm cache presence (vary
Accept-Encoding
and watchVary
/body changes).Log layered error styles (400 vs 408 vs 502) to fingerprint which hop chokes.
Work low-and-slow. You’re interrogating parsers, not load testing.
1) Anatomy of an HTTP Request (Fast but Surgical)
Parsers clash on three axes:
A) Message delimiting
CL path: “read exactly N bytes.”
TE path: “read chunks until
0\r\n\r\n
.”Ambiguity: both present, duplicated CLs, malformed TE, whitespace/casing tricks.
B) Header normalization
Duplicates: first-wins, last-wins, or comma-merge.
Casing/spacing: header names are case-insensitive, but some intermediaries mis-handle
Transfer-Encoding : chunked
,Transfer-Encoding:chunked
, or stray tabs.Obs-fold/comma quirks: legacy folding still bites modern stacks through bridges.
C) Protocol bridges (2025 reality)
H2→H1 downgrade: gateways re-emit H1; origin parses literal bytes.
:authority
vsHost
: routing can drift if bridges “fix” one and not the other.h2c upgrades: rare but catastrophic when left on; treat as a separate tunnel.
Where queues actually exist
The juicy queue is proxy↔origin (keep-alive). Edge/CDN often buffers and re-emits “clean” requests; the origin consumes a stream. Desync there and you control the next read.
2) Classic Smuggling Families (and why they still pay)
The sketches below are illustrative (non-destructive). Use them as shapes to reason about parser behavior, not as drop-in payloads.
2.1 TE.CL
Model: Front-end honors TE; back-end honors CL.
Flow (conceptual):
Edge (TE) Origin (CL)
────────── ───────────
"0\r\n\r\n" → done keeps reading N more bytes (from CL)
↓
leftover bytes become start of next request
Signals that suggest TE.CL
Edge happily accepts TE; small CL tweaks change only the origin’s behavior.
You observe 408 or delayed responses on a subsequent client fetch (desync residue).
Edge logs “OK”, origin logs framing/body-length errors intermittently.
Non-destructive recon routine (safe probes)
Force h1:
curl -sv --http1.1 https://target/ -H 'Connection: keep-alive'
Header normalization check: send harmless duplicates to see wins/merge:
X-Echo: A
X-Echo: B
Compare reflection/echo points in responses (or timing).
TE presence toggle, no body:
Probe A: add
Transfer-Encoding: chunked
with no body and no CL.Probe B: remove TE, add
Content-Length: 0
.
Watch which layer errors (200 vs 400 at edge vs origin-style error page).
Timing tell: back-to-back identical GETs; if the second occasionally stalls/returns odd content, mark as “potential queue influence” and stop. No bodies, no pipelining.
If probes fit TE.CL: prepare a controlled lab reproduction (your infra) to validate your reasoning before any target-specific tests.
2.2 CL.TE
Model: Front-end honors CL; back-end honors TE.
Flow (conceptual):
Edge (CL) Origin (TE)
────────── ───────────
reads N bytes interprets as chunked stream
(forwards exactly N) tail bytes = next request prefix
Signals that suggest CL.TE
±1 on CL flips origin behavior (chunk errors) while edge is steady.
Removing TE header removes the origin-side error entirely (edge unbothered).
Non-destructive recon routine
Duplicate CL sanity (no body):
Content-Length: 0
plus and minus a duplicateContent-Length: 0
(order flipped).
Watch if one hop rejects duplicates outright.
Benign TE hint: set
Transfer-Encoding: identity
(deprecated/no-op) and see who strips or echoes; you’re mapping who parses TE at all.Bodyless toggles: alternate TE present/absent with no entity body; never send chunk frames—just observe parsing attitudes via status codes and header mutations.
2.3 CL.CL (dual CL ambiguity)
Model: Two CLs; edge and origin pick different winners (first/last/merged).
Flow (conceptual):
Edge picks CL#1 → forwards that many bytes
Origin picks CL#2 → expects different length
→ inconsistency seeds queue residue
Signals that suggest CL.CL
Reversing the order of two
Content-Length: 0
headers flips which layer complains.Adding unrelated headers (e.g.,
X-Foo
) changes the normalization outcome (reordering/merge).
Non-destructive recon routine
Order test (no body):
Content-Length: 0
Content-Length: 0
vs the reversed order; log which hop returns the error page.
Spacing test:
Content-Length : 0
Compare whether a hop rewrites it to canonical form on the way back (via debugging endpoints, if any).
Duplicate + harmless payload marker (no pipelining): never send extra bytes—your goal is to see selection policy, not to cause desync.
2.4 TE obfuscations (TE.*)
Model: One hop has a lax TE parser; the other is strict. TE.CL/CL.TE can reappear under casing/spacing/comma disguises.
Signals that suggest TE.*
Tiny changes in TE formatting flip 200↔400 at different layers.
Only one hop ever logs chunk errors; the other behaves like a normal app response.
Non-destructive recon routine
Formatting toggles (no body):
Transfer-Encoding: chunked
Transfer-Encoding : chunked
Transfer-Encoding: chunked
Transfer-Encoding: chunked, identity
Compare which variants are silently normalized (edge) vs rejected (origin).
Echo/strip check: look for the header’s survival to origin (debug/trace endpoints, or differential behavior), without sending chunk frames.
Reaction matrix (quick heuristic)
Edge 200, origin-style 400 in logs: normalization at edge, choke at origin → chase CL.TE or TE.*
Immediate edge 400: died at hop #1 → try the other family.
Second request stalls/odd timing: likely queue touched—stop and reassess before further probing.
Cache present: if you can predict cache keys, desync can become poison. Validate existence only (do not poison during recon).
Visual: TE.CL desync timeline (conceptual)
t0 Client → Edge: POST with TE … "0\r\n\r\n"
t1 Edge thinks: request complete; forwards framed body to Origin
t2 Origin ignores TE, trusts CL → keeps reading…
t3 Extra bytes on the wire now sit at the head of the keep-alive queue
t4 Victim’s next request arrives → starts mid-stream with attacker’s prefix
3) Modern Desync: HTTP/2 & Edge Quirks (2025 Reality)
Frames on the wire. Bytes at the origin. Mismatch = money.
HTTP/2 kills some classic ambiguity (no Transfer-Encoding: chunked
), but it adds translation layers that split parsers in nastier ways—especially at H2 → H1 downgrade gateways. You’re playing interpreter between a framed protocol (H2) and a bytestream protocol (H1). Anywhere the gateway reconstructs headers/body for the origin, you can wedge a disagreement.
3.1 H2→H1 Downgrade Desync (the modern workhorse)
How H2 thinks:
Request =
HEADERS
(pseudo-headers like:method
,:path
,:authority
) + optionalDATA
frames.End of message = END_STREAM flag on the last frame.
Content-Length
is allowed but must match the total DATA bytes; many stacks still forward it.
How breakage happens:
A gateway receives an H2 request and re-serializes it to H1 for the origin. If it:
trusts the
Content-Length
header more than the actual DATA length, orre-emits both
Host
(from you) and a generatedHost
(from:authority
), orcoalesces/merges headers differently than the origin,
…you can produce a classic “leftover bytes become the next request” situation downstream.
Concept timeline (CL mismatch):
H2 client → Gateway: HEADERS(:method=POST, :path=/), CL: 5, END_STREAM=1 (no DATA)
Gateway → Origin(H1): POST / HTTP/1.1
Host: target
Content-Length: 5
# Origin waits for 5 bytes that never come → queue desync window opens.
# Next bytes on the connection (your next write or a victim request) start mid-message.
Non-destructive probes (H2 mode):
# Force H2 over TLS
curl -sv --http2 https://target/ -H 'Content-Length: 5' --data ''
# (no body; watch for origin-style 408/400 vs edge 200, and any stall patterns)
# Force cleartext H2 (if h2c is enabled)
curl -sv --http2-prior-knowledge http://target/ -H 'Content-Length: 5' --data ''
What to log:
Does a zero-length DATA with
CL: 5
cause delayed responses or 408 on a subsequent fetch?Does the gateway strip/normalize
Content-Length
or pass it through? (Compare with a debug echo endpoint if present.)
3.2 :authority
vs Host
drift (routing + cache poison setups)
In H2, :authority
is the “real” authority. Gateways often inject Host
for H1 origins. If you provide both (many stacks allow it) you might get:
Duplicate Host headers on the H1 side (first-wins vs last-wins ambiguity at the origin).
Routing drift: edge routes by
:authority
while origin trustsHost
, or vice versa.Cache key chaos: CDN keys off
:authority
/path; origin caches byHost
/path.
Probe shape (H2):
HEADERS:
:method: GET
:path: /probe
:authority: site-A.com
host: site-B.com # yes, add a normal 'host' header too
Signals:
Different content or headers depending on whether you include
host:
in addition to:authority
.Varying cache behavior (ETag/Cache-Control differ) with no app-level reason.
Origin logs show the “other” host.
If you confirm drift, you’re in the neighborhood of cross-host cache poison or misroute-to-internal (if internal hostnames leak into the downgrade).
3.3 H2 Header Coalescing & Case-Insensitive Weirdness
H2 canonicalizes header names to lowercase, forbids obs-fold, and defines a merge rule for repeated fields (e.g., cookies). The gateway may re-expand these back into H1 with commas or duplicates—exactly where H1 servers disagree.
Why it matters:
A single logical H2 header (merged) can become two physical H1 headers or a comma-joined value—origin’s policy (first/last/merge) may not match the gateway’s.
cookie
coalescing can break CSRF/session logic in unexpected ways (and cache keys, if any cache is origin-side).
Probe (benign echo patterns):
# Observe how repeats get re-serialized
curl -sv --http2 https://target/ \
-H 'x-dupe: one' -H 'x-dupe: two' -H 'cookie: a=1' -H 'cookie: b=2'
Compare responses, cache behavior, and logs with/without repeats. You’re mapping serialization policy—the precondition for several desync routes.
3.4 h2c (cleartext) upgrade funnels
If a server or gateway accepts h2c (HTTP/2 over cleartext), you can:
Upgrade from H1 to H2 mid-connection (
Upgrade: h2c
), then rely on the gateway to downgrade back to H1 for the origin.Some stacks mishandle this double-bridge, especially around Content-Length enforcement and header carryover.
Probe (only if plain HTTP is in-scope):
# Try a direct h2c session
curl -sv --http2-prior-knowledge http://target/ -H 'Content-Length: 3' --data ''
Signals: upgrade accepted + CL mismatch artifacts (stalls/408/odd second-request behavior). If you see it, you’ve found a very soft target—treat carefully and keep probes sparse.
3.5 Connection Coalescing & alt-svc traps
Modern clients coalesce H2 connections across multiple origins that share the same certificate/SAN. If the edge/CDN coalesces and the gateway misroutes on downgrade, you can end up with a cross-origin corridor:
Edge thinks the tunnel is fine (
:authority
A, cert covers A+B).Gateway re-serializes with
Host: B
or vice versa.Origin B sees traffic intended for A, potentially with cacheable artifacts.
Probe hints:
Toggle SNI/Host pairs spanning the same SAN cert; compare where content actually comes from.
Watch for Alt-Svc hints and whether subsequent requests quietly shift authorities.
3.6 H2 Reaction Matrix (turn signals into next moves)
Zero DATA + nonzero CL causes 408/stall → chase CL desync via downgrade.
Adding
host:
alongside:authority
changes routing/cache → map Host drift; explore cache-key poison.Repeated headers collapse/expand oddly across the bridge → test merge vs duplicate behaviors for CL/Host-sensitive endpoints.
h2c accepted → prioritize upgrade/downgrade desync path.
3.7 Safe H2 Recon Checklist (copy/paste)
Force protocol explicitly:
curl -sv --http2 https://target/; curl -sv --http1.1 https://target/
Diff status codes, headers, timing.
CL vs DATA mismatch (no body):
curl -sv --http2 https://target/ -H 'Content-Length: 5' --data ''
Authority/Host drift:
curl -sv --http2 https://target/ -H 'host: alt.example.com'
Header coalescing map:
curl -sv --http2 https://target/ -H 'x-dupe: one' -H 'x-dupe: two' -H 'cookie: a=1' -H 'cookie: b=2'
h2c presence (only if plain HTTP endpoint exists):
curl -sv --http2-prior-knowledge http://target/
Always follow with two quick GETs to a harmless path and compare timings/bodies; note any second-request anomaly.
3.8 Visual: H2→H1 downgrade gone wrong (CL mismatch)
[H2 Client]
HEADERS(:method=POST, :path=/submit, :authority=app)
Content-Length: 7
END_STREAM=1 # no DATA frames
[Gateway (H2→H1)]
Re-emit:
POST /submit HTTP/1.1
Host: app
Content-Length: 7
[Origin (H1)]
Waits for 7 bytes...
↓
Next bytes on the TCP stream (attacker/victim) become the start of the "body" or next request.
4) Recon That Actually Matters (Signals, Not Scans)
This is where hunters separate noise from gold. Don’t blast ports—read the seams.
The art of request smuggling recon isn’t about “sending weird payloads.” It’s about listening: nudging headers, watching who chokes, and mapping parser allegiances. Every target leaks signals—you just need to ask the right questions.
4.1 Fingerprint the Hop Chain
Your first job is to draw the line between edge and origin.
Signals to pull:
Error page branding: CDN 400 vs framework 400 (Akamai’s boilerplate vs Spring’s JSON stack trace).
Date skew: Compare
Date:
headers; CDN and origin clocks are rarely synced.Via / X-Forwarded-For / X-Cache: cheap giveaways of middleboxes.
Newline quirks: Some proxies still normalize
\r\n
vs\n
differently; watch if the response breaks on malformed headers.
Practical probe:
# Single GET with deliberate bad header
curl -sv --http1.1 https://target/ -H 'X-Bad: \nInject'
If the edge drops it immediately → fast 400 with CDN branding.
If it reaches the origin → custom 500 or framework trace.
4.2 Differential Probes
Instead of brute forcing payloads, flip one bit and compare.
CL vs TE toggles:
Request A:
Content-Length: 0
Request B:
Transfer-Encoding: chunked
(no body)
→ Which layer errors? Which one logs?
Duplicate headers:
X-Foo: A
X-Foo: B
→ Does the origin echo
A
,B
, orA,B
? That’s your merge policy.Whitespace games (non-dangerous):
Transfer-Encoding : chunked
Transfer-Encoding:chunked
→ Which are accepted, which stripped?
This tells you who is parsing what. You’re not smuggling yet—you’re just sketching parser borders.
4.3 Time-Based Tells
Timing is everything in smuggling. Watch for stalls.
408 Request Timeout: usually means the back-end is still waiting for bytes the edge thought it finished sending.
502 Bad Gateway: front-end closed the channel because the origin desynced.
Delayed echo: first request returns fine, the second on the same connection hangs—classic desync symptom.
Probe pattern:
# Send two harmless GETs on the same connection
printf 'GET /1 HTTP/1.1\r\nHost: target\r\n\r\nGET /2 HTTP/1.1\r\nHost: target\r\n\r\n' | nc target 80
If both come back clean, queues are aligned.
If the second stalls or misbehaves, something deeper is happening.
4.4 Cache Presence
Caches are the multiplier—without them, you might only desync yourself. With them, you can poison victims.
Recon moves:
Vary sniff:
curl -sv https://target/ -H 'Accept-Encoding: gzip'
curl -sv https://target/ -H 'Accept-Encoding: identity'
→ Different bodies? CDN cache in play.
Header injection tests:
TryX-Cache-Buster: 123
. If the response shows it inVary
or logs → candidate for poison.Timing: Cached hits respond faster (low ms). Cache misses slower (hundreds ms). Use that delta.
4.5 Recon Decision Tree
When you see signals, this is the flow:
First 400?
│
┌────────┴─────────┐
│ │
CDN-branded Origin-styled
400 400
│ │
Check TE/CL You’re past edge;
toggles. probe CL/TE drift.
Second GET stalls?
│
┌────┴────┐
│ │
Yes No
│ │
Desync possible Clean queue.
Log timing. Move on.
Cache varies by header?
│
┌────┴────┐
│ │
Yes No
│ │
Poison vector. Pure desync.
High payout. Still valid.
4.6 Field Checklist (Copy/Paste into Notes)
Confirm protocol: force H1 vs H2, compare responses.
Error branding: edge vs origin.
Differential toggle: CL vs TE.
Duplicate header reflection.
Whitespace/casing acceptance.
Double-GET timing test.
Cache present? (Vary, latency, X-Cache headers).
Visual: Recon Flow
┌─────────────────────┐
│ Start: baseline GET │
└───────┬─────────────┘
▼
Check error page → Edge or Origin?
▼
Toggle CL vs TE → Who honors what?
▼
Two-GET test → Any stall/desync hints?
▼
Cache checks → Poison possible?
▼
Map hop-chain, choose smuggling family.