Serialización y deserialización en Java: explicación de la vulnerabilidad de deserialización de Java
18 de diciembre de 2020
0 minutos de lecturaNota del editor — 11 de julio de 2022
Actualizamos este artículo para reflejar los cambios en las versiones más recientes de Java y las vulnerabilidades que pueden explotarse mediante la deserialización. También incluimos una charla reciente de una conferencia en la que se mostraron exploits de deserialización de Java en una demostración en vivo.
La serialización de Java es un mecanismo para transformar un objeto en un flujo de bytes. La deserialización de Java, por otro lado, permite recrear un objeto a partir de un flujo de bytes. La serialización de Java —y, en particular, la deserialización— se conoce como «el regalo que sigue dando» porque a lo largo de los años ha provocado muchos problemas y vulnerabilidades de seguridad.
¿Qué es la serialización en Java?
Usamos Java para crear objetos. Estos objetos se almacenan en la memoria y el recolector de basura los elimina cuando dejan de utilizarse. Si queremos transferir un objeto y, por ejemplo, guardarlo en un disco o enviarlo a través de una red, tenemos que transformarlo en un flujo de bytes. Para hacerlo, la clase de ese objeto debe implementar la interfaz Serializable. Como explicamos antes, la serialización permite convertir el estado de un objeto en un flujo de bytes. Este flujo de bytes no contiene el código real.
¿Qué es la deserialización en Java?
La deserialización es exactamente lo contrario de la serialización. Para deserializar, se parte de un flujo de bytes y se vuelve a crear el objeto que se había serializado, con su estado original. Sin embargo, para recrearlo correctamente, se necesita la definición del objeto.
¿Cómo funciona la serialización en Java?
La serialización de Java usa la reflexión para recopilar todos los datos necesarios de los campos del objeto, incluidos los campos privados y finales. Si un campo contiene un objeto, ese objeto se serializa de forma recursiva. Aunque tengas getters y setters, estas funciones no se usan al serializar un objeto en Java.
¿Cómo funciona la deserialización en Java?
Al volver a convertir un flujo de bytes en un objeto, la deserialización no usa el constructor. Simplemente crea un objeto vacío y usa la reflexión para escribir los datos en los campos. Al igual que en la serialización, también se incluyen los campos privados y finales.
¿Qué es una vulnerabilidad de deserialización en Java?
Una vulnerabilidad de deserialización en Java es una vulnerabilidad de seguridad que ocurre cuando un usuario malicioso intenta insertar en el sistema un objeto serializado modificado para comprometer el sistema o sus datos. Piensa en una vulnerabilidad de ejecución de código arbitrario que puede activarse al deserializar un objeto serializado. Para explicar mejor las vulnerabilidades de deserialización de Java, primero tenemos que explorar cómo funciona la deserialización en Java.
Explicación de las vulnerabilidades de deserialización de Java
Un objeto serializado en Java es una matriz de bytes que contiene información de estado. Incluye el nombre del objeto al que hace referencia y los datos del campo. Si abres un objeto serializado almacenado con un editor hexadecimal, puedes ver y manipular la información rápidamente.
Ya sabemos que la deserialización de Java no usa el constructor para crear un objeto, sino que carga los campos mediante la reflexión. Esto significa que las comprobaciones de validación que se realizan en el constructor nunca se ejecutan al recrear el objeto. Por ejemplo, se pueden comprobar que la fecha de inicio sea anterior a la fecha de finalización al describir un período. Al deserializar un objeto Java, el nuevo objeto puede tener un estado no válido.
Veamos el siguiente ejemplo de una vulnerabilidad de deserialización de Java, en el que serializamos un objeto de una clase serializable ValueObject:
"seriAl leer el archivo que contiene el objeto serializado, ValueObject.ser, con un editor hexadecimal, se muestra el siguiente resultado:

Ahora podemos manipular fácilmente el valor de la cadena. Por ejemplo, cambiemos Hi por Hallo en el resultado del editor hexadecimal.

