daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

request-smuggling-in-http-2-downgrades.md (10547B)


      1 ---
      2 title: "Request Smuggling in HTTP/2 Downgrades"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/http-request-smuggling/request-smuggling-in-http-2-downgrades.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/http-request-smuggling/request-smuggling-in-http-2-downgrades.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Request Smuggling in HTTP/2 Downgrades
     14 
     15 HTTP/2 is generally considered immune to classic request-smuggling because the length of each DATA frame is explicit. **That protection disappears as soon as a front-end proxy “downgrades” the request to HTTP/1.x before forwarding it to a back-end**. The moment two different parsers (the HTTP/2 front-end and the HTTP/1 back-end) try to agree on where one request ends and the next begins, all the old desync tricks come back – plus a few HTTP/2-only injection gadgets.<sup>[[1]](#references)</sup>
     16 
     17 Recent desync research reached the same conclusion from the defensive side: **HTTP/2 at the edge does not save you if the proxy still speaks HTTP/1.1 upstream**. The downgrade boundary is the attack surface.<sup>[[2]](#references)</sup>
     18 
     19 ---
     20 ## Why downgrades happen
     21 
     22 1. Browsers already speak HTTP/2, but much legacy origin infrastructure still only understands HTTP/1.1.
     23 2. Reverse-proxies (CDNs, WAFs, load-balancers) therefore terminate TLS + HTTP/2 at the edge and **rewrite every request as HTTP/1.1** for the origin.
     24 3. That translation layer has to emit a single, valid HTTP/1.1 body delimiter for the origin.
     25 4. RFC 9113 is stricter than many downgrade implementations:
     26    * connection-specific headers such as `transfer-encoding`, `upgrade`, `keep-alive`, or `proxy-connection` make the HTTP/2 message malformed;
     27    * `te` is only valid as `te: trailers`;
     28    * if `content-length` is present, it must match the sum of the DATA frame payload lengths.
     29 
     30 Whenever the front-end trusts the HTTP/2 frame length **but** the back-end trusts CL or TE, an attacker can force them to disagree.
     31 
     32 ---
     33 ## Two dominant primitive classes
     34 
     35 | Variant | Front-end length | Back-end length | Typical payload |
     36 |---------|-----------------|-----------------|-----------------|
     37 | **H2.TE** | HTTP/2 frame | `Transfer-Encoding: chunked` | Embed an extra chunked message body whose final `0\r\n\r\n` is *not* sent, so the back-end waits for the attacker-supplied “next” request. |
     38 | **H2.CL** | HTTP/2 frame | `Content-Length` | Send a *smaller* CL than the real body, or inject `content-length: 0` during downgrade, so the back-end reads past the boundary into the following request. |
     39 
     40 > These are identical in spirit to classic TE.CL / CL.TE, just with HTTP/2 replacing one of the parsers.
     41 
     42 ---
     43 ## Identifying a downgrade chain
     44 
     45 1. Use **ALPN** in a TLS handshake (`openssl s_client -alpn h2 -connect host:443`) or **curl**:
     46    ```bash
     47    curl -v --http2 https://target
     48    ```
     49    If `* Using HTTP2` appears, the edge speaks H2.
     50 2. If the site *looks* HTTP/1.1-only, probe for **hidden HTTP/2** anyway:
     51    ```bash
     52    curl --http2-prior-knowledge https://target
     53    ```
     54    Some stacks support H2 but forget to advertise it via ALPN, which means you would otherwise miss downgrade-only bugs.
     55 3. Send a deliberately malformed CL/TE request *over* HTTP/2 (Burp Repeater can force HTTP/2). If the response is an HTTP/1.1-style error such as `400 Bad chunk` or a back-end-specific parse failure, you have strong evidence that the edge converted the traffic for an HTTP/1 parser downstream.
     56 
     57 ---
     58 ## Exploitation workflow (H2.TE example)
     59 
     60 ```http
     61 :method: POST
     62 :path: /login
     63 :scheme: https
     64 :authority: example.com
     65 content-length: 13      # ignored by the edge
     66 transfer-encoding: chunked
     67 
     68 5;ext=1\r\nHELLO\r\n
     69 0\r\n\r\nGET /admin HTTP/1.1\r\nHost: internal\r\nX: X
     70 ```
     71 1. The **front-end** reads exactly 13 bytes (`HELLO\r\n0\r\n\r\nGE`), thinks the request is finished and forwards that much to the origin.
     72 2. The **back-end** trusts the TE header, keeps reading until it sees the *second* `0\r\n\r\n`, thereby consuming the prefix of the attacker’s second request (`GET /admin …`).
     73 3. The remainder (`GET /admin …`) is treated as a *new* request queued behind the victim’s.
     74 
     75 Replace the smuggled request with:
     76 * `POST /api/logout` to force session fixation
     77 * `GET /users/1234` to steal a victim-specific resource
     78 
     79 If you can only poison requests on your **own** client-mapped upstream connection, the bug can still be useful for cache poisoning, internal-header leaks, or front-end control bypass. For those reuse-locked cases, pivot to [HTTP Connection Request Smuggling](/hacktricks/pentesting-web/http-connection-request-smuggling).
     80 
     81 ---
     82 ## Modern downgrade-only injection gadgets
     83 
     84 Many modern front-ends already strip a literal `transfer-encoding: chunked` header. Recent findings often work by **manufacturing dangerous HTTP/1.1 bytes during the downgrade itself** rather than sending them as a normal header.<sup>[[1]](#references)</sup>
     85 
     86 ### CRLF / LF injection in header values
     87 
     88 HTTP/2 field values are binary. If the downgrade code fails to sanitize `\r\n` — or even a bare `\n` — you can often synthesize a backend-only header:
     89 
     90 ```http
     91 :method: POST
     92 :path: /
     93 :authority: example.com
     94 foo: bar\r\ntransfer-encoding: chunked
     95 
     96 0
     97 
     98 GET /admin HTTP/1.1
     99 Host: example.com
    100 ```
    101 
    102 The edge sees a legal-ish HTTP/2 header block; the back-end receives a fresh `Transfer-Encoding` header and starts parsing the body as chunked. The same trick can be used to inject `content-length: 0`, terminate the header section early, or split one downgraded request into two.
    103 
    104 ### Header-name injection
    105 
    106 Some vendors fixed newline injection in **values** but forgot to validate **names**. If the downgrade code accepts spaces, colons, or other non-HTTP/1.1-safe bytes in a header name, the generated HTTP/1.1 request can contain multiple backend-visible header lines. This is a common way to re-introduce `transfer-encoding` after a partial hotfix.
    107 
    108 ### Pseudo-header / request-line injection
    109 
    110 A buggy mapper that copies `:method`, `:path`, `:authority`, or `:scheme` into the HTTP/1.1 request line without strict validation can let you:
    111 
    112 * inject a full secondary request line,
    113 * supply an ambiguous host or path,
    114 * prepend a URL prefix that changes routing, cache keys, or SSRF targets.
    115 
    116 If a literal `TE` header is blocked but pseudo-headers are not normalized before downgrade, this is the next place to look.
    117 
    118 ---
    119 ## Related but distinct: h2c smuggling (clear-text upgrades)
    120 
    121 This is not a CL/TE downgrade bug, but during the same assessment you should also test whether the edge forwards the HTTP/1.1 `Upgrade: h2c` header to a back-end that supports clear-text HTTP/2. If it does, you may be able to tunnel *raw* HTTP/2 frames through an edge that only validated the initial HTTP/1.1 request.
    122 
    123 Key requirements:
    124 * Edge forwards **both** `Connection: Upgrade` and `Upgrade: h2c` unchanged.
    125 * Origin upgrades to HTTP/2 and keeps the connection-reuse semantics that enable request queueing or direct internal access.
    126 
    127 For proxy-specific quirks and tunnel-focused payloads, see [Upgrade Header Smuggling](/hacktricks/pentesting-web/h2c-smuggling).
    128 
    129 ---
    130 ## Notable real-world examples
    131 
    132 * **2025 desync research** – large shared edge providers were still exploitable because the dangerous trust boundary remained **upstream HTTP/1.1**, not the client-facing HTTP/2 session.<sup>[[2]](#references)</sup>
    133 * **CVE-2023-25690** – Apache HTTP Server `mod_proxy` rewrite rules could be chained into request splitting and smuggling when rewritten bytes were forwarded downstream. (fixed in 2.4.56)
    134 * **CVE-2023-25950** – HAProxy 2.7.0 and 2.6.1-2.6.7 had an HTTP request/response smuggling issue in HTX handling that could alter a legitimate user’s request.
    135 * **CVE-2022-41721** – Go `MaxBytesHandler` left unread body bytes that could later be interpreted as **HTTP/2** frames, showing how “leftover bytes become a new protocol message” is not limited to classic H1 desync.
    136 
    137 ---
    138 ## Tooling
    139 
    140 * **Burp Request Smuggler** – since **v3.0 (2025)** it includes parser-discrepancy detection plus HTTP/2 tunnelling / header-injection probes. Enable **HTTP/2 probing** and ALPN override when hunting hidden H2 support.
    141 * [**http2smugl**](https://github.com/neex/http2smugl) – purpose-built H2→H1 detector and raw requester. The `request` subcommand accepts escaped bytes such as `\r`, `\n`, and `\x3a`, which is handy when normal clients refuse malformed headers:
    142   ```bash
    143   go install github.com/neex/http2smugl@latest
    144   http2smugl detect https://target
    145   ```
    146 * [**SmuggleFuzz**](https://github.com/Moopinger/smugglefuzz) – fast downgrade scanner with customizable gadget lists and an explicit confirm mode:
    147   ```bash
    148   go install github.com/moopinger/smugglefuzz@latest
    149   smugglefuzz scan -u https://target --confirm
    150   ```
    151 * **h2cSmuggler** – Python PoC by Bishop Fox to automate the clear-text upgrade attack:
    152   ```bash
    153   python3 h2csmuggler.py -u https://target -x 'GET /admin HTTP/1.1\r\nHost: target\r\n\r\n'
    154   ```
    155 * **curl** / **hyper** – useful for quick ALPN checks and for replaying handcrafted HTTP/2 payloads: `curl --http2-prior-knowledge -X POST --data-binary @payload.raw https://target`
    156 
    157 ---
    158 ## Defensive measures
    159 
    160 1. **Use upstream HTTP/2 end-to-end whenever possible** – removing the H2→H1 translation step is the cleanest fix.
    161 2. **Enforce RFC 9113 on ingress** – reject HTTP/2 requests carrying connection-specific headers; only allow `te: trailers`; reject mismatched `content-length` values.
    162 3. **Strip and regenerate body-length metadata on downgrade** – never forward attacker-supplied `Content-Length` / `Transfer-Encoding` into the downgraded request.
    163 4. **Normalize before mapping to HTTP/1.1** – reject or canonicalize CR, LF, colon, obs-fold, and non-ASCII bytes in header names, header values, and pseudo-headers *before* routing / rewrite logic.
    164 5. **Reduce or isolate upstream connection reuse** – if you are stuck on upstream HTTP/1.1, limiting shared back-end connections sharply reduces queue-poisoning impact.
    165 6. **Strip `Upgrade` unless it is explicitly required for WebSocket** – prevents `h2c` tunnelling.
    166 
    167 ---
    168 ## References
    169 
    170 - [1] [PortSwigger Research - HTTP/2: The Sequel is Always Worse](https://portswigger.net/research/http2)
    171 - [2] [PortSwigger Research - HTTP/1.1 must die: the desync endgame](https://portswigger.net/research/http1-must-die)