
Los ataques de deserialización —la manipulación no autorizada de datos serializados para ejecutar código malicioso— pueden causar graves daños a las organizaciones. Los desarrolladores y los equipos de seguridad deben identificar de forma proactiva las vulnerabilidades clave que revelen cómo, cuándo y dónde se produjo el ataque.
Un ejemplo reciente de esta amenaza es CVE-2023-34040, un vector de ataque por deserialización en Spring para Apache Kafka que puede dar lugar a la ejecución remota de código (RCE). De hecho, la prevalencia de las vulnerabilidades de deserialización queda patente en la lista OWASP Top 10, que incluye la «Deserialización de datos no fiables» como uno de los 10 riesgos de seguridad más críticos para las aplicaciones web.
Herramientas como OPSWAT MetaDefender , con su motor SBOM (Software de componentesSoftware ), son esenciales para detectar y prevenir los ataques de deserialización. Estas herramientas permiten a los desarrolladores escanear y analizar su código de forma eficaz, garantizando que no se pase por alto ninguna vulnerabilidad.
En este blog, nuestros becarios de posgrado analizarán los detalles del vulnerabilidad CVE-2023-34040 y su explotación, así como las medidas para proteger los componentes de código abierto frente a amenazas similares.
Acerca de CVE-2023-34040
CVE-2023-34040 revela un vector de ataque por deserialización en Spring for Apache Kafka, que puede ser explotado cuando se aplica una configuración inusual. Esta vulnerabilidad permite a un atacante crear un objeto serializado malicioso en uno de los encabezados de registro de excepciones de deserialización, lo que puede dar lugar a la ejecución remota de código (RCE). La vulnerabilidad afecta a las versiones 2.8.1 a 2.9.10 y 3.0.0 a 3.0.9 de Spring para Apache Kafka.

En concreto, una aplicación o un usuario es vulnerable en las siguientes configuraciones y condiciones específicas:
- La clase `ErrorHandlingDeserializer` no está configurada para la clave y/o el valor del registro.
- Las propiedades checkDeserExWhenKeyNull y/o checkDeserExWhenValueNull del consumidor están establecidas en «true».
- Se permite a las fuentes no fiables publicar en un tema de Kafka.
Apache Kafka
Apache Kafka, desarrollado por la Apache Software , es una plataforma distribuida de transmisión de eventos diseñada para capturar, procesar, responder y enrutar flujos de datos en tiempo real procedentes de diversas fuentes, como bases de datos, sensores y mobile .
Por ejemplo, puede enviar notificaciones a servicios que reaccionan ante las acciones de los clientes, como al finalizar la compra de un producto o al realizar un pago.

En Apache Kafka, un evento —también denominado «registro» o «mensaje»— es una unidad de datos que representa una acción en la aplicación cada vez que se leen o se escriben datos. Cada evento incluye una clave, un valor, una marca de tiempo y encabezados de metadatos opcionales.
binario de clave (puede ser nulo) | Valor binario (puede ser nulo) | ||||
| Tipo de compresión [none, gzip, snappy, lz4, zstd] | |||||
Encabezados (opcional)
| |||||
| Partición + Desplazamiento | |||||
| Marca de tiempo (establecida por el sistema o por el usuario) | |||||
Los eventos se almacenan de forma permanente y se organizan en temas. Las aplicaciones cliente que envían (escriben) eventos a los temas de Kafka se denominan «productores», mientras que aquellas que se suscriben a los eventos (los leen y procesan) se conocen como «consumidores».

Spring para Apache Kafka
Para conectar Apache Kafka con el ecosistema Spring, los desarrolladores pueden utilizar Spring for Apache Kafka, que simplifica la integración en aplicaciones Java.
Spring for Apache Kafka ofrece herramientas y API robustas que simplifican el proceso de envío y recepción de eventos con Kafka, lo que permite a los desarrolladores realizar estas tareas sin necesidad de una programación extensa y compleja.

