
PoC für die Spring-Kafka-Deserialisierungsschwachstelle CVE-2023-34040
Starten Sie zuerst die Dockerisierte Kafka-Instanz. Dadurch wird Kafka gestartet und auf Port 29092 verfügbar gemacht.
docker-compose up
Starten Sie die Consumer-Anwendung.
cd spring-kafka-consumer
mvn clean install
mvn spring-boot:run
Dadurch wird die Consumer-Anwendung erstellt und gestartet. Der Consumer wartet maximal 10 Minuten auf eine Nachricht, bevor er heruntergefahren wird.
Producer
cd spring-kafka-producer
mvn clean install
mvn spring-boot:run
Dadurch wird die Producer-Anwendung erstellt und gestartet. Der Producer sendet eine Nachricht an die Kafka-Warteschlange und fährt dann herunter.
Damit diese Schwachstelle erfolgreich ausgenutzt werden kann, muss eines oder beide der folgenden Flags im Consumer aktiviert sein.
CheckDeserExWhenValueNull
CheckDeserExWhenKeyNull
Dies erfolgt in der Consumer-Anwendung unter KafkaConsumerConfig.greetingKafkaListenerContainerFactory().
Es gibt zwei Payloads, die ausgelöst werden können. Standardmäßig wird der RCE-Payload verwendet.
Um den DOS-Payload zu aktivieren, ändern Sie die Methode KafkaApplication.sendGreetingMessage()
Ändern Sie den Payload, der als Header hinzugefügt wird, auf dosPayload und erstellen und starten Sie den Producer neu.
HINWEIS: Da der DOS auftritt, während die Nachricht gelesen wird, und das Lesen nie abgeschlossen wird, bleibt die Nachricht in der Warteschlange, bis sie entweder manuell gelöscht wird oder die Nachrichtenaufbewahrungszeit abläuft. Dies erhöht die Wirkung des DOS, da die Warteschlange unbrauchbar wird, bis entweder ein manueller Eingriff erfolgt oder die Aufbewahrungszeit abläuft.
Möglicherweise führt dies zu Datenverlust für Nachrichten, die unmittelbar nach der DOS-Nachricht gesendet werden.
Um den DOS-Payload zu aktivieren, ändern Sie die Methode KafkaApplication.sendGreetingMessage()
Ändern Sie den Payload, der als Header hinzugefügt wird, auf rcePayload und erstellen und starten Sie den Producer neu. Standardmäßig wird der Befehl touch /tmp/newfile ausgeführt. Um zu prüfen, ob der Angriff erfolgreich war, suchen Sie unter /tmp nach einer Datei namens newfile.
Wenn Sie unter Windows arbeiten, können Sie die Befehlszeichenfolge in etwas Passenderes ändern.
Dieses Gadget ist wirklich nur ein POC. Damit es in der realen Welt zu einer RCE kommt, müsste eine Gadget-Klasse im Klassenpfad des Consumers verfügbar sein.
Der Denial of Service erfordert keine spezifische Gadget-Klasse im Klassenpfad des Consumers. Er beruht darauf, eine modifizierte Version der Klasse org.springframework.kafka.support.serializer.DeserializationException zu erzeugen, die ein Object enthält.
Dadurch ist es einfach, dem serialisierten Objekt einen beliebigen Payload hinzuzufügen.
Diese modifizierte Klasse heißt xrg.springframework.kafka.support.serializer.DeserializationException (Beachten Sie das x am Anfang des Paketnamens).
Sobald der Payload injiziert ist, in diesem Fall ein Billion-Laughs-Angriff unter Verwendung von java.util.Set und java.lang.Object, wird er serialisiert.
Anschließend werden die Binärdaten modifiziert, um das x in ein o zu ändern, passend zu dem, was der Consumer erwartet.
Diese serialisierte Ausnahmeklasse wird dann als Nachrichten-Header sowohl im springDeserializerExceptionValue-Header als auch im springDeserializerExceptionKey-Header hinzugefügt.
Diese werden vom Consumer gelesen, wenn entweder der Schlüssel oder die Nachricht null ist. Danach muss nur noch sichergestellt werden, dass der Schlüssel oder die Nachricht null ist, und der Consumer wird sie lesen.
Es gibt einen gewissen Deserialisierungsschutz innerhalb von Spring-Kafka. In 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;
}
}
Sie können sehen, dass eine Prüfung durchgeführt wird, um sicherzustellen, dass die oberste Klasse org.springframework.kafka.support.serializer.DeserializationException ist. Beachten Sie jedoch, dass nur die oberste Klasse geprüft wird, und zwar nur der Klassenname (der innerhalb der Möglichkeiten des Angreifers liegt, ihn zu ändern). Jeder Payload unterhalb dieser Ebene wird also deserialisiert.