
Spring Kafka のデシリアライゼーション脆弱性 CVE-2023-34040 の POC
まず、Docker化されたKafkaインスタンスを起動します。これによりKafkaが起動し、ポート29092で利用可能になります。
docker-compose up
Consumerアプリケーションを起動します。
cd spring-kafka-consumer
mvn clean install
mvn spring-boot:run
これにより、Consumerアプリケーションがビルドされ、起動します。Consumerはシャットダウンする前に、メッセージを最大10分間待機します。
Producer
cd spring-kafka-producer
mvn clean install
mvn spring-boot:run
これにより、Producerアプリケーションがビルドされ、起動します。ProducerはKafkaキューにメッセージを送信してからシャットダウンします。
この脆弱性を成立させるには、Consumerで以下のフラグのいずれかまたは両方が有効になっている必要があります。
CheckDeserExWhenValueNull
CheckDeserExWhenKeyNull
これはConsumerアプリケーションの KafkaConsumerConfig.greetingKafkaListenerContainerFactory() 内で行われます。
トリガーできるペイロードは2つあります。デフォルトではRCEペイロードが使用されます。
DOSペイロードを有効にするには、メソッド KafkaApplication.sendGreetingMessage() を変更します。
ヘッダーとして追加されるペイロードを dosPayload に変更し、Producerを再ビルドして実行してください。
注:DOSはメッセージが読み取られる際に発生し、読み取りが完了しないため、メッセージは手動で削除されるか、メッセージの
保持期間が切れるまでキューに残ります。これにより、手動介入が行われるか保持期間が切れるまでそのキューが使用不能になるため、DOSの影響力が高まります。
DOSメッセージの直後に送信されたメッセージが失われる可能性もあります。
DOSペイロードを有効にするには、メソッド KafkaApplication.sendGreetingMessage() を変更します。
ヘッダーとして追加されるペイロードを rcePayload に変更し、Producerを再ビルドして実行してください。デフォルトで実行されるコマンドは
touch /tmp/newfile です。攻撃が成功したかどうかを確認するには、/tmp の下に newfile という名前のファイルを探してください。
Windowsで実行する場合は、コマンド文字列をより適切なものに変更できます。
このGadgetはまさにPOCにすぎません。実際の環境でRCEが発生するには、Consumerのクラスパス上にガジェットクラスが存在する必要があります。
サービス拒否は、Consumerのクラスパス上に特定のガジェットクラスが存在する必要はありません。これは、org.springframework.kafka.support.serializer.DeserializationException クラスの変更版を生成することに依存しています。この変更版にはオブジェクトが含まれます。
これにより、シリアライズされたオブジェクトに任意のペイロードを簡単に追加できるようになります。
この変更されたクラスは xrg.springframework.kafka.support.serializer.DeserializationException と呼ばれます(パッケージ名の先頭の x に注意してください)。
ペイロードが注入されると、この場合は java.util.Set と java.lang.Object を使ったbillion laughs方式の攻撃がシリアライズされます。
その後、バイナリデータが変更され、Consumerが期待するものに一致するようxがoに変更されます。
このシリアライズされた例外クラスは、メッセージヘッダーとして springDeserializerExceptionValue ヘッダーと springDeserializerExceptionKey ヘッダーの両方に追加されます。
これらは、キーまたはメッセージがnullの場合にConsumerによって読み取られます。その後、キーまたはメッセージがnullになっていることを確認するだけで、Consumerがそれを読み取ります。
Spring-Kafkaにはデシリアライゼーション保護があります。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;
}
}
トップレベルのクラスが org.springframework.kafka.support.serializer.DeserializationException であることを確認するチェックが行われていることがわかります。ただし、チェックされるのはトップレベルのクラスだけで、しかもクラス名のみです(これは攻撃者が変更できる範囲です)。そのため、そのレベルの下にあるペイロードはすべてデシリアライズされます。