Skip to content

Back to the catalog

calif.iowriteup

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.

TL;DR Calif shows how forced memory_limit fatals leave stale PHP interpreter state that yields pre-auth RCE on mod_php and PHP-FPM.
Document

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_size against 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 toward max_file_uploads, yet each still creates six $_FILES entries. With nested names such as files[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:

  1. _zend_hash_str_add_or_update_i increments nNumUsed and nNumOfElements before zend_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 during php_request_startup(). Under mod_php, php_request_shutdown() destroys the malformed table. Under PHP-FPM, startup failure exits the worker, so nothing consumes it.
  2. 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, but PS(id) and PS(http_session_vars) still point into it. A second multipart request with a session ID and PHP_SESSION_UPLOAD_PROGRESS but 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.
  3. 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, and php_var_unserialize_init() reuses the stale pointer. A negative used_slots in the destructor block then selects memory before the array. Duplicate keys make var_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() or unserialize(), including WordPress's maybe_unserialize(). The decoded data can be legitimate. phpMyAdmin was exploited through session_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=-1

Caveats

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