FlawOpen

flawopen.com/Incidents/equifax-struts-ognl-rce-july-2017

● Equifax · CVE-2017-5638 · July 2017 · CVSS 9.8 · Crítica
Investigación · FlawOpen

Equifax: la brecha expuso datos de casi 150 millones

CVE-2017-5638 permitió inyectar OGNL mediante la cabecera Content-Type en el portal Struts sin parchear de Equifax. El ataque duró unos 76 días y expuso datos de casi 150 millones de personas.[1], [4]

💡 Explicación en Lenguaje Sencillo (ELI5)

Imagina un sistema de mensajería que convierte una etiqueta de envío rechazada en un aviso de error. Si el aviso interpreta la etiqueta como una plantilla de instrucciones ejecutable, quien la envía puede hacer que el sistema obedezca en vez de limitarse a informar de una dirección incorrecta. Struts ya tenía una corrección, pero el aviso de Equifax llegó a una lista obsoleta de administradores y un análisis de red no detectó la vulnerabilidad en el portal.[1], [4]

Conceptos Clave y Términos

OGNL
Object-Graph Navigation Language, el lenguaje de expresiones que usa Struts para leer y asignar propiedades de objetos Java. Evaluar texto del atacante como OGNL equivale a ejecutar su código.[1], [6]
Parser multipart Jakarta
El componente de Struts que gestiona las subidas de ficheros. Al rechazar un Content-Type mal formado, construía un mensaje de error que después se evaluaba.[1], [3], [2]
Inyección de expresiones (CWE-917)
Una entrada no confiable acaba dentro de una expresión que evalúa un intérprete, de modo que los datos se convierten en instrucciones.[6]
Inventario de activos
Un registro de qué software y qué versión corre en cada sistema. Sin él, no se puede relacionar un aviso de parche con las máquinas que lo necesitan.[4]

Mapeos de marcos

Cronología del Incidente

2017-03-07

Apache publica el boletín de seguridad S2-045 y las versiones corregidas 2.3.32 y 2.5.10.1 de Struts.[1]

2017-05-13

Equifax explicó después a la GAO que una intrusión distinta comenzó el 13 de mayo y que entonces los atacantes empezaron a extraer datos personales; antes ya se había producido un acceso no autorizado, sin robo de datos.[4]

2017-07-29

Equifax detecta tráfico sospechoso tras renovar un certificado caducado en un equipo de inspección de tráfico.[4]

2017-09-07

Equifax anuncia públicamente la brecha. La GAO informa después de que se vieron afectados datos de al menos 145,5 millones de personas en EE. UU. y casi un millón fuera del país.[4]

Flujo de Ataque Paso a Paso

Paso 1

Una cabecera Content-Type manipulada

El atacante envía una petición de subida cuya cabecera Content-Type contiene una expresión OGNL en lugar de un tipo de medio.[1]

Paso 2

El mensaje de error arrastra la cabecera

El parser Jakarta rechaza la cabecera y construye un mensaje de error localizado que incluye su texto en bruto.[1], [3], [2]

Paso 3

Se evalúa el mensaje

Struts evalúa las expresiones de ese mensaje, así que el OGNL del atacante se ejecuta como código Java con los privilegios de la aplicación web.[1], [3], [2], [6]

Paso 4

Una exfiltración lenta y silenciosa

Los atacantes usaron credenciales halladas en un repositorio para ampliar el acceso desde las tres bases de datos asociadas al portal a otras 48 bases de datos no relacionadas con el portal. La GAO informa de unas 9.000 consultas y un ataque de unos 76 días; un certificado caducado impedía inspeccionar el tráfico cifrado.[4]

Código Fuente: Vulnerable vs. Seguro

Java ilustrativo, no es código fuente de Struts; la corrección real de las versiones 2.3.32 y 2.5.10.1 cambió cómo se construía el error del multipart. La versión reforzada usa una clave de mensaje fija y pasa la cabecera solo como argumento posicional, de modo que se formatea como texto y nunca forma parte de una expresión. Además sustituye por un marcador cualquier valor que no parezca un tipo de medio, así que ni un cambio futuro del mensaje puede devolver la cabecera a una cadena evaluada.[3], [2], [6]

IMPLEMENTACIÓN VULNERABLE
import com.opensymphony.xwork2.TextProvider;
import javax.servlet.http.HttpServletRequest;

public final class UploadErrors {
    private final TextProvider text;   // resolves messages and evaluates expressions in them

    public UploadErrors(TextProvider text) {
        this.text = text;
    }

    public String describe(HttpServletRequest request) {
        String contentType = request.getHeader("Content-Type");
        // El encabezado sin procesar pasa a formar parte del mensaje que se evalúa.
        return text.getText("Invalid content type: " + contentType);
    }
}
PARCHE SEGURO Y ROBUSTO
import com.opensymphony.xwork2.TextProvider;
import java.util.Locale;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;

public final class UploadErrors {
    private static final Pattern MEDIA_TYPE = Pattern.compile(
        "^[a-z]+/[a-z0-9.+-]+(;\\s*[a-z0-9-]+=[a-z0-9._-]+)*$");
    private final TextProvider text;

    public UploadErrors(TextProvider text) {
        this.text = text;
    }

    public String describe(HttpServletRequest request) {
        String contentType = request.getHeader("Content-Type");
        boolean wellFormed = contentType != null && contentType.length() <= 256
            && MEDIA_TYPE.matcher(contentType.toLowerCase(Locale.ROOT)).matches();
        // Use una clave de mensaje fija; el encabezado solo se admite como argumento posicional,
        // y únicamente si tiene el formato de un tipo de contenido.
        return text.getText("upload.invalid.content.type",
            new String[] { wellFormed ? contentType : "(malformed)" });
    }
}

Lista de Verificación de Seguridad para Ingeniería

Fuentes