Basic 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)
Перегляньте повний каталог HackTricks Training для assessment tracks (ARTA/GRTA/AzRTA) і Linux Hacking Expert (LHE).
Підтримайте HackTricks
- Перевірте плани підписки!
- Приєднуйтесь до 💬 Discord group, telegram group, слідкуйте за @hacktricks_live на X/Twitter, або перегляньте сторінку LinkedIn і YouTube channel.
- Діліться hacking tricks, надсилаючи PRs до репозиторіїв github HackTricks і HackTricks Cloud.
У цьому пості буде показано приклад використання java.io.Serializable та пояснено, чому перевизначення readObject() може бути надзвичайно небезпечним, якщо вхідний потік контролюється зловмисником.
Serializable
Інтерфейс Java Serializable (java.io.Serializable) — це маркерний інтерфейс, який ваші класи повинні реалізувати, якщо їх потрібно серіалізувати та десеріалізувати. Серіалізація обʼєктів Java (запис) виконується за допомогою ObjectOutputStream, а десеріалізація (читання) — за допомогою ObjectInputStream.
Reminder: Which methods are implicitly invoked during deserialization?
readObject()– логіка читання, специфічна для класу (якщо реалізована і private).readResolve()– може замінити десеріалізований обʼєкт іншим.validateObject()– через зворотні викликиObjectInputValidation.readExternal()– для класів, що реалізуютьExternalizable.- Конструктори не виконуються – тому gadget chains повністю покладаються на попередні callback-и.
Будь-який метод у цьому ланцюжку, який в кінцевому рахунку викликає дані, контрольовані зловмисником (виконання команд, JNDI lookups, reflection тощо), перетворює рутину десеріалізації на RCE gadget.
Давайте розглянемо приклад із класом Person, який є serializable. Цей клас перевизначає readObject, тому коли будь-який обʼєкт цього класу десеріалізується, ця функція буде виконана.
У прикладі функція readObject класу Person викликає функцію eat() свого улюбленця, а функція eat() у Dog (з якоїсь причини) викликає calc.exe. Ми побачимо, як серіалізувати та десеріалізувати обʼєкт Person, щоб виконати цей калькулятор:
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");
}
}
Висновок (класичний сценарій)
Як видно з цього дуже простого прикладу, «вразливість» тут виникає тому, що метод readObject() calling other attacker-controlled code. У реальних gadget chains тисячі класів зі зовнішніх бібліотек (Commons-Collections, Spring, Groovy, Rome, SnakeYAML тощо) можуть бути зловживані — attacker only needs one reachable gadget для отримання виконання коду.
2023-2025: What changed in real-world Java deserialization bugs?
Останні випадки нагадують, що баги ObjectInputStream більше не обмежуються лише «upload a .ser file to a legacy HTTP endpoint»:
- Broker / queue consumers: Spring-Kafka (
CVE-2023-34040) показав, що десеріалізація заголовків виключень з attacker-controlled topics достатня, якщо consumer вмикає незвичні прапориcheckDeserExWhen*. - Client-side trust of remote servers: the Aerospike Java client (
CVE-2023-36480) десеріалізував об’єкти, отримані від сервера. Відповідь вендора була примітною: новіші клієнти видалили підтримку Java runtime serialization/deserialization замість того, щоб намагатися зберегти її за допомогою слабкого фільтра. - “Restricted” streams are often still too broad:
pac4j-core(CVE-2023-25581) намагався захистити десеріалізацію черезRestrictedObjectInputStream, але набір дозволених класів усе ще був достатньо великий, щоб зробити possible gadget abuse.
Офенсивний урок: небезпечна межа довіри часто не в тому, що «user uploads a blob», а в тому, що «деякий компонент, який розробник вважав trusted, може інжектити байти в стрім, який зрештою доходить до readObject()».
Якщо потрібні low-noise reachability checks перед тим, як витрачати час на повне дослідження gadget-ланцюгів, використовуйте спеціальні Java-сторінки для:
Java DNS Deserialization, GadgetProbe and Java Deserialization Scanner
readObject() anti-patterns that still create gadget entrypoints
Навіть якщо ваш клас сам по собі не є очевидним RCE gadget, наступні патерни достатні, щоб зробити його експлойтабельним, коли в граф вставлені attacker-controlled об’єкти:
- Виклик перевизначуваних методів або методів інтерфейсу з
readObject()(pet.eat()у PoC вище — класичний приклад). - Виконання lookups, reflection, class loading, оцінки виразів або JNDI-операцій під час десеріалізації.
- Ітерація по attacker-controlled колекціях або мапах, що може викликати
hashCode(),equals(), компаратори або transformers як побічні ефекти. - Реєстрація
ObjectInputValidationcallbacks, які виконують небезпечну постобробку. - Припущення, що «private
readObject()» достатньо для захисту. Воно контролює лише семантику диспетчеризації; воно не робить десеріалізацію безпечною.
Modern mitigations you should deploy
- JEP 290 / Serialization Filtering (Java 9+) Use an allow-list and explicit graph limits:
-Djdk.serialFilter="com.example.dto.*;java.base/*;maxdepth=5;maxrefs=1000;maxbytes=16384;!*"
- 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();
}
- JEP 415 (Java 17+) Context-Specific Filter Factories Переважно використовувати, коли в одному JVM є кілька контекстів десеріалізації (RMI, cache replication, message consumers, admin-only imports) і кожному потрібен свій allow-list.
- Keep
readObject()boring Викликайте лишеdefaultReadObject()/ явне читання полів, а потім виконуйте суворі перевірки інваріантів. Не робіть I/O, логування, що дереференсує attacker-controlled об’єкти, динамічні lookups або виклики методів на десеріалізованих суб-об’єктах. - If possible, remove Java native serialization from the design
Фікс Aerospike — хороший приклад: коли фіча не є критичною, видалення використання
readObject()/writeObject()часто безпечніше, ніж намагання підтримувати ідеальні фільтри назавжди.
Detection and research workflow
ysoserialзалишається базою для валідації gadget-ів і швидких RCE/URLDNS перевірок.marshalsecвсе ще корисний, коли sink зсувається в область JNDI/LDAP/RMI.GadgetInspectorкорисний, коли у вас є таргетні jar-и і потрібно шукати application-specific gadget chains.- Java 17 додав
jdk.DeserializationFlight Recorder event, який корисний для бачення, де саме використовуєтьсяObjectInputStreamі чи застосовуються фільтри.
Quick checklist for secure readObject() implementations
- Зробіть метод
privateі анотуйте serialization hooks з@Serial, щоб компілятори ловили неправильно оголошені сигнатури. - Викликайте
defaultReadObject()спочатку, якщо у вас немає вагомих причин вручну читати весь object graph. - Розглядайте кожен вкладений об’єкт як attacker-controlled, доки він не буде валідований.
- Ніколи не викликайте методи на десеріалізованих співпрацівниках зсередини
readObject(). - Поєднуйте код-рев’ю з рев’ю
ObjectInputFilter; «safe-lookingreadObject()code» недостатньо, якщо стрім все ще приймає довільні класи.
References
- OpenJDK JEP 415: Context-Specific Deserialization Filters
- GitHub Security Lab: GHSL-2022-085 / CVE-2023-25581 (
pac4j-coredeserialization leading to RCE)
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
- Перевірте плани підписки!
- Приєднуйтесь до 💬 Discord group, telegram group, слідкуйте за @hacktricks_live на X/Twitter, або перегляньте сторінку LinkedIn і YouTube channel.
- Діліться hacking tricks, надсилаючи PRs до репозиторіїв github HackTricks і HackTricks Cloud.


