
POC pour la vulnérabilité de désérialisation Spring Kafka CVE-2023-34040
Commencez par démarrer l'instance Kafka dockerisée. Cela démarrera Kafka et le rendra disponible sur le port 29092.
docker-compose up
Démarrez l'application consommateur.
cd spring-kafka-consumer
mvn clean install
mvn spring-boot:run
Cela compilera et démarrera l'application consommateur. Le consommateur attendra un message pendant 10 minutes maximum avant de s'arrêter.
Producteur
cd spring-kafka-producer
mvn clean install
mvn spring-boot:run
Cela compilera et démarrera l'application producteur. Le producteur enverra un message dans la file Kafka puis s'arrêtera.
Pour que cette vulnérabilité aboutisse, l'un ou les deux paramètres suivants doivent être activés sur le consommateur.
CheckDeserExWhenValueNull
CheckDeserExWhenKeyNull
Cela se fait dans l'application consommateur sous KafkaConsumerConfig.greetingKafkaListenerContainerFactory()
Il existe deux charges utiles qui peuvent être déclenchées ; par défaut, la charge utile RCE est utilisée.
Pour activer la charge utile DOS, modifiez la méthode KafkaApplication.sendGreetingMessage()
Remplacez la charge utile ajoutée comme en-tête par dosPayload, puis reconstruisez et exécutez le producteur.
NOTE : Comme le déni de service se produit pendant la lecture du message, et que la lecture ne se termine jamais, le message reste dans la file d'attente jusqu'à ce qu'il soit supprimé manuellement ou que le délai de rétention des messages expire. Cela augmente la puissance du déni de service, car cela rend la file d'attente inutilisable jusqu'à ce qu'une intervention manuelle ait lieu ou que le délai de rétention expire.
Cela peut entraîner une perte de données pour les messages envoyés immédiatement après le message DOS.
Pour activer la charge utile RCE, modifiez la méthode KafkaApplication.sendGreetingMessage()
Remplacez la charge utile ajoutée comme en-tête par rcePayload, puis reconstruisez et exécutez le producteur. Par défaut, la commande exécutée est
touch /tmp/newfile ; pour vérifier si l'attaque a réussi, cherchez un fichier nommé newfile dans /tmp.
Si vous êtes sous Windows, vous pouvez modifier la chaîne de commande pour quelque chose de plus approprié.
Ce Gadget n'est véritablement qu'un POC. Pour qu'une RCE se produise dans le monde réel, il faudrait qu'une classe gadget soit disponible dans le classpath du consommateur.
Le déni de service ne nécessite aucune classe gadget spécifique dans le classpath du consommateur. Il repose sur la génération d'une version modifiée de la classe
org.springframework.kafka.support.serializer.DeserializationException qui contient un Object.
Cela permet ensuite d'ajouter facilement la charge utile de notre choix dans l'objet sérialisé.
Cette classe modifiée est appelée xrg.springframework.kafka.support.serializer.DeserializationException ( notez le x au début du nom du package).
Une fois la charge utile injectée — dans ce cas, une attaque de type billion laughs utilisant java.util.Set et java.lang.Object — elle est sérialisée.
Ensuite, les données binaires sont modifiées pour remplacer le x par un o, correspondant à ce que le consommateur attend.
Cette classe d'exception sérialisée est ensuite ajoutée comme en-tête de message dans les en-têtes springDeserializerExceptionValue et springDeserializerExceptionKey.
Ces en-têtes sont ensuite lus par le consommateur si la clé ou le message est nul. Après cela, il suffit de s'assurer que la clé ou le message est nul et le consommateur le lira.
Il existe une certaine protection contre la désérialisation dans Spring-Kafka. Dans 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;
}
}
Vous pouvez voir qu'une vérification est effectuée pour s'assurer que la classe de premier niveau est org.springframework.kafka.support.serializer.DeserializationException. Mais notez que seule la classe de premier niveau est vérifiée, et uniquement son nom (que l'attaquant peut modifier). Ainsi, toute charge utile située en dessous de ce niveau est désérialisée.