
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()에서 수행됩니다.
트리거할 수 있는 페이로드는 두 가지이며, 기본적으로 RCE 페이로드가 사용됩니다.
DoS 페이로드를 활성화하려면 KafkaApplication.sendGreetingMessage() 메서드를 수정하세요.
헤더로 추가되는 페이로드를 dosPayload로 변경하고 producer를 다시 빌드하고 실행하세요.
참고: DoS는 메시지를 읽을 때 발생하고 읽기가 완료되지 않으므로 메시지는 수동으로 삭제하거나 메시지 보존 기간이 만료될 때까지 큐에 남아 있습니다. 이는 수동 개입이 있거나 보존 기간이 만료될 때까지 해당 큐를 사용할 수 없게 만들어 DoS의 위력을 증가시킵니다. DoS 메시지 직후에 전송된 메시지의 데이터 손실로 이어질 수 있습니다.
DoS 페이로드를 활성화하려면 KafkaApplication.sendGreetingMessage() 메서드를 수정하세요.
헤더로 추가되는 페이로드를 rcePayload로 변경하고 producer를 다시 빌드하고 실행하세요. 기본적으로 실행되는 명령은 touch /tmp/newfile이며, 공격이 성공했는지 확인하려면 /tmp에서 newfile이라는 파일을 찾아보세요.
Windows에서 실행하는 경우 명령 문자열을 더 적합한 것으로 수정할 수 있습니다.
이 가젯은 매우 단순한 PoC에 불과합니다. 실제 환경에서 RCE가 발생하려면 Consumer의 클래스 경로에 가젯 클래스가 있어야 합니다.
서비스 거부(DoS)는 Consumer 클래스 경로에 특정 가젯 클래스가 필요하지 않습니다. 이는 객체를 포함하는 org.springframework.kafka.support.serializer.DeserializationException 클래스의 수정된 버전을 생성하는 방식에 의존합니다.
이렇게 하면 직렬화된 객체에 원하는 페이로드를 쉽게 추가할 수 있습니다.
이 수정된 클래스는 xrg.springframework.kafka.support.serializer.DeserializationException이라고 합니다(패키지 이름 시작 부분의 x에 주목).
페이로드가 주입되면, 이 경우 java.util.Set과 java.lang.Object를 사용하는 billion laughs 방식 공격, 직렬화됩니다.
그런 다음 이진 데이터를 수정하여 x를 consumer가 기대하는 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인지 확인하는 검사가 수행되는 것을 볼 수 있습니다. 그러나 최상위 클래스만 검사되며, 클래스 이름만 검사됩니다(공격자가 수정할 수 있는 범위 내에 있음). 따라서 그 아래 수준의 모든 페이로드는 역직렬화됩니다.