Serialisierung und Deserialisierung in Java: Die Java-Deserialisierungs-Schwachstelle erklärt
18. Dezember 2020
0 Min. LesezeitHinweis der Redaktion – 11. Juli 2022
Dieser Blogbeitrag wurde aktualisiert, um Änderungen in neueren Java-Versionen sowie durch Deserialisierung ausnutzbare Schwachstellen zu berücksichtigen. Außerdem haben wir einen aktuellen Konferenzvortrag ergänzt, in dem Java-Deserialisierungs-Exploits live demonstriert wurden.
Java-Serialisierung ist ein Mechanismus, mit dem ein Objekt in einen Byte-Stream umgewandelt wird. Die Java-Deserialisierung ermöglicht dagegen, ein Objekt aus einem Byte-Stream wiederherzustellen. Die Java-Serialisierung – insbesondere die Deserialisierung – gilt als „ein Geschenk, das immer weiter Freude bereitet“, denn sie hat im Laufe der Jahre zahlreiche Sicherheitsprobleme und Schwachstellen verursacht.
Was ist Serialisierung in Java?
Mit Java erstellen wir Objekte. Diese Objekte werden im Speicher abgelegt und vom Garbage Collector entfernt, sobald sie nicht mehr verwendet werden. Wenn wir ein Objekt übertragen und es beispielsweise auf einer Festplatte speichern oder über ein Netzwerk senden möchten, müssen wir es in einen Byte-Stream umwandeln. Dazu muss die Klasse des Objekts das Serializable-Interface implementieren. Wie bereits erwähnt, lässt sich mit der Serialisierung der Zustand eines Objekts in einen Byte-Stream umwandeln. Dieser Byte-Stream enthält nicht den eigentlichen Code.
Was ist Deserialisierung in Java?
Die Deserialisierung ist genau das Gegenteil der Serialisierung. Bei der Deserialisierung beginnen Sie mit einem Byte-Stream und erstellen das zuvor serialisierte Objekt in seinem ursprünglichen Zustand neu. Dazu benötigen Sie jedoch die Definition des Objekts.
Wie funktioniert die Java-Serialisierung?
Bei der Java-Serialisierung wird Reflection verwendet, um alle erforderlichen Daten aus den Feldern eines Objekts auszulesen – auch aus privaten und finalen Feldern. Enthält ein Feld ein Objekt, wird dieses Objekt rekursiv serialisiert. Getter und Setter werden bei der Serialisierung eines Java-Objekts nicht verwendet.
Wie funktioniert die Java-Deserialisierung?
Beim Zurückwandeln eines Byte-Streams in ein Objekt wird der Konstruktor nicht verwendet. Stattdessen wird ein leeres Objekt erstellt und die Daten werden mithilfe von Reflection in die Felder geschrieben. Wie bei der Serialisierung werden auch private und finale Felder berücksichtigt.
Was ist eine Java-Deserialisierungs-Schwachstelle?
Eine Java-Deserialisierungs-Schwachstelle ist eine Sicherheitslücke, die entsteht, wenn eine böswillige Person versucht, ein manipuliertes serialisiertes Objekt in das System einzuschleusen, um das System oder seine Daten zu kompromittieren. Denken Sie an eine Schwachstelle zur Ausführung beliebigen Codes, die beim Deserialisieren eines serialisierten Objekts ausgelöst werden kann. Um Java-Deserialisierungs-Schwachstellen besser zu erklären, müssen wir zunächst betrachten, wie die Deserialisierung in Java funktioniert.
Java-Deserialisierungs-Schwachstellen erklärt
Ein serialisiertes Objekt in Java ist ein Byte-Array mit Zustandsinformationen. Es enthält den Namen des Objekts, auf das es sich bezieht, sowie die Daten des Felds. Wenn Sie ein gespeichertes serialisiertes Objekt mit einem Hex-Editor ansehen, können Sie die Informationen schnell eingrenzen und manipulieren.
Wie wir bereits wissen, verwendet die Java-Deserialisierung nicht den Konstruktor, um ein Objekt zu erstellen, sondern lädt die Felder stattdessen mithilfe von Reflection. Das bedeutet, dass Validierungsprüfungen im Konstruktor beim Wiederherstellen des Objekts nie ausgeführt werden. Denken Sie beispielsweise an die Prüfung, ob das Startdatum vor dem Enddatum liegt, wenn ein Zeitraum beschrieben wird. Bei der Deserialisierung eines Java-Objekts kann dieses neue Objekt einen ungültigen Zustand haben.
Sehen wir uns das folgende Beispiel für eine Java-Deserialisierungs-Schwachstelle an. Dabei serialisieren wir ein Objekt aus einer serialisierbaren Klasse namens ValueObject:
"seriBeim Lesen der Datei mit dem serialisierten Objekt ValueObject.ser wird im Hex-Editor folgende Ausgabe angezeigt:

