Desserialização Java Básica com ObjectInputStream readObject

Tip

Aprenda e pratique AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Navegue pelo catálogo completo do HackTricks Training para as trilhas de assessment (ARTA/GRTA/AzRTA) e Linux Hacking Expert (LHE).

Support HackTricks

Neste POST será explicado um exemplo usando java.io.Serializable e por que sobrescrever readObject() pode ser extremamente perigoso se o stream de entrada for controlado por um atacante.

Serializable

A interface Java Serializable (java.io.Serializable) é uma marker interface que suas classes devem implementar se elas forem serializadas e desserializadas. A serialização de objetos Java (escrita) é feita com o ObjectOutputStream e a desserialização (leitura) é feita com o ObjectInputStream.

Lembrete: Quais métodos são invocados implicitamente durante a desserialização?

  1. readObject() – lógica de leitura específica da classe (se implementado e privado).
  2. readResolve() – pode substituir o objeto desserializado por outro.
  3. validateObject() – via callbacks de ObjectInputValidation.
  4. readExternal() – para classes que implementam Externalizable.
  5. Construtores não são executados – portanto gadget chains dependem exclusivamente dos callbacks anteriores.

Qualquer método nessa cadeia que acabe invocando dados controlados por um atacante (execução de comandos, JNDI lookups, reflection, etc.) transforma a rotina de desserialização em um gadget de RCE.

Vamos ver um exemplo com uma class Person que é serializable. Esta classe sobrescreve o readObject, então quando qualquer objeto desta classe for desserializado esta função vai ser executada.
No exemplo, a função readObject da classe Person chama a função eat() do seu pet e a função eat() de um Dog (por algum motivo) executa um calc.exe. Vamos ver como serializar e desserializar um objeto Person para executar essa 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");
}
}

Conclusão (cenário clássico)

Como você pode ver neste exemplo bem básico, a “vulnerabilidade” aqui ocorre porque o método readObject() está chamando outro código controlado pelo atacante. Em gadget chains do mundo real, milhares de classes contidas em bibliotecas externas (Commons-Collections, Spring, Groovy, Rome, SnakeYAML, etc.) podem ser abusadas – o atacante precisa de apenas um gadget alcançável para obter execução de código.


2023-2025: O que mudou em bugs reais de deserialização Java?

Casos recentes lembram que bugs em ObjectInputStream não são mais apenas “fazer upload de um arquivo .ser para um endpoint HTTP legado”:

  • Brokers / consumidores de filas: Spring-Kafka (CVE-2023-34040) mostrou que deserializar headers de exceção de tópicos controlados pelo atacante é suficiente se o consumer habilita as incomuns flags checkDeserExWhen*.
  • Confiança do lado do cliente em servidores remotos: o cliente Java Aerospike (CVE-2023-36480) deserializou objetos recebidos do servidor. A resposta do fornecedor foi notável: clientes mais novos removeram o suporte à serialização/deserialização nativa do Java em vez de tentar preservá-lo atrás de um filtro fraco.
  • Streams “restritos” ainda costumam ser amplos demais: pac4j-core (CVE-2023-25581) tentou proteger a deserialização com RestrictedObjectInputStream, mas o conjunto de classes aceitas ainda era grande o bastante para possibilitar abuso de gadget.

A lição ofensiva é que o boundary de confiança perigoso muitas vezes não é “usuário faz upload de um blob”, mas “algum componente que o desenvolvedor considerou confiável pode injetar bytes em um stream que eventualmente chega em readObject()”.

Se você precisa de checks de reachability de baixo ruído antes de gastar tempo com pesquisa completa de gadgets, use as páginas Java dedicadas para:

Java DNS Deserialization, GadgetProbe and Java Deserialization Scanner

Anti-padrões em readObject() que ainda criam pontos de entrada para gadgets

Mesmo que sua própria classe não seja um gadget RCE óbvio, os seguintes padrões são suficientes para torná-la explorável quando objetos controlados pelo atacante estão embutidos no grafo:

  1. Chamar métodos sobrescritíveis ou métodos de interface a partir de readObject() (pet.eat() no PoC acima é o exemplo clássico).
  2. Realizar lookups, reflection, carregamento de classes, avaliação de expressões ou operações JNDI durante a deserialização.
  3. Iterar sobre collections ou maps controlados pelo atacante, o que pode disparar hashCode(), equals(), comparators ou transformers como efeitos colaterais.
  4. Registrar callbacks ObjectInputValidation que realizam pós-processamento perigoso.
  5. Assumir que “readObject() privado” é proteção suficiente. Isso apenas controla a semântica de dispatch; não torna a deserialização segura.

Mitigações modernas que você deve implantar

  1. JEP 290 / Serialization Filtering (Java 9+) Use uma allow-list e limites explícitos do grafo:
-Djdk.serialFilter="com.example.dto.*;java.base/*;maxdepth=5;maxrefs=1000;maxbytes=16384;!*"
  1. Aplique um filter em todo stream não confiável, não apenas 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 Prefira isso quando a mesma JVM tiver múltiplos contextos de deserialização (RMI, cache replication, message consumers, admin-only imports) e cada um precisar de uma allow-list diferente.
  2. Mantenha readObject() monótono Chame apenas defaultReadObject() / leituras explícitas de campos, então execute checagens estritas de invariantes. Não faça I/O, logging que desreferencie objetos controlados pelo atacante, lookups dinâmicos ou chamadas de método em sub-objetos deserializados.
  3. Se possível, remova a serialização nativa do Java do design O fix do Aerospike é um bom modelo: quando o recurso não é essencial, deletar o uso de readObject() / writeObject() costuma ser mais seguro do que tentar manter filtros perfeitos para sempre.

Fluxo de trabalho para detecção e pesquisa

  • ysoserial continua sendo a base para validação de gadgets e probes rápidos de RCE/URLDNS.
  • marshalsec ainda é útil quando o sink pivota para território JNDI/LDAP/RMI.
  • GadgetInspector é útil quando você tem os jars do alvo e precisa procurar por gadget chains específicos da aplicação.
  • Java 17 adicionou o evento jdk.Deserialization no Flight Recorder, que é útil para ver onde ObjectInputStream é realmente usado e se filtros estão sendo aplicados.

Checklist rápido para implementações seguras de readObject()

  1. Torne o método private e anote hooks de serialização com @Serial para que compiladores possam detectar assinaturas declaradas incorretamente.
  2. Chame defaultReadObject() primeiro, a menos que você tenha uma razão forte para ler manualmente todo o object graph.
  3. Trate todo objeto aninhado como controlado pelo atacante até ser validado.
  4. Nunca invoque métodos em colaboradores deserializados de dentro de readObject().
  5. Pareie a revisão de código com uma revisão de ObjectInputFilter; “código readObject() que parece seguro” não é suficiente se o stream ainda aceita classes arbitrárias.

Referências

Tip

Aprenda e pratique AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Navegue pelo catálogo completo do HackTricks Training para as trilhas de assessment (ARTA/GRTA/AzRTA) e Linux Hacking Expert (LHE).

Support HackTricks