Files
php2shell/fcgi/FINDINGS.md
T
neko 8147ce0edb 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.
2026-10-21 21:19:25 +00:00

129 lines
5.4 KiB
Markdown

# RCE via FastCGI + Session Upload Progress (No LFI)
## Summary
When an attacker can reach PHP-FPM's FastCGI socket (TCP or unix), the
session upload progress write primitive from CVE-2026-109618 /
CVE-2026-109619 becomes RCE without any application-level vulnerability.
No LFI needed. No framework needed. No special PHP configuration needed.
Works on default php.ini settings.
## Attack chain
```
Worker A (held open) Worker B (executes)
--- ---
FastCGI POST with FastCGI GET with
multipart body: PHP_VALUE:
PHP_SESSION_UPLOAD_PROGRESS auto_prepend_file=
= <?php system(...); ?> /tmp/sess_<SID>
SCRIPT_FILENAME=
File upload content /var/www/html/index.php
dripped one byte at a time
-> session file persists PHP includes the session
-> parser blocks for file before index.php
the entire drip window -> attacker PHP runs
-> RCE
```
### Step by step
1. **Open a FastCGI connection to FPM worker A.** Send a POST request
with `Content-Type: multipart/form-data` and `PHPSESSID=<SID>` in the
`HTTP_COOKIE` param. The body contains:
- A `PHP_SESSION_UPLOAD_PROGRESS` field with embedded PHP code
- A file upload whose content is dripped slowly (byte by byte)
2. **FPM worker A enters the multipart parser.** As soon as the progress
field and file header are read, `session.upload_progress` writes the
attacker's PHP code into `/tmp/sess_<SID>`. The parser then blocks
reading the file content from STDIN, which we control.
3. **While worker A is blocked, open a second FastCGI connection to
worker B.** Send a GET request with:
- `SCRIPT_FILENAME = /var/www/html/index.php` (any existing .php file)
- `PHP_VALUE = auto_prepend_file = /tmp/sess_<SID>`
4. **Worker B executes.** PHP's `auto_prepend_file` directive causes it to
`include()` the session file before running the main script. The session
file contains the attacker's `<?php ... ?>` code, which executes.
5. **Cleanup.** The drip finishes, worker A completes normally (or times
out), the session file is cleaned up. The attacker's code has already
executed on worker B.
## Why it works on default configuration
| Setting | Default | Required |
|---------|---------|----------|
| `session.upload_progress.enabled` | `1` | `1` |
| `session.save_path` | `/tmp` | Any known path |
| `pm.max_children` | `5` (dynamic) | `>= 2` |
| `security.limit_extensions` | `.php .phar` | Any (not checked for auto_prepend) |
| `session.upload_progress.cleanup` | `1` | Does not matter (drip defeats it) |
`security.limit_extensions` blocks execution of files without `.php`
extension as `SCRIPT_FILENAME`. But `auto_prepend_file` loads files via
`include()`, which ignores `security.limit_extensions` entirely. The
session file (which has no extension) is included as PHP code regardless
of `limit_extensions`.
## When FPM is reachable
This is the main precondition. The attacker needs raw TCP or unix socket
access to the FPM process. This is realistic in:
1. **Docker containers.** The official `php:*-fpm` images configure FPM to
listen on TCP `0.0.0.0:9000`. Any container on the same Docker network
can reach it.
2. **Kubernetes pods.** PHP-FPM pods are typically addressed by service
name. Adjacent pods, including compromised ones, can send raw FastCGI.
3. **SSRF.** An SSRF vulnerability in any application on the same host
can reach `127.0.0.1:9000` or the unix socket. The `gopher://` scheme
can carry arbitrary TCP payloads including FastCGI.
4. **Exposed port.** Misconfigured firewalls, cloud security groups, or
Docker port mappings exposing 9000 to the network.
5. **Same-host access.** A low-privilege shell on the same machine can
often read the unix socket (permissions vary by distro).
## Test results
Tested against PHP-FPM 8.3.6 with default settings:
- `session.upload_progress.enabled=1` (default)
- `session.save_path=/tmp` (default)
- `session.upload_progress.cleanup=1` (default)
- `security.limit_extensions = .php .phar` (default)
- `pm = static`, `pm.max_children = 2`
Results: 3/3 tests successful. Race hit on first attempt within 200ms
of the drip starting. Reliable with `--drip-time 5` (5 second window).
## Mitigations
1. **Set `session.upload_progress.enabled=0`** if upload progress tracking
is not needed. This removes the write primitive entirely.
2. **Block network access to FPM.** FPM should only be reachable from the
web server process. Use unix sockets with restricted file permissions,
or firewall TCP port 9000.
3. **Use `pm.max_children=1`** as a last resort. With only one worker, the
racer cannot execute while the drip holds a worker. This severely impacts
performance and is not practical.
4. Setting `security.limit_extensions` does NOT help. The inclusion
happens via `auto_prepend_file`, which bypasses limit_extensions.
5. Setting `php_admin_value[auto_prepend_file]` in the FPM pool config
does NOT help. Verified: with `php_admin_value[auto_prepend_file] =
/dev/null` in the pool, a FastCGI request with `PHP_VALUE:
auto_prepend_file = /tmp/sess_xxx` still overrides it. Raw FastCGI
access bypasses the web server that would normally enforce admin
values. The only effective mitigation is blocking FastCGI access.