Βασική Java αποσειριοποίηση με ObjectInputStream readObject

Tip

Μάθε & εξασκήσου στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθε & εξασκήσου στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Μάθε & εξασκήσου στο Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Περιηγήσου στον πλήρη κατάλογο HackTricks Training για τα assessment tracks (ARTA/GRTA/AzRTA) και στο Linux Hacking Expert (LHE).

Υποστήριξε το HackTricks

Σε αυτήν την POST θα εξηγηθεί ένα παράδειγμα που χρησιμοποιεί java.io.Serializable και γιατί η υπερφόρτωση του readObject() μπορεί να είναι εξαιρετικά επικίνδυνη αν η εισερχόμενη ροή ελέγχεται από επιτιθέμενο.

Serializable

Η Java Serializable interface (java.io.Serializable) είναι ένα marker interface που οι κλάσεις σας πρέπει να υλοποιήσουν αν πρόκειται να είναι σειριοποιημένες και αποσειριοποιημένες. Η Java object serialization (εγγραφή) γίνεται με το ObjectOutputStream και η deserialization (ανάγνωση) γίνεται με το ObjectInputStream.

Υπενθύμιση: Ποιες μέθοδοι καλούνται έμμεσα κατά την αποσειριοποίηση;

  1. readObject() – class-specific read logic (if implemented and private).
  2. readResolve() – can replace the deserialized object with another one.
  3. validateObject() – via ObjectInputValidation callbacks.
  4. readExternal() – for classes implementing Externalizable.
  5. Constructors are not executed – therefore gadget chains rely exclusively on the previous callbacks.

Οποιαδήποτε μέθοδος σε αυτή την αλυσίδα που καταλήγει να χρησιμοποιεί δεδομένα ελεγχόμενα από επιτιθέμενο (εκτέλεση εντολών, JNDI lookups, reflection, κ.λπ.) μετατρέπει τη διαδικασία αποσειριοποίησης σε RCE gadget.

Ας δούμε ένα παράδειγμα με μια class Person η οποία είναι serializable. Αυτή η κλάση overwrites the readObject function, έτσι όταν any object αυτής της class is deserialized αυτή η function πρόκειται να εκτελεστεί.
Στο παράδειγμα, η readObject function της κλάσης Person καλεί τη συνάρτηση eat() του κατοικίδιού του και η συνάρτηση eat() ενός Dog (για κάποιο λόγο) καλεί ένα calc.exe. Θα δούμε πώς να σειριοποιήσουμε και να αποσειριοποιήσουμε ένα αντικείμενο Person για να εκτελέσουμε αυτόν τον calculator:

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");
}
}

Conclusion (classic scenario)

Όπως φαίνεται στο πολύ απλό αυτό παράδειγμα, η «ευπάθεια» εδώ προκύπτει επειδή η μέθοδος readObject() καλεί άλλο attacker-controlled code. Σε πραγματικά gadget chains, χιλιάδες κλάσεις που περιέχονται σε εξωτερικές βιβλιοθήκες (Commons-Collections, Spring, Groovy, Rome, SnakeYAML, κ.λπ.) μπορούν να κακοποιηθούν – ο attacker χρειάζεται μόνο ένα προσβάσιμο gadget για να αποκτήσει code execution.


2023-2025: Τι άλλαξε σε πραγματικά Java deserialization bugs;

Πρόσφατες υποθέσεις υπενθυμίζουν ότι τα σφάλματα σε ObjectInputStream δεν είναι πλέον απλώς «ανέβασε ένα .ser αρχείο σε ένα legacy HTTP endpoint»:

  • Broker / queue consumers: Το Spring-Kafka (CVE-2023-34040) έδειξε ότι το deserializing exception headers από attacker-controlled topics είναι αρκετό εάν ο consumer ενεργοποιεί τα ασυνήθιστα flags checkDeserExWhen*.
  • Client-side trust of remote servers: Ο Aerospike Java client (CVE-2023-36480) deserialized objects που λαμβάνονται από τον server. Η αντίδραση του vendor ήταν αξιοσημείωτη: οι νεότεροι clients αφαίρεσαν την υποστήριξη Java runtime serialization/deserialization αντί να προσπαθήσουν να τη διατηρήσουν πίσω από ένα αδύναμο φίλτρο.
  • “Restricted” streams are often still too broad: Το pac4j-core (CVE-2023-25581) προσπάθησε να προστατεύσει το deserialization με RestrictedObjectInputStream, αλλά το αποδεκτό σύνολο κλάσεων ήταν ακόμα αρκετά μεγάλο ώστε να επιτρέπεται η εκμετάλλευση gadget.