Nun können wir den Zeichenfolgenwert ganz einfach manipulieren. Ändern wir beispielsweise in der Ausgabe des Hex-Editors Hi in Hallo.

Wenn wir die angepasste Binärdatei deserialisieren, stellen wir fest, dass sich der value des Objekts geändert hat. Außerdem bemerken wir, dass sich der Zeitstempel nicht geändert hat. Das beweist, dass der Konstruktor nie aufgerufen wurde. Wenn eine Anwendung serialisierte Objekte akzeptiert, lassen sich die Werte relativ leicht manipulieren. Durch Änderungen an serialisierten Objekten können wir ungültige Objekte erstellen, die Integrität der Daten beeinträchtigen oder Schlimmeres verursachen.
Ausführung beliebigen Codes, Gadgets und Gadget-Ketten
Die Manipulation von Objektdaten ist bereits schädlich. Sie kann aber auch zur Ausführung von Code führen, wenn die richtige Kombination von Objekten deserialisiert wird. Bevor wir darauf eingehen, sehen wir uns Gadgets und Gadget-Ketten an.
Gadgets
Ein Gadget – so bezeichnet von Lawrence & Frohoff in ihrem Vortrag Marschalling Pickle auf der AppSecCali 2015 – ist eine Klasse oder Funktion mit bereits vorhandenem ausführbarem Code im anfälligen Prozess. Dieser vorhandene ausführbare Code kann für böswillige Zwecke wiederverwendet werden. Bei serialisierbaren Java-Objekten werden einige spezielle Methoden – wie die private Methode readObject() – während der Deserialisierung reflektiv aufgerufen.
Sehen wir uns das folgende vereinfachte Gadget an:
Diese Gadget-Klasse überschreibt die Standardmethode readObject. Dadurch wird jedes Mal, wenn ein Objekt der Klasse Gadget deserialisiert wird, das Runnable-Objekt command ausgeführt. Wenn eine Command-Klasse in etwa wie im folgenden Beispiel aussieht, lässt sich dieses serialisierte Objekt leicht manipulieren und Code einschleusen.
Beachten Sie außerdem: Wenn eine Anwendung serialisierte Objekte akzeptiert, wird das Objekt deserialisiert, bevor es in den gewünschten Typ umgewandelt wird. Das bedeutet, dass die Deserialisierung bereits abgeschlossen ist und die Methode readObject() ausgeführt wurde – selbst wenn die Typumwandlung fehlschlägt.
Deserialisierungsangriff mit Gadget-Ketten
Ein typischer Deserialisierungsangriff besteht aus einer raffiniert aufgebauten Gadget-Kette. Angreifende suchen nach einem Gadget, mit dem sich ein Angriff starten lässt, und verknüpfen mehrere Ausführungen, die schließlich zur Ausführung beliebigen Codes führen. Zum Beispiel:
Ein praxisnäheres Beispiel ist die Implementierung von java.util.HashMap. Diese Klasse verfügt über eine eigene Implementierung der Methode readObject(), die für jeden Schlüssel die Funktion hashcode() aufruft.
Bibliotheken
Wichtig ist: Die in Ihrer Anwendung verfügbaren Gadget-Ketten hängen nicht mit Ihrem eigenen Code zusammen. Da wir viel Code aus Bibliotheken und Frameworks importieren, beeinflusst die Anzahl der Klassen in Ihren (transitiven) Abhängigkeiten, welche Gadget-Ketten möglich sind. Eine solche böswillige Gadget-Kette zu erstellen, ist zwar schwierig und arbeitsintensiv, doch Java-Deserialisierungs-Schwachstellen stellen ein reales und gefährliches Sicherheitsrisiko dar.
Ein anschauliches Beispiel dafür ist die Log4Shell-Schwachstelle vom Dezember 2021. Im Kern handelte es sich um eine Schwachstelle zur Remote-Code-Ausführung, die ihren Ursprung im nativen Java-Serialisierungs-Framework haben könnte. In älteren Java-Versionen haben wir Remote-Code standardmäßig vertraut und benötigten die Klasse nicht in unserem Classpath. In neueren Java-Versionen, selbst in den neuesten, kann die Log4Shell-Schwachstelle jedoch eine Gadget-Kette ausnutzen, die in Ihrem Classpath verfügbar ist. Das kann zur Remote-Code-Ausführung oder Schlimmerem führen.
Wie lässt sich eine Java-Deserialisierungs-Schwachstelle verhindern?
Am besten verhindern Sie Java-Deserialisierungs-Schwachstellen, indem Sie die Java-Serialisierung ganz vermeiden. Wenn Ihre Anwendung keine serialisierten Objekte akzeptiert, können diese ihr auch nicht schaden.
Wenn Sie das `serializable`-Interface jedoch aufgrund von Vererbung implementieren müssen, können Sie die Methode readObject() wie unten gezeigt überschreiben, um die eigentliche Deserialisierung zu verhindern.
Wenn Ihre Anwendung auf serialisierte Objekte angewiesen ist, können Sie Ihren ObjectInputStream vor der Deserialisierung überprüfen. Die Bibliothek Apache Commons IO kann Sie dabei unterstützen. Sie stellt einen ValidatedObjectInputStream bereit, mit dem Sie die Objekte, die Sie deserialisieren möchten, explizit zulassen können. So können Sie verhindern, dass unerwartete Typen überhaupt deserialisiert werden.
Eine weitere Möglichkeit sind die nativen Serialisierungsfilter von Java. Sie wurden mit JEP 290 implementiert und mit Java 9 veröffentlicht (März 2018). Sie können gezielt Filter festlegen, die Klassen blockieren oder zulassen. Auf Grundlage eines Musters können Sie einen Filter erstellen, der die deserialisierbaren Klassen einschränkt.
Mit der Veröffentlichung von JEP 415 in Java 17 (September 2021) wurde dieses Filtersystem um kontextspezifische Funktionen erweitert. So können Sie das Verhalten eines Filters passend zu Ihrem Kontext steuern.
Im Artikel Neue Java-17-Funktionen für mehr Sicherheit und bessere Serialisierung gehen wir ausführlicher auf diese neueren Funktionen ein und erklären, wie Sie Filter in Java implementieren und warum diese kontextspezifischen Filter so wichtig sind.
Ein Tool wie ysoserial ist ebenfalls äußerst hilfreich, um Java-Deserialisierungs-Schwachstellen zu finden. Es generiert Payloads, um Gadget-Ketten in gängigen Java-Bibliotheken aufzuspüren, die unter den richtigen Bedingungen Java-Anwendungen ausnutzen können, wenn diese Objekte unsicher deserialisieren.
Java-Deserialisierungs-Schwachstellen treten nicht nur bei der benutzerdefinierten Java-Serialisierung auf. Obwohl dieser Beitrag sich darauf konzentriert, gibt es dieselben Schwachstellen auch in Serialisierungs- oder Marshalling-Frameworks, die diese Aufgabe für Sie übernehmen. Wenn ein Framework POJOs scheinbar wie von Zauberhand aus XML, JSON, YAML oder ähnlichen Formaten erstellt, verwendet es wahrscheinlich Reflection auf die oben beschriebene Weise. Damit bestehen dieselben Probleme.
Um diese Arten von Java-Deserialisierungs-Schwachstellen in externen Bibliotheken zu verhindern, scannen Sie Ihre Bibliotheken kostenlos und regelmäßig mit Snyk Open Source. Weitere Informationen zu dieser Art von Schwachstelle finden Sie in unserer Snyk-Learn-Lektion zu unsicherer Deserialisierung.
Konferenzvortrag: „Deserialisierungs-Exploits in Java: Warum sollte mich das interessieren?“
Vor Kurzem habe ich auf der Devoxx UK, der größten und renommiertesten Java-Community-Konferenz im Vereinigten Königreich, einen Vortrag über Deserialisierungs-Exploits gehalten.
In diesem Vortrag erkläre ich, wie Deserialisierungs-Schwachstellen nativ in Java funktionieren und wie Angriffsketten entstehen. Hacker bezeichnen die Java-Deserialisierung als „ein Geschenk, das immer weiter Freude bereitet“. Deshalb habe ich einen Vortrag mit Live-Demo erstellt, um die möglichen Auswirkungen auf die Sicherheit zu zeigen.
Natürlich gehören auch die jüngsten Sicherheitsprobleme rund um die Log4Shell-Schwachstelle dazu. Ich habe erklärt, wie Log4Shell als Ausgangspunkt für eine Deserialisierungs-Gadget-Kette dienen kann, bei der das Sink-Gadget beliebigen Code ausführt.
Auch andere Arten der Deserialisierung, etwa mit JSON, XML und YAML, können Probleme verursachen. Weitere Informationen finden Sie im Blogbeitrag Probleme bei der JSON-Deserialisierung in Java mit dem Jackson ObjectMapper. In diesem Vortrag gehe ich jedoch noch etwas tiefer ins Detail und demonstriere die konkreten Folgen.

Starten Sie mit Capture the Flag
Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.


