monstertov@php-session-confusion

CVE-2026-115560

· monster research · monstertov · tov.monster

Een sessiebestand, twee verschillende formaten

in mijn lab kreeg ik session serializer confusion werkend op php 8.5.12. onder de geteste configuratie kwam upload-progress data bij een application gadget terecht en leidde dat tot command execution als www-data.

het belangrijkste is de overgang tussen twee sessieformaten. php schrijft de upload-progress informatie weg met de serializer die in de configuratie staat. de applicatie wisselt daarna van serializer voordat de sessie wordt geopend, waardoor dezelfde bytes anders worden gelezen.

CVE-2026-115560 is een openbaar gemelde kwetsbaarheid. dit stuk gaat over mijn labreproductie, wat de applicatie daarvoor nodig heeft, en de negatieve controles die de keten onderbraken.

php 8.5.12 lost dit niet op. die versie repareert CVE-2026-109618 en CVE-2026-109619, en dat zijn aparte kwetsbaarheden. op 24 oktober 2026 is het gepubliceerde advies voor CVE-2026-115560 om session upload progress uit te zetten totdat de fix in de volgende reguliere bugfix-release zit.

De labconfiguratie

het build-recept van de labimage checkt de php-8.5.12 source tag uit en compileert php met apache mod_php. de opgeslagen fingerprint laat php 8.5.12, de apache php-module en prefork zien.

het kwetsbare profiel gebruikt:

session.save_handler=files
session.serialize_handler=php_binary
session.upload_progress.enabled=1
session.upload_progress.cleanup=0

de applicatie registreert eerst haar class autoloader en draait daarna:

ini_set('session.serialize_handler', 'php');
session_start();

die volgorde maakt de mismatch. de serverconfiguratie kiest php_binary, terwijl de applicatie php kiest voordat de sessie wordt gedecodeerd.

de cleanup-instelling doet er ook toe. de directe positieve reproductie die ik hier beschrijf gebruikt session.upload_progress.cleanup=0. dat is geen standaardinstelling, en daardoor blijft de progress-informatie staan nadat de upload klaar is.

Wat upload progress toevoegt

met session upload progress kan php informatie over een lopende upload in een sessie bijhouden. een multipart request met het ingestelde progress-veld kan die verwerking al in gang zetten voordat het eigen script van de applicatie draait.

de opgeslagen informatie bevat de geuploade bestandsnaam. daarmee wordt een deel van de sessie-inhoud van buitenaf bepaald, terwijl de applicatie die inhoud zelf nooit aan $_SESSION heeft toegekend.

een bestandsnaam met rare bytes zou gewoon als data moeten blijven staan. het probleem in deze configuratie is hoe die data wordt uitgelegd zodra de applicatie de sessie opent met een andere serializer.

Waar de betekenis verandert

php_binary en php coderen een sessierecord niet op dezelfde manier.

de php-handler gebruikt | om de naam van een sessievariabele van de geserialiseerde waarde te scheiden. een pipe die binnen data zit die onder een ander formaat is weggeschreven kan daardoor tijdens het decoderen een andere betekenis krijgen.

de labketen zet geserialiseerde objectdata in een geuploade bestandsnaam. de upload-progress tracking draagt die bestandsnaam het sessierecord in. de applicatie wisselt daarna naar php, en door het verkeerd decoderen kan het ingebedde object bij deserialisatie terechtkomen.

de volgorde ziet er zo uit:

ingestelde serializer: php_binary
                 |
multipart upload maakt progress data
                 |
applicatie wisselt serializer naar php
                 |
session_start() decodeert de sessie
                 |
ingebed object bereikt een application gadget

er zit geen heap-offset calibratie in deze keten. het mechanisme leunt op een formaatmismatch en op het gedrag van een applicatie-object.

De application gadget

het lab bevat een cache-writer class met twee properties: een pad op het filesystem en de inhoud die weggeschreven moet worden.

de destructor schrijft die inhoud naar dat pad. bij normaal gebruik maakt de applicatie het object zelf en kiest het allebei die waardes. in de reproductie levert de deserialisatie ze aan.

die destructor maakt van object injection een file write. de bestemming ligt binnen de schrijfbare document root, waar het resulterende php-bestand via de webserver opgevraagd kan worden.

dat is een noodzakelijke voorwaarde voor de getoonde command execution. het lab heeft een geschikte gadget, het webproces mag de bestemming schrijven, en de server voert php-bestanden daar uit.

