Saltar al contenido

Deserialización insegura: el gadget chain explicado sin drama

    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 / mecanismoContextoPor que es peligroso con entrada no fiable
    Java serialization (ObjectInputStream)RMI, JMS, caches, cookies, endpoints heredadosReconstruye cualquier clase Serializable del classpath e invoca readObject; base de la mayoria de cadenas historicas
    .NET BinaryFormatter / NetDataContractSerializer / LosFormatterViewState, remoting, ficheros de estadoIncluye el tipo en los datos e instancia clases arbitrarias; BinaryFormatter esta oficialmente obsoleto por esto
    PHP unserialize()Cookies, campos ocultos, cache en ficherosReconstruye objetos e invoca metodos magicos (__wakeup, __destruct, __toString); las cadenas se llaman POP chains
    Python pickle / cPickle / shelveCache, colas, modelos de ML distribuidosEl formato permite invocar callables durante la carga por diseno; cargar un pickle no fiable equivale a ejecutar codigo
    PyYAML yaml.load() sin loader seguroFicheros de configuracion, plantillas de despliegueLas etiquetas de tipo permiten instanciar objetos Python arbitrarios; hay que usar yaml.safe_load
    Ruby Marshal.loadCache, sesiones, colasReconstruye objetos arbitrarios de forma analoga a Java/PHP
    JSON con type hints (polymorphic deserialization)APIs que serializan jerarquias de clasesSeguro 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:

    FormatoMarca al inicioAspecto habitual en la peticion
    Java serializadoBytes AC ED 00 05 (hex); ‘rO0’ en Base64Cookie o parametro largo que empieza por rO0AB…
    .NET BinaryFormatter‘AAEAAAD/////’ en Base64Campo __VIEWSTATE o valor de estado que empieza por AAEAAAD
    PHP serializadoTexto tipo O:8:»NombreClase»:3:{…}Cookie legible con O:, a:, s: y llaves
    Python pickleBytes 80 04 (protocolo 4) u 80 05; a veces ‘gAS’ en Base64Blob 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

    CapaMedidaPor que funciona
    DisenoUsar formatos solo-datos (JSON, Protobuf) en toda frontera con datos no fiablesElimina la causa: no hay instrucciones sobre que clases instanciar
    DisenoEl estado de cliente es un identificador opaco; los datos reales viven en el servidorEl usuario no puede manipular un grafo de objetos que no viaja
    DeserializadorSi el formato con tipos es inevitable, restringir a una allowlist minima de clasesEl runtime rechaza cualquier clase fuera de la lista antes de instanciarla
    DeserializadorSustituir APIs obsoletas (BinaryFormatter, ObjectInputStream a pelo) por alternativas acotadasCierra la API que permite reconstruir grafos arbitrarios
    IntegridadFirmar 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
    EjecucionProceso con minimo privilegio, sin secretos en el entorno, egress de red filtradoSi la deserializacion llega a ejecucion, el alcance es menor
    DependenciasInventario de librerias con SCA y actualizacion; vigilar avisos de gadget chainsReduce los eslabones disponibles, aunque no sustituye a no deserializar entrada no fiable
    DeteccionLog y alerta ante errores de deserializacion, clases inesperadas o marcas de tipo en parametrosUn 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 pickle

    Conclusió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)

    Resumen de privacidad

    Esta web utiliza cookies para que podamos ofrecerte la mejor experiencia de usuario posible. La información de las cookies se almacena en tu navegador y realiza funciones tales como reconocerte cuando vuelves a nuestra web o ayudar a nuestro equipo a comprender qué secciones de la web encuentras más interesantes y útiles.