Το επιθετικό μάθημα είναι ότι το επικίνδυνο trust boundary συχνά δεν είναι «ο χρήστης ανεβάζει ένα blob», αλλά «κάποιο component που ο developer θεωρούσε trusted μπορεί να εγχύσει bytes σε ένα stream που τελικά φτάνει στο readObject()».

Αν χρειάζεστε low-noise reachability checks πριν αφιερώσετε χρόνο σε full gadget research, χρησιμοποιήστε τις αφιερωμένες Java σελίδες για:

Java DNS Deserialization, GadgetProbe and Java Deserialization Scanner

readObject() anti-patterns που εξακολουθούν να δημιουργούν gadget entrypoints

Ακόμα και αν η κλάση σας δεν είναι προφανές RCE gadget, τα παρακάτω μοτίβα αρκούν για να γίνει εκμεταλλεύσιμη όταν attacker-controlled objects είναι ενσωματωμένα στο graph:

  1. Κλήση overridable μεθόδων ή interface μεθόδων από readObject() (pet.eat() στο PoC παραπάνω είναι το κλασικό παράδειγμα).
  2. Εκτέλεση lookups, reflection, class loading, expression evaluation, ή JNDI operations κατά το deserialization.
  3. Επανάληψη πάνω σε attacker-controlled collections ή maps, που μπορεί να ενεργοποιήσουν hashCode(), equals(), comparators, ή transformers ως side effects.
  4. Εγγραφή ObjectInputValidation callbacks που εκτελούν επικίνδυνη post-processing.
  5. Η υπόθεση ότι «private readObject()» αρκεί ως προστασία. Ελέγχει μόνο τη dispatch semantics· δεν κάνει το deserialization ασφαλές.

Modern mitigations που πρέπει να εφαρμόσετε

  1. JEP 290 / Serialization Filtering (Java 9+) Χρησιμοποιήστε allow-list και explicit graph limits:
-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 Προτιμήστε αυτό όταν η ίδια JVM έχει πολλαπλά deserialization contexts (RMI, cache replication, message consumers, admin-only imports) και καθένα χρειάζεται διαφορετικό allow-list.
  2. Keep readObject() boring Καλείτε μόνο defaultReadObject() / explicit field reads, και μετά εκτελέστε αυστηρούς ελέγχους invariants. Μην κάνετε I/O, μην καταγράφετε που dereference attacker-controlled objects, μην κάνετε dynamic lookups ή method calls σε deserialized sub-objects.
  3. If possible, remove Java native serialization from the design Η διόρθωση του Aerospike είναι καλό μοντέλο: όταν το feature δεν είναι απαραίτητο, η διαγραφή της χρήσης readObject() / writeObject() είναι συχνά πιο ασφαλής από το να προσπαθείς να διατηρήσεις τέλεια φίλτρα επ’ αόριστον.

Detection and research workflow

  • ysoserial παραμένει η βάση για gadget validation και quick RCE/URLDNS probes.
  • marshalsec εξακολουθεί να είναι χρήσιμο όταν ο sink pivots σε JNDI/LDAP/RMI περιοχή.
  • GadgetInspector είναι χρήσιμο όταν έχετε τα target jars και χρειάζεται να ψάξετε για application-specific gadget chains.
  • Το Java 17 πρόσθεσε το jdk.Deserialization Flight Recorder event, χρήσιμο για να δείτε πού χρησιμοποιείται πραγματικά το ObjectInputStream και αν εφαρμόζονται φίλτρα.

Quick checklist για secure readObject() implementations

  1. Κάντε τη μέθοδο private και σχολιάστε τα serialization hooks με @Serial ώστε οι compilers να πιάσουν λάθη σε mis-declared signatures.
  2. Καλέστε defaultReadObject() πρώτα εκτός αν έχετε ισχυρό λόγο να διαβάσετε χειροκίνητα όλο το object graph.
  3. Θεωρήστε κάθε nested object ως attacker-controlled μέχρι να επικυρωθεί.
  4. Μην επικαλείστε μεθόδους σε deserialized collaborators από μέσα σε readObject().
  5. Συνδυάστε τον code review με έναν ObjectInputFilter review· «φαίνονται ασφαλείς readObject() κώδικες» δεν αρκούν αν το stream εξακολουθεί να δέχεται αυθαίρετες κλάσεις.

References

Tip

Μάθε & εξασκήσου στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθε & εξασκήσου στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Μάθε & εξασκήσου στο Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Περιηγήσου στον πλήρη κατάλογο HackTricks Training για τα assessment tracks (ARTA/GRTA/AzRTA) και στο Linux Hacking Expert (LHE).

Υποστήριξε το HackTricks