Zum Inhalt springen

Insecure Deserialization

CWE-502OWASP A08:2021Aktualisiert 1. Oktober 20267 Min. Lesezeit

Insecure Deserialization ist eine Schwachstelle, bei der eine Anwendung Objekte aus nicht vertrauenswürdigen Bytes aufbaut, sodass ein Angreifer eine Gadget Chain auslösen kann, die Code ausführt. Übliche Verursacher sind native Deserializer wie pickle, PHP unserialize und Java readObject. Abhilfe schafft ein reines Datenformat wie JSON mit Schema, dazu signierte Payloads und eine Allowlist erlaubter Typen.

Anwendungen verwandeln ständig Objekte in Bytes und wieder zurück: eine Sitzung, die in einem Cookie liegt, ein Datensatz im Cache, eine Nachricht in einer Warteschlange, ein Objekt, das zwischen zwei Diensten weitergereicht wird. Die Rückverwandlung der Bytes in ein lebendiges Objekt heißt Deserialisierung, und sobald diese Bytes von jemandem stammen, dem Sie nicht vertrauen, kann dieser Schritt weit mehr tun, als nur Daten wiederherzustellen. Dieser Beitrag erklärt, wie Insecure Deserialization zu Remote Code Execution führt und wie Sie das Problem schon im Design ausschließen.

Was ist Insecure Deserialization?

Insecure Deserialization, auf Deutsch unsichere Deserialisierung, ist eine Schwachstelle, bei der eine Anwendung Programmobjekte aus Daten wiederherstellt, die sie nicht kontrolliert, und bei der schon dieses Wiederherstellen vom Angreifer gewählten Code ausführt oder einen vom Angreifer gewählten Zustand setzt. Die Serialisierung schreibt ein Objekt als Bytefolge heraus; die Deserialisierung liest diese Folge ein und rekonstruiert das Objekt. Die Gefahr liegt darin, wie viel ein nativer Deserializer, also der eingebaute Mechanismus einer Programmiersprache, bereitwillig in Ihrem Namen erledigt, während er das Objekt wieder zusammensetzt.

Ein Vergleich aus dem Alltag: Ein Schrank zum Selbstaufbau wird mit einer Aufbauanleitung geliefert. Normalerweise sagt die Anleitung dem Monteur, welche Platten er zusammenschrauben soll. Ein nativer Deserializer ist ein Monteur, der jede Anweisung auf dem Blatt befolgt: Schmuggelt ein Fremder die Zeile “und schließen Sie danach die Haustür auf” hinein, erledigt der Monteur auch das. Die Bytes sind also nicht nur Möbelteile, sondern Anweisungen, und manche Formate erlauben diesen Anweisungen, Code aufzurufen.

Die Formate, die das zulassen, sind bekannt: pickle in Python, unserialize in PHP, readObject in Java und BinaryFormatter in .NET. Sie stellen ganze Objekte wieder her statt bloßer Daten und rufen dabei automatisch Methoden auf. Den Weg zur Codeausführung nennt man Gadget Chain. Ein Angreifer findet selten eine einzelne Methode, die unmittelbar einen Befehl ausführt. Stattdessen kombiniert er gewöhnliche Methoden, die in der Anwendung und ihren Bibliotheken ohnehin vorhanden sind und beim Rekonstruieren eines Objekts von selbst anspringen, bis die Kette bei etwas Gefährlichem endet, etwa dem Start eines Prozesses. Fertige Ketten für verbreitete Bibliotheken liefern Werkzeuge wie ysoserial für Java und phpggc für PHP gleich mit, weshalb ein Angreifer oft keinerlei eigene Forschung braucht.

Wie funktioniert ein Insecure-Deserialization-Angriff?

Nehmen wir einen Dienst, der ein wenig Sitzungszustand beim Client ablegt. Er serialisiert ein Dictionary mit pickle, kodiert das Ergebnis in Base64, legt es in einem Parameter ab und liest es bei der nächsten Anfrage wieder ein. Der Entwickler vertraut darauf, dass der Wert unverändert zurückkommt.