het vinden van serializer confusion betekent op zichzelf nog niet dat je in elke applicatie dezelfde command execution haalt.

Het vastgelegde positieve resultaat

het opgeslagen transcript van de directe run bevat:

direct: arm+fire -> HTTP 200
shell OK (nonce verified)
uid=33(www-data) gid=33(www-data) groups=33(www-data)
exit=0

de driver genereert een willekeurige bestandsnaam voor het weggeschreven bestand. hij voegt ook een verse marker toe aan het verificatiecommando en eist die marker terug in een HTTP 200 response voordat hij succes meldt.

mijn harness doet er nog een check bij: het externe proces moet exit status nul teruggeven, en de gecombineerde output moet uid= bevatten.

dats sterker bewijs dan alleen een HTTP 200 response als succes behandelen. de opgeslagen output bevat de verwachte identiteit van het webproces en een geslaagde markerverificatie.

het plaatsen van het bestand gaat via http. de driver stuurt het geserialiseerde object mee in het multipart request, en de application gadget doet de file write. het inspectiecommando dat daarna is vastgelegd lijst alleen de resulterende bestanden op.

Twee negatieve controles

de bruikbare vergelijking was om steeds maar een voorwaarde tegelijk te veranderen.

elk profiel heeft zijn eigen document root. een bestand dat in het kwetsbare profiel wordt geschreven kan dus niet in een controle opduiken puur omdat beide containers dezelfde map delen.

profielverschil in configuratievastgelegd resultaat
vulnerableupload progress aan, php_binary gewisseld naar phpsuccess
nouploadupload progress uitmiss
consistentingestelde serializer het hele request aangehoudenmiss

het noupload-profiel houdt dezelfde applicatie met de handler-wissel aan, maar verandert:

session.upload_progress.enabled=0

het consistent-profiel laat upload progress aan staan en houdt de gadget class. de applicatie roept session_start() aan zonder de serializer te wisselen.

het opgeslagen transcript van de negatieve controles legt exit status 1 vast voor beide profielen, in zowel direct- als race-modus. het legt ook nul passende shell-bestanden vast voor en na die runs.

mijn harness-logs leggen apart de missers vast tegen beide controlepoorten, naast de geslaagde runs tegen het kwetsbare profiel.

die uitkomsten passen bij het voorgestelde mechanisme. upload progress uitzetten haalt de bron van het vergiftigde progress-record weg. de ingestelde serializer aanhouden haalt de verandering in interpretatie weg.

Wat het bewijs wel en niet laat zien

de vastgelegde resultaten ondersteunen command execution in het geconfigureerde php 8.5.12 lab, en het falen van de geteste keten onder beide negatieve controles.

het bewijs stelt geen volledige reeks getroffen versies vast. het stelt ook niet vast dat elke php-applicatie een bruikbare gadget of een schrijfbaar uitvoerpad heeft.

er zitten ook meetgrenzen aan. de opgeslagen sessie-fingerprint komt van php CLI, dus je moet hem niet zien als een volledige inspectie van de effectieve configuratie van apache. het positieve http-executiebewijs en de configuratiebestanden geven extra context, maar dat zijn losse waarnemingen.

de successchecks ondersteunen het vastgelegde resultaat. ze zijn geen onvoorwaardelijke garantie tegen false positives, en ook geen volledige geschiedenis van elk bestand dat in het lab is achtergebleven.

Waar je in een applicatie op moet letten

het specifieke patroon om naar te zoeken is een server-ingestelde session serializer die de applicatiecode verandert voor session_start().

voor dit probleem is de gemelde combinatie php_binary of php_serialize in de configuratie, gevolgd door een runtime-wissel naar php, met upload progress nog aan.

waar upload progress niet nodig is, is de gepubliceerde mitigatie:

session.upload_progress.enabled=0

zet hem in een configuratie die van kracht wordt voordat het request wordt geparsed, en herstart daarna de php-workers. het binnen de applicatie aanpassen is te laat voor upload-progress verwerking die al heeft plaatsgevonden.

voor dit lab was het sterkste resultaat juist de vergelijking: uitvoering met de vereiste combinatie aanwezig, en missers zodra of upload progress of de handler-wissel werd weggehaald.

scope: dit stuk beschrijft opgeslagen labbewijs en een statische review van de broncode. de getoonde command execution hangt af van de geteste configuratie en de application gadget.