Al deserializar el archivo binario modificado, vemos que el value del objeto cambió. También observamos que la marca de tiempo no cambió, lo que demuestra que el constructor nunca se ejecutó. Si una aplicación acepta objetos serializados, es relativamente fácil alterar sus valores. Al modificar los objetos serializados, podemos crear objetos no válidos, alterar la integridad de los datos o algo peor.
Ejecución de código arbitrario, gadgets y cadenas
Manipular los datos de un objeto ya es perjudicial. Sin embargo, esto también puede provocar la ejecución de código si se deserializa el conjunto adecuado de objetos. Antes de llegar a eso, repasemos los gadgets y las cadenas.
Gadgets
Un gadget —tal como lo describieron Lawrence y Frohoff en su charla Marschalling Pickle en AppSecCali 2015— es una clase o función que ya cuenta con código ejecutable en el proceso vulnerable. Ese código ejecutable puede reutilizarse con fines maliciosos. En los objetos serializables de Java, algunos métodos mágicos —como el método privado readObject()— se llaman mediante reflexión durante la deserialización.
Veamos el siguiente gadget simplificado:
Esta clase gadget sobrescribe el método predeterminado readObject. Como resultado, cada vez que se deserializa un objeto de la clase Gadget, se ejecuta el objeto de tipo Runnable llamado command. Si una clase de comando se parece al ejemplo de abajo, es fácil manipular este objeto serializado y ejecutar una inyección de código.
Ten en cuenta también que, si una aplicación acepta objetos serializados, estos se deserializan antes de convertirse al tipo deseado. Esto significa que, aunque falle la conversión de tipo, la deserialización ya se completó y se ejecutó el método readObject().
Ataques de deserialización con cadenas de gadgets
Un ataque de deserialización típico consiste en una cadena de gadgets creada con ingenio. Un atacante busca un gadget para iniciar un ataque y encadena varias ejecuciones que terminan en la ejecución de código arbitrario. Por ejemplo:
Como ejemplo más cercano a la realidad, veamos la implementación de java.util.HashMap. Esta clase tiene una implementación personalizada del método readObject() que activa la función hashcode() de cada clave.
Bibliotecas
Es importante saber que las cadenas de gadgets disponibles en tu aplicación no dependen de tu código. Como importamos mucho código de bibliotecas y frameworks, la cantidad de clases que importan tus dependencias (transitivas) influye en la posibilidad de que existan determinadas cadenas de gadgets. Aunque crear una cadena de gadgets maliciosa es difícil y requiere mucho trabajo, las vulnerabilidades de deserialización de Java son un riesgo de seguridad real y peligroso.
Un gran ejemplo de esto fue la vulnerabilidad Log4Shell de diciembre de 2021. En esencia, era una vulnerabilidad de ejecución remota de código que podía originarse en el framework de serialización nativo de Java. En versiones antiguas de Java, confiábamos de manera predeterminada en el código remoto y no necesitábamos tener la clase en nuestro classpath. Sin embargo, en las versiones más recientes de Java, incluso en la más reciente, la vulnerabilidad Log4Shell puede aprovechar una cadena de gadgets disponible en tu classpath, lo que puede provocar la ejecución remota de código o algo peor.
¿Cómo prevenir una vulnerabilidad de deserialización en Java?
La mejor manera de prevenir una vulnerabilidad de deserialización en Java es evitar por completo la serialización de Java. Si tu aplicación no acepta objetos serializados, no pueden hacerte daño.
Sin embargo, si necesitas implementar la interfaz `serializable` debido a la herencia, puedes sobrescribir readObject(), como se muestra abajo, para evitar la deserialización real.
Si tu aplicación depende de objetos serializados, puedes inspeccionar ObjectInputStream antes de deserializar. La biblioteca Apache Commons IO puede ayudarte. Esta biblioteca ofrece un ValidatedObjectInputStream que te permite autorizar explícitamente los objetos que quieres deserializar. Así puedes evitar por completo que se deserialicen tipos inesperados.
Otra opción es usar los filtros de serialización nativos de Java. Se implementaron en JEP 290 y se publicaron en Java 9 (marzo de 2018). Puedes configurar filtros específicos para bloquear o permitir clases. Según un patrón, puedes crear un filtro para limitar las clases que se pueden deserializar.
Con la publicación de JEP 415 en Java 17 (septiembre de 2021), este sistema de filtros se mejoró para que sea específico del contexto. Esto significa que puedes controlar el comportamiento de un filtro de una manera adecuada para tu contexto.
En el artículo Nuevas funciones de Java 17 para mejorar la seguridad y la serialización, analizamos más a fondo estas nuevas funciones y explicamos los filtros de Java, cómo implementarlos y por qué son tan importantes los filtros específicos del contexto.
Una herramienta como ysoserial también es muy útil para encontrar vulnerabilidades de deserialización en Java. Genera cargas útiles para descubrir cadenas de gadgets en bibliotecas comunes de Java que, en las condiciones adecuadas, pueden explotar aplicaciones Java que deserializan objetos de forma insegura.
Ten en cuenta que las vulnerabilidades de deserialización de Java no se limitan a la implementación de serialización personalizada de Java. Aunque esta es el tema central de este artículo, las mismas vulnerabilidades existen en los frameworks de serialización o marshalling que se encargan de esto por ti. Si un framework crea automáticamente objetos POJO a partir de XML, JSON, YAML u otros formatos similares, probablemente use la reflexión de la misma manera que describimos antes. Esto significa que existen los mismos problemas.
Para prevenir este tipo de vulnerabilidades de deserialización de Java en tus bibliotecas externas, analízalas gratis con Snyk Open Source desde el principio y con frecuencia. Para obtener más información sobre este tipo de vulnerabilidad, consulta la lección sobre deserialización insegura de Snyk Learn.
Charla de conferencia: «Exploits de deserialización en Java: ¿por qué debería importarme?»
Hace poco di una charla sobre exploits de deserialización en Devoxx UK, la conferencia de la comunidad Java más grande y prestigiosa del Reino Unido.
En esta charla, explico cómo funcionan de forma nativa en Java las vulnerabilidades de deserialización y cómo se crean las cadenas de ataque. Los hackers se refieren a la deserialización en Java como «el regalo que sigue dando», así que preparé una charla y una demostración en vivo para mostrar el posible impacto en la seguridad.
Por supuesto, los recientes problemas de seguridad relacionados con la vulnerabilidad Log4Shell también forman parte de esto. Expliqué cómo Log4Shell puede ser el punto de partida de una cadena de gadgets de deserialización, en la que el gadget final ejecuta código arbitrario.
Otros tipos de deserialización, como JSON, XML y YAML, también pueden causar problemas. Para obtener más información, consulta el artículo Problemas de deserialización de JSON en Java con Jackson ObjectMapper. En esta charla analizamos el tema con más profundidad y mostramos las consecuencias reales.

Comienza con Capture the Flag
Aprende a resolver desafíos de Capture the Flag con nuestro taller virtual 101 a pedido.


