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.
5.4 KiB
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
-
Open a FastCGI connection to FPM worker A. Send a POST request with
Content-Type: multipart/form-dataandPHPSESSID=<SID>in theHTTP_COOKIEparam. The body contains:- A
PHP_SESSION_UPLOAD_PROGRESSfield with embedded PHP code - A file upload whose content is dripped slowly (byte by byte)
- A
-
FPM worker A enters the multipart parser. As soon as the progress field and file header are read,
session.upload_progresswrites the attacker's PHP code into/tmp/sess_<SID>. The parser then blocks reading the file content from STDIN, which we control. -
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>
-
Worker B executes. PHP's
auto_prepend_filedirective causes it toinclude()the session file before running the main script. The session file contains the attacker's<?php ... ?>code, which executes. -
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:
-
Docker containers. The official
php:*-fpmimages configure FPM to listen on TCP0.0.0.0:9000. Any container on the same Docker network can reach it. -
Kubernetes pods. PHP-FPM pods are typically addressed by service name. Adjacent pods, including compromised ones, can send raw FastCGI.
-
SSRF. An SSRF vulnerability in any application on the same host can reach
127.0.0.1:9000or the unix socket. Thegopher://scheme can carry arbitrary TCP payloads including FastCGI. -
Exposed port. Misconfigured firewalls, cloud security groups, or Docker port mappings exposing 9000 to the network.
-
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
-
Set
session.upload_progress.enabled=0if upload progress tracking is not needed. This removes the write primitive entirely. -
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.
-
Use
pm.max_children=1as 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. -
Setting
security.limit_extensionsdoes NOT help. The inclusion happens viaauto_prepend_file, which bypasses limit_extensions. -
Setting
php_admin_value[auto_prepend_file]in the FPM pool config does NOT help. Verified: withphp_admin_value[auto_prepend_file] = /dev/nullin the pool, a FastCGI request withPHP_VALUE: auto_prepend_file = /tmp/sess_xxxstill overrides it. Raw FastCGI access bypasses the web server that would normally enforce admin values. The only effective mitigation is blocking FastCGI access.