Podstawowa deserializacja w Javie z ObjectInputStream readObject

Tip

Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Przeglądaj pełny katalog HackTricks Training dla ścieżek assessment (ARTA/GRTA/AzRTA) oraz Linux Hacking Expert (LHE).

Wsparcie HackTricks

W tym poście zostanie wyjaśniony przykład użycia java.io.Serializable oraz dlaczego nadpisanie readObject() może być niezwykle niebezpieczne, jeśli strumień wejściowy jest kontrolowany przez atakującego.

Serializable

Interfejs Java Serializable (java.io.Serializable) jest interfejsem markerowym, który Twoje klasy muszą implementować, jeśli mają być serializowane i deserializowane. Java object serialization (zapis) odbywa się za pomocą ObjectOutputStream, a deserializacja (odczyt) za pomocą ObjectInputStream.

Przypomnienie: Które metody są wywoływane implicitnie podczas deserializacji?

  1. readObject() – specyficzna dla klasy logika odczytu (jeśli zaimplementowana i private).
  2. readResolve() – może zastąpić deserializowany obiekt innym.
  3. validateObject() – poprzez callbacki ObjectInputValidation.
  4. readExternal() – dla klas implementujących Externalizable.
  5. Konstruktory nie są wykonywane – dlatego łańcuchy gadgetów opierają się wyłącznie na powyższych callbackach.

Każda metoda w tym łańcuchu, która ostatecznie wywołuje dane kontrolowane przez atakującego (wykonanie poleceń, JNDI lookups, reflection, itd.), zamienia procedurę deserializacji w gadget umożliwiający RCE.

Zobaczmy przykład z klasą Person, która jest serializable. Ta klasa nadpisuje metodę readObject, więc gdy dowolny obiekt tej klasy zostanie deserializowany, ta metoda zostanie wykonana.
W przykładzie metoda readObject klasy Person wywołuje metodę eat() swojego zwierzaka, a metoda eat() klasy Dog (z niewiadomego powodu) uruchamia calc.exe. Zobaczymy, jak serializować i deserializować obiekt Person, aby uruchomić ten kalkulator:

The following example is from https://medium.com/@knownsec404team/java-deserialization-tool-gadgetinspector-first-glimpse-74e99e493649

import java.io.Serializable;
import java.io.*;

public class TestDeserialization {
interface Animal {
public void eat();
}
//Class must implements Serializable to be serializable
public static class Cat implements Animal,Serializable {
@Override
public void eat() {
System.out.println("cat eat fish");
}
}
//Class must implements Serializable to be serializable
public static class Dog implements Animal,Serializable {
@Override
public void eat() {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("dog eat bone");
}
}
//Class must implements Serializable to be serializable
public static class Person implements Serializable {
private Animal pet;
public Person(Animal pet){
this.pet = pet;
}
//readObject implementation, will call the readObject from ObjectInputStream  and then call pet.eat()
private void readObject(java.io.ObjectInputStream stream)
throws IOException, ClassNotFoundException {
pet = (Animal) stream.readObject();
pet.eat();
}
}
public static void GeneratePayload(Object instance, String file)
throws Exception {
//Serialize the constructed payload and write it to the file
File f = new File(file);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
out.flush();
out.close();
}
public static void payloadTest(String file) throws Exception {
//Read the written payload and deserialize it
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Object obj = in.readObject();
System.out.println(obj);
in.close();
}
public static void main(String[] args) throws Exception {
// Example to call Person with a Dog
Animal animal = new Dog();
Person person = new Person(animal);
GeneratePayload(person,"test.ser");
payloadTest("test.ser");
// Example to call Person with a Cat
//Animal animal = new Cat();
//Person person = new Person(animal);
//GeneratePayload(person,"test.ser");
//payloadTest("test.ser");
}
}

Wnioski (klasyczny scenariusz)

Jak widać w tym bardzo prostym przykładzie, „luka” pojawia się, ponieważ metoda readObject() wywołuje inny kod kontrolowany przez atakującego. W rzeczywistych łańcuchach gadgetów tysiące klas zawartych w zewnętrznych bibliotekach (Commons-Collections, Spring, Groovy, Rome, SnakeYAML, itp.) mogą być nadużyte – atakujący potrzebuje tylko jednego osiągalnego gadgetu, aby uzyskać wykonanie kodu.


2023-2025: Co zmieniło się w rzeczywistych błędach deserializacji Java?

Najnowsze przypadki przypominają, że błędy ObjectInputStream to już nie tylko „prześlij plik .ser do przestarzałego endpointu HTTP”:

  • Broker / queue consumers: Spring-Kafka (CVE-2023-34040) pokazał, że deserializacja nagłówków wyjątków z tematów kontrolowanych przez atakującego wystarcza, jeśli consumer włączy nietypowe flagi checkDeserExWhen*.
  • Client-side trust of remote servers: klient Aerospike Java (CVE-2023-36480) deserializował obiekty otrzymane od serwera. Reakcja dostawcy była godna uwagi: nowsi klienci usunęli wsparcie dla Java runtime serialization/deserialization zamiast próbować je zachować za słabym filtrem.
  • “Restricted” streams are often still too broad: pac4j-core (CVE-2023-25581) próbował chronić deserializację za pomocą RestrictedObjectInputStream, ale zestaw akceptowanych klas wciąż był na tyle duży, że umożliwiał nadużycie gadgetów.

