Deserialización insegura:
el gadget chain explicado sin drama
Seguridad de aplicaciones web | unfantasmaenelsistema.com
Serializar es convertir un objeto que vive en memoria —con sus campos, sus tipos y sus referencias— en una secuencia de bytes que se puede guardar en disco, meter en una cookie o mandar por la red. Deserializar es el camino inverso: reconstruir el objeto a partir de esos bytes. Es una operación cotidiana en casi cualquier aplicación: sesiones, cachés, colas de mensajes, llamadas entre microservicios y ficheros de configuración pasan por ahí.
El problema aparece cuando esos bytes vienen de una fuente que no controlas y el formato permite reconstruir objetos arbitrarios, no solo datos. En ese momento el atacante no te envía un valor: te envía la descripción de un grafo de objetos que tu proceso va a instanciar, y con él una cadena de métodos que se ejecutan durante o justo después de la reconstrucción. Esa cadena es lo que se llama gadget chain. Este artículo explica de dónde sale el riesgo, en qué lenguajes y formatos aparece, cómo se detecta y cómo se corta de raíz, sin entrar en la construcción de payloads.
Que aprenderas en este articulo
La diferencia entre deserializar datos y deserializar objetos con comportamiento · Que es un gadget chain a alto nivel y por que no hace falta un fallo en tu codigo para que exista · En que lenguajes y formatos aparece: Java, .NET, PHP unserialize, Python pickle, YAML inseguro · Como se detecta: magic bytes, blobs en cookies y parametros, revision de codigo · Defensa: formatos solo-datos, allowlist de clases, firmar el estado, aislar el proceso
Encuadre legal y de pruebas
Practica estos conceptos solo contra aplicaciones propias o laboratorios deliberadamente vulnerables (un contenedor propio, la maquina de la serie en 192.168.56.x, retos tipo Juice Shop o entornos con autorizacion escrita) · Enviar objetos serializados manipulados a un servicio ajeno para provocar la ejecucion de codigo o leer datos internos puede constituir acceso ilicito a sistemas y danos informaticos (arts. 197 bis, 197 ter y 264 del Codigo Penal espanol) · Si detectas una deserializacion insegura en un servicio de terceros, no la explotes: documenta la evidencia minima y usa el canal de divulgacion responsable del proveedor o el INCIBE-CERT
1. Datos frente a objetos con comportamiento
No toda deserialización es peligrosa. Cuando lees un JSON y obtienes cadenas, números, listas y diccionarios, lo peor que puede pasar es que los datos sean incorrectos y tu validación tenga que rechazarlos. No se instancia nada que ejecute lógica por el hecho de existir.
El riesgo llega con los formatos que serializan el tipo además del valor y son capaces de reconstruir cualquier clase disponible en el proceso. Al deserializar, el runtime crea instancias de esas clases y, según el lenguaje, invoca métodos especiales: constructores, callbacks de post-construcción, métodos de lectura personalizados, destructores. Si entre las clases cargadas en tu aplicación (las tuyas, las del framework, las de cualquier dependencia) hay métodos que hacen algo interesante al ejecutarse —escribir un fichero, cargar una clase por nombre, abrir una conexión, invocar un comando—, el atacante puede encadenarlos.
La idea clave: no necesitas un bug en tu código. El fallo es aceptar bytes no fiables en un deserializador potente. Los gadgets ya están en el classpath, puestos ahí por librerías legítimas.
2. Qué es un gadget chain
Un gadget es un fragmento de código que ya existe en la aplicación y que se ejecuta como efecto secundario de la deserialización o de operaciones inmediatamente posteriores (comparar objetos, calcular un hash, representarlos como texto, recorrer una colección). Aislado, cada gadget hace algo modesto.
Una gadget chain es una secuencia de esos fragmentos colocados de forma que la salida de uno alimenta la entrada del siguiente, hasta llegar a una primitiva potente: cargar una clase remota, invocar un método por reflexión con parámetros controlados o lanzar un proceso. El atacante construye un grafo de objetos anidados tal que, al reconstruirlo, el runtime va disparando esos métodos en el orden deseado.
Por eso la comunidad mantiene catálogos de cadenas conocidas para librerías muy extendidas: el trabajo duro de encontrar los eslabones ya está hecho. Para el defensor, la consecuencia práctica es que actualizar la dependencia no siempre elimina el riesgo —pueden aparecer cadenas nuevas— y que la única defensa robusta es no deserializar datos no fiables con un formato que permita todo esto.
3. Dónde aparece: lenguajes y formatos
| Formato / mecanismo | Contexto | Por que es peligroso con entrada no fiable |
|---|---|---|
| Java serialization (ObjectInputStream) | RMI, JMS, caches, cookies, endpoints heredados | Reconstruye cualquier clase Serializable del classpath e invoca readObject; base de la mayoria de cadenas historicas |
| .NET BinaryFormatter / NetDataContractSerializer / LosFormatter | ViewState, remoting, ficheros de estado | Incluye el tipo en los datos e instancia clases arbitrarias; BinaryFormatter esta oficialmente obsoleto por esto |
| PHP unserialize() | Cookies, campos ocultos, cache en ficheros | Reconstruye objetos e invoca metodos magicos (__wakeup, __destruct, __toString); las cadenas se llaman POP chains |
| Python pickle / cPickle / shelve | Cache, colas, modelos de ML distribuidos | El formato permite invocar callables durante la carga por diseno; cargar un pickle no fiable equivale a ejecutar codigo |
| PyYAML yaml.load() sin loader seguro | Ficheros de configuracion, plantillas de despliegue | Las etiquetas de tipo permiten instanciar objetos Python arbitrarios; hay que usar yaml.safe_load |
| Ruby Marshal.load | Cache, sesiones, colas | Reconstruye objetos arbitrarios de forma analoga a Java/PHP |
| JSON con type hints (polymorphic deserialization) | APIs que serializan jerarquias de clases | Seguro por defecto, pero activar la resolucion de tipos por nombre reintroduce el problema (caso historico de Jackson con default typing) |
El patrón común salta a la vista: los formatos solo-datos (JSON, un CSV, Protocol Buffers, MessagePack sin extensiones peligrosas) son seguros frente a esta clase de ataque porque no llevan instrucciones sobre qué clases instanciar. Los formatos que preservan el tipo y el grafo completo de objetos son cómodos para comunicación interna entre componentes de confianza, y una mala idea en cualquier frontera donde entren datos de terceros.
JSON no es inmune por si mismo
Un parser JSON estandar solo produce tipos primitivos y es seguro · El riesgo vuelve si activas ‘default typing’, ‘polymorphic deserialization’ o un TypeNameHandling amplio para que el JSON diga que clase construir · Si necesitas polimorfismo, limita los tipos permitidos a una allowlist explicita y minima
4. Cómo se detecta
4.1 En el código
Igual que con otras clases de inyección, la detección fiable es leer el código y localizar dónde entra un flujo de bytes externo en un deserializador potente:
- Java: ObjectInputStream.readObject() sobre datos que provienen de HTTP, de una cola o de un fichero subido; uso de librerias de serializacion configuradas para aceptar tipos arbitrarios.
- .NET: BinaryFormatter, SoapFormatter, NetDataContractSerializer, LosFormatter o ObjectStateFormatter con entrada que no sea puramente interna; ViewState sin MAC.
- PHP: unserialize() sobre cookies, parametros o contenido de ficheros controlables; considerar json_decode como alternativa.
- Python: pickle.loads(), pickle.load(), pandas.read_pickle(), joblib.load() o torch.load() sobre ficheros o mensajes de origen no confiable; yaml.load() sin SafeLoader.
- Señal transversal: cualquier estado de cliente (sesion, carrito, preferencias, ‘volver a esta pagina’) que viaje en un blob en vez de en un identificador opaco contra almacenamiento del servidor.
4.2 En tránsito: magic bytes y blobs
Los objetos serializados tienen firmas reconocibles al principio del contenido, y a menudo viajan codificados en Base64 dentro de cookies o parámetros. Ver uno de estos marcadores en una petición es indicio de que hay deserialización del lado del servidor y conviene revisar cómo se valida:
| Formato | Marca al inicio | Aspecto habitual en la peticion |
|---|---|---|
| Java serializado | Bytes AC ED 00 05 (hex); ‘rO0’ en Base64 | Cookie o parametro largo que empieza por rO0AB… |
| .NET BinaryFormatter | ‘AAEAAAD/////’ en Base64 | Campo __VIEWSTATE o valor de estado que empieza por AAEAAAD |
| PHP serializado | Texto tipo O:8:»NombreClase»:3:{…} | Cookie legible con O:, a:, s: y llaves |
| Python pickle | Bytes 80 04 (protocolo 4) u 80 05; a veces ‘gAS’ en Base64 | Blob binario o Base64 en un campo de estado o cache |
Detectar la marca no dice si la implementación es vulnerable —puede haber una firma HMAC que impida manipular el blob— pero sí marca el punto exacto que hay que auditar.
Patrones a revisar en tu propio codigo y trafico
Estado de sesion o de UI que viaja como blob serializado en cookies o campos ocultos · Endpoints heredados de RMI, remoting, SOAP o colas expuestos mas alla de la red interna · Deserializadores potentes (readObject, unserialize, pickle.loads, yaml.load) con entrada de red · Ficheros subidos por usuarios que luego se cargan como modelo, configuracion o cache · Librerias de serializacion con ‘default typing’ o resolucion de tipos por nombre activada
5. Defensa en capas
| Capa | Medida | Por que funciona |
|---|---|---|
| Diseno | Usar formatos solo-datos (JSON, Protobuf) en toda frontera con datos no fiables | Elimina la causa: no hay instrucciones sobre que clases instanciar |
| Diseno | El estado de cliente es un identificador opaco; los datos reales viven en el servidor | El usuario no puede manipular un grafo de objetos que no viaja |
| Deserializador | Si el formato con tipos es inevitable, restringir a una allowlist minima de clases | El runtime rechaza cualquier clase fuera de la lista antes de instanciarla |
| Deserializador | Sustituir APIs obsoletas (BinaryFormatter, ObjectInputStream a pelo) por alternativas acotadas | Cierra la API que permite reconstruir grafos arbitrarios |
| Integridad | Firmar o cifrar el estado que deba salir del servidor (HMAC con clave del servidor) | El servidor solo deserializa lo que el mismo emitio; la manipulacion se detecta antes de reconstruir |
| Ejecucion | Proceso con minimo privilegio, sin secretos en el entorno, egress de red filtrado | Si la deserializacion llega a ejecucion, el alcance es menor |
| Dependencias | Inventario de librerias con SCA y actualizacion; vigilar avisos de gadget chains | Reduce los eslabones disponibles, aunque no sustituye a no deserializar entrada no fiable |
| Deteccion | Log y alerta ante errores de deserializacion, clases inesperadas o marcas de tipo en parametros | Un pico de fallos de deserializacion suele ser alguien probando |
El orden importa: las dos primeras filas resuelven el problema de verdad. La allowlist de clases y la firma del estado son contención sólida cuando no puedes cambiar el formato de golpe. El resto son redes de seguridad. Y una advertencia concreta: firmar el blob protege la integridad, no la confidencialidad ni el diseño; si además el contenido es sensible, cífralo, y si puedes evitar mandarlo, mejor.
Migrar una aplicacion que ya usa serializacion binaria
Empieza por el inventario: donde se serializa, con que formato y quien puede tocar esos bytes · Prioriza las fronteras expuestas a usuarios o a Internet sobre la comunicacion interna de confianza · Cambia a JSON u otro formato solo-datos el estado de cliente; mueve los datos reales a almacenamiento del servidor · Donde no puedas migrar ya, aplica allowlist de clases y firma HMAC como medida puente · Trata cada cambio como codigo: revision, pruebas y despliegue por anillos
6. Un caso ilustrativo en laboratorio
Monta un servicio propio en Python con un endpoint que guarde las preferencias del usuario en una cookie. En la versión vulnerable, serializa el diccionario de preferencias con pickle y lo devuelve en Base64; al recibir la cookie, hace pickle.loads sobre su contenido. Cualquiera que entienda el formato puede sustituir ese blob por otro y el proceso ejecutará lo que el nuevo objeto exprese durante la carga.
# ANTES (vulnerable): pickle sobre datos que vuelven del cliente
import pickle, base64
def guardar_prefs(prefs: dict) -> str:
return base64.b64encode(pickle.dumps(prefs)).decode()
def cargar_prefs(cookie: str) -> dict:
return pickle.loads(base64.b64decode(cookie)) # reconstruye lo que sea
# DESPUES (correcto): formato solo-datos y validacion explicita
import json
CLAVES_PERMITIDAS = {'tema', 'idioma', 'resultados_por_pagina'}
def guardar_prefs(prefs: dict) -> str:
limpio = {k: prefs[k] for k in prefs.keys() & CLAVES_PERMITIDAS}
return base64.b64encode(json.dumps(limpio).encode()).decode()
def cargar_prefs(cookie: str) -> dict:
datos = json.loads(base64.b64decode(cookie))
return {k: datos[k] for k in datos.keys() & CLAVES_PERMITIDAS}Con la versión corregida, el peor caso es una cookie con claves o valores inesperados, que el filtrado descarta. No hay ningún camino por el que el contenido de la cookie se convierta en código. Si el estado tuviera que ir firmado para detectar manipulaciones, se añade un HMAC con una clave que solo conoce el servidor y se verifica antes de parsear.
# Opcion puente si el estado debe viajar y quieres detectar manipulacion
import hmac, hashlib
def firmar(payload: bytes, clave: bytes) -> bytes:
mac = hmac.new(clave, payload, hashlib.sha256).digest()
return base64.b64encode(mac + payload)
def verificar(blob: bytes, clave: bytes) -> bytes:
crudo = base64.b64decode(blob)
mac, payload = crudo[:32], crudo[32:]
esperado = hmac.new(clave, payload, hashlib.sha256).digest()
if not hmac.compare_digest(mac, esperado):
raise ValueError('estado manipulado')
return payload # solo ahora se parsea, y con json, no con pickleConclusión
La deserialización insegura no es un exploit puntual: es una decisión de arquitectura. Aceptar bytes no fiables en un deserializador que sabe reconstruir objetos arbitrarios pone a disposición del atacante todos los métodos con efectos secundarios que hay en tus dependencias, sin necesidad de un solo fallo en tu código.
La corrección es tan estable como la causa: usa formatos solo-datos en toda frontera con datos externos, guarda el estado real en el servidor y deja que el cliente lleve solo un identificador. Donde no puedas migrar de inmediato, restringe las clases permitidas y firma el estado. Con eso, la clase entera de vulnerabilidad desaparece de tu aplicación en lugar de quedar pendiente del próximo gadget chain que alguien publique.
Referencias y recursos
OWASP – Deserialization of untrusted data y OWASP Deserialization Cheat Sheet · OWASP Top 10 – A08:2021 Software and Data Integrity Failures (marco general) · PortSwigger Web Security Academy – Insecure deserialization (teoria y laboratorios propios) · CWE-502: Deserialization of Untrusted Data · Documentacion oficial: aviso de seguridad de Python pickle, obsolescencia de BinaryFormatter en .NET, guia de serializacion de Java (filtros de deserializacion, JEP 290) · Retos de practica: OWASP Juice Shop y maquinas vulnerables en un laboratorio aislado
unfantasmaenelsistema.com | Seguridad, sistemas y todo lo que no debería funcionar (pero funciona)

