बुनियादी Java Deserialization with 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) assessment tracks (ARTA/GRTA/AzRTA) और Linux Hacking Expert (LHE) के लिए full HackTricks Training catalog ब्राउज़ करें।

HackTricks का समर्थन करें

In this POST it’s going to be explained an example using java.io.Serializable and why overriding readObject() can be extremely dangerous if the incoming stream is attacker-controlled.

Serializable

The Java Serializable interface (java.io.Serializable) एक marker interface है जिसे आपकी classes को implement करना चाहिए यदि उन्हें serialized और deserialized किया जाना है। Java object serialization (writing) ObjectOutputStream के साथ किया जाता है और deserialization (reading) ObjectInputStream के साथ किया जाता है।

Reminder: Which methods are implicitly invoked during deserialization?

  1. readObject() – क्लास-विशिष्ट read लॉजिक (यदि लागू और private हो).
  2. readResolve() – deserialized object को किसी दूसरे object से बदल सकता है.
  3. validateObject()ObjectInputValidation callbacks के माध्यम से.
  4. readExternal() – उन क्लासेस के लिए जो Externalizable implement करती हैं.
  5. Constructors execute नहीं होते – इसलिए gadget chains पूरी तरह से पिछले callbacks पर निर्भर करते हैं.

chain में कोई भी method जो attacker-controlled data (command execution, JNDI lookups, reflection, आदि) को invoke कर देता है, deserialization routine को एक RCE gadget में बदल देता है।

Lets see an example with a class Person which is serializable. This class overwrites the readObject function, so when any object of this class is deserialized this function is going to be executed.
In the example, the readObject function of the class Person calls the function eat() of his pet and the function eat() of a Dog (for some reason) calls a calc.exe. We are going to see how to serialize and deserialize a Person object to execute this 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");
}
}

निष्कर्ष (क्लासिक परिदृश्य)

जैसा कि आप इस बहुत ही बुनियादी उदाहरण में देख सकते हैं, यहाँ “vulnerability” इसलिए दिखती है क्योंकि the readObject() method calling other attacker-controlled code. वास्तविक दुनिया के gadget chains में, बाहरी लाइब्रेरीज़ (Commons-Collections, Spring, Groovy, Rome, SnakeYAML, आदि) में शामिल हजारों क्लासेस का दुरुपयोग किया जा सकता है — attacker को केवल one reachable gadget चाहिए होता है ताकि कोड execution मिल सके।


2023-2025: वास्तविक दुनिया के Java deserialization बग्स में क्या बदला?

हालिया मामले यह याद दिलाते हैं कि ObjectInputStream bugs अब सिर्फ “एक .ser फाइल को legacy HTTP endpoint पर अपलोड करें” तक सीमित नहीं हैं:

  • Broker / queue consumers: Spring-Kafka (CVE-2023-34040) ने दिखाया कि अगर consumer असामान्य checkDeserExWhen* flags सक्षम करता है तो attacker-controlled topics से deserializing exception headers ही काफी हो सकते हैं।
  • Client-side trust of remote servers: the Aerospike Java client (CVE-2023-36480) ने सर्वर से प्राप्त objects को deserialized किया। विक्रेता की प्रतिक्रिया उल्लेखनीय थी: नए clients ने Java runtime serialization/deserialization support को हटाना चुना बजाय इसके कि उसे एक कमजोर filter के पीछे बचाए रखें।
  • “Restricted” streams are often still too broad: pac4j-core (CVE-2023-25581) ने deserialization को RestrictedObjectInputStream से सुरक्षित करने की कोशिश की, पर स्वीकार्य क्लास सेट फिर भी इतना बड़ा था कि gadget abuse संभव था।

आक्रामक सबक यह है कि खतरनाक trust boundary अक्सर not “user uploads a blob”, बल्कि “कोई घटक जिसे developer ने trusted माना था, वह ऐसे बाइट्स inject कर सकता है जो अंततः readObject() तक पहुँचते हैं”।

यदि आपको पूरी gadget रिसर्च में समय लगाने से पहले low-noise reachability checks की ज़रूरत है, तो समर्पित Java पृष्ठों का उपयोग करें:

Java DNS Deserialization, GadgetProbe and Java Deserialization Scanner

readObject() anti-patterns जो अभी भी gadget entrypoints बनाते हैं

भले ही आपकी क्लास खुद एक स्पष्ट RCE gadget न हो, निम्नलिखित पैटर्न तब भी इसे exploitable बना देते हैं जब attacker-controlled objects graph में embedded हों:

  1. readObject() से overridable methods या interface methods कॉल करना (pet.eat() ऊपर के PoC में क्लासिक उदाहरण है)।
  2. deserialization के दौरान lookups, reflection, class loading, expression evaluation, या JNDI ऑपरेशन्स करना।
  3. attacker-controlled collections या maps पर iterate करना, जो side effects के रूप में hashCode(), equals(), comparators, या transformers को ट्रिगर कर सकता है।
  4. खतरनाक post-processing करने वाले ObjectInputValidation callbacks को register करना।
  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 कॉल करें, फिर कड़े invariant checks लागू करें। I/O न करें, ऐसे logging से बचें जो attacker-controlled objects को dereference करे, dynamic lookups न करें, और deserialized sub-objects पर method calls न करें।
  3. If possible, remove Java native serialization from the design
    Aerospike का fix इस बात का अच्छा मॉडल है: जब यह फीचर अनिवार्य न हो, तो readObject() / writeObject() का उपयोग हटाना कई बार perfect filters को हमेशा बनाए रखने से ज्यादा सुरक्षित होता है।

Detection and research workflow

  • ysoserial gadget validation और quick RCE/URLDNS probes के लिए बेसलाइन बना हुआ है।
  • marshalsec तब भी उपयोगी है जब sink JNDI/LDAP/RMI क्षेत्र में pivot करे।
  • GadgetInspector तब उपयोगी है जब आपके पास target jars हों और application-specific gadget chains खोजनी हों।
  • Java 17 ने jdk.Deserialization Flight Recorder event जोड़ा, जो यह देखने में उपयोगी है कि ObjectInputStream वास्तव में कहाँ उपयोग हो रहा है और क्या filters लागू हो रहे हैं।

Quick checklist for secure readObject() implementations

  1. मेथड को private बनाएं और serialization hooks को @Serial से annotate करें ताकि compilers गलत-declared signatures पकड़ सकें।
  2. जब तक कोई मजबूत वजह न हो, पहले defaultReadObject() कॉल करें अन्यथा पूरे object graph को मैन्युअली पढ़ने का कारण स्पष्ट होना चाहिए।
  3. हर nested object को validate होने तक attacker-controlled मानें।
  4. readObject() के अंदर deserialized collaborators पर methods invoke कभी न करें।
  5. code review के साथ एक ObjectInputFilter review भी जोड़ें; “safe-looking readObject() code” तब भी पर्याप्त नहीं है यदि stream अभी भी arbitrary classes स्वीकार करता है।

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) assessment tracks (ARTA/GRTA/AzRTA) और Linux Hacking Expert (LHE) के लिए full HackTricks Training catalog ब्राउज़ करें।

HackTricks का समर्थन करें