Osnovna Java deserializacija sa ObjectInputStream readObject

Tip

Nauči i vežbaj AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Nauči i vežbaj GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Nauči i vežbaj Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Pregledaj kompletan HackTricks Training katalog za assessment tracks (ARTA/GRTA/AzRTA) i Linux Hacking Expert (LHE).

Podrži HackTricks

U ovom POST-u biće objašnjen primer koji koristi java.io.Serializable i zašto prepisivanje readObject() može biti izuzetno opasno ako je dolazeći stream pod kontrolom napadača.

Serializable

Java Serializable interface (java.io.Serializable) je marker interfejs koji vaše klase moraju implementirati ako treba da budu serializovane i deserializovane. Java object serialization (pisanje) se obavlja pomoću ObjectOutputStream, a deserialization (čitanje) se obavlja pomoću ObjectInputStream.

Podsetnik: Koji se metodi implicitno pozivaju tokom deserializacije?

  1. readObject() – class-specific read logic (if implemented and private).
  2. readResolve() – može zameniti deserializovani objekat drugim.
  3. validateObject() – putem ObjectInputValidation callbacks.
  4. readExternal() – za klase koje implementiraju Externalizable.
  5. Konstruktori se ne izvršavaju – stoga gadget chains zavise isključivo od prethodnih callbacks.

Bilo koji metod u tom lancu koji dovodi do pozivanja podataka pod kontrolom napadača (command execution, JNDI lookups, reflection, itd.) pretvara rutinu deserializacije u RCE gadget.

Pogledajmo primer sa class Person koji je serializable. Ova klasa overwrites the readObject funkciju, tako da kada se bilo koji objekat ove klase deserializuje, ova funkcija će biti izvršena.
U primeru, readObject funkcija klase Person poziva funkciju eat() svog ljubimca, a funkcija eat() klase Dog (iz nekog razloga) pokreće calc.exe. Videćemo kako da serializujemo i deserializujemo Person objekat da bismo pokrenuli ovaj kalkulator:

Sledeći primer je iz 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");
}
}

Zaključak (klasični scenario)

Kao što vidite u ovom vrlo osnovnom primeru, “vulnerabilnost” ovde se pojavljuje zato što metoda readObject() poziva drugi kod koji je pod kontrolom napadača. U realnim gadget lancima, hiljade klasa iz eksternih biblioteka (Commons-Collections, Spring, Groovy, Rome, SnakeYAML, itd.) mogu biti zloupotrebljene – napadaču je dovoljan jedan dostižan gadget da dobije izvršavanje koda.


2023-2025: Šta se promenilo u realnim Java deserialization bagovima?

Nedavni slučajevi podsećaju da bagovi u ObjectInputStream više nisu samo “upload .ser fajla na legacy HTTP endpoint”:

  • Broker / queue consumers: Spring-Kafka (CVE-2023-34040) je pokazao da deserializacija exception header-a iz topika koje kontroliše napadač može biti dovoljna ako consumer uključi neuobičajene checkDeserExWhen* flag-ove.
  • Client-side trust of remote servers: Aerospike Java client (CVE-2023-36480) je deserializovao objekte primljene od servera. Reakcija vendora je bila značajna: noviji client-i su uklonili podršku za Java runtime serialization/deserialization umesto da pokušaju da je sačuvaju iza slabog filtera.
  • “Restricted” streams are often still too broad: pac4j-core (CVE-2023-25581) je pokušao da zaštiti deserializaciju sa RestrictedObjectInputStream, ali skup prihvaćenih klasa je i dalje bio dovoljno veliki da omogući zloupotrebu gadgeta.

Ofanzivna lekcija je da je opasna granica poverenja često nije “korisnik uploaduje blob”, već “neki komponent koji je developer smatrao pouzdanim može ubaciti bajtove u stream koji na kraju stiže do readObject()”.

Ako trebate low-noise provere dostižnosti pre nego što uložite vreme u kompletno istraživanje gadgeta, koristite posvećene Java stranice za:

Java DNS Deserialization, GadgetProbe and Java Deserialization Scanner

readObject() anti-patterni koji i dalje kreiraju ulazne tačke za gadgete

Čak i ako vaša klasa sama po sebi nije očigledan RCE gadget, sledeći paterni su dovoljni da je učine eksploatabilnom kada su objekti pod kontrolom napadača ugnježdeni u graf:

  1. Pozivanje overridable metoda ili metoda interfejsa iz readObject() (pet.eat() u PoC iznad je klasičan primer).
  2. Izvođenje lookup-a, reflection-a, učitavanja klasa, evaluacije izraza ili JNDI operacija tokom deserializacije.
  3. Iteriranje preko kolekcija ili mapa koje kontroliše napadač, što može pokrenuti hashCode(), equals(), comparatore ili transformere kao sporedne efekte.
  4. Registracija ObjectInputValidation callback-ova koji vrše opasnu post-obradu.
  5. Pretpostavka da je “private readObject()” dovoljna zaštita. Ona samo kontroliše dispatch semantiku; ne čini deserializaciju bezbednom.

Moderni mitigations koje treba da uložite

  1. JEP 290 / Serialization Filtering (Java 9+) Koristite allow-listu i eksplicitna ograničenja grafa:
-Djdk.serialFilter="com.example.dto.*;java.base/*;maxdepth=5;maxrefs=1000;maxbytes=16384;!*"
  1. Apply a filter on every untrusted stream, not just globally:
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 Preferirajte ovo kada ista JVM ima više konteksta deserializacije (RMI, cache replication, message consumers, admin-only imports) i svaki zahteva drugačiju allow-listu.
  2. Keep readObject() boring Pozovite samo defaultReadObject() / eksplicitno čitanje polja, pa zatim izvedite stroge provere invariantnosti. Ne radite I/O, logovanje koje dereferencira objekte koje kontroliše napadač, dinamičke lookup-e ili pozive metoda na deserializovanim pod-objektima.
  3. If possible, remove Java native serialization from the design Aerospike-ovo rešenje je dobar model: kada funkcionalnost nije neophodna, brisanje upotrebe readObject() / writeObject() često je sigurnije nego pokušaj da se zauvek održavaju savršeni filteri.

Detection and research workflow

  • ysoserial ostaje osnov za validaciju gadgeta i brze RCE/URLDNS probe.
  • marshalsec je i dalje koristan kada sink pivotuje u JNDI/LDAP/RMI teritoriju.
  • GadgetInspector je koristan kada imate target jar-ove i treba da tražite application-specific gadget lance.
  • Java 17 je dodao jdk.Deserialization Flight Recorder event, koji pomaže da se vidi gde se ObjectInputStream zaista koristi i da li se filteri primenjuju.

Quick checklist for secure readObject() implementations

  1. Napravite metodu private i anotirajte serialization hook-ove sa @Serial tako da kompajleri mogu uhvatiti pogrešno deklarisane signaturne.
  2. Pozovite defaultReadObject() prvo osim ako imate jak razlog da ručno čitate ceo objektni graf.
  3. Tretirajte svaki ugnježdeni objekat kao pod kontrolom napadača dok nije validiran.
  4. Nikada ne pozivajte metode na deserializovanim saradnicima iznutra readObject().
  5. Uskladite code review sa pregledom ObjectInputFilter; “naizgled bezbedan readObject() kod” nije dovoljan ako stream i dalje prihvata proizvoljne klase.

References

Tip

Nauči i vežbaj AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Nauči i vežbaj GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Nauči i vežbaj Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Pregledaj kompletan HackTricks Training katalog za assessment tracks (ARTA/GRTA/AzRTA) i Linux Hacking Expert (LHE).

Podrži HackTricks