
POC para Vulnerabilidade de Desserialização do Spring Kafka CVE-2023-34040
Primeiro, inicie a instância Kafka Dockerizada. Isso iniciará o Kafka e o disponibilizará na porta 29092.
docker-compose up
Inicie a aplicação do Consumidor.
cd spring-kafka-consumer
mvn clean install
mvn spring-boot:run
Isso irá compilar e iniciar a aplicação do Consumidor. O Consumidor aguardará no máximo 10 minutos por uma mensagem antes de desligar.
Produtor
cd spring-kafka-producer
mvn clean install
mvn spring-boot:run
Isso irá compilar e iniciar a aplicação do Produtor. O Produtor enviará uma mensagem para a fila do Kafka e então desligará.
Para que essa vulnerabilidade tenha sucesso, uma ou ambas as seguintes flags precisam estar habilitadas no consumidor.
CheckDeserExWhenValueNull
CheckDeserExWhenKeyNull
Isso é feito na aplicação do Consumidor em KafkaConsumerConfig.greetingKafkaListenerContainerFactory()
Existem dois payloads que podem ser acionados; por padrão, o payload RCE é usado.
Para habilitar o payload DoS, modifique o método KafkaApplication.sendGreetingMessage()
Altere o payload que é adicionado como cabeçalho para dosPayload e recompile e execute o produtor.
NOTA: Como o DoS ocorre enquanto a mensagem é lida, e a leitura nunca termina, a mensagem permanece na fila até ser excluída manualmente ou o tempo de retenção da mensagem expirar. Isso aumenta a potência do DoS, pois torna essa fila inútil até que ocorra intervenção manual ou o tempo de retenção expire. Potencialmente levando à perda de dados para mensagens enviadas imediatamente após a mensagem DoS.
Para habilitar o payload RCE, modifique o método KafkaApplication.sendGreetingMessage()
Altere o payload que é adicionado como cabeçalho para rcePayload e recompile e execute o produtor. Por padrão, o comando executado é touch /tmp/newfile; para verificar se o ataque foi bem-sucedido, procure por um arquivo chamado newfile em /tmp.
Se estiver executando no Windows, você pode modificar a string de comando para algo mais adequado.
Este Gadget é apenas uma POC. Para que um RCE ocorra no mundo real, seria necessário que houvesse uma classe gadget disponível no classpath do Consumidor.
A Negação de Serviço não requer que haja nenhuma classe gadget específica disponível no classpath do Consumidor. Ela depende da geração de uma versão modificada da classe org.springframework.kafka.support.serializer.DeserializationException que contém um Object.
Isso facilita adicionar qualquer payload que desejarmos ao objeto serializado.
Essa classe modificada é chamada xrg.springframework.kafka.support.serializer.DeserializationException (Observe o x no início do nome do pacote).
Uma vez que o payload é injetado, neste caso um ataque estilo billion laughs usando java.util.Set e java.lang.Object, ele é serializado.
Então os dados binários são modificados para alterar o x para o, correspondendo ao que o consumidor espera.
Essa classe de exceção serializada é então adicionada como cabeçalho de mensagem tanto no cabeçalho springDeserializerExceptionValue quanto no cabeçalho springDeserializerExceptionKey.
Estes são então lidos pelo consumidor se a chave ou a mensagem for nula. Depois disso, basta garantir que a chave ou a mensagem seja nula e o consumidor a lerá.
Existe alguma proteção de desserialização dentro do Spring-Kafka. Em ListenerUtils.
public static DeserializationException byteArrayToDeserializationException(LogAccessor logger, byte[] value) {
try {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(value)) {
boolean first = true;
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (this.first) {
this.first = false;
Assert.state(desc.getName().equals(DeserializationException.class.getName()),
"Header does not contain a DeserializationException");
}
return super.resolveClass(desc);
}
};
return (DeserializationException) ois.readObject();
}
catch (IOException | ClassNotFoundException | ClassCastException e) {
logger.error(e, "Failed to deserialize a deserialization exception");
return null;
}
}
Você pode ver que uma verificação é feita para garantir que a classe de nível superior seja org.springframework.kafka.support.serializer.DeserializationException. Mas note que apenas a classe de nível superior é verificada, e apenas o nome da classe (que está ao alcance do invasor para modificar). Portanto, qualquer payload abaixo desse nível é desserializado.