
PoC para la vulnerabilidad de deserialización en Spring Kafka CVE-2023-34040
Primero inicia la instancia Kafka dockerizada. Esto arrancará Kafka y lo dejará disponible en el puerto 29092.
docker-compose up
Inicia la aplicación consumidora.
cd spring-kafka-consumer
mvn clean install
mvn spring-boot:run
Esto compilará e iniciará la aplicación consumidora. El consumidor esperará un máximo de 10 minutos para recibir un mensaje antes de apagarse.
Productor
cd spring-kafka-producer
mvn clean install
mvn spring-boot:run
Esto compilará e iniciará la aplicación productora. El productor enviará un mensaje a la cola de Kafka y luego se apagará.
Para que esta vulnerabilidad tenga éxito, se deben habilitar en el consumidor uno o ambos de los siguientes indicadores:
CheckDeserExWhenValueNull
CheckDeserExWhenKeyNull
Esto se hace en la aplicación consumidora en KafkaConsumerConfig.greetingKafkaListenerContainerFactory()
Hay dos payloads que se pueden activar; por defecto, se usa el payload de RCE.
Para habilitar el payload de DoS, modifica el método KafkaApplication.sendGreetingMessage().
Cambia el payload que se añade como cabecera a dosPayload y reconstruye y ejecuta el productor.
NOTA: como el DoS ocurre cuando se lee el mensaje, y la lectura nunca termina, el mensaje permanece en la cola hasta que se elimine manualmente o hasta que
expire el tiempo de retención del mensaje. Esto aumenta la potencia del DoS, ya que inutiliza esa cola hasta que se produzca una intervención manual o expire el tiempo de retención.
Potencialmente, puede provocar pérdida de datos para los mensajes enviados inmediatamente después del mensaje DoS.
Para habilitar el payload de DoS, modifica el método KafkaApplication.sendGreetingMessage().
Cambia el payload que se añade como cabecera a rcePayload y reconstruye y ejecuta el productor. Por defecto, el comando que se ejecuta es
touch /tmp/newfile; para ver si el ataque tuvo éxito, busca un archivo llamado newfile en /tmp.
Si ejecutas en Windows, puedes modificar la cadena de comando por una más adecuada.
Este gadget es solo una POC. Para que ocurra una RCE en el mundo real, debería haber una clase gadget disponible en el classpath del consumidor.
La denegación de servicio no requiere que haya ninguna clase gadget específica en el classpath del consumidor. Se basa en generar una versión modificada de la clase
org.springframework.kafka.support.serializer.DeserializationException que contiene un Object.
Esto facilita agregar después cualquier payload que queramos al objeto serializado.
Esta clase modificada se llama xrg.springframework.kafka.support.serializer.DeserializationException (observa la x al inicio del nombre del paquete).
Una vez inyectado el payload, en este caso un ataque estilo billion laughs usando java.util.Set y java.lang.Object, este se serializa.
luego, los datos binarios se modifican para cambiar la x por una o, coincidiendo con lo que espera el consumidor.
Esta clase de excepción serializada se añade como cabecera de mensaje tanto en la cabecera springDeserializerExceptionValue como en la cabecera springDeserializerExceptionKey.
El consumidor las lee si la clave o el mensaje es nulo. Después, solo asegúrate de que la clave o el mensaje sea nulo y el consumidor lo leerá.
Hay cierta protección de deserialización dentro de Spring-Kafka. En 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;
}
}