AI-Powered Cyberattacks: How to Detect, Prevent & Defend Against Intelligent Threats

Read Now
We utilize artificial intelligence for site translations, and while we strive for accuracy, they may not always be 100% precise. Your understanding is appreciated.

Safeguarding Against Deserialization Attacks in Spring for Apache Kafka

by OPSWAT
Share this Post
A professional image of Nguyen Phu Hung and Tran Anh Huy, representing their affiliations with HUTECH and HCMC University of Science.

Deserialization attacks—the unauthorized manipulation of serialized data to execute malicious code—can result in severe damage to organizations. Developers and security teams must proactively identify key vulnerabilities that reveal how, when, and where the attack occurred.  

A recent example of this threat is CVE-2023-34040, a deserialization attack vector in Spring for Apache Kafka that can lead to RCE (remote code execution). In fact, the prevalence of deserialization vulnerabilities is highlighted by the OWASP Top 10 list, which includes "Deserialization of Untrusted Data" as one of the top 10 most critical web application security risks.  

Tools like OPSWAT MetaDefender Core™ with its SBOM (Software Bill of Materials) engine are essential in detecting and preventing deserialization attacks. These allow developers to efficiently scan and analyze their code, ensuring that no vulnerabilities are overlooked.  

In this blog, our Graduate Fellows will discuss the details of CVE-2023-34040 and its exploitation, and how to secure open-source components against similar threats. 

About CVE-2023-34040

CVE-2023-34040 reveals a deserialization attack vector in Spring for Apache Kafka, which can be exploited when an unusual configuration is applied. This vulnerability allows an attacker to construct a malicious serialized object in one of the deserialization exception record headers, which may result in RCE. The vulnerability affects Spring for Apache Kafka versions 2.8.1 to 2.9.10 and 3.0.0 to 3.0.9.

A screenshot displaying CVSS 3.x severity scores for vulnerabilities, with high and medium risk levels from NIST and VMware.

Specifically, an application/consumer is vulnerable under the following specific configurations and conditions: 

  • The ErrorHandlingDeserializer class is not configured for the key and/or value of the record. 
  • The checkDeserExWhenKeyNull and/or checkDeserExWhenValueNull properties of the consumer are set to true. 
  • Untrusted sources are permitted to publish to a Kafka topic.

Apache Kafka

Apache Kafka, developed by the Apache Software Foundation, is a distributed event streaming platform designed to capture, process, respond to, and route real-time data streams from various sources, including databases, sensors, and mobile devices. 

For example, it can stream notifications to services that react to customer activities, such as completing a product checkout or making a payment.

A diagram illustrating an Apache Kafka-based data processing pipeline, linking product microservices to SMS, push notification, and email microservices.

In Apache Kafka, an event - also referred to as a record or message - serves as a data unit that represents an occurrence in the application whenever data is read or written. Each event includes a key, value, timestamp, and optional metadata headers.

Key-binary
(can be null)
Value-binary
(can be null)
Compression Type
[none, gzip, snappy, lz4, zstd]
Headers (optional)
KeyValue
KeyValue
Partition + Offset
Timestamp (system or user set)

Events are stored durably and organized into topics. Client applications that send (write) events to Kafka topics are called producers, while those that subscribe to (read and process) events are known as consumers. 

A flowchart showing how user input is processed through a producer, recorded, consumed, and delivered to an endpoint.

Spring for Apache Kafka

To connect Apache Kafka with the Spring ecosystem, developers can use Spring for Apache Kafka, which simplifies integration in Java applications.  

Spring for Apache Kafka offers robust tools and APIs that simplify the process of sending and receiving events with Kafka, enabling developers to accomplish these tasks without extensive and complex coding.

A graphic displaying logos for Spring Boot and Apache Kafka, signifying integration for event-driven applications.

Serialization and Deserialization

Serialization is a mechanism of converting the state of an object into a string or a byte stream. In contrast, deserialization is the reverse process, where the serialized data is converted back into its original object or data structure. Serialization allows complex data to be transformed so it can be saved to a file, sent over a network, or stored in a database. Serialization and deserialization are essential for data exchange in distributed systems and promote communication among the various components of a software application. In Java, writeObject() is used for serialization and readObject() is used for deserialization.

A technical diagram showing how data objects are serialized into memory, files, or databases and later deserialized back into objects.

As deserialization permits the conversion of a byte stream or string into an object, improper handling or lack of proper validation of input data can result in a significant security vulnerability, potentially leading to an RCE attack.

Vulnerability Analysis

According to the CVE description, OPSWAT Fellows configured checkDeserExWhenKeyNull and checkDeserExWhenValueNull to true in order to trigger the security vulnerability. By sending a record with an empty key/value and conducting a detailed analysis by debugging the consumer as it received a Kafka record from the producer, our graduate fellows uncovered the following workflow during record processing: 

Step 1: Receiving Records (Messages)

Upon receiving records, the consumer invokes the invokeIfHaveRecords() method, which then calls the invokeListener() method to trigger a registered record listener (a class annotated with the @KafkaListener annotation) for the actual processing of the records. 

A screenshot of Java code defining a Kafka message listener container for handling records in an event-driven system.

The invokeListener() then invokes the invokeOnMessage() method.

Step 2: Checking Records

Within the invokeOnMessage() method, several conditions are evaluated against the record value and configuration properties, which subsequently determine the next step to be executed.

A highlighted Java code snippet demonstrating exception handling for message deserialization in a Kafka consumer.

