hacktricks/src/pentesting-web/deserialization/basic-java-deserialization-objectinputstream-readobject.md

11 KiB
Raw Blame History

Basic Java Deserialization with ObjectInputStream readObject

{{#include ../../banners/hacktricks-training.md}}

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

Serializable

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

Нагадування: Які методи імпліцитно викликаються під час десеріалізації?

  1. readObject() специфічна для класу логіка читання (якщо реалізована та приватна).
  2. readResolve() може замінити десеріалізований об'єкт на інший.
  3. validateObject() через зворотні виклики ObjectInputValidation.
  4. readExternal() для класів, що реалізують Externalizable.
  5. Конструктори не виконуються тому ланцюги гаджетів покладаються виключно на попередні зворотні виклики.

Будь-який метод у цьому ланцюзі, який в кінцевому підсумку викликає дані, контрольовані атакуючим (виконання команд, запити JNDI, рефлексія тощо), перетворює рутину десеріалізації на гаджет RCE.

Давайте розглянемо приклад з класом Person, який є серіалізованим. Цей клас перезаписує функцію readObject, тому коли будь-який об'єкт цього класу буде десеріалізований, ця функція буде виконана.
У прикладі функція readObject класу Person викликає функцію eat() його домашньої тварини, а функція eat() собаки (з якоїсь причини) викликає calc.exe. Ми побачимо, як серіалізувати та десеріалізувати об'єкт Person, щоб виконати цей калькулятор:

Наступний приклад взято з 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() викликає інший код, контрольований атакуючим. У реальних ланцюгах гаджетів тисячі класів, що містяться в зовнішніх бібліотеках (Commons-Collections, Spring, Groovy, Rome, SnakeYAML тощо), можуть бути зловживані атакуючому потрібен лише один досяжний гаджет для отримання виконання коду.


2023-2025: Що нового в атаках десеріалізації Java?

  • 2023 CVE-2023-34040: десеріалізація заголовків записів помилок Spring-Kafka, коли увімкнені прапорці checkDeserExWhen*, дозволила довільне створення гаджетів з тем, опублікованих атакуючими. Виправлено в 3.0.10 / 2.9.11. ¹
  • 2023 CVE-2023-36480: порушено припущення про довірений сервер Java-клієнта Aerospike зловмисні відповіді сервера містили серіалізовані корисні навантаження, які були десеріалізовані клієнтом → RCE. ²
  • 2023 CVE-2023-25581: парсинг атрибутів профілю користувача pac4j-core приймав Base64 блохи з префіксом {#sb64} і десеріалізував їх, незважаючи на RestrictedObjectInputStream. Оновлення ≥ 4.0.0.
  • 2023 CVE-2023-4528: JSCAPE MFT Manager Service (порт 10880) приймав XML-кодовані Java-об'єкти, що призводило до RCE як root/SYSTEM.
  • 2024 до ysoserial-plus(mod) додано кілька нових ланцюгів гаджетів, включаючи класи Hibernate5, TomcatEmbed і SnakeYAML 2.x, які обходять деякі старі фільтри.

Сучасні заходи, які ви повинні впровадити

  1. JEP 290 / Фільтрація серіалізації (Java 9+) Додайте список дозволених або заборонених класів:
# Приймати лише ваші DTO та java.base, відхиляти все інше
-Djdk.serialFilter="com.example.dto.*;java.base/*;!*"

Приклад програмного коду:

var filter = ObjectInputFilter.Config.createFilter("com.example.dto.*;java.base/*;!*" );
ObjectInputFilter.Config.setSerialFilter(filter);
  1. JEP 415 (Java 17+) Фабрики фільтрів, специфічних для контексту використовуйте BinaryOperator<ObjectInputFilter> для застосування різних фільтрів для кожного контексту виконання (наприклад, для кожного виклику RMI, для кожного споживача черги повідомлень).
  2. Не піддавайте сирий ObjectInputStream через мережу надавайте перевагу JSON/бінарним кодуванням без семантики виконання коду (Jackson після відключення DefaultTyping, Protobuf, Avro тощо).
  3. Обмеження захисту в глибині встановіть максимальну довжину масиву, глибину, посилання:
-Djdk.serialFilter="maxbytes=16384;maxdepth=5;maxrefs=1000"
  1. Безперервне сканування гаджетів запускайте інструменти, такі як gadget-inspector або serialpwn-cli у вашій CI, щоб зупинити збірку, якщо небезпечний гаджет стає досяжним.

Оновлений чек-лист інструментів (2024)

  • ysoserial-plus.jar спільний форк з > 130 ланцюгами гаджетів:
java -jar ysoserial-plus.jar CommonsCollections6 'calc' | base64 -w0
  • marshalsec все ще еталон для генерації гаджетів JNDI (LDAP/RMI).
  • gadget-probe швидке виявлення гаджетів чорного ящика проти мережевих сервісів.
  • SerialSniffer агент JVMTI, який друкує кожен клас, прочитаний ObjectInputStream (корисно для створення фільтрів).
  • Порада з виявлення увімкніть -Djdk.serialDebug=true (JDK 22+), щоб записувати рішення фільтра та відхилені класи.

Швидкий чек-лист для безпечних реалізацій readObject()

  1. Зробіть метод private і додайте анотацію @Serial (допомагає статичному аналізу).
  2. Ніколи не викликайте методи, надані користувачем, або не виконуйте I/O у методі лише читайте поля.
  3. Якщо потрібна валідація, виконуйте її після десеріалізації, поза readObject().
  4. Віддавайте перевагу реалізації Externalizable і виконуйте явні читання полів замість стандартної серіалізації.
  5. Зареєструйте посилений ObjectInputFilter навіть для внутрішніх сервісів (дизайн, стійкий до компрометації).

Посилання

  1. Консультація з безпеки Spring CVE-2023-34040 Десеріалізація Java в Spring-Kafka (серпень 2023)
  2. GitHub Security Lab GHSL-2023-044: Небезпечна десеріалізація в Java-клієнті Aerospike (липень 2023)

{{#include ../../banners/hacktricks-training.md}}