Wniosek ofensywny jest taki, że niebezpieczna granica zaufania często nie polega na „użytkownik przesyła blob”, lecz na tym, że „jakiś komponent, który deweloper uznał za zaufany, może wstrzyknąć bajty do strumienia, które ostatecznie trafią do readObject()”.

Jeśli potrzebujesz niskoszumowych sprawdzeń dosięgalności zanim poświęcisz czas na pełne badanie gadgetów, skorzystaj z dedykowanych stron Java:

Java DNS Deserialization, GadgetProbe and Java Deserialization Scanner

Antywzorce readObject() które nadal tworzą wejścia dla gadgetów

Nawet jeśli twoja klasa sama w sobie nie jest oczywistym gadgetem RCE, poniższe wzorce wystarczają, by stała się podatna, gdy w grafie osadzone są obiekty kontrolowane przez atakującego:

  1. Wywoływanie metod możliwych do nadpisania lub metod interfejsu z readObject() (pet.eat() w powyższym PoC jest klasycznym przykładem).
  2. Wykonywanie lookupów, reflection, ładowania klas, ewaluacji wyrażeń lub operacji JNDI podczas deserializacji.
  3. Iterowanie po kolekcjach lub mapach kontrolowanych przez atakującego, co może wywołać hashCode(), equals(), comparatory lub transformers jako efekty uboczne.
  4. Rejestrowanie callbacków ObjectInputValidation, które wykonują niebezpieczne postprocessingi.
  5. Zakładanie, że „private readObject()” to wystarczająca ochrona. Kontroluje to tylko semantykę dispatchu; to nie sprawia, że deserializacja jest bezpieczna.

Nowoczesne środki zaradcze, które warto wdrożyć

  1. JEP 290 / Serialization Filtering (Java 9+) Użyj listy dozwolonych i jawnych limitów grafu:
-Djdk.serialFilter="com.example.dto.*;java.base/*;maxdepth=5;maxrefs=1000;maxbytes=16384;!*"
  1. Zastosuj filtr na każdym nieufnym strumieniu, nie tylko globalnie:
try (var ois = new ObjectInputStream(input)) {
var filter = ObjectInputFilter.Config.createFilter(
"com.example.dto.*;java.base/*;maxdepth=5;maxrefs=1000;!*"
);
ois.setObjectInputFilter(filter);
return (Message) ois.readObject();
}
  1. JEP 415 (Java 17+) Context-Specific Filter Factories Preferuj to, gdy ta sama JVM ma wiele kontekstów deserializacji (RMI, cache replication, message consumers, admin-only imports) i każdy z nich potrzebuje innej allow-listy.
  2. Utrzymuj readObject() nudnym Wywołuj tylko defaultReadObject() / jawne odczyty pól, a następnie wykonuj ścisłe sprawdzenia inwariantów. Nie wykonuj I/O, logowania które dereferencjonuje obiekty kontrolowane przez atakującego, dynamicznych lookupów ani wywołań metod na zdeserializowanych pod-obiektach.
  3. Jeśli to możliwe, usuń natywną serializację Java z projektu Poprawka Aerospike jest dobrym wzorem: gdy funkcja nie jest niezbędna, usunięcie użycia readObject() / writeObject() często jest bezpieczniejsze niż próba utrzymania doskonałych filtrów na wieki.

Workflow wykrywania i badań

  • ysoserial pozostaje bazą do walidacji gadgetów i szybkich probe’ów RCE/URLDNS.
  • marshalsec wciąż jest użyteczne, gdy sink pivotuje w stronę JNDI/LDAP/RMI.
  • GadgetInspector jest przydatny, gdy masz jar-y celu i potrzebujesz wyszukać specyficzne łańcuchy gadgetów w aplikacji.
  • Java 17 dodała event jdk.Deserialization do Flight Recorder, co pomaga zobaczyć, gdzie ObjectInputStream jest faktycznie używany i czy filtry są stosowane.

Szybka lista kontrolna dla bezpiecznych implementacji readObject()

  1. Zadeklaruj metodę jako private i oznacz hooki serializacyjne za pomocą @Serial, aby kompilatory mogły wykryć błędnie zadeklarowane sygnatury.
  2. Wywołaj defaultReadObject() najpierw, chyba że masz silny powód, by ręcznie odczytać cały graf obiektów.
  3. Traktuj każdy zagnieżdżony obiekt jako kontrolowany przez atakującego, dopóki nie zostanie zwalidowany.
  4. Nigdy nie wywołuj metod na zdeserializowanych współpracownikach wewnątrz readObject().
  5. Połącz code review z przeglądem ObjectInputFilter; „bezpiecznie wyglądający kod readObject()” nie wystarcza, jeśli strumień wciąż akceptuje dowolne klasy.

References

Tip

Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Przeglądaj pełny katalog HackTricks Training dla ścieżek assessment (ARTA/GRTA/AzRTA) oraz Linux Hacking Expert (LHE).

Wsparcie HackTricks