If a record has a null key or value and the checkDeserExWhenKeyNull and/or checkDeserExWhenValueNull properties are explicitly set to true, the checkDeser() method will be called to examine the record.

Step 3: Checking Exception from Headers

In checkDesr(), the consumer continuously invokes getExceptionFromHeader() to retrieve any exceptions from the record's metadata, if present, and stores the result in a variable called exception.

Java method checkDeser calls getExceptionFromHeader to handle deserialization exceptions in Kafka message processing.

Step 4: Extracting Exception from Headers

The getExceptionFromHeader() method is designed to extract and return an exception from the header of a Kafka record. It first retrieves the record's header and then obtains the header's value, which is stored in a byte array.

Java method getExceptionFromHeader extracts deserialization exceptions from Kafka message headers

Subsequently, it forwards the byte array of the header’s value to the byteArrayToDeserializationException() method for further handling. 

Step 5: Deserializing Data

In the byteArrayToDeserializationException(), the resolveClass() function is overridden to restrict deserialization to only allowed classes. This approach prevents the deserialization of any class that is not explicitly permitted. The byte array value of the header can be deserialized within byteArrayToDeserializationException() only if it meets the condition set in resolveClass(), which allows deserialization exclusively for the DeserializationException class.

Java method byteArrayToDeserializationException converts a byte array into a deserialization exception

However, the DeserializationException class extends the standard Exception class and includes a constructor with four parameters. The last parameter, cause, represents the original exception that triggered the DeserializationException, such as an IOException or a ClassNotFoundException.

Java constructor for DeserializationException with parameters for message, data, key flag, and throwable cause

The Throwable class serves as the superclass for all objects that can be thrown as exceptions or errors in Java. In the Java programming language, exception handling classes like Throwable, Exception, and Error can be safely deserialized. When an exception is deserialized, Java permits the Throwable parent of classes to be loaded and instantiated with less demanding checks than those applied to regular classes. 

Based on the workflow and comprehensive analysis, if the serialized data corresponds to a malicious class that inherits from the parent class Throwable, it may bypass condition checks. This allows the deserialization of a malicious object, which can execute harmful code and potentially result in an RCE attack.

Exploitation

Diagram showing a cyberattack exploitation process from an attacker injecting malicious data to remote code execution

As indicated in the analysis, exploiting this vulnerability requires generating malicious data sent to the consumer via the Kafka header record. Initially, the attacker must create a malicious class that extends the Throwable class and then utilize a gadget chain to achieve remote code execution. Gadgets are exploitable code snippets within the application, and by chaining them together, the attacker can reach a "sink gadget" that triggers harmful actions. 

The following is a malicious class that can be utilized to exploit this vulnerability in Spring for Apache Kafka: 

Java class CustomExceptionClass demonstrating a payload execution vulnerability using PowerShell commands

Next, an instance of the malicious class is created and passed as an argument to the cause parameter in the constructor of the DeserializationException class. The DeserializationException instance is then serialized into a byte stream, which is subsequently used as the value in the header of the malicious Kafka record.

Java class for constructing a malicious serialized object for deserialization exploits
Java method sendMessage constructs and sends a Kafka message, injecting a malicious serialized payload if both key and data are null

If the attacker successfully deceives the victim into using their malicious producer, they can control the Kafka records sent to the consumer, creating an opportunity to compromise the system. 

UI for sending Kafka messages, showing a message form and received messages from two producers

When the vulnerable consumer receives a Kafka record from the malicious producer containing null keys and values, along with a malicious serialized instance in the record header, the consumer processes the record, including the deserialization process. This ultimately leads to remote code execution, allowing the attacker to compromise the system.

Terminal and Kafka message sender UI, demonstrating message injection with a command execution in a Windows environment

Mitigating CVE-2023-34040 with SBOM in MetaDefender Core

To effectively mitigate the risks associated with CVE-2023-34040, organizations require a comprehensive solution that provides visibility and control over their open-source components.  

SBOM, a foundational technology within MetaDefender Core, provides a powerful answer. By acting as a comprehensive inventory of all the software components, libraries, and dependencies in use, SBOM enables organizations to track, secure, and update their open-source components in a proactive and efficient manner.

Screenshot of a dashboard displaying a blocked XML file with critical and high vulnerabilities

With SBOM, security teams can:

  • Quickly locate vulnerable components: Immediately identify the open-source components affected by deserialization attacks. This ensures swift action in either patching or replacing the vulnerable libraries. 
  • Ensure proactive patching and updates: Continuously monitor open-source components through SBOM to stay ahead of deserialization vulnerabilities. SBOM can detect outdated or insecure components, allowing timely updates and reducing exposure to attacks. 
  • Maintain compliance and reporting: SBOM helps organizations meet compliance requirements as regulatory frameworks increasingly mandate transparency in software supply chains.

Closing Thoughts

Deserialization vulnerabilities are a significant security threat that can be used to exploit a wide range of applications. These vulnerabilities occur when an application deserializes malicious data, allowing attackers to execute arbitrary code or access sensitive information. The CVE-2023-34040 vulnerability in Spring for Apache Kafka serves as a stark reminder of the dangers of deserialization attacks. 

To prevent deserialization attacks, it is essential to implement advanced tools such as OPSWAT MetaDefender Core and its SBOM technology. Organizations can gain deep visibility into their software supply chain, ensure timely patching of vulnerabilities, and protect themselves against the ever-evolving threat landscape. Proactively securing open-source components isn't just a best practice—it's a necessity for protecting modern systems against potential exploitation.


Stay Up-to-Date With OPSWAT!

Sign up today to receive the latest company updates, stories, event info, and more.