Verwundbar:

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 baut jedes Python-Objekt auf, das die Bytes beschreiben
    session = pickle.loads(raw)
    return f"Willkommen zurück, {session['user']}"

Ein normaler Client sendet ein mit pickle serialisiertes Dictionary, und alles funktioniert. Doch das pickle-Format kennt einen Opcode, der ein aufrufbares Objekt, etwa eine Funktion, mit Argumenten aufruft, und jede Klasse kann ihn über ihre Methode __reduce__ nutzen. Der Angreifer schreibt eine winzige Klasse, deren __reduce__ eine Funktion samt den Argumenten für ihren Aufruf zurückgibt, und serialisiert eine Instanz davon:

import base64, os, pickle

class Payload:
    def __reduce__(self):
        return (os.system, ("id",))

print(base64.b64encode(pickle.dumps(Payload())).decode())

Die resultierende Zeichenkette setzt er in die Anfrage ein:

GET /session/load?state=gASVJAAAAA... HTTP/1.1
Host: app.example.com

Sobald pickle.loads den Reduce-Opcode erreicht, ruft es os.system("id") auf, noch bevor auch nur eine Zeile Ihres eigenen Codes läuft. Es gibt kein Semikolon zu injizieren und kein Anführungszeichen zu escapen: Das Format selbst trägt die Anweisung zur Ausführung in sich. Java verhält sich genauso, wenn readObject einen Bytestrom wiederherstellt, und PHP ebenso, wenn unserialize ein Objekt rekonstruiert und dessen Magic Methods anspringen.

Die Abhilfe besteht nicht darin, die Bytes zu filtern, sondern das Format zu wechseln. Steigen Sie auf eine reine Datendarstellung um, die Werte beschreibt, keine Objekte und keinen Code. JSON, das in gewöhnliche Dictionaries und Listen eingelesen wird, kann beim Parsen keine Funktion aufrufen. Validieren Sie das Ergebnis anschließend gegen ein Schema, damit die Struktur Ihren Erwartungen entspricht. Muss die Payload eine Rundreise über den Client überstehen, signieren Sie sie, damit der Server nur Daten akzeptiert, die er selbst erzeugt hat.

Sicher:

import base64, hashlib, hmac, json
from flask import Flask, request

app = Flask(__name__)
KEY = app.config["SESSION_KEY"]  # aus der Umgebung geladen, nie fest im Code

def verify(state, tag):
    expected = hmac.new(KEY, state, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, tag):
        raise ValueError("ungültige Signatur")

@app.route("/session/load")
def load_session():
    state = base64.b64decode(request.args["state"])
    verify(state, request.args["sig"])       # alles abweisen, was wir nicht selbst signiert haben
    data = json.loads(state)                  # JSON erzeugt nur dict, list, str und Zahlen
    user = data.get("user")
    if not isinstance(user, str) or not user.isalnum():
        raise ValueError("ungültiger Benutzer")
    return f"Willkommen zurück, {user}"

Hier greifen drei Verteidigungslinien ineinander. JSON rekonstruiert nur Daten, es gibt also keinen Reduce-Opcode, den man missbrauchen könnte. Die HMAC-Signatur sorgt dafür, dass eine manipulierte oder vom Angreifer selbst erstellte Payload abgewiesen wird, bevor sie überhaupt geparst wird. Die Typ- und Wertprüfungen weisen ein Dokument ab, das zwar wohlgeformt, aber nicht das erwartete ist. Die pickle-Payload von vorhin scheitert jetzt bereits an der Signaturprüfung, und selbst unsigniert könnte sie über json.loads niemals os.system erreichen.

