PHP2Shell: pre-auth RCE via PHP memory-limit fatal errors (CVE-2026-109618/109619)
PHP2Shell is Calif's writeup of pre-auth RCE in PHP (CVE-2026-109618, CVE-2026-109619) triggered by memory_limit fatal errors that leave dangling state. It covers a HashTable bucket bug in _zend_hash_str_add_or_update_i, a Session upload-progress stale-pointer bug, and an unserialize destructor-list primitive that reaches PHP-FPM. It lists affected apps (WordPress, Drupal, Joomla, Magento and others) and gives temporary mitigations.
What it is
Calif's Ryan Austin describes pre-auth RCE in PHP (CVE-2026-109618, CVE-2026-109619) that starts with a memory_limit fatal error. The fatal interrupts native code before cleanup, and request shutdown then frees memory that surviving state still points to. The page says the earliest affected path dates to PHP 5.4.0 and that the flaws are exploitable under default configurations. It confirms pre-auth RCE on WordPress 7.1.2, Drupal 11.4.3, Joomla 6.1.4, MediaWiki 1.46.2, Roundcube 1.7.4, Magento 2.4.9, phpMyAdmin 5.2.3, Nextcloud 35.0.1, Laravel 13.11, Symfony 8.1 and WHMCS.
How it works
Forcing the fatal. Two tricks reach memory_limit despite the default 8 MiB post_max_size:
- PHP checks
post_max_sizeagainst the declared Content-Length only. A chunked HTTP/1.1 or lengthless HTTP/2 multipart request under Apache has no meaningful length. An input-decompression filter creates the same mismatch. - Multipart parts with
filename=""are not counted towardmax_file_uploads, yet each still creates six$_FILESentries. With nested names such asfiles[a][b][c], one short record becomes many HashTables and buckets. Hundreds of them exceed the default 128 MiB heap while the body stays under 8 MiB.
Three interruption points:
_zend_hash_str_add_or_update_iincrementsnNumUsedandnNumOfElementsbeforezend_string_init()allocates the key. If that allocation fatals, teardown treats uninitialized bucket bytes as live. It is reached from POST and RFC1867 variable registration duringphp_request_startup(). Under mod_php,php_request_shutdown()destroys the malformed table. Under PHP-FPM, startup failure exits the worker, so nothing consumes it.- Session upload progress can start a session during multipart parsing, before
PG(modules_activated)is set. If parsing fatals, the END callback and session RSHUTDOWN are skipped. The heap is freed, butPS(id)andPS(http_session_vars)still point into it. A second multipart request with a session ID andPHP_SESSION_UPLOAD_PROGRESSbut no file part reaches END cleanup without the reset. It then reads and destroys the freed values. This works on mod_php because the worker survives startup failure. - The generic unserializer publishes its state in
BG(unserialize)and may not clear it after a fatal. If the fatal comes after successful startup, a PHP-FPM worker survives. The next multipart request reaches upload progress before Basic RINIT, andphp_var_unserialize_init()reuses the stale pointer. A negativeused_slotsin the destructor block then selects memory before the array. Duplicate keys makevar_push_dtor_value()copy a zval there, which gives a controlled pointer and type write.
flowchart TD
A[Memory-limit fatal] --> B{SAPI}
B -->|mod_php| C[Request shutdown frees heap, worker reused]
B -->|PHP-FPM| D{Fatal timing}
D -->|During startup| E[Worker exits, no exploit]
D -->|After startup, in unserialize| F[Worker survives, stale decoder state]
C --> G[Stale bucket or Session state consumed]
F --> H[Next upload-progress request reuses state]Preconditions / Affected
- HashTable bug: arbitrary PHP scripts on Apache mod_php with PHP >= 7.3.
- Session upload-progress bug: arbitrary PHP scripts on Apache mod_php with PHP >= 5.4.0.
- Unserialize primitive: application-dependent, on both mod_php and PHP-FPM (the demo uses Nginx with PHP-FPM). The app needs a reachable post-startup decode such as
session_start(),session_decode()orunserialize(), including WordPress'smaybe_unserialize(). The decoded data can be legitimate. phpMyAdmin was exploited throughsession_start().
What to do
Upgrade to a fixed PHP release. The page points to the PHP October 2026 Security Release and does not name fixed version numbers. Temporary, partial mitigations:
; PHP-FPM
session.upload_progress.enabled=0
memory_limit=-1
; mod_php
memory_limit=-1Caveats
The page says these settings do not replace a patch. memory_limit=-1 removes the memory cap and raises the risk of memory exhaustion. The page gives no detection guidance. It does not say which CVE maps to which bug.
Organisations named here
- Ryan Austin
- Stefan Esser
CVEs named here
- CVE-2026-109618
- CVE-2026-109619
Technologies named here
- PHP
- WordPress
- nginx
- phpMyAdmin
Techniques named here
- Use-after-free
Threat actors named here
- PHP2Shell
- Calif
Related in AppSec
Flowise MCP Security Bypass Enables RCE
Inside an Oracle Database SQL Injection Attack | Huntress
OASIS
