Insecure deserialization
CWE-502OWASP A08:2021Bijgewerkt 1 oktober 20267 min leestijd
Insecure deserialization is een kwetsbaarheid waarbij een applicatie objecten opbouwt uit onvertrouwde bytes, zodat een aanvaller een gadget chain kan activeren die code uitvoert. De gebruikelijke boosdoeners zijn native deserializers als pickle, PHP unserialize en Java readObject. De oplossing is een formaat met alleen data, zoals JSON met een schema, plus ondertekende payloads en een allowlist van typen.
Applicaties zetten voortdurend objecten om in bytes en weer terug: een sessie die in een cookie wordt bewaard, een record in de cache, een bericht in een wachtrij, een object dat tussen twee diensten wordt doorgegeven. Het terugzetten van die bytes naar een levend object heet deserialisatie, en zodra de bytes afkomstig zijn van iemand die u niet vertrouwt, kan die stap veel meer doen dan alleen gegevens herstellen. Hieronder leest u hoe insecure deserialization tot remote code execution leidt en hoe u het probleem al in het ontwerp uitsluit.
Wat is insecure deserialization?
Insecure deserialization, in het Nederlands onveilige deserialisatie, is een kwetsbaarheid waarbij een applicatie programma-objecten opbouwt uit gegevens die zij niet zelf in de hand heeft, en waarbij juist dat opbouwen code uitvoert of een toestand instelt die de aanvaller heeft gekozen. Serialisatie schrijft een object weg als een reeks bytes; deserialisatie leest die reeks in en reconstrueert het object. Het gevaar zit in hoeveel een native deserializer, het ingebouwde mechanisme van een programmeertaal, namens u wil doen terwijl hij het object weer in elkaar zet.
Een alledaagse vergelijking: een kast uit een bouwpakket wordt geleverd met een montagevoorschrift. Normaal vertelt dat vel de monteur welke panelen hij aan elkaar moet schroeven. Een native deserializer is een monteur die elke instructie op dat vel opvolgt: smokkelt een vreemde er de regel “en doe daarna de voordeur van het slot” tussen, dan doet de monteur ook dat. De bytes zijn dus niet alleen onderdelen van het meubel, het zijn instructies, en sommige formaten laten die instructies code aanroepen.
De formaten die dit toelaten zijn bekend: pickle in Python, unserialize in PHP, readObject in Java en BinaryFormatter in .NET. Ze bouwen complete objecten op in plaats van kale gegevens, en roepen daarbij automatisch methodes aan. De route naar code-uitvoering heet een gadget chain. Een aanvaller vindt zelden één methode die rechtstreeks een commando uitvoert. In plaats daarvan rijgt hij gewone methodes aaneen die al in de applicatie en haar bibliotheken aanwezig zijn, methodes die vanzelf in werking treden terwijl een object wordt gereconstrueerd, totdat de keten eindigt bij iets gevaarlijks, zoals het starten van een proces. Kant-en-klare ketens voor veelgebruikte bibliotheken zitten in tools als ysoserial voor Java en phpggc voor PHP, en daarom hoeft een aanvaller vaak zelf geen onderzoek te doen.
Hoe werkt een insecure deserialization-aanval?
Neem een dienst die een beetje sessiestatus bij de client bewaart. Hij serialiseert een dictionary met pickle, codeert het resultaat in base64, zet het in een parameter en leest het bij het volgende verzoek weer in. De ontwikkelaar vertrouwt erop dat de waarde ongewijzigd terugkomt.
Kwetsbaar:
import base64, pickle
from flask import Flask, request
app = Flask(__name__)
@app.route("/session/load")
def load_session():
raw = base64.b64decode(request.args["state"])
# pickle.loads bouwt elk Python-object op dat in de bytes beschreven staat
session = pickle.loads(raw)
return f"Welkom terug, {session['user']}"
Een normale client stuurt een met pickle geserialiseerde dictionary en alles werkt. Maar het pickle-formaat kent een opcode die een aanroepbaar object, zoals een functie, met argumenten aanroept, en elke klasse kan die opcode via haar methode __reduce__ inzetten. De aanvaller schrijft een piepkleine klasse waarvan __reduce__ een functie teruggeeft plus de argumenten waarmee die moet worden aangeroepen, en serialiseert daar een instantie van:
import base64, os, pickle
class Payload:
def __reduce__(self):
return (os.system, ("id",))
print(base64.b64encode(pickle.dumps(Payload())).decode())
De tekenreeks die dat oplevert, zet hij in het verzoek:
GET /session/load?state=gASVJAAAAA... HTTP/1.1
Host: app.example.com
Zodra pickle.loads bij de reduce-opcode aankomt, roept het os.system("id") aan, nog voordat ook maar één regel van uw eigen code draait. Er valt geen puntkomma te injecteren en geen aanhalingsteken te escapen: het formaat zelf draagt de opdracht tot uitvoering mee. Java gedraagt zich net zo wanneer readObject een bytestroom herbouwt, en PHP evengoed wanneer unserialize een object reconstrueert en de magic methods ervan in werking treden.
De remedie is niet de bytes filteren, maar het formaat veranderen. Stap over op een representatie die alleen data bevat: waarden, geen objecten en geen code. JSON dat wordt ingelezen als gewone dictionaries en lijsten kan tijdens het parsen geen functie aanroepen. Valideer het resultaat vervolgens tegen een schema, zodat de vorm is wat u verwacht. Moet de payload een retourtje via de client overleven, onderteken hem dan, zodat de server alleen gegevens accepteert die hij zelf heeft gemaakt.
Veilig:
import base64, hashlib, hmac, json
from flask import Flask, request
app = Flask(__name__)
KEY = app.config["SESSION_KEY"] # komt uit de omgeving, staat nooit in de code
def verify(state, tag):
expected = hmac.new(KEY, state, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, tag):
raise ValueError("ongeldige handtekening")
@app.route("/session/load")
def load_session():
state = base64.b64decode(request.args["state"])
verify(state, request.args["sig"]) # weiger alles wat wij niet zelf ondertekenden
data = json.loads(state) # JSON levert alleen dict, list, str en getallen op
user = data.get("user")
if not isinstance(user, str) or not user.isalnum():
raise ValueError("ongeldige gebruiker")
return f"Welkom terug, {user}"
Hier stapelen zich drie verdedigingslagen. JSON reconstrueert alleen data, dus er is geen reduce-opcode om te misbruiken. Door de HMAC-handtekening wordt een gemanipuleerde of door de aanvaller zelf opgestelde payload geweigerd voordat hij ook maar wordt geparset. De controles op type en waarde wijzen een document af dat technisch klopt, maar niet is wat de applicatie verwacht. De pickle-payload van zonet strandt nu al bij de handtekening, en zelfs zonder handtekening zou hij via json.loads nooit os.system bereiken.
Wat is de impact van insecure deserialization?
Het meest in het oog springende gevolg is remote code execution (RCE): een werkende gadget chain voert commando’s uit met de rechten van het applicatieproces, en veel erger wordt een bug niet. Toch is het spectrum breder. Afhankelijk van welke klassen beschikbaar zijn, kan dezelfde fout uitmonden in het omzeilen van rechten, door een object te vervalsen dat de sessie als beheerder markeert; in een denial of service, door een structuur te deserialiseren die geheugen of CPU uitput; of in het manipuleren van gegevens en path traversal via objecten die het bestandssysteem aanraken.
Daarom loopt de ernst van hoog tot kritiek. Een payload die alleen een workerproces laat crashen, is ernstig; een payload die een shell oplevert op een server met toegang tot de database en vrij uitgaand verkeer, is kritiek. Omdat de gevaarlijke gadgets meestal in bibliotheken van derden zitten, kan een codebase uitbuitbaar zijn via een afhankelijkheid die het team nooit rechtstreeks aanroept. Dat maakt de fout makkelijk over het hoofd te zien en lastig te doorgronden op basis van alleen uw eigen code.
Hoe spoort u insecure deserialization op?
Begin met in kaart brengen waar geserialiseerde data een vertrouwensgrens passeert: cookies, verborgen formuliervelden, API-parameters, berichtenwachtrijen, cache-items en geüploade bestanden. Leer de vingerafdrukken herkennen. Geserialiseerde Java-objecten beginnen met de hexbytes ac ed 00 05 en leveren in base64 een tekenreeks op die met rO0 begint. Geserialiseerde PHP-data herkent u aan de prefixen O: en a:. Python-pickle heeft eigen opcodes en komt meestal base64-gecodeerd binnen. Bereikt een van deze formaten vanaf een client de server, dan verdient dat een nadere blik.
Zoek in de code naar de sinks: pickle.loads, yaml.load zonder veilige loader, unserialize in PHP, readObject en ObjectInputStream in Java en BinaryFormatter in .NET. Om vast te stellen dat zo’n sink echt uitbuitbaar is en niet alleen aanwezig, bouwen testers een payload uit gepubliceerde gadget chains in ysoserial of phpggc, afgestemd op precies de bibliotheken die in gebruik zijn. Een blind geval bewijzen ze vaak met een meetbare vertraging of met een uitgaande DNS-lookup naar een domein dat zij zelf beheren. Scanners signaleren een deel van de patronen, maar bewijzen zelden een volledige keten van begin tot eind. AssistSec onderzoekt deze paden tijdens een penetratietest en toont per bevinding welk object de uitvoering in gang zette.
Hoe voorkomt u insecure deserialization?
- Deserialiseer geen onvertrouwde data met een native deserializer. pickle, unserialize, readObject en BinaryFormatter zijn nooit ontworpen om bestand te zijn tegen kwaadwillende invoer.
- Kies een formaat dat alleen data bevat, met een schema. JSON, Protocol Buffers of een vergelijkbaar formaat dat wordt ingelezen als gewone data, kan tijdens het parsen geen code uitvoeren; valideer het resultaat tegen een strikt schema.
- Onderteken payloads die een retourtje via de client maken. Met een HMAC over de bytes accepteert de server alleen data die hij zelf heeft gemaakt, zodat manipulatie al wordt geweigerd voordat het parsen begint.
- Werk met een allowlist van typen als een native formaat onvermijdelijk is. Beperk deserialisatie tot een expliciete set verwachte klassen, bijvoorbeeld met een Java-serialisatiefilter, en weiger al het andere.
- Houd afhankelijkheden gepatcht en beperkt. Gadget chains zitten in bibliotheekcode; wie ongebruikte afhankelijkheden verwijdert en updates doorvoert, verkleint de verzameling gadgets die een aanvaller kan bereiken.
- Draai met minimale rechten en beperk uitgaand verkeer. Gaat een keten toch af, dan maakt een afgeschermd proces zonder vrije uitgaande verbindingen de volgende stap van de aanvaller aanzienlijk moeilijker.
Bronnen
Veelgestelde vragen
Wat is een gadget chain bij insecure deserialization?
Een gadget chain is een reeks gewone methodes, al aanwezig in de applicatie en haar bibliotheken, die automatisch worden aangeroepen terwijl een object wordt opgebouwd. De aanvaller rijgt die methodes zo aan elkaar dat de keten eindigt bij iets gevaarlijks, zoals het starten van een proces of het wegschrijven van een bestand. Kant-en-klare ketens voor populaire bibliotheken zijn gepubliceerd in tools als ysoserial voor Java en phpggc voor PHP, dus eigen onderzoek is voor de aanvaller vaak niet eens nodig.
Is JSON veilig tegen insecure deserialization?
JSON dat wordt ingelezen als gewone data, zoals dictionaries, lijsten en strings, kan tijdens het parsen geen code uitvoeren en neemt daarmee het klassieke risico van een gadget chain weg. Het addertje zit in typebewuste bibliotheken die klassenamen in het document opnemen en die klassen vervolgens instantiëren, zoals Jackson met default typing of een .NET-serializer met TypeNameHandling ingeschakeld. Die brengen hetzelfde probleem bovenop JSON terug, dus houd type-informatie buiten het document.
Waarom geldt Python pickle als gevaarlijk?
Het pickle-formaat bevat een opcode die een aanroepbaar object met argumenten aanroept, beschikbaar via de methode __reduce__ van een object. Een aanvaller die de bytes in handen heeft, kan pickle.loads daardoor tijdens het reconstrueren elke willekeurige functie laten uitvoeren, nog voordat uw eigen code de data te zien krijgt. De Python-documentatie waarschuwt zelf dat u nooit data uit een onvertrouwde bron met pickle mag inlezen.
Hoe verhelp ik insecure deserialization in Java?
Geef geen onvertrouwde bytes meer aan readObject en ObjectInputStream, en zet de data over naar een formaat met schemavalidatie, zoals JSON of Protocol Buffers. Is native serialisatie onvermijdelijk, gebruik dan een serialisatiefilter (JEP 290) om precies de klassen die u verwacht op een allowlist te zetten en al het andere te weigeren. Houd bibliotheken gepatcht, want gadget chains zitten in de code van uw afhankelijkheden en niet in uw eigen code.
Verwante artikelen
- CVE'sCWE-502A08:2021PTC Windchill en FlexPLM (CVE-2026-12569)CVE-2026-12569 uitgelegd: hoe een deserialisatielek in PTC Windchill en FlexPLM, vermoedelijk door Cl0p, werd misbruikt om technische data te stelen.
- BegrippenPentestEen pentest is een gecontroleerde aanval op uw systemen door ethische hackers. Lees hoe een penetratietest werkt en welke kwetsbaarheden u ermee vindt.
- KwetsbaarhedenCWE-78A03:2021OS command injectionOS command injection uitgelegd: hoe shell-tekens in een systeemaanroep belanden, wat een aanvaller ermee kan en hoe argumentarrays het voorkomen.
- KwetsbaarhedenCWE-1104A06:2021Verouderde en kwetsbare componentenEen verouderde bibliotheek of webserver draagt publiek bekende kwetsbaarheden met kant-en-klare exploits. Lees hoe u dat beheersbaar maakt.
- KwetsbaarhedenCWE-94A03:2021Remote code execution (RCE)Remote code execution (RCE) uitgelegd: hoe aanvallers via ongefilterde invoer eigen commando's of code op uw server draaien, en hoe u het voorkomt.
- KwetsbaarhedenCWE-611A05:2021XML external entity injection (XXE)XML external entity injection (XXE) uitgelegd: hoe aanvallers via een XML-parser bestanden lezen en interne systemen bereiken, en hoe u het voorkomt.