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

У цьому пості буде показано приклад використання java.io.Serializable та пояснено, чому перевизначення readObject() може бути надзвичайно небезпечним, якщо вхідний потік контролюється зловмисником.

Serializable

Інтерфейс Java Serializable (java.io.Serializable) — це маркерний інтерфейс, який ваші класи повинні реалізувати, якщо їх потрібно серіалізувати та десеріалізувати. Серіалізація обʼєктів Java (запис) виконується за допомогою ObjectOutputStream, а десеріалізація (читання) — за допомогою ObjectInputStream.

Reminder: Which methods are implicitly invoked during deserialization?

  1. readObject() – логіка читання, специфічна для класу (якщо реалізована і private).
  2. readResolve() – може замінити десеріалізований обʼєкт іншим.
  3. validateObject() – через зворотні виклики ObjectInputValidation.
  4. readExternal() – для класів, що реалізують Externalizable.
  5. Конструктори не виконуються – тому 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 об’єкти:

  1. Виклик перевизначуваних методів або методів інтерфейсу з readObject() (pet.eat() у PoC вище — класичний приклад).
  2. Виконання lookups, reflection, class loading, оцінки виразів або JNDI-операцій під час десеріалізації.
  3. Ітерація по attacker-controlled колекціях або мапах, що може викликати hashCode(), equals(), компаратори або transformers як побічні ефекти.
  4. Реєстрація ObjectInputValidation callbacks, які виконують небезпечну постобробку.
  5. Припущення, що «private readObject()» достатньо для захисту. Воно контролює лише семантику диспетчеризації; воно не робить десеріалізацію безпечною.

Modern mitigations you should deploy

  1. 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;!*"
  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 є кілька контекстів десеріалізації (RMI, cache replication, message consumers, admin-only imports) і кожному потрібен свій allow-list.
  2. Keep readObject() boring Викликайте лише defaultReadObject() / явне читання полів, а потім виконуйте суворі перевірки інваріантів. Не робіть I/O, логування, що дереференсує attacker-controlled об’єкти, динамічні lookups або виклики методів на десеріалізованих суб-об’єктах.
  3. 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.Deserialization Flight Recorder event, який корисний для бачення, де саме використовується ObjectInputStream і чи застосовуються фільтри.

Quick checklist for secure readObject() implementations

  1. Зробіть метод private і анотуйте serialization hooks з @Serial, щоб компілятори ловили неправильно оголошені сигнатури.
  2. Викликайте defaultReadObject() спочатку, якщо у вас немає вагомих причин вручну читати весь object graph.
  3. Розглядайте кожен вкладений об’єкт як attacker-controlled, доки він не буде валідований.
  4. Ніколи не викликайте методи на десеріалізованих співпрацівниках зсередини readObject().
  5. Поєднуйте код-рев’ю з рев’ю ObjectInputFilter; «safe-looking readObject() code» недостатньо, якщо стрім все ще приймає довільні класи.

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