Skip to content
php2shell

php2shell

Sploitus • October 21, 2026

> **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*