From 083e08e094650a1c69c56cc8537ce6860189bb0e Mon Sep 17 00:00:00 2001 From: ashton <63224111+bikini@users.noreply.github.com> Date: Fri, 26 Jun 2026 05:52:17 -0500 Subject: [PATCH] Add nghttp2 nghttpx upgrade queue poison PoC --- README.md | 3 +- .../README.md | 193 ++++++++++++ .../evidence/local-verification.txt | 56 ++++ .../poc.py | 278 ++++++++++++++++++ 4 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 nghttp2-nghttpx-upgrade-queue-poison-poc/README.md create mode 100644 nghttp2-nghttpx-upgrade-queue-poison-poc/evidence/local-verification.txt create mode 100644 nghttp2-nghttpx-upgrade-queue-poison-poc/poc.py diff --git a/README.md b/README.md index 85041c9..c7ab650 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Most folders contain one of my former standalone PoC repos, preserved with its o | `libssh2-publickey-list-calc-poc` | direct entry, June 25, 2026 | 10 | | `lunar-modrinth-chain-poc` | `ffd02120708b6503f11585858ce3724872f3b7a7` | 6 | | `mybb-limited-acp-to-admin` | `1610e0373943c2f6562a99f917d3a3d1fdd9056d` | 5 | +| `nghttp2-nghttpx-upgrade-queue-poison-poc` | direct entry, June 26, 2026 | 3 | | `nmap-ipv6-extlen-wrap-poc` | direct entry, June 23, 2026 | 4 | | `objdump-dlx-calc-poc` | `7df01e4e20c7375a89e8ccf760526c52eb6ad582` | 41 | | `openvpn-connect-echo-script-ace-poc` | `d2f904d9272d4388c9862131d40e32e072e85e38` | 8 | @@ -53,4 +54,4 @@ Matching Git blob IDs means the tracked file bytes are identical. The check cove This repository preserves the contents of those PoCs. Repository-level metadata such as stars, issues, pull requests, releases, and separate Git history remain in the original repository histories. -Direct entries, including `c-ares-tcp-uaf-calc-poc`, `firefox-smartwindow-private-url-exfil-poc`, `floci-apigateway-vtl-rce-poc`, `libssh2-cve-2026-55200-poc`, `libssh2-publickey-list-calc-poc`, `nmap-ipv6-extlen-wrap-poc`, `php857-streambucket-soap-rce-rpoc`, `rustdesk-session-permission-pocs`, and `systeminformer-phsvc-trusted-host-lpe-poc`, are tracked by this repository's commit history. +Direct entries, including `c-ares-tcp-uaf-calc-poc`, `firefox-smartwindow-private-url-exfil-poc`, `floci-apigateway-vtl-rce-poc`, `libssh2-cve-2026-55200-poc`, `libssh2-publickey-list-calc-poc`, `nghttp2-nghttpx-upgrade-queue-poison-poc`, `nmap-ipv6-extlen-wrap-poc`, `php857-streambucket-soap-rce-rpoc`, `rustdesk-session-permission-pocs`, and `systeminformer-phsvc-trusted-host-lpe-poc`, are tracked by this repository's commit history. diff --git a/nghttp2-nghttpx-upgrade-queue-poison-poc/README.md b/nghttp2-nghttpx-upgrade-queue-poison-poc/README.md new file mode 100644 index 0000000..4cf8d3a --- /dev/null +++ b/nghttp2-nghttpx-upgrade-queue-poison-poc/README.md @@ -0,0 +1,193 @@ +# nghttp2 nghttpx HTTP/1.1 Upgrade Body Response Queue Poisoning PoC + +This directory documents and validates an HTTP/1.1 Upgrade request body desynchronization in `nghttpx`, the reverse proxy shipped in the nghttp2 project. + +`nghttpx` accepts an HTTP/1.1 Upgrade request carrying `Content-Length`, forwards the request header set to an HTTP/1.1 backend, and forwards the body bytes on the same backend connection. An Upgrade-aware backend can treat the message as an Upgrade attempt and leave bytes after the header terminator to be parsed as the next HTTP request. When `nghttpx` reuses that backend connection for another frontend client, the queued response to the attacker-supplied backend request can be delivered to the victim client. + +The PoC uses a benign payload string, `SMUGGLED-BENIGN-PAYLOAD`, as the attacker-controlled response body. The exploit result is visible when a victim request for `/victim` receives this payload instead of the backend's normal `VICTIM-RESPONSE`. + +Research status: verified locally end to end against a real `nghttpx` binary from nghttp2 `v1.69.0`, with a fixed-control run against upstream master. + +## Affected Target + +- Product: nghttp2 +- Component: `nghttpx` +- Version analyzed: `v1.69.0` +- Release commit: `68cb6900fde14c77f0cd7add0e094a862960eb99` +- Fixed upstream commit: `ab28105c4a0197da24f8bfc414bc116055249e1e` +- Fixed commit title: `nghttpx: Tighten up CONNECT and HTTP Upgrade handling` +- Service surface: cleartext HTTP/1.1 frontend to HTTP/1.1 backend reverse proxying +- Impact: cross-client response queue poisoning through backend connection reuse + +## Impact + +An unauthenticated client can send a single crafted Upgrade request to place an attacker-chosen request into the backend HTTP/1.1 response queue. A subsequent client routed onto the same backend connection can receive the attacker's response. + +In an application deployment, the primitive can be used to serve attacker-controlled same-origin content to another user, confuse application routing, poison an intermediate cache that trusts `nghttpx` response boundaries, or deliver a response belonging to an attacker-selected backend route under a victim request. + +The local exploit demonstrates the queue poisoning directly: + +```text +attacker request: GET /upgrade with Upgrade: websocket and a body containing GET /poisoned +attacker response: UPGRADE-REJECT +victim request: GET /victim +victim response: SMUGGLED-BENIGN-PAYLOAD +``` + +## Preconditions + +- The attacker can connect to an `nghttpx` HTTP/1.1 frontend. +- `nghttpx` proxies to an HTTP/1.1 backend with reusable keep-alive connections. +- The backend applies Upgrade-oriented parsing behavior where bytes after an Upgrade request header terminator are interpreted as the next HTTP request rather than as an HTTP request body. +- The attacker can choose a backend route whose response can be delayed long enough for the next frontend request to be assigned to the same backend connection. + +The PoC includes a small backend server that implements the relevant Upgrade behavior and response delay. The target process is the supplied real `nghttpx` binary. + +## Root Cause + +The vulnerable flow starts in the HTTP/1.1 frontend parser. An Upgrade request is accepted even when it carries `Content-Length`: + +```text +GET /upgrade HTTP/1.1 +Host: target +Connection: Upgrade +Upgrade: websocket +Content-Length: 43 + +GET /poisoned HTTP/1.1 +Host: backend +``` + +The request is converted and forwarded to the backend through `HttpDownstreamConnection::push_request_headers()`. In `v1.69.0`, the backend request preserves both the Upgrade headers and the `Content-Length` header. The buffered frontend body is then copied into the backend request buffer by `HttpDownstreamConnection::process_blocked_request_buf()`. + +Relevant source behavior in `v1.69.0`: + +| File | Behavior | +| --- | --- | +| `src/shrpx_downstream.cc` | Marks non-h2c HTTP/1.1 Upgrade requests through `req_.upgrade_request` | +| `src/shrpx_https_upstream.cc` | Accepts an Upgrade request with `Content-Length` and dispatches it to a backend connection | +| `src/shrpx_http_downstream_connection.cc` | Forwards original request headers, including `Content-Length`, and forwards the buffered body | +| `src/shrpx_https_upstream.cc` | Detaches and reuses backend connections after response completion when the request state is complete | + +The fixed upstream commit adds explicit rejection for `CONNECT` or Upgrade requests carrying `Transfer-Encoding` or `Content-Length`: + +```text +transfer-encoding and content-length are not allowed in CONNECT or upgrade request +``` + +It also changes request body blocking around Upgrade handling so body bytes are held until the backend Upgrade response establishes a tunnel. + +## Exploit Flow + +1. The PoC starts a backend that keeps HTTP/1.1 connections alive. +2. The backend treats `Upgrade` as a boundary and parses bytes after the header terminator as another request. +3. The PoC starts the supplied `nghttpx` binary with a cleartext frontend and the local backend. +4. The attacker client sends `GET /upgrade` with `Upgrade: websocket` and `Content-Length`. +5. The attacker body contains a complete smuggled request: `GET /poisoned HTTP/1.1`. +6. `nghttpx` forwards the Upgrade request and the body to the backend. +7. The backend replies to `/upgrade` with `UPGRADE-REJECT`. +8. The backend parses `/poisoned` from the forwarded body and delays its response. +9. The attacker connection closes after receiving `UPGRADE-REJECT`. +10. The victim client sends `GET /victim`. +11. `nghttpx` reuses the same backend connection. +12. The delayed `/poisoned` response is read as the victim response. + +## Files + +- `poc.py` - stdlib-only Python exploit driver. It starts a real `nghttpx` target process, drives attacker and victim frontend clients, and prints a JSON result. +- `evidence/local-verification.txt` - local vulnerable and fixed-control transcripts. + +## Usage + +Build or provide an nghttp2 `v1.69.0` `nghttpx` binary, then run: + +```bash +python3 poc.py --nghttpx ./build/src/nghttpx --cwd ./nghttp2-v1.69.0 +``` + +Run with verbose protocol traces: + +```bash +python3 poc.py --nghttpx ./build/src/nghttpx --cwd ./nghttp2-v1.69.0 --verbose +``` + +Run with a custom benign payload: + +```bash +python3 poc.py --nghttpx ./build/src/nghttpx --cwd ./nghttp2-v1.69.0 --payload "BENIGN-QUEUE-POISON" +``` + +Expected vulnerable output: + +```json +{ + "attacker_body": "UPGRADE-REJECT", + "victim_body": "SMUGGLED-BENIGN-PAYLOAD", + "victim_received_poison": true, + "victim_received_expected": false +} +``` + +Run a fixed-control binary: + +```bash +python3 poc.py --nghttpx ./build-fixed/src/nghttpx --cwd ./nghttp2-fixed --expect-fixed +``` + +Expected fixed-control output: + +```json +{ + "attacker_body": "", + "victim_body": "VICTIM-RESPONSE", + "victim_received_poison": false, + "victim_received_expected": true +} +``` + +## Local Verification + +The vulnerable run used `nghttpx nghttp2/1.69.0`. The fixed-control run used upstream master after `ab28105c`, reporting `nghttpx nghttp2/1.69.90`. + +Vulnerable result: + +```text +"attacker_body": "UPGRADE-REJECT" +"victim_body": "SMUGGLED-BENIGN-PAYLOAD" +"victim_received_poison": true +"victim_received_expected": false +"backend_requests": [ + [ + "GET /upgrade HTTP/1.1", + "GET /poisoned HTTP/1.1", + "GET /victim HTTP/1.1" + ], + [] +] +``` + +Fixed-control result: + +```text +"attacker_body": "