# REPRO-2026-00386: curl HTTP/2 upload Use-After-Free (RST_STREAM HTTP_1_1_REQUIRED/REFUSED_STREAM) leading to potential RCE ## Summary Status: published Severity: high CVSS: Unknown CWE: CWE-416 Use After Free (Use After Free) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00386 CVE: CVE-2026-92392 ## Package Name: Unknown Ecosystem: Unknown Affected: Unknown Fixed: Unknown ## Root Cause # Root Cause Analysis: CVE-2026-92392 ## Summary curl 8.22.0 contains a use-after-free in the HTTP/2 upload retry path. When a malicious HTTP/2 peer resets an active upload with `HTTP_1_1_REQUIRED` or `REFUSED_STREAM`, libcurl converts the reset into a retry. In the vulnerable cleanup order, curl first tells the connection filters that the transfer is done and then lets request cleanup flush buffered upload bytes. Those bytes can be reinterpreted as a new HTTP/2 request, creating an unintended stream whose user data is the soon-to-be-freed `Curl_easy`. This run reproduced the complete disclosed exploit chain through the real curl tool: a malicious peer reclaimed and corrupted the freed object, induced a 99,056-byte forged-stream heap over-read, recovered fresh current-process libc and libcurl addresses despite ASLR, replaced the object with a fake `Curl_easy`, and triggered its attacker-selected debug callback as libc `system()`, creating a verifiable target-local marker. ## Impact - **Affected package/component:** curl/libcurl HTTP/2 upload and retry handling with nghttp2. - **Affected versions:** curl 8.22.0 and earlier according to the disclosed advisory. The vulnerable target used here is the exact curl 8.22.0 commit `01346829096c61b372692f6dc43ffa778c6caccd`. - **Fixed control:** commit `8b0744af5b1b631da5b938ca31c3f9895ad5a321` (`multi: adjust the cleanup order in multi_done`). - **Risk level:** High. A malicious HTTP/2 server can execute a command in the victim curl process when the victim uploads a suitable file. The exploit needs a large upload containing request-like text; the demonstrated ASLR-enabled chain also uses curl parallel mode (`-Z`) and a heap-layout-calibrated runtime. - **Consequence:** Remote command execution with the privileges and confinement of the curl process. The marker commands in the final isolated proof were `echo Vv1>mk_none` and `echo Vv2>mk_none`. ## Impact Parity - **Disclosed/claimed maximum impact:** Remote code execution (`code_execution`). - **Reproduced impact from this run:** Attacker-induced command execution through the production curl CLI and TCP/TLS/HTTP/2 boundary. Two fresh vulnerable processes created unique distinct marker files after independently leaking their randomized libc/libcurl bases. Two fixed processes reached the same malicious reset/stall path but created no resurrected stream, disclosure, callback trigger, or marker. - **Parity:** `full`. - **Not demonstrated:** The marker command is intentionally harmless and does not establish persistence or privilege escalation. Reliability outside the pinned exploit runtime was not claimed; the underlying UAF is broader than this heap-grooming calibration. ## Root Cause The failure is an ordering bug in `multi_done()` combined with HTTP/2 stream user data retaining a raw `Curl_easy *`: 1. curl uploads a large file over HTTP/2. The peer limits stream flow control so an upload remainder stays queued. 2. The malicious peer replies and sends `RST_STREAM(HTTP_1_1_REQUIRED or REFUSED_STREAM)`, causing libcurl to schedule a retry. 3. Vulnerable `multi_done()` calls `Curl_conn_ev_data_done(data, premature)` before `Curl_req_done(&data->req, data, premature)`. 4. Connection-filter teardown makes curl forget the original transfer/stream association. Request teardown then flushes the buffered upload remainder into the still-live connection. 5. If those file bytes parse as an HTTP request head, `h2_submit()` opens a new/resurrected stream and nghttp2 stores the raw `Curl_easy *` as stream user data. 6. The normal retry completes. The curl tool frees the easy handle, but the pooled HTTP/2 connection and resurrected stream retain the stale pointer. 7. The exploit uses attacker-controlled TLS 1.3 session-ticket sizes and HPACK header allocations to reclaim the freed 3,368-byte `Curl_easy`, then corrupt a `buf_chunk` associated with the orphaned stream. A window update makes curl transmit bytes outside the intended upload buffer. 8. The peer receives 99,056 leaked heap bytes and identifies `main_arena` and libcurl pointers. Those per-process values resolve libc `system()` under ASLR. 9. A final session ticket reoccupies the stale object with a fake `Curl_easy`: the command string begins at offset zero, `set.fdebug` points to the freshly resolved `system()`, and the verbose flag forces the callback path. An invalid HTTP/2 frame invokes `Curl_failf()`/tracing through the fake callback, executing the marker command. The disclosed exploit was originally calibrated with `ZT_V1=34660`, which leaked only upload prose in this runtime. Live heap analysis showed the current unsorted-piece geometry was smaller; changing only that first carve value to `ZT_V1=32400` preserved the intended `0xc50` remainder. Adjacent controls (`32160` and `32640`) failed to disclose pointers, while `32400` succeeded repeatedly. The upstream fixed commit is . It moves `Curl_req_done()` before `Curl_conn_ev_data_done()` so request bytes are finished while the connection filter still knows the transfer, and then explicitly tells filters to give up references before detaching the connection. In this run, the fixed build never created the resurrected stream. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh` from any directory. `PRUVA_ROOT` may be set to the bundle path. 2. The script resolves the prepared project cache, verifies the exact vulnerable and fixed source commits and cleanup-order hunk, and verifies or rebuilds a pinned Ubuntu 24.04 exploit image containing stock non-ASAN curl 8.22.0, nghttp2 1.59.0, OpenSSL 3.0.13, and glibc 2.39. 3. It builds the exact fixed commit with the same compiler, flags, dependencies, and runtime closure. 4. For each attempt, the script starts the real malicious TLS/HTTP/2 TCP peer and invokes the actual curl tool as: ```text curl -Z -T sample_docs.md https://127.0.0.1:/x https://127.0.0.1:/y ``` 5. It runs two isolated vulnerable attempts and requires each to show a resurrected stream, a full 115,439-byte forged transfer, a 99,056-byte leaked-memory artifact, independently resolved libc/libcurl bases, the final fake-object callback trigger, and an actual `marker.txt` copied from the curl process's working directory. 6. It runs two isolated fixed attempts with the same peer and packet sequence and requires each to reach the HTTP/2 reset/stall path without resurrection, disclosure, callback trigger, or marker. 7. It verifies that the two vulnerable libc/libcurl bases differ, proving that ASLR is active and that addresses were reacquired from each live process rather than reused as constants. 8. Expected final output is: ```text [+] CONFIRMED: malicious HTTP/2 peer achieved command execution in curl 8.22.0 (2/2); fixed commit blocked the chain (2/2) ``` ## Evidence The final script was run twice consecutively and passed both times. The latest immutable proof is bound by `bundle/repro/runtime_manifest.json`. ### Vulnerable process 1 - Peer transcript: `bundle/repro/rce-vuln-1/server.log` - curl transcript: `bundle/repro/rce-vuln-1/victim.log` - Leaked bytes: `bundle/repro/rce-vuln-1/leak.bin` (99,056 bytes) - Actual command marker: `bundle/repro/rce-vuln-1/marker.txt` - Marker value: ```text Vv1 ``` - Key peer evidence: ```text *** RESURRECTED stream 5 ... Z9: pulled 115439 bytes of DATA on S'=5 (wanted 115439) Z9: leaked 99056 bytes Z9: libc_base=0x7f8bb0400000 by 5 vote(s) ... system=0x7f8bb0458750; libcurl_base=0x7f8bb0e4b000 Z10: sent NST(final fake, 2832 B, fdebug=0x7f8bb0458750, cmd=b'echo Vv1>mk_none') ... ``` ### Vulnerable process 2 - Peer transcript: `bundle/repro/rce-vuln-2/server.log` - Leaked bytes: `bundle/repro/rce-vuln-2/leak.bin` (99,056 bytes) - Marker: `bundle/repro/rce-vuln-2/marker.txt` - Independently randomized/disclosed addresses: ```text libc_base=0x7fa11e705000 libcurl_base=0x7fa11f150000 system=0x7fa11e75d750 ``` These differ from vulnerable process 1. `/proc/sys/kernel/randomize_va_space` was `2`; the exploit did not read `/proc` in the final proof. ### Fixed negative controls - `bundle/repro/rce-fixed-1/server.log` - `bundle/repro/rce-fixed-2/server.log` - `bundle/repro/rce-fixed-1/victim-cwd-ls.txt` - `bundle/repro/rce-fixed-2/victim-cwd-ls.txt` Both controls show the listener, TLS/HTTP/2 preface, 586,035-byte upload, first reset, retry, and second stalled upload. For example: ```text conn0: learned 586035 body bytes ... RST_STREAM(REFUSED_STREAM) conn1: A#2 stalled at 360447 body bytes -> trigger refused ``` Neither control contains `RESURRECTED`, `Z9`, or `Z10`; each victim directory is empty and `marker-name.txt` contains only a newline. ### Build and mitigation identity - `bundle/logs/repro/rce-vulnerable-identity.log`: curl 8.22.0 executable/library/libc checksums and version. - `bundle/logs/repro/rce-vulnerable-mitigations.log`: PIE (`ET_DYN`), NX stack, RELRO, symbols, build ID, and loader resolution. - `bundle/logs/repro/rce-fixed-mitigations.log`: corresponding fixed-build evidence. - `bundle/logs/repro/randomize_va_space.log`: kernel ASLR setting. - Vulnerable executable SHA-256: `ee31384e2df166f57d7dbdf3f8a7f9f368a38f8ddcf8ef4e80ca038c06931ae7`. - Vulnerable libcurl SHA-256: `ee986196347fe90ba808c6fa5dcde03ce4d27a08a16fa339b454a74e6785ef53`. - glibc SHA-256: `d8db8739a1633c972cec6a4fe0566bdcec6fd088f98723492ab0361f66238f75`. - Fixed ELF SHA-256: `216e8087e5b4a496d76eb3133687c6a748ce7da1dc7cc808de7a1e001393d68f`. - Fixed libcurl SHA-256: `74b322c9f6492924604a8546d3c0302a8be05f02a544b5f0425c2ff7c334f53d`. ## Recommendations / Next Steps - Upgrade to curl 8.23.0 or another release containing commit `8b0744af5b1b631da5b938ca31c3f9895ad5a321`. - For backports, ensure `Curl_req_done()` occurs before `Curl_conn_ev_data_done()` and that connection filters release all `Curl_easy` references before the object can be detached/freed. - Add regression tests for both `HTTP_1_1_REQUIRED` and `REFUSED_STREAM`, large flow-controlled uploads, retry paths, request-like patterns at multiple upload offsets, parallel transfers, connection-pool shutdown, and frame delivery to stale stream IDs. - Exercise those tests with ASAN and non-sanitized hardened builds. - Consider replacing raw `Curl_easy *` nghttp2 stream user data with a validated identifier/lookup so a stale stream cannot directly retain an object address. ## Additional Notes - The final production proof uses no sanitizer and keeps certificate verification enabled. The malicious peer certificate is signed by an isolated test CA installed in the pinned container. - The reproducer is idempotent and passed twice consecutively, with two fresh vulnerable and two fresh fixed processes per invocation. - The vulnerability trigger itself is not tied to this exact heap. Full command execution is allocator/build sensitive, so the reproducer pins the target closure and verifies its binary hashes before running. - Later glibc `double free or corruption` aborts are expected after the marker has been created because the exploit deliberately corrupts allocator/object state. Marker creation and fixed negative controls, not the final abort, are the command-execution oracle. - The final bounded marker commands are `echo Vv1>mk_none` and `echo Vv2>mk_none`; each is at most 23 bytes because the fake object's `multi` field begins at offset 24. Fixed attempts receive distinct candidate commands `echo Vf1>mk_none` and `echo Vf2>mk_none`, but create no file. ## Reproduction Details Reproduced: 2026-10-14T20:02:27.468Z Duration: 18345 seconds Tool calls: 846 Turns: Unknown Handoffs: 4 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00386 pruva-verify CVE-2026-92392 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00386&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00386/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-92392 - Source: https://www.cve.org/CVERecord?id=CVE-2026-92392 ## Artifacts - bundle/repro/rca_report.md (analysis, 11607 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 19211 bytes) - bundle/repro/evil_server.py (script, 31427 bytes) - bundle/repro/rce-evidence-pinned/server.log (log, 22809 bytes) - bundle/repro/rce-fixed-1/marker-name.txt (other, 1 bytes) - bundle/repro/rce-fixed-1/victim-cwd-ls.txt (other, 93 bytes) - bundle/repro/rce-fixed-1/victim-exit.txt (other, 4 bytes) - bundle/repro/rce-fixed-1/victim.log (log, 3418 bytes) - bundle/repro/rce-fixed-2/marker-name.txt (other, 1 bytes) - bundle/repro/rce-fixed-2/observation.json (other, 135 bytes) - bundle/repro/rce-fixed-2/server.log (log, 12904 bytes) - bundle/repro/rce-fixed-2/victim-cwd-ls.txt (other, 93 bytes) - bundle/repro/rce-fixed-2/victim-exit.txt (other, 4 bytes) - bundle/repro/rce-fixed-2/victim.log (log, 3418 bytes) - bundle/repro/rce-vuln-1/leak-size.txt (other, 6 bytes) - bundle/repro/rce-vuln-1/marker-name.txt (other, 8 bytes) - bundle/repro/rce-vuln-1/victim-cwd-ls.txt (other, 141 bytes) - bundle/repro/rce-vuln-1/victim-exit.txt (other, 4 bytes) - bundle/repro/rce-vuln-1/victim.log (log, 822 bytes) - bundle/repro/rce-vuln-2/leak-size.txt (other, 6 bytes) - bundle/repro/rce-vuln-2/marker-name.txt (other, 8 bytes) - bundle/repro/rce-vuln-2/victim-cwd-ls.txt (other, 141 bytes) - bundle/repro/rce-vuln-2/victim-exit.txt (other, 4 bytes) - bundle/repro/rce-vuln-2/victim.log (log, 894 bytes) - bundle/repro/rce_kit/Dockerfile (other, 4258 bytes) - bundle/repro/rce_kit/rce/ca/labca.crt (other, 1176 bytes) - bundle/repro/rce_kit/rce/ca/server.pem (other, 4092 bytes) - bundle/repro/rce_kit/rce/run_server.sh (other, 1737 bytes) - bundle/repro/rce_kit/rce/sample_docs.md (documentation, 586035 bytes) - bundle/repro/rce_kit/rce/server/h2srv_gate.py (script, 13937 bytes) - bundle/repro/rce_kit/rce/server/mkstage1.py (script, 939 bytes) - bundle/repro/rce_kit/rce/server/scenario_learn.py (script, 8698 bytes) - bundle/repro/rce_kit/rce/server/scenario_z2.py (script, 33977 bytes) - bundle/repro/rce_kit/rce/server/stage1_z.bin (other, 3368 bytes) - bundle/repro/rce_kit/rce/server/tls13_takeover.py (script, 10457 bytes) - bundle/repro/rce_kit/rce/server/z_tail_g.py (script, 47846 bytes) - bundle/repro/runtime_manifest.json (other, 5689 bytes) - bundle/repro/validation_verdict.json (other, 1907 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00386 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00386/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00386 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://www.pruva.dev