Entendendo Insecure Deserialization
A desserialização insegura é uma vulnerabilidade crítica que ocorre quando uma aplicação deserializa dados não confiáveis sem validação adequada. Em termos simples, significa converter dados de um formato externo (como JSON, XML ou bytes) de volta para objetos que o código pode usar. O problema surge quando um atacante injeta código malicioso nesses dados, e a aplicação o executa automaticamente durante o processo de desserialização.
Esta vulnerabilidade é particularmente perigosa porque não requer interação do usuário ou condições especiais — frequentemente é suficiente apenas fazer o upload de um arquivo ou enviar uma requisição com dados serializados. A OWASP classifica a desserialização insegura como uma das dez vulnerabilidades mais críticas em aplicações web. Diferente de injeção SQL ou XSS, que atacam a lógica de negócio, a desserialização insegura ataca a própria máquina virtual ou runtime da aplicação.
Gadget Chains: O Coração do Ataque
O que é uma Gadget Chain?
Uma gadget chain (corrente de gadgets) é uma sequência de chamadas de métodos existentes no código da aplicação ou em suas dependências que, quando encadeadas, resultam na execução de código arbitrário. Os "gadgets" são fragmentos de código já presentes no projeto — geralmente métodos como toString(), equals(), hashCode(), readObject() ou construtores que são chamados automaticamente durante a desserialização.
O brilhantismo desta abordagem está no fato de que não é necessário injetar novo código. O atacante apenas orquestra código legítimo que já existe. É como um hacker encontrar uma série de portas destrancadas em um edifício e, ao passar por todas elas em sequência específica, conseguir alcançar o cofre. A vulnerabilidade não está em nenhuma porta individualmente, mas na forma como elas se conectam.
Como as Gadget Chains Funcionam
Quando um objeto é desserializado, métodos especiais são chamados automaticamente. Em Java, por exemplo, o método readObject() é invocado. Um atacante cria uma cadeia onde o readObject() de um objeto chama um método de outro objeto, que por sua vez chama um método de um terceiro objeto, e assim por diante, até que uma operação perigosa (como Runtime.exec()) seja executada.
Considere este exemplo conceitual em Java:
// Classe A que será desserializada
public class GadgetA implements Serializable {
private GadgetB gadgetB;
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
// Automaticamente chamará método de GadgetB
gadgetB.process();
}
}
// Classe B que chama Classe C
public class GadgetB implements Serializable {
private GadgetC gadgetC;
public void process() {
gadgetC.execute();
}
}
// Classe C que executa comando do sistema
public class GadgetC implements Serializable {
public void execute() {
try {
Runtime.getRuntime().exec("touch /tmp/pwned");
} catch (IOException e) {
e.printStackTrace();
}
}
}
Na prática, as gadget chains usam bibliotecas comuns do projeto (Apache Commons, Spring, Jackson, etc.) que já possuem métodos úteis para este propósito. Um atacante não precisa escrever código novo — ele apenas encontra o caminho através do código existente.
Ysoserial: A Ferramenta Padrão do Atacante
O que é Ysoserial?
Ysoserial é uma ferramenta open-source que gera payloads de desserialização Java maliciosos. Ela automatiza a criação de gadget chains, permitindo que um atacante, sem profundo conhecimento das bibliotecas subjacentes, gere um payload que execute um comando arbitrário. A ferramenta está disponível no GitHub e é mantida por pesquisadores de segurança renomados.
A força do ysoserial está em sua capacidade de gerar chains para diferentes bibliotecas. Se seu projeto usa Commons Collections, o ysoserial gera uma chain baseada em Commons Collections. Se usa Spring Framework, gera uma chain apropriada. Isso a torna uma ferramenta universal para testar vulnerabilidades de desserialização.
Usando Ysoserial na Prática
Para usar o ysoserial, você precisa baixá-lo ou compilá-lo. Aqui está como gerar um payload básico:
# Baixar a versão compilada
wget https://github.com/frohoff/ysoserial/releases/download/v0.0.6/ysoserial-0.0.6-SNAPSHOT-all.jar
# Gerar um payload usando Commons Collections (cc5)
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 'touch /tmp/pwned' | base64
# Alguns gadgets disponíveis:
# CommonsCollections1-7 — baseado em Apache Commons Collections
# Spring1, Spring2 — baseado em Spring Framework
# ROME — baseado em biblioteca ROME
# JDK7u21, JDK8u20 — exploram vulnerabilidades da JDK
O comando acima gera um payload Base64 codificado que, quando desserializado por uma aplicação vulnerável, executará touch /tmp/pwned. Este payload pode ser enviado como dado serializado para a aplicação. Veja um exemplo de servidor vulnerável:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class VulnerableServer {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(9999);
System.out.println("Servidor aguardando conexões na porta 9999...");
while (true) {
Socket client = server.accept();
try (ObjectInputStream ois = new ObjectInputStream(client.getInputStream())) {
Object obj = ois.readObject(); // VULNERÁVEL!
System.out.println("Objeto recebido: " + obj.getClass().getName());
} catch (Exception e) {
System.err.println("Erro ao desserializar: " + e.getMessage());
}
}
}
}
Um atacante poderia enviar um payload gerado pelo ysoserial para este servidor e conseguir execução remota de código. A aplicação não faz nenhuma validação — apenas chama readObject() e confia que os dados são seguros.
Diferentes Gadgets e Seus Contextos
Cada gadget disponível no ysoserial funciona melhor em determinado contexto. Commons Collections é extremamente comum em projetos legados. Spring é ubíquo em aplicações modernas. Entender quais bibliotecas seu projeto usa é o primeiro passo para saber qual gadget será efetivo:
| Gadget | Biblioteca | Uso Comum | Requisitos |
|---|---|---|---|
| CommonsCollections1-7 | Apache Commons Collections | Transformações de dados | Apache Commons Collections 3.x |
| Spring1, Spring2 | Spring Framework | Injeção de dependência | Spring 4.x ou 5.x |
| ROME | ROME Feed Parser | Parse de feeds RSS/Atom | ROME 1.0+ |
| Groovy1 | Groovy | Dinâmica de linguagem | Groovy 2.x |
Defesas Contra Desserialização Insegura
1. Validação e Whitelist de Classes
A defesa mais direta é nunca desserializar dados não confiáveis ou, se precisar, validar rigorosamente quais classes são permitidas. Java 9+ fornece um mecanismo integrado chamado "Serial Killers" ou filtros de desserialização:
import java.io.*;
import java.io.ObjectInputStream;
public class SafeDeserialization {
// Implementar um ObjectInputStream customizado
static class SafeObjectInputStream extends ObjectInputStream {
private static final List<String> WHITELIST = Arrays.asList(
"java.lang.String",
"java.lang.Integer",
"java.util.ArrayList",
"com.exemplo.ClienteSeguro"
);
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
// Java 9+: usar filtro de desserialização
setObjectInputFilter(info -> {
String className = info.getClassName();
if (WHITELIST.contains(className)) {
return ObjectInputFilter.Status.ALLOWED;
}
System.err.println("Classe bloqueada: " + className);
return ObjectInputFilter.Status.REJECTED;
});
}
}
public static Object deserialize(byte[] data) throws Exception {
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
SafeObjectInputStream ois = new SafeObjectInputStream(bais)) {
return ois.readObject();
}
}
}
Este código cria um lista branca de classes permitidas. Qualquer tentativa de desserializar uma classe fora desta lista é rejeitada. Esta é a defesa mais efetiva porque bloqueia gadget chains na raiz — se a classe maliciosa não pode ser instanciada, a chain quebra.
2. Migrar para Formatos Seguros
A maneira mais robusta de evitar desserialização insegura é simplesmente não usar serialização nativa da linguagem. Use formatos como JSON, XML ou Protocol Buffers, que não possuem o mesmo nível de risco:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
public class JSONDeserialization {
private static final ObjectMapper mapper = new ObjectMapper();
static class Cliente {
public String nome;
public String email;
public int idade;
// Getters e setters...
}
public static Cliente deserializeFromJSON(String json) throws Exception {
// Jackson desserializa como POJO, sem executar gadget chains
return mapper.readValue(json, Cliente.class);
}
public static void main(String[] args) throws Exception {
String jsonDados = "{\"nome\": \"João\", \"email\": \"joao@example.com\", \"idade\": 30}";
Cliente cliente = deserializeFromJSON(jsonDados);
System.out.println("Cliente: " + cliente.nome);
}
}
JSON é seguro porque é apenas texto — ele não carrega instrução de qual classe instanciar, e o mapeador JSON não executa métodos arbitrários. Você controla explicitamente para qual classe os dados serão mapeados.
3. Assinatura Criptográfica de Dados Serializados
Se você precisa usar serialização nativa, assine os dados com HMAC. Desta forma, qualquer manipulação (inclusive injeção de gadget chains) invalidará a assinatura:
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class SignedSerialization {
private static final String HMAC_KEY = "minha-chave-secreta-super-segura";
// Serializar com assinatura
public static String serializeWithSignature(Object obj) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
}
byte[] serialized = baos.toByteArray();
String signature = calculateHMAC(serialized);
// Formato: signature||dados_base64
return signature + "||" + Base64.getEncoder().encodeToString(serialized);
}
// Desserializar apenas se a assinatura for válida
public static Object deserializeWithSignature(String data) throws Exception {
String[] parts = data.split("\\|\\|");
if (parts.length != 2) {
throw new SecurityException("Formato inválido");
}
String signature = parts[0];
byte[] serialized = Base64.getDecoder().decode(parts[1]);
String expectedSignature = calculateHMAC(serialized);
if (!signature.equals(expectedSignature)) {
throw new SecurityException("Assinatura inválida! Dados podem ter sido manipulados.");
}
try (ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}
private static String calculateHMAC(byte[] data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec key = new SecretKeySpec(HMAC_KEY.getBytes(), "HmacSHA256");
mac.init(key);
byte[] hmac = mac.doFinal(data);
return Base64.getEncoder().encodeToString(hmac);
}
}
Nesta abordagem, o servidor calcula um HMAC (Hash-based Message Authentication Code) dos dados serializados usando uma chave secreta. Mesmo que um atacante injete uma gadget chain, ao desserializar, a assinatura não corresponderá e os dados serão rejeitados.
4. Isolamento e Sandboxing
Em ambientes onde desserialização é absolutamente necessária, execute-a em um processo isolado com permissões mínimas:
import java.io.*;
import java.util.concurrent.*;
public class SandboxedDeserialization {
private static final ExecutorService executor = Executors.newSingleThreadExecutor();
// Desserializar em thread separada com timeout
public static Object deserializeWithTimeout(byte[] data, long timeoutSeconds)
throws Exception {
Future<Object> future = executor.submit(() -> {
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
});
try {
return future.get(timeoutSeconds, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
throw new SecurityException("Desserialização demorou demais. Possível ataque?");
}
}
}
Este código executa a desserialização em uma thread separada com um timeout. Se a operação demorar muito (por exemplo, enquanto executa código malicioso), será cancelada. Também é recomendado usar containers Docker ou máquinas virtuais para isolar completamente o processo.
Conclusão
Insecure deserialization é uma vulnerabilidade fundamentalmente diferente de outras porque explora a confiança implícita em dados estruturados. Primeiro aprendemos que gadget chains não são código novo injetado, mas sim orquestração inteligente de código legítimo já presente no projeto. Esta é a razão pela qual a vulnerabilidade é tão silenciosa — não há código suspeito no seu repositório.
Segundo, o ysoserial democratizou o ataque ao automatizar a construção de gadget chains, tornando a exploração de desserialização insegura acessível a atacantes sem profundo conhecimento técnico. Entender que ferramentas como esta existem nos motiva a adotar defesas proativas.
Por fim, a defesa de verdade é não usar serialização nativa para dados não confiáveis. Se você migrar para JSON ou XML, implementar whitelist de classes, ou assinar dados criptograficamente, torna a exploração praticamente impossível. Não é sobre patchear a desserialização — é sobre evitá-la quando possível ou controlá-la rigorosamente quando necessária.