Serialización y deserialización
La serialización es un mecanismo que permite convertir el estado de un objeto en una cadena de caracteres o en un flujo de bytes. Por el contrario, la deserialización es el proceso inverso, en el que los datos serializados se reconvierten en su objeto o estructura de datos original. La serialización permite transformar datos complejos para que puedan guardarse en un archivo, enviarse a través de una red o almacenarse en una base de datos. La serialización y la deserialización son esenciales para el intercambio de datos en sistemas distribuidos y facilitan la comunicación entre los distintos componentes de una aplicación de software. En Java, se utiliza writeObject() para la serialización y readObject() para la deserialización.

Dado que la deserialización permite convertir un flujo de bytes o una cadena de caracteres en un objeto, un manejo inadecuado o la falta de una validación adecuada de los datos de entrada pueden dar lugar a una vulnerabilidad de seguridad significativa, lo que podría dar lugar a un ataque de ejecución remota de código (RCE).
Análisis de vulnerabilidades
Según la descripción del CVE, OPSWAT configuraron «checkDeserExWhenKeyNull» y «checkDeserExWhenValueNull» en «true» para activar la vulnerabilidad de seguridad. Al enviar un registro con una clave/valor vacíos y llevar a cabo un análisis detallado mediante la depuración del consumidor en el momento en que recibía un registro de Kafka del productor, nuestros becarios de posgrado descubrieron el siguiente flujo de trabajo durante el procesamiento del registro:
Paso 1: Recepción de registros (mensajes)
Al recibir los registros, el consumidor invoca el método `invokeIfHaveRecords()`, el cual, a su vez, llama al método `invokeListener() ` para activar un oyente de registros registrado (una clase anotada con la anotación `@KafkaListener` ) con el fin de llevar a cabo el procesamiento efectivo de los registros.

A continuación, invokeListener() invoca el método invokeOnMessage().
Paso 2: Comprobación de los registros
Dentro del método `invokeOnMessage()`, se evalúan varias condiciones en función del valor del registro y las propiedades de configuración, lo que determina posteriormente el siguiente paso que se debe ejecutar.

Si un registro tiene una clave o un valor nulo y las propiedades ` checkDeserExWhenKeyNull ` y/o `checkDeserExWhenValueNull ` se han establecido explícitamente en `true`, se llamará al método `checkDeser()` para examinar el registro.
Paso 3: Comprobación de excepciones a partir de los encabezados
En checkDesr(), el consumidor invoca continuamente getExceptionFromHeader() para recuperar cualquier excepción de los metadatos del registro, si las hay, y almacena el resultado en una variable llamada exception.

Paso 4: Extraer la excepción de los encabezados
El método `getExceptionFromHeader() ` está diseñado para extraer y devolver una excepción del encabezado de un registro de Kafka. En primer lugar, recupera el encabezado del registro y, a continuación, obtiene el valor del encabezado, que se almacena en una matriz de bytes.

A continuación, envía la matriz de bytes del valor del encabezado al método ` byteArrayToDeserializationException() ` para su posterior procesamiento.
Paso 5: Deserialización de datos
En el método `byteArrayToDeserializationException()`, se sobrescribe la función `resolveClass() ` para restringir la deserialización únicamente a las clases permitidas. Este enfoque impide la deserialización de cualquier clase que no esté explícitamente permitida. El valor de la matriz de bytes del encabezado solo se puede deserializar dentro de byteArrayToDeserializationException() si cumple la condición establecida en resolveClass(), que permite la deserialización exclusivamente para la clase DeserializationException.

Sin embargo, la clase `DeserializationException` hereda de la clase estándar `Exception` e incluye un constructor con cuatro parámetros. El último parámetro, `cause`, representa la excepción original que provocó la `DeserializationException`, como una `IOException` o una `ClassNotFoundException`.

La clase `Throwable` actúa como superclase de todos los objetos que pueden lanzarse como excepciones o errores en Java. En el lenguaje de programación Java, las clases de gestión de excepciones como `Throwable`, `Exception` y `Error` pueden deserializarse de forma segura. Cuando se deserializa una excepción, Java permite que las clases derivadas de `Throwable` se carguen y se instancien con comprobaciones menos estrictas que las que se aplican a las clases normales.
Según el flujo de trabajo y un análisis exhaustivo, si los datos serializados corresponden a una clase maliciosa que hereda de la clase padre `Throwable`, es posible que se eludan las comprobaciones de condiciones. Esto permite la deserialización de un objeto malicioso, que puede ejecutar código dañino y dar lugar, potencialmente, a un ataque de ejecución remota de código (RCE).
Explotación

