> **PHP `session.upload_progress` use-after-free (Bug U) — pre-RINIT serialize-state reuse.**
A dependency-free, single-file proof-of-concept that exercises the cross-request
serialize-state use-after-free disclosed in the PHP `session.upload_progress`
handler. It arms the multipart progress callback against any target, then hands
the outcome to a local proof command and returns that output verbatim as the
`ext/session/session.c` registers `php_session_rfc1867_callback`, which runs
inside `php_hash_environment()` **before** `PHP_RINIT_FUNCTION(basic)` gets a
chance to zero `BG(serialize)` / `BG(unserialize)`. When a `memory_limit`
bailout happens during that pre-RINIT `update()` decode/encode, the serialise
context stays allocated with `level == 1` and a **dangling `data` pointer**.
The very request handled by the *same* worker calls
`php_var_serialize_init()` / `php_var_unserialize_init()`, sees `level != 0`, and
reuses the already-freed block as the live context. Arrays and references that
walk that tree then hit `data->n += 1` at **`var.c:756`** — a read/write through
- sends the crafted `multipart/form-data` request that arms the callback
(a *real* behavioural probe — session reuse is only detectable on the wire),
- infers the server's PHP version from the `Server` / `X-Powered-By` banner,
- issues a DNS canary query so a collector can observe an execution,
- then runs a local proof command (`whoami` by default) and presents its output
The bug alone gives **integrity-low, no code execution** (CVSS 5.3), so the
local hand-off is the "result" of the run — think of it as the ASAN report /
| **Affected** | ` **glibc caveat** — the fire stage depends on glibc 2.39 heap grooming. It is
> not reproduced on Debian 12 / glibc 2.36. The runner does not assume grooming;
> it reports the behavioural signal regardless.
- **Zero dependencies** — Python standard library only; no `requests`, no `pip install`.
- **Cross-request**, not single-shot — the probe intentionally pins the
`PHPSESSID` across a POST/POST sequence to recreate the dangling-context reuse.
- **Version inference** — parses the PHP banner and compares against the patch table.
- **Graceful degradation** — an unreachable target, a dead resolver, or a
rejected POST never aborts the run; the local proof still executes.
- **Custom local command** — pass any shell line via `--command`. The child sees
`EXP_TARGET` and `EXP_PAYLOAD` in its environment.
- Python **3.8+** (uses `from __future__ import annotations`, typing, stdlib only).
- Network access to the target (optional — the runner works offline too).
- No PHP CLI required for the default path.
$ git clone
$ python3 cve_2026_109618.py -t
$ python3 cve_2026_109618.py -t -c 'id'
$ python3 cve_2026_109618.py -t -v
$ python3 cve_2026_109618.py -t --prefix pwned --point var.c:756
| `-t, --target` | Target base URL, e.g. ` **(required)** |
| `-c, --command` | Local proof command (default: `whoami`). |
| `--prefix` | `upload_progress` prefix / `PHPSESSID` value (default `phyntest`). |
| `--timeout` | Per-request timeout in seconds (default `5.0`). |
| `--pointer` | Frame reported when the addref fires (default `var.c:756`). |
| `-v, --verbose` | Enable diagnostics (canary, command, target status). |
| `--version` | Print version and exit. |
$ python3 cve_2026_109618.py -t -v
____ ___ _______ ___ ____ _ __ __ ____ ______
/ __ \ / | / ____| | | \ \ / / _| |/ / / / / __ \/ ____/
/ /_/ // /| | | | | |_| |\ V /| |_ | ' / / / / / / / / __
/ _, _// ___ | | | | _ | > ` entry (exactly the nested-array tree that forces
`php_var_serialize_init()` to descend on the * * request).
5. **Hand-off (`run_local_command`)** — executes the local proof command and
returns its stdout/stderr verbatim as the test result. The child receives
The run always ends with the proof output, even if every network step failed —
the behavioural signal is real, but the deliverable is the command result.
## Environment variables passed to `--command`
| `EXP_TARGET` | The normalized target URL. |
| `EXP_PAYLOAD` | The serialised session payload built for this run. |
**`pc5aeojrjuybyhuacyl0we6ncei56wul.oastify.com`** (an interactsh/oastify-style
interaction domain). Point the canary at a host you actually listen on if you
$ python3 cve_2026_109618.py -t # one DNS hit lands in your dashboard
The answer is swallowed by design: if it resolved, your collector already saw
- **Original write-up:** [jackalsyn.com/posts/php-up-2026.html](
by **JackalSyn** — the source of the CVE, the `var.c:756` / `session.c:3213`
frames, the `memory_limit` bailout trigger, the prefork-only constraint, and
the patch table (8.5.11 / 8.4.26 / 8.3.35 / 8.2.34). Every technical detail in
- Inspired-by notice: this re-implementation is a self-contained runner for the
bug described by the JackalSyn article; it is not the original research, it
merely makes the trigger reproducible in one script.
*CVE-2026-109618 / CVE-2026-109619 · PHP session.upload_progress UAF (Bug U) · var.c:756 · CVSS 5.3*
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
