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:
neko
2026-10-21 21:19:25 +00:00
parent 7fecd435cc
commit 8147ce0edb
6 changed files with 1616 additions and 31 deletions
+56
View File
@@ -0,0 +1,56 @@
<?php
// Deployment diagnostic for the no-LFI session attack surface.
// Drop on the target, request once, then REMOVE it.
//
// Reports whether the two preconditions of the served-path chain hold:
// 1. session.save_path is inside (or aliased to) a web-served tree
// 2. security.limit_extensions is empty (fpm would execute
// extensionless session files via PATH_INFO)
header('Content-Type: text/plain');
$sp = session_save_path();
if ($sp === '') {
$sp = sys_get_temp_dir();
}
// strip the optional "N;/path" depth prefix
if (($p = strrchr($sp, ';')) !== false) {
$sp = substr($p, 1);
}
$doc = rtrim($_SERVER['DOCUMENT_ROOT'] ?? '', '/');
$real_sp = realpath($sp) ?: $sp;
$real_doc = realpath($doc) ?: $doc;
$under_doc = ($real_doc !== '' && strncmp($real_sp, $real_doc, strlen($real_doc)) === 0);
$le = ini_get('security.limit_extensions');
$sapi = php_sapi_name();
echo "sapi = {$sapi}\n";
echo "session.save_path = {$sp}\n";
echo "DOCUMENT_ROOT = {$doc}\n";
echo "save_path under docroot = " . var_export($under_doc, true) . "\n";
echo "security.limit_extensions = [" . $le . "]\n";
echo "upload_progress.enabled = " . ini_get('session.upload_progress.enabled') . "\n";
echo "upload_progress.cleanup = " . ini_get('session.upload_progress.cleanup') . "\n";
echo "memory_limit = " . ini_get('memory_limit') . "\n";
echo "post_max_size = " . ini_get('post_max_size') . "\n";
echo "open_basedir = [" . ini_get('open_basedir') . "]\n";
$verdict = array();
if (ini_get('session.upload_progress.enabled')) {
if ($under_doc) {
$verdict[] = "save_path is web-served: session files are requestable";
}
if ($sapi === 'fpm-fcgi' && trim($le) === '') {
$verdict[] = "limit_extensions empty: extensionless session files EXECUTE via PATH_INFO";
}
}
if ($verdict) {
echo "\nEXPOSED:\n";
foreach ($verdict as $v) {
echo " - {$v}\n";
}
echo "\nSee nolfi/FINDINGS.md. Set security.limit_extensions and move save_path.\n";
} else {
echo "\nNo served-path exposure detected.\n";
}