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.
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.
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)
| |||||
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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:
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.
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.
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.
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.
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.