Skip to main content

Serialização e desserialização em Java: entenda a vulnerabilidade de desserialização

Escrito por

18 de dezembro de 2020

0 minutos de leitura

Nota da edição — 11 de julho de 2022

Este artigo foi atualizado para refletir as mudanças nas versões mais recentes do Java e as vulnerabilidades exploráveis por meio da desserialização. Também incluímos uma palestra recente em que exploits de desserialização em Java foram demonstrados ao vivo.


A serialização em Java é um mecanismo que transforma um objeto em um fluxo de bytes. Já a desserialização em Java permite recriar um objeto a partir de um fluxo de bytes. A serialização em Java — e, em particular, a desserialização — é conhecida como “o presente que não para de dar frutos”, pois ao longo dos anos causou diversos problemas e vulnerabilidades de segurança.

O que é serialização em Java?

Usamos Java para criar objetos. Esses objetos ficam armazenados na memória e são removidos pelo coletor de lixo quando deixam de ser usados. Para transferir um objeto e, por exemplo, armazená-lo em um disco ou enviá-lo pela rede, precisamos transformá-lo em um fluxo de bytes. Para isso, a classe do objeto precisa implementar a interface Serializable. Como vimos, a serialização permite converter o estado de um objeto em um fluxo de bytes. Esse fluxo de bytes não contém o código em si.

O que é desserialização em Java?

A desserialização é exatamente o oposto da serialização. Nela, você parte de um fluxo de bytes e recria o objeto que foi serializado, recuperando seu estado original. No entanto, é preciso ter a definição do objeto para recriá-lo corretamente.

Como funciona a serialização em Java?

A serialização em Java usa reflexão para coletar todos os dados necessários dos campos do objeto, inclusive os campos privados e finais. Se um campo contiver um objeto, ele também será serializado recursivamente. Mesmo que você tenha getters e setters, essas funções não são usadas na serialização de um objeto em Java.

Como funciona a desserialização em Java?

Ao converter um fluxo de bytes de volta em um objeto, a desserialização não usa o construtor. Ela simplesmente cria um objeto vazio e usa reflexão para gravar os dados nos campos. Assim como na serialização, os campos privados e finais também são incluídos.

O que é uma vulnerabilidade de desserialização em Java?

Uma vulnerabilidade de desserialização em Java é uma falha de segurança que ocorre quando um usuário mal-intencionado tenta inserir no sistema um objeto serializado modificado para comprometer o sistema ou seus dados. Pense em uma vulnerabilidade de execução arbitrária de código que pode ser acionada durante a desserialização de um objeto serializado. Para explicar melhor as vulnerabilidades de desserialização em Java, primeiro precisamos entender como a desserialização funciona nessa linguagem.

Entenda as vulnerabilidades de desserialização em Java

Um objeto serializado em Java é um array de bytes que contém informações de estado. Ele inclui o nome do objeto ao qual se refere e os dados do campo. Se você abrir um objeto serializado armazenado em um editor hexadecimal, poderá visualizar e manipular essas informações facilmente.

Como já sabemos, a desserialização em Java não usa o construtor para criar um objeto — em vez disso, carrega os campos por reflexão. Isso significa que as verificações de validação feitas no construtor nunca são executadas durante a recriação do objeto. Pense, por exemplo, em validar se a data de início vem antes da data de término ao descrever um período. Durante a desserialização de um objeto Java, o novo objeto pode ficar em um estado inválido.

Veja o exemplo a seguir de uma vulnerabilidade de desserialização em Java, em que serializamos um objeto de uma classe serializável chamada ValueObject:

public class ValueObject implements Serializable {

   private String value;
   private String sideEffect;

   public ValueObject() {
       this("empty");
   }

   public ValueObject(String value) {
       this.value = value;
       this.sideEffect = java.time.LocalTime.now().toString();
   }
}