JSON ist nicht automatisch sicher. Bibliotheken, die Klassennamen in das Dokument einbetten und diese Klassen instanziieren, etwa Jackson mit eingeschaltetem Default Typing, .NET BinaryFormatter oder ein Serializer mit TypeNameHandling auf All, holen das Gadget-Chain-Problem direkt zurück, nur eben auf JSON-Basis. Halten Sie Typinformationen aus dem Dokument heraus und lassen Sie den Parser niemals anhand der Eingabe entscheiden, welche Klasse er erzeugt.

Welche Auswirkungen hat Insecure Deserialization?

Das gravierendste Ergebnis ist Remote Code Execution (RCE): Eine funktionierende Gadget Chain führt Befehle mit den Rechten des Anwendungsprozesses aus, und viel schlimmer kann ein Fehler kaum werden. Die Bandbreite ist allerdings größer. Je nachdem, welche Klassen verfügbar sind, kann dieselbe Lücke in eine Umgehung von Berechtigungen münden, wenn der Angreifer ein Objekt fälscht, das die Sitzung als Administrator ausweist, in einen Denial of Service durch eine deserialisierte Struktur, die Arbeitsspeicher oder CPU erschöpft, oder in Datenmanipulation und Path Traversal über Objekte, die auf das Dateisystem zugreifen.

Deshalb reicht der Schweregrad von hoch bis kritisch. Eine Payload, die lediglich einen Worker-Prozess zum Absturz bringt, ist ernst; eine, die eine Shell auf einem Server mit Datenbankzugriff und freiem ausgehendem Verkehr verschafft, ist kritisch. Da die gefährlichen Gadgets meist in Bibliotheken von Drittanbietern stecken, kann eine Codebasis über eine Abhängigkeit angreifbar sein, die das Team nie direkt aufruft. Das macht die Lücke leicht zu übersehen und allein anhand des eigenen Codes schwer zu beurteilen.

Wie erkennen Sie Insecure Deserialization?

Ermitteln Sie zuerst, wo serialisierte Daten eine Vertrauensgrenze überschreiten: Cookies, versteckte Formularfelder, API-Parameter, Message Queues, Cache-Einträge und hochgeladene Dateien. Lernen Sie die Fingerabdrücke kennen. Serialisierte Java-Objekte beginnen mit den Hex-Bytes ac ed 00 05 und ergeben in Base64 eine Zeichenkette, die mit rO0 beginnt. Serialisierte PHP-Daten erkennen Sie an den Präfixen O: und a:. Python-pickle bringt eigene Opcodes mit und kommt meist Base64-kodiert an. Erreicht eines dieser Formate vom Client aus den Server, lohnt sich ein genauerer Blick.

Suchen Sie im Code nach den Sinks: pickle.loads, yaml.load ohne sicheren Loader, unserialize in PHP, readObject und ObjectInputStream in Java sowie BinaryFormatter in .NET. Um zu bestätigen, dass ein Sink tatsächlich ausnutzbar und nicht nur vorhanden ist, bauen Tester eine Payload aus veröffentlichten Gadget Chains in ysoserial oder phpggc, passend zu genau den eingesetzten Bibliotheken. Einen blinden Fall weisen sie oft über eine messbare Verzögerung oder einen ausgehenden DNS-Lookup zu einer Domain unter eigener Kontrolle nach. Scanner markieren manche Muster, weisen aber selten eine Kette vollständig von Anfang bis Ende nach. AssistSec untersucht diese Pfade im Rahmen eines Penetrationstests und weist je Befund nach, welches Objekt die Ausführung ausgelöst hat.

