Deserialización Java básica con ObjectInputStream readObject

Tip

Aprende y practica AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Revisa el catálogo completo de HackTricks Training para las rutas de evaluación (ARTA/GRTA/AzRTA) y Linux Hacking Expert (LHE).

Apoya a HackTricks

En esta entrada se explicará un ejemplo usando java.io.Serializable y por qué sobreescribir readObject() puede ser extremadamente peligroso si el stream entrante está controlado por un atacante.

Serializable

La interfaz Java Serializable (java.io.Serializable) es una interfaz marcador que tus clases deben implementar si van a ser serializadas y deserializadas. La serialización de objetos Java (escritura) se realiza con el ObjectOutputStream y la deserialización (lectura) se realiza con el ObjectInputStream.

Recordatorio: ¿Qué métodos se invocan implícitamente durante la deserialización?

  1. readObject() – lógica de lectura específica de la clase (si está implementada y es private).
  2. readResolve() – puede reemplazar el objeto deserializado con otro.
  3. validateObject() – vía callbacks de ObjectInputValidation.
  4. readExternal() – para clases que implementan Externalizable.
  5. Los constructores no se ejecutan – por lo tanto las gadget chains dependen exclusivamente de las callbacks anteriores.

Cualquier método en esa cadena que termine invocando datos controlados por el atacante (ejecución de comandos, búsquedas JNDI, reflection, etc.) convierte la rutina de deserialización en un gadget de RCE.

Veamos un ejemplo con una clase Person que es serializable. Esta clase sobrescribe el readObject function, so when any object of this class is deserialized this function is going to be executed.
En el ejemplo, la función readObject de la clase Person llama a la función eat() de su mascota y la función eat() de un Dog (por alguna razón) ejecuta un calc.exe. Vamos a ver cómo serializar y deserializar un objeto Person para ejecutar esta calculadora:

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

Conclusión (escenario clásico)

Como se puede ver en este ejemplo muy básico, la “vulnerabilidad” aquí aparece porque el método readObject() está llamando a otro código controlado por el atacante. En cadenas de gadgets del mundo real, miles de clases contenidas en bibliotecas externas (Commons-Collections, Spring, Groovy, Rome, SnakeYAML, etc.) pueden ser abusadas: el atacante solo necesita un gadget alcanzable para obtener ejecución de código.


2023-2025: ¿Qué cambió en los bugs de deserialización Java en el mundo real?

Los casos recientes recuerdan que los bugs de ObjectInputStream ya no son solo “subir un archivo .ser a un endpoint HTTP legado”:

  • Broker / queue consumers: Spring-Kafka (CVE-2023-34040) demostró que deserializar encabezados de excepciones desde topics controlados por el atacante es suficiente si el consumidor habilita las inusuales banderas checkDeserExWhen*.
  • Client-side trust of remote servers: el cliente Java de Aerospike (CVE-2023-36480) deserializó objetos recibidos del servidor. La respuesta del proveedor fue notable: los clientes más nuevos eliminaron el soporte de serialización/deserialización del runtime de Java en lugar de intentar preservarlo detrás de un filtro débil.
  • “Restricted” streams are often still too broad: pac4j-core (CVE-2023-25581) intentó proteger la deserialización con RestrictedObjectInputStream, pero el conjunto de clases aceptadas seguía siendo lo suficientemente grande como para permitir el abuso de gadgets.

La lección ofensiva es que el límite de confianza peligroso a menudo no es “el usuario sube un blob”, sino “algún componente que el desarrollador consideraba confiable puede inyectar bytes en un stream que eventualmente llega a readObject()”.

Si necesitas comprobaciones de alcanzabilidad de bajo ruido antes de dedicar tiempo a la investigación completa de gadgets, usa las páginas Java dedicadas para:

Java DNS Deserialization, GadgetProbe and Java Deserialization Scanner

Anti-patterns de readObject() que aún crean puntos de entrada para gadgets

Aunque tu clase no sea por sí misma un gadget RCE obvio, los siguientes patrones son suficientes para hacerla explotable cuando objetos controlados por el atacante estén incrustados en el grafo:

  1. Llamar a métodos sobrescribibles o métodos de interfaz desde readObject() (pet.eat() en el PoC arriba es el ejemplo clásico).
  2. Realizar lookups, reflection, carga de clases, evaluación de expresiones u operaciones JNDI durante la deserialización.
  3. Iterar sobre colecciones o mapas controlados por el atacante, lo cual puede desencadenar hashCode(), equals(), comparators o transformers como efectos secundarios.
  4. Registrar callbacks ObjectInputValidation que realicen post-procesamiento peligroso.
  5. Asumir que “private readObject()” es suficiente protección. Solo controla la semántica de despacho; no hace que la deserialización sea segura.

Mitigaciones modernas que deberías desplegar

  1. JEP 290 / Serialization Filtering (Java 9+) Usa una allow-list y límites explícitos del grafo:
-Djdk.serialFilter="com.example.dto.*;java.base/*;maxdepth=5;maxrefs=1000;maxbytes=16384;!*"
  1. Aplica un filtro en cada stream no confiable, no solo globalmente:
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 Prefiere esto cuando la misma JVM tiene múltiples contextos de deserialización (RMI, cache replication, message consumers, admin-only imports) y cada uno necesita una allow-list diferente.
  2. Mantén readObject() aburrido Solo llama a defaultReadObject() / lecturas explícitas de campos, luego realiza comprobaciones estrictas de invariantes. No hagas I/O, logging que desreferencie objetos controlados por el atacante, lookups dinámicos, ni llamadas a métodos sobre subobjetos deserializados.
  3. Si es posible, elimina la serialización nativa de Java del diseño El arreglo de Aerospike es un buen modelo: cuando la característica no es esencial, eliminar el uso de readObject() / writeObject() suele ser más seguro que intentar mantener filtros perfectos para siempre.

Flujo de trabajo de detección e investigación

  • ysoserial sigue siendo la referencia para la validación de gadgets y probes rápidos de RCE/URLDNS.
  • marshalsec sigue siendo útil cuando el sink pivota hacia territorio JNDI/LDAP/RMI.
  • GadgetInspector es útil cuando tienes los jars del objetivo y necesitas buscar cadenas de gadgets específicas de la aplicación.
  • Java 17 añadió el evento jdk.Deserialization del Flight Recorder, que es útil para ver dónde se usa realmente ObjectInputStream y si se están aplicando filtros.

Lista de verificación rápida para implementaciones seguras de readObject()

  1. Haz el método private y anota los hooks de serialización con @Serial para que los compiladores detecten firmas mal declaradas.
  2. Llama a defaultReadObject() primero a menos que tengas una razón sólida para leer manualmente todo el grafo de objetos.
  3. Trata cada objeto anidado como controlado por el atacante hasta que esté validado.
  4. Nunca invoques métodos sobre colaboradores deserializados desde dentro de readObject().
  5. Acompaña la revisión de código con una revisión de ObjectInputFilter; el “código readObject() que parece seguro” no es suficiente si el stream aún acepta clases arbitrarias.

References

Tip

Aprende y practica AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Revisa el catálogo completo de HackTricks Training para las rutas de evaluación (ARTA/GRTA/AzRTA) y Linux Hacking Expert (LHE).

Apoya a HackTricks