ValueObject vo1 = new ValueObject("Hi");
FileOutputStream fileOut = new FileOutputStream("ValueObject.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(vo1);
out.close();
fileOut.close();

"seriAo abrir em um editor hexadecimal o arquivo que contém o objeto serializado, ValueObject.ser, vemos o seguinte:

Janela de terminal exibindo um despejo hexadecimal em verde com dados de objeto Java serializado e deslocamentos de arquivo

Agora, podemos manipular facilmente o valor da string. Por exemplo, vamos trocar Hi por Hallo na saída do editor hexadecimal.

Janela do terminal exibindo um dump hexadecimal verde com dados de serialização Java e bytes destacados que formam “Hallo”.
FileInputStream fileIn = new FileInputStream("ValueObject2.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
ValueObject vo2 = (ValueObject) in.readObject();

Ao desserializar o arquivo binário alterado, descobrimos que o value do objeto mudou. Também vemos que o timestamp não mudou, o que prova que o construtor nunca foi chamado. Se uma aplicação aceita objetos serializados, é relativamente fácil adulterar seus valores. Ao alterar os objetos serializados, podemos criar objetos inválidos, comprometer a integridade dos dados ou algo ainda pior.

Execução arbitrária de código, gadgets e cadeias

Adulterar os dados de um objeto já é prejudicial. No entanto, isso também pode levar à execução de código se o conjunto certo de objetos for desserializado. Antes de chegar a esse ponto, vamos entender os gadgets e as cadeias.

Gadgets

Um gadget — no sentido usado por Lawrence e Frohoff na palestra Marschalling Pickle, na AppSecCali 2015 — é uma classe ou função que já tem código executável disponível no processo vulnerável. Esse código existente pode ser reutilizado para fins maliciosos. Em objetos serializáveis do Java, alguns métodos especiais — como o método privado readObject() — são chamados por reflexão durante a desserialização.

Veja o gadget simplificado abaixo:

public class Gadget implements Serializable {

   private Runnable command;

   public Gadget(Command command) {
       this.command = command;
   }

   private final void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
       in.defaultReadObject();
       command.run();
   }
}

Essa classe gadget sobrescreve o método padrão readObject. Como resultado, sempre que um objeto da classe Gadget é desserializado, o comando do objeto Runnable é executado. Quando uma classe de comando se parece com o exemplo abaixo, é fácil manipular esse objeto serializado e injetar código.

public class Command implements Runnable, Serializable {

   private String command;

   public Command(String command) {
       this.command = command;
   }

   @Override
   public void run() {
       try {
           Runtime.getRuntime().exec(command);
       } catch (IOException e) {
           throw new RuntimeException(e);
       }
   }

Observe também que, se uma aplicação aceitar objetos serializados, eles serão desserializados antes de serem convertidos para o tipo desejado. Isso significa que, mesmo se a conversão falhar, a desserialização já terá sido concluída e o método readObject() será executado.

FileInputStream fileIn = new FileInputStream("Gadget.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
var obj = (ValueObject)in.readObject();

Ataques de desserialização com cadeias de gadgets

Um ataque de desserialização típico usa uma cadeia de gadgets criada com habilidade. O invasor procura um gadget para iniciar o ataque e encadeia várias execuções que culminam na execução arbitrária de código. Por exemplo:

Gadget -> readObject() -> command.run() -> Runtime.getRuntime().exec()

Para um exemplo mais próximo da realidade, veja a implementação de java.util.HashMap. Essa classe tem uma implementação personalizada do método readObject(), que aciona a função hashcode() de cada chave.

Bibliotecas

É bom saber que as cadeias de gadgets disponíveis na sua aplicação não dependem do seu código. Como importamos muito código de bibliotecas e frameworks, a quantidade de classes trazidas pelas suas dependências (transitivas) influencia a possibilidade de certas cadeias de gadgets. Embora criar uma cadeia de gadgets maliciosa seja difícil e trabalhoso, as vulnerabilidades de desserialização em Java são um risco real e perigoso para a segurança.

Um ótimo exemplo disso foi a vulnerabilidade Log4Shell, de dezembro de 2021. Essencialmente, ela era uma vulnerabilidade de execução remota de código que poderia ter sua origem no framework nativo de serialização do Java. Em versões mais antigas do Java, confiávamos no código remoto por padrão e não precisávamos ter a classe no classpath. Nas versões mais novas do Java, até mesmo nas mais recentes, a vulnerabilidade Log4Shell pode explorar uma cadeia de gadgets disponível no seu classpath, podendo levar à execução remota de código ou algo ainda pior.

Como evitar uma vulnerabilidade de desserialização em Java?

A melhor maneira de evitar uma vulnerabilidade de desserialização em Java é não usar a serialização em Java. Se a sua aplicação não aceitar objetos serializados, eles não poderão causar problemas.

No entanto, se você precisar implementar a interface `serializable` por causa de herança, poderá sobrescrever o método readObject(), como mostrado abaixo, para impedir a desserialização efetiva.

private final void readObject(ObjectInputStream in) throws java.io.IOException {
   throw new java.io.IOException("Deserialized not allowed");
}

Se a sua aplicação depende de objetos serializados, você pode inspecionar o ObjectInputStream antes de desserializá-lo. A biblioteca Apache Commons IO pode ajudar. Ela oferece um ValidatedObjectInputStream que permite especificar explicitamente quais objetos podem ser desserializados. Assim, você impede a desserialização de tipos inesperados.

FileInputStream fileIn = new FileInputStream("Gadget.ser");
ValidatingObjectInputStream in = new ValidatingObjectInputStream(fileIn);
in.accept(ValueObject.class);
var obj = (ValueObject)in.readObject();

Outra opção é usar os filtros de serialização nativos do Java. Eles foram implementados na JEP 290 e lançados no Java 9, em março de 2018. Você pode definir filtros específicos para bloquear ou permitir classes. Com base em um padrão, é possível criar um filtro que limite as classes que podem ser desserializadas.

ObjectInputStream in = new ObjectInputStream(fileIn);
ObjectInputFilter filesOnlyFilter = ObjectInputFilter.Config.createFilter("io.snyk.package.Object;!*");
in.setObjectInputFilter(filesOnlyFilter);

Com o lançamento da JEP 415 no Java 17, em setembro de 2021, esse sistema de filtros foi aprimorado para levar em conta o contexto. Isso significa que você pode controlar o comportamento de um filtro de acordo com o seu contexto.

No artigo Novos recursos do Java 17 para melhorar a segurança e a serialização, exploramos esses recursos mais recentes em detalhes e explicamos os filtros do Java, como implementá-los e por que os filtros específicos para cada contexto são tão importantes.

Uma ferramenta como ysoserial também é extremamente útil para encontrar vulnerabilidades de desserialização em Java. Ela gera payloads para descobrir cadeias de gadgets em bibliotecas Java comuns que, nas condições certas, podem explorar aplicações Java que desserializam objetos de forma insegura.

Vale observar que as vulnerabilidades de desserialização em Java não se limitam à implementação de serialização personalizada do Java. Embora ela seja o foco deste artigo, as mesmas vulnerabilidades existem em frameworks de serialização ou marshaling que cuidam disso para você. Se um framework cria automaticamente POJOs a partir de XML, JSON, YAML ou formatos semelhantes, provavelmente usa reflexão da mesma maneira descrita acima. Ou seja, os mesmos problemas continuam existindo.

Para evitar esse tipo de vulnerabilidade de desserialização em Java nas suas bibliotecas externas, analise-as gratuitamente com Snyk Open Source desde o início e com frequência. Para saber mais sobre esse tipo de vulnerabilidade, confira também a lição desserialização insegura do Snyk Learn.

Palestra: “Exploits de desserialização em Java: por que devo me preocupar?”

Recentemente, apresentei uma palestra sobre exploits de desserialização na Devoxx UK, a maior e mais prestigiada conferência da comunidade Java no Reino Unido.

Nessa palestra, explico como funcionam as vulnerabilidades de desserialização nativas do Java e como são criadas as cadeias de ataque. Hackers se referem à desserialização em Java como “o presente que não para de dar frutos”. Por isso, criei uma palestra com demonstração ao vivo para mostrar o possível impacto na segurança.

É claro que os problemas de segurança recentes relacionados à vulnerabilidade Log4Shell também fazem parte da palestra. Expliquei como o Log4Shell pode ser o ponto de partida de uma cadeia de gadgets de desserialização, na qual o gadget final executa código arbitrário.

Outros tipos de desserialização, como JSON, XML e YAML, também podem causar problemas. Para saber mais, confira o artigo Problemas de desserialização de JSON em Java com o Jackson ObjectMapper. Nesta palestra, vamos nos aprofundar um pouco mais e demonstrar as consequências na prática.

Deserialization exploits in Java: why should I care? by Brian Vermeer

Comece a jogar Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo à gravação sob demanda do nosso workshop virtual introdutório.

Leia mais

Blog

Modelos de ponta encontraram as vulnerabilidades. Só o atacante encontrou as cadeias.

A análise estática encontrou as falhas, mas só os testes de ataque em aplicações ativas provaram como elas poderiam ser encadeadas para causar invasões. Uma comparação entre Evo COS, Claude Security e Claude Code Security.

feature insights context
Blog

Os ataques autônomos já chegaram. A defesa precisa acompanhar o ritmo.

Os atacantes autônomos estão reduzindo o tempo disponível para a defesa. Saiba como a descoberta, a correção, a validação e a prevenção contínuas ajudam as equipes de segurança a acompanhar esse ritmo.

Blog

Por que agentes de programação com IA continuam criando falhas de controle de acesso

Agentes de programação com IA podem gerar uma lógica de autorização que compila e passa pela revisão, mas permite que um tenant acesse os dados de outro. Saiba por que é difícil detectar falhas de controle de acesso e como evitá-las.