Technische details

Hoe PHP2Shell werkt

Eén oorzaak, twee CVE’s, vier bugs: PHP ruimt na een fatale fout — vooral bij een tekort aan geheugen — midden in een request niet betrouwbaar op.

Laatst gecontroleerd 2026-10-22

Oorzaak

Raakt PHP de memory_limit, dan geeft het een fatale fout en springt het terug met longjmp(). Daarmee wordt code overgeslagen die state had moeten afmaken of resetten. Er gaan twee dingen mis:

  • Half gebouwde data (CVE-2026-109618). Geheugen dat wel is toegewezen maar nog niet geïnitialiseerd, wordt tijdens de request-shutdown gelezen alsof het geldig is.
  • Achtergebleven globals (CVE-2026-109619). Globale pointers per request (error-, sessie-, object-, extensie-state) worden niet gewist, maar het geheugen waar ze naar wijzen wordt bij het einde van de request vrijgegeven. De volgende request op dezelfde worker gebruikt ze vóór de normale initialisatie.

Aanvallers bereiken deze paden met gemanipuleerde multipart-requests die PHP tijdens het parsen van de input in een OOM drijven — voordat je router, framework of authenticatie draait. Daarom is een “Hello World”-script net zo blootgesteld als WordPress.

De exploitketen

  1. Stap 1

    Lekken

    Gebruik het geheugenlek om actief workergeheugen te lezen en ASLR te omzeilen.

  2. Stap 2

    Corrumperen

    Gebruik een use-after-free (unserializer of backtrace) om gecontroleerde data te schrijven.

  3. Stap 3

    Uitvoeren

    Zet de corruptie om in code-uitvoering als de workergebruiker — vóór elke login.

Misbruik lukt niet in één keer: de aanvaller tast herhaaldelijk af om zich aan te passen aan de geheugenindeling van elk doelwit. De onderzoekers combineerden de keten in een script met de naam php2shell (niet gepubliceerd) en demonstreerden het op de toen actuele containerimages van wordpress, drupal en joomla.

De vier bugs

CVE-2026-109618

Use-after-free in de upload-progress-unserializer

De sessiemodule decodeert data voor upload-progress-tracking voordat de request-initialisatie klaar is en houdt de boekhouding bij in BG(unserialize). Een mislukte allocatie breekt de request-heap af maar laat die global staan. De volgende request op dezelfde worker behandelt door de aanvaller hergebruikt geheugen als actieve unserializer-state.

CVE-2026-109618

Lek van procesgeheugen op afstand

Tijdens het vroege parsen van RFC 1867-multipart kan upload-progress-code een sessie-ID publiceren in PS(id). Na een OOM-afbreking hangt die pointer los. Een latere multipart-request vertrouwt pointer en lengte en URL-codeert wat daar nu staat in een PHPSESSID-cookie — zelfs als de app nooit session_start() aanroept.

CVE-2026-109619

Use-after-free in de fatal-error-backtrace

EG(last_fatal_error_backtrace) overleeft een OOM die de request-heap vernietigt. Vroege diagnostiek in de volgende request volgt de losse pointer. Het meest direct op PHP 8.5 met fatal_error_backtraces ingeschakeld.

CVE-2026-109619

Type confusion in het gedeelde geheugen van OPcache

In zend_accel_script_persist() wordt CG(map_ptr_last) opgehoogd voordat de gedeelde grens wordt gepubliceerd. Een mislukte allocatie daartussen laat map-pointer-slots achter waarover workers het oneens zijn, zodat een worker een slot voor een ander objecttype kan hergebruiken.

De indeling van bugs per CVE volgt het artikel van Wiz Research. De PHP-advisories beschrijven de klassen breder en noemen meer varianten.

Blootstelling per serveropstelling

Gebaseerd op de PHP-advisories. “Vatbaar” betekent kwetsbaar volgens broncodeanalyse, zonder gepubliceerde generieke exploit voor die SAPI.

Opstelling…109618…109619Toelichting
Apache + mod_phpRCE bevestigdRCE bevestigdBeide CVE’s bevestigd tot RCE met standaardlimieten. Opstelling met het hoogste risico.
PHP-FPM (nginx, Apache, Caddy…)VatbaarRCE bevestigdRCE via de sessielevenscyclus bevestigd op FPM. Het OOM-pad is bevestigd via codepaden op applicatieniveau; er is geen generieke FPM-exploit gepubliceerd.
FrankenPHP / LiteSpeed LSAPIWaarschijnlijkWaarschijnlijkDe advisory noemt ze waarschijnlijk vatbaar. Langlevende workers maken achtergebleven state tussen requests juist relevanter.
CLI (cron, queue-workers, artisan)Niet behandeldNiet behandeldNiet behandeld in de advisories en bij normaal gebruik niet bereikbaar via HTTP-requests. Upgrade toch — het is hetzelfde pakket.

CVE-records

CVE-2026-109618GHSA-w8hw-w9fx-j79wCVSS 4.0 · 9.2

RCE via out-of-memory-corruptie (OOM)

PHP breekt de huidige context af met longjmp() als het geheugen op is. Gebeurt dat terwijl het requestgeheugen maar half is geïnitialiseerd, dan leest of vertakt de request-shutdown later op ongeldige data. Verschillende multipart-parsepaden kunnen PHP binnen normale limieten in een OOM duwen, nog voordat er applicatiecode draait.

Impact:
Code-uitvoering zonder authenticatie als de PHP-webworker-gebruiker.
Gedemonstreerd:
Pre-auth RCE op WordPress, Drupal en Joomla (mod_php, standaardlimieten).
Kwetsbaar
<8.2.35, <8.3.36, <8.4.27, <8.5.12 (en EOL-branches terug tot 5.4)
CWE
CWE-416, CWE-755, CWE-824, CWE-1325, CWE-1419
CVE-2026-109619GHSA-6wmf-4rv5-p3rpCVSS 4.0 · 9.2

RCE via achtergebleven globale state tussen requests

Een fatale fout (ook OOM) kan de reset per request overslaan, waardoor globale error-, sessie-, object- of extensie-state blijft wijzen naar geheugen dat aan het einde van de request is vrijgegeven. De volgende request op dezelfde worker kan dat vrijgegeven geheugen gebruiken vóór de normale initialisatie.

Impact:
Use-after-free die leidt tot het lekken van workergeheugen of code-uitvoering.
Gedemonstreerd:
Pre-auth RCE op WordPress en Drupal; applicatie-onafhankelijk geheugenlek op afstand.
Kwetsbaar
<8.2.35, <8.3.36, <8.4.27, <8.5.12 (en EOL-branches terug tot 5.4)

Vector (beide): CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N. Bereikbaar via het netwerk, geen rechten of gebruikersinteractie; “AC:H / AT:P” geeft aan dat de exploit op het doelwit moet worden afgestemd. Vroege uitgelekte berichten noemden CVSS 10.0 — dat getal klopt niet.

Wat de fix verandert

De patch gebruikt geen longjmp() meer bij out-of-memory. In plaats daarvan zet het bereiken van memory_limit een zachte OOM die op het volgende veilige punt in de VM wordt opgeworpen, waar de state consistent is. Een tweede, hogere drempel activeert een harde OOM die het proces beëindigt met _exit() — geen shutdown-handlers, geen half opgeruimde state.

Omdat de fix het foutpatroon aanpakt in plaats van losse ingangen, stelt het PHP-team dat ook de bekende varianten zijn afgedekt.

Bijwerking: codepaden die erop rekenden dat shutdown-functies na een OOM draaien, gedragen zich anders zodra de harde limiet wordt bereikt.

Gerelateerd