
POC per la vulnerabilità di deserializzazione di Spring Kafka CVE-2023-34040
Prima avvia l'istanza Kafka in Docker. Questo avvierà Kafka e lo renderà disponibile sulla porta 29092.
docker-compose up
Avvia l'applicazione Consumer.
cd spring-kafka-consumer
mvn clean install
mvn spring-boot:run
Questo compilerà e avvierà l'applicazione Consumer. Il Consumer attenderà al massimo 10 minuti per un messaggio prima di spegnersi.
Producer
cd spring-kafka-producer
mvn clean install
mvn spring-boot:run
Questo compilerà e avvierà l'applicazione Producer. Il Producer invierà un messaggio alla coda Kafka e poi si spegnerà.
Affinché questa vulnerabilità abbia successo, è necessario abilitare uno o entrambi i seguenti flag sul consumer. Questo viene fatto nell'applicazione Consumer sotto
CheckDeserExWhenValueNullCheckDeserExWhenKeyNullKafkaConsumerConfig.greetingKafkaListenerContainerFactory()Ci sono due payload che possono essere attivati. Per impostazione predefinita viene utilizzato il payload RCE.
Per abilitare il payload DOS, modifica il metodo KafkaApplication.sendGreetingMessage()
Cambia il payload aggiunto come header in dosPayload e ricompila ed esegui il producer.
NOTA: Poiché il DOS si verifica mentre il messaggio viene letto, e la lettura non termina mai, il messaggio rimane nella coda fino a quando non viene eliminato manualmente o
scade il tempo di retention del messaggio. Ciò aumenta la potenza del DOS poiché rende quella coda inutilizzabile fino a quando non si verifica un intervento manuale, o
scade il tempo di retention.
Potenzialmente porta alla perdita di dati per i messaggi inviati immediatamente dopo il messaggio DOS.
Per abilitare il payload DOS, modifica il metodo KafkaApplication.sendGreetingMessage()
Cambia il payload aggiunto come header in rcePayload e ricompila ed esegui il producer. Per impostazione predefinita, il comando eseguito è
touch /tmp/newfile. Per verificare se l'attacco ha avuto successo, cerca un file chiamato newfile in /tmp.
Se sei su Windows, puoi modificare la stringa del comando in qualcosa di più adatto.
Questo gadget è solo un POC. Perché si verifichi un RCE nel mondo reale, sarebbe necessario che una classe gadget fosse disponibile nel class path del Consumer.
Il Denial of Service non richiede che sia disponibile alcuna classe gadget specifica nel class path del Consumer. Si basa sulla generazione di una versione modificata della classe
org.springframework.kafka.support.serializer.DeserializationException che contiene un Object.
Ciò rende facile aggiungere qualsiasi payload desideriamo nell'oggetto serializzato.
Questa classe modificata è chiamata xrg.springframework.kafka.support.serializer.DeserializationException (nota la x all'inizio del nome del pacchetto).
Una volta che il payload è stato iniettato, in questo caso un attacco stile billion laughs usando java.util.Set e java.lang.Object, viene serializzato.
Successivamente i dati binari vengono modificati per cambiare la x in o, corrispondendo a ciò che il consumatore si aspetta.
Questa classe di eccezione serializzata viene quindi aggiunta come header del messaggio sia nell'header springDeserializerExceptionValue sia nell'header springDeserializerExceptionKey.
Questi vengono poi letti dal consumatore se la chiave o il messaggio sono null. Dopodiché basta assicurarsi che la chiave o il messaggio siano null e il consumatore lo leggerà.
C'è una qualche protezione dalla deserializzazione all'interno di 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;
}
}
Puoi vedere che viene effettuato un controllo per garantire che la classe di livello superiore sia org.springframework.kafka.support.serializer.DeserializationException. Ma nota che viene controllata solo la classe di livello superiore, e solo il nome della classe (che è modificabile dall'attaccante). Quindi qualsiasi payload al di sotto di quel livello viene deserializzato.