Wie verhindern Sie Insecure Deserialization?

  • Deserialisieren Sie nicht vertrauenswürdige Daten nie mit einem nativen Deserializer. pickle, unserialize, readObject und BinaryFormatter wurden nie dafür entworfen, feindseligen Eingaben standzuhalten.
  • Bevorzugen Sie ein reines Datenformat mit Schema. JSON, Protocol Buffers oder ein vergleichbares Format, das in gewöhnliche Daten eingelesen wird, kann beim Parsen keinen Code ausführen; validieren Sie das Ergebnis gegen ein striktes Schema.
  • Signieren Sie Payloads, die eine Rundreise über den Client machen. Ein HMAC über die Bytes sorgt dafür, dass der Server nur Daten akzeptiert, die er selbst erzeugt hat, sodass Manipulationen abgewiesen werden, bevor das Parsen beginnt.
  • Arbeiten Sie mit einer Allowlist von Typen, wenn ein natives Format unvermeidbar ist. Beschränken Sie die Deserialisierung auf eine explizite Menge erwarteter Klassen, etwa mit einem Java-Serialisierungsfilter, und weisen Sie alles andere ab.
  • Halten Sie Abhängigkeiten gepatcht und schlank. Gadget Chains stecken im Bibliothekscode; wer ungenutzte Abhängigkeiten entfernt und Updates einspielt, verkleinert den Vorrat an Gadgets, die ein Angreifer erreichen kann.
  • Betreiben Sie den Dienst mit minimalen Rechten und begrenztem ausgehendem Verkehr. Springt eine Kette doch an, erschwert ein abgeschotteter Prozess ohne freie ausgehende Verbindungen den nächsten Schritt des Angreifers erheblich.

Quellen

Häufige Fragen

Was ist eine Gadget Chain bei Insecure Deserialization?

Eine Gadget Chain ist eine Folge gewöhnlicher Methoden, die in der Anwendung und ihren Bibliotheken bereits vorhanden sind und beim Wiederherstellen eines Objekts automatisch ausgeführt werden. Der Angreifer verknüpft diese Methoden so, dass die Kette bei etwas Gefährlichem endet, etwa einem Prozessaufruf oder dem Schreiben einer Datei. Fertige Ketten für verbreitete Bibliotheken sind in Werkzeugen wie ysoserial für Java und phpggc für PHP veröffentlicht, sodass der Angreifer oft keine eigene Forschung betreiben muss.

Ist JSON vor Insecure Deserialization sicher?

JSON, das in reine Daten wie Dictionaries, Listen und Strings eingelesen wird, kann beim Parsen keinen Code ausführen und beseitigt damit das klassische Gadget-Chain-Risiko. Der Haken sind typbewusste Bibliotheken, die Klassennamen in das Dokument einbetten und diese Klassen instanziieren, etwa Jackson mit Default Typing oder ein .NET-Serializer mit aktiviertem TypeNameHandling. Sie bringen dasselbe Problem auf JSON-Basis zurück, halten Sie Typinformationen deshalb aus dem Dokument heraus.

Warum gilt Python pickle als gefährlich?

Das pickle-Format enthält einen Opcode, der ein aufrufbares Objekt mit Argumenten aufruft und über die Methode __reduce__ eines Objekts zugänglich ist. Ein Angreifer, der die Bytes kontrolliert, kann pickle.loads deshalb beim Wiederherstellen jede beliebige Funktion ausführen lassen, noch bevor Ihr eigener Code die Daten zu sehen bekommt. Die Python-Dokumentation warnt selbst davor, jemals Daten aus einer nicht vertrauenswürdigen Quelle mit pickle einzulesen.

Wie behebe ich Insecure Deserialization in Java?

Übergeben Sie keine nicht vertrauenswürdigen Bytes mehr an readObject und ObjectInputStream, und stellen Sie die Daten auf ein schemavalidiertes Format wie JSON oder Protocol Buffers um. Wo native Serialisierung unvermeidbar ist, setzen Sie einen Serialisierungsfilter (JEP 290) ein, der genau die erwarteten Klassen per Allowlist zulässt und alles andere abweist. Halten Sie Bibliotheken gepatcht, denn Gadget Chains stecken im Code der Abhängigkeiten und nicht in Ihrem eigenen.

Verwandte Artikel

Zum Suchen / drücken · Esc