Tal y como se indica en el análisis, para aprovechar esta vulnerabilidad es necesario generar datos maliciosos que se envían al consumidor a través del registro de encabezado de Kafka. En primer lugar, el atacante debe crear una clase maliciosa que extienda la clase `Throwable` y, a continuación, utilizar una cadena de «gadgets» para lograr la ejecución remota de código. Los «gadgets» son fragmentos de código explotables dentro de la aplicación y, al encadenarlos, el atacante puede llegar a un «gadget de destino» que desencadena acciones dañinas.
A continuación se muestra una clase maliciosa que puede utilizarse para aprovechar esta vulnerabilidad en Spring para Apache Kafka:

A continuación, se crea una instancia de la clase maliciosa y se pasa como argumento al parámetro «cause» en el constructor de la clase DeserializationException. La instancia de DeserializationException se serializa entonces en un flujo de bytes, que posteriormente se utiliza como valor en el encabezado del registro malicioso de Kafka.


Si el atacante logra engañar a la víctima para que utilice su productor malicioso, podrá controlar los registros de Kafka enviados al consumidor, lo que le brindará la oportunidad de comprometer el sistema.

Cuando el consumidor vulnerable recibe un registro de Kafka del productor malintencionado que contiene claves y valores nulos, junto con una instancia serializada maliciosa en el encabezado del registro, el consumidor procesa el registro, incluido el proceso de deserialización. Esto acaba provocando la ejecución remota de código, lo que permite al atacante comprometer el sistema.

Mitigación de CVE-2023-34040 mediante SBOM en MetaDefender Core
Para mitigar de forma eficaz los riesgos asociados al CVE-2023-34040, las organizaciones necesitan una solución integral que les proporcione visibilidad y control sobre sus componentes de código abierto.
SBOM, una tecnología fundamental de MetaDefender Core, ofrece una solución eficaz. Al actuar como un inventario exhaustivo de todos los componentes de software, bibliotecas y dependencias en uso, SBOM permite a las organizaciones realizar un seguimiento, proteger y actualizar sus componentes de código abierto de forma proactiva y eficiente.

Gracias a la SBOM, los equipos de seguridad pueden:
- Localice rápidamente los componentes vulnerables: identifique de inmediato los componentes de código abierto afectados por ataques de deserialización. Esto garantiza una respuesta rápida, ya sea mediante la aplicación de parches o la sustitución de las bibliotecas vulnerables.
- Garantizar la aplicación proactiva de parches y actualizaciones: supervisar continuamente los componentes de código abierto mediante la SBOM para adelantarse a las vulnerabilidades de deserialización. La SBOM permite detectar componentes obsoletos o inseguros, lo que facilita las actualizaciones oportunas y reduce la exposición a los ataques.
- Garantizar el cumplimiento normativo y la presentación de informes: la SBOM ayuda a las organizaciones a cumplir los requisitos normativos, ya que los marcos reguladores exigen cada vez más transparencia en las cadenas de suministro de software.
Reflexiones finales
Las vulnerabilidades de deserialización constituyen una grave amenaza para la seguridad que puede aprovecharse para atacar una amplia variedad de aplicaciones. Estas vulnerabilidades se producen cuando una aplicación deserializa datos maliciosos, lo que permite a los atacantes ejecutar código arbitrario o acceder a información confidencial. La vulnerabilidad CVE-2023-34040 en Spring for Apache Kafka sirve como un claro recordatorio de los peligros que entrañan los ataques de deserialización.
Para prevenir los ataques de deserialización, es fundamental implementar herramientas avanzadas como OPSWAT MetaDefender Core su tecnología SBOM. Las organizaciones pueden obtener una visibilidad detallada de su cadena de suministro de software, garantizar la aplicación oportuna de parches para las vulnerabilidades y protegerse frente a un panorama de amenazas en constante evolución. Proteger de forma proactiva los componentes de código abierto no es solo una buena práctica, sino una necesidad para proteger los sistemas modernos contra posibles ataques.
