tutoriales · 12 min de lectura
XZ utils CVE-2024-3094: el backdoor que un mantenedor metió en tres años
Andres Freund encuentra el 29 de marzo un backdoor en xz-utils 5.6.0 y 5.6.1. El payload llega por un hook de build en m4/build-to-host.m4 que extrae un objeto precompilado de un archivo de test. El resultado modifica liblzma para interceptar RSA_public_decrypt en sshd. "Jia Tan" llevaba dos años y medio ganando trust.
· Manuel López Pérez · tutoriales


CVE-2024-3094 es un backdoor introducido en xz-utils por un mantenedor que entró al proyecto en octubre de 2021 y se ganó commit access en diciembre de 2022. Afecta a las versiones 5.6.0 (24 feb 2024) y 5.6.1 (9 mar 2024). CVSS 10.0. Andres Freund (PostgreSQL, empleado de Microsoft) lo encuentra el 29 de marzo de 2024 investigando un sshd lento en Debian sid mientras corre benchmarks de PostgreSQL.
El payload se entrega en un sitio que nadie miraba: dos archivos binarios en tests/files/ que un hook escondido en m4/build-to-host.m4 extrae y ejecuta solo en el momento del ./configure. La librería resultante, liblzma.so, hookea RSA_public_decrypt cuando sshd la carga indirectamente a través de libsystemd, el patch de systemd-notify que aplican Debian, Ubuntu, Fedora y derivados al paquete de OpenSSH.
No es una vulnerabilidad: es un backdoor a propósito, con dos años y medio de social engineering detrás.
Lab: el bug no se reproduce contra un sistema en producción; las distros revirtieron a 5.4.x en menos de 48 horas. El análisis técnico que sigue se basa en la divulgación de Andres Freund, el repositorio archivado de tukaani-project, el dump del payload de Filippo Valsorda y la timeline reconstruida por Russ Cox.
Timeline del mantenedor — Jia Tan / JiaT75
La parte más útil del caso no es el payload. Es la cadena de eventos que coloca a un mantenedor hostil con commit access en una librería que sshd carga indirectamente. Russ Cox publica una reconstrucción detallada en research.swtch.com/xz-timeline.
| Fecha | Evento |
|---|---|
| 2021-10-29 | Primer patch de Jia Tan a xz-devel: un .editorconfig inocuo. |
| 2022-02-07 | Primer commit merge con jiat0218@gmail.com como autor. |
| 2022-04 a -06 | Campaña de presión por sockpuppets: Jigar Kumar y Dennis Ens quejándose de la lentitud de Lasse Collin, mantenedor original. |
| 2022-06-08 | Lasse Collin admite en la lista problemas personales y de salud mental; señala que Jia Tan tendrá “un rol mayor”. |
| 2022-06-29 | Lasse Collin anuncia a Jia Tan como “prácticamente co-maintainer”. |
| 2022-10-28 | Jia Tan añadido a la organización GitHub tukaani-project. |
| 2022-12-30 | Direct commit access. |
| 2023-03-18 | Jia Tan publica su primera release (5.4.2). |
| 2024-02-23 | Se mergea código del backdoor en archivos binarios de test. |
| 2024-02-24 | Release 5.6.0 con build-to-host.m4 malicioso. |
| 2024-03-09 | Release 5.6.1 con payload actualizado. |
| 2024-03-25 | Hans Jansen — cuenta nueva, primer commit de ifunc en xz a principios de 2023 — abre un bug en Debian pidiendo subir 5.6.1 a unstable. |
| 2024-03-28 | Andres Freund reporta el hallazgo en privado a distros@vs.openwall.org. |
| 2024-03-29 | Divulgación pública en oss-security. |
Jigar Kumar y Dennis Ens no tienen historial fuera de los hilos de presión a Lasse Collin. Su único uso documentado es empujar a un mantenedor en crisis personal a delegar commit access. Russ Cox los identifica como cuentas operacionales del mismo actor que controla Jia Tan.
Dos años y medio entre primer patch y release backdoorada. Tres mantenedores ficticios distintos coordinados. Un commit de ifunc de Hans Jansen en 2023 que sirve de pieza de la cadena. No es un atacante oportunista; es una operación con paciencia.
El bug — payload entregado por hook de autotools
xz se distribuye como tarball generado con autotools. El tarball no es el repositorio git: incluye scripts generados (configure), macros m4 expandidas y archivos auxiliares que no están versionados. Esta brecha entre repo y tarball es el sitio donde Jia Tan mete el código.
El archivo m4/build-to-host.m4
build-to-host.m4 viene originalmente de gnulib. La versión upstream gestiona conversión de paths para builds cross-platform. La versión que viaja en los tarballs 5.6.0 y 5.6.1 reemplaza esa macro por una variante que, al final del ./configure, ejecuta un script construido al vuelo.
El script busca en el árbol de fuentes archivos que cumplan un patrón regex específico:
grep -aErls "#{4}\[\[:alnum:\]\]{5}#{4}$"Cuatro #, cinco caracteres alfanuméricos, cuatro # y final de línea. Los únicos archivos que cumplen ese patrón están en tests/files/:
tests/files/bad-3-corrupt_lzma2.xztests/files/good-large_compressed.lzma
Esto es lo elegante del ataque: para cualquiera que mire el repo casualmente, tests/files/ contiene archivos .xz corruptos a propósito que sirven para validar el parser. Ningún reviewer humano se va a sentar a hexdumpear binarios marcados como bad corrupt test data.
Extracción del payload
El script encadena tr, xz y RC4 con XOR sobre los bytes de los dos archivos. La cabecera “corrupta” del .xz es lo que la marca como descartable para el test; los bytes posteriores forman, tras varias rondas de des-XOR y descompresión, un objeto ELF precompilado.
El script verifica antes de extraer que se cumplen condiciones específicas. Reconstruido a partir del análisis de gynvael.coldwind.pl:
- Arquitectura
x86_64. - Linux con
gcc+ldGNU +glibc. - Build vía
dpkg-buildpackageorpmbuild. - Target del build no es Alpine (musl). No se activa con
clang.
Si las condiciones se cumplen, el script inyecta el objeto ELF extraído en el sistema de build de liblzma. El Makefile resultante encadena el objeto malicioso con el resto de la librería sin que aparezca en ninguna parte del source tree visible.
Lasse Collin publica el 30 de marzo una nota confirmando que ni el build-to-host.m4 ni los archivos de test del backdoor estaban en el repositorio git: solo en los tarballs que firmaba Jia Tan.
La librería resultante — IFUNC abusado
liblzma compilada con el payload se comporta como librería normal para todas las llamadas legítimas. La diferencia está en la inicialización: registra un resolver IFUNC que ejecuta antes de main.
IFUNC es una feature de glibc que permite a una librería decidir en runtime qué implementación de una función debe usarse según el CPU. Es legítimo: glibc lo usa para que memcpy salte a la implementación AVX2 si el procesador la soporta. El backdoor lo abusa: el resolver no devuelve una versión optimizada de una función de compresión. Lee la tabla de símbolos del proceso que está cargando liblzma, busca RSA_public_decrypt@plt de OpenSSL, y reemplaza el puntero por una función propia.
Si el proceso es sshd y está enlazado contra OpenSSL, cualquier autenticación por clave pública pasa por la versión maliciosa de RSA_public_decrypt.
El puente — libsystemd carga liblzma, sshd carga libsystemd
OpenSSH upstream no depende de libsystemd. No depende de liblzma. Si compilas sshd desde el tarball oficial de openssh.com, liblzma no se carga.
Lo que pasa en Debian, Ubuntu, Fedora, openSUSE y derivados es otra cosa: el paquete openssh-server de la distro aplica un patch downstream para integrar sshd con systemd-notify. El patch añade una dependencia de runtime sobre libsystemd. libsystemd está enlazada contra liblzma (para soportar journal comprimido). Eso significa que en cuanto sshd arranca, libsystemd se carga, y libsystemd carga liblzma. El resolver IFUNC del backdoor se ejecuta antes de que el proceso atienda ninguna conexión.
sshd (binario distro)
→ libsystemd.so (sd_notify para systemd)
→ liblzma.so (backdoor: registra IFUNC resolver)
→ modifica plt de RSA_public_decrypt en sshdOpenSSH upstream nunca tuvo esta cadena. Es producto del patch downstream + diseño modular de systemd. Damien Miller (mantenedor de OpenSSH) deja claro en su lista en abril que la cadena no existe sin el patch que distros añaden, y que sshd vanilla no expone esta superficie.
La activación — Ed448 + system()
La función RSA_public_decrypt hookeada recibe los datos que un cliente envía durante la autenticación SSH por clave pública. La versión legítima decodifica la firma RSA contra la clave pública del cliente. La versión maliciosa:
- Verifica que un campo específico del payload contiene una firma Ed448 válida sobre la clave pública del host.
- La clave pública Ed448 que valida está hardcodeada en el objeto malicioso. La privada solo la tiene el atacante.
- Si la firma valida, extrae bytes del payload y los pasa a
system()directamente. - RCE pre-auth como
root(sshd corre como root antes de derivar al usuario).
Diseño limpio:
- Solo el atacante puede activar el backdoor, porque solo él tiene la Ed448 privada.
- La firma se ata a la clave pública del host, así que un payload válido contra
host-a.exampleno sirve contrahost-b.example. - No deja firmas detectables en logs, porque la autenticación falla de cara al cliente (con la versión legítima detrás) si no se cumple la condición, y la ejecución se hace antes de que sshd registre el intento como aceptado.
Filippo Valsorda y otros analistas reconstruyen el detalle del payload durante el fin de semana del 30-31 de marzo. La pieza Ed448 + system() la confirman varios trazados independientes contra el objeto malicioso extraído del paquete 5.6.1.
Cómo lo encuentra Andres Freund
Freund estaba haciendo benchmarks de PostgreSQL sobre Debian sid. Notó dos cosas:
- Los logins por SSH consumían más CPU de lo normal.
valgrinddaba ruido nuevo sobreliblzma.
Mide tiempos: un login pasa de ~0.3 s a ~0.8 s. Hace strace, mira el call graph, llega a liblzma interviniendo en una ruta que no debería tocar autenticación. Reduce el caso, confirma el hook, escribe el reporte.
... since liblzma is a dependency of openssh's sshd on debian and many other
distros, this means that ssh logins (and many other things) are way slower
than they should be ... I assumed the system was very tight on memory, but
then valgrind started complaining ...— Andres Freund, oss-security, 29-mar-2024.
Si Freund no hubiera estado mirando latencias de SSH durante un benchmark, 5.6.1 habría llegado a stable de Ubuntu y Fedora en cuestión de semanas.
Detección — los trucos que aparecen el 29 y 30 de marzo
Mientras los maintainers de distro empiezan a revertir, varios analistas publican one-liners de detección. El más limpio se basa en una observación simple: liblzma no tiene nada que ver con OpenSSL. Si el binario menciona símbolos de OpenSSL, es porque está modificado.
# strings en liblzma legítima no debería mencionar OpenSSL ni RSA
strings /usr/lib/x86_64-linux-gnu/liblzma.so.5 | grep -i 'rsa\|openssl'Otro indicador es el tamaño: el liblzma.so.5.6.0 malicioso pesa ~100 KB más que el de 5.4.6. Vegard Nossum y otros publican comparativas hexdump del bloque que cambia.
Detección rápida por versión:
# Debian / Ubuntu
dpkg -l | grep xz-utils
# Fedora / RHEL
rpm -q xz-libs
# Cualquier distro con xz instalado
xz --versionCualquier 5.6.0 o 5.6.1 es vulnerable. 5.4.x y 5.6.2+ no.
Los scripts de detección “oficiales” (Red Hat, GitHub, Binarly) llegan en las 24 horas posteriores y comparan hashes contra una lista conocida.
Hashes y artefactos públicos
Hashes publicados por Red Hat y CISA para los binarios maliciosos:
| Archivo | SHA-256 |
|---|---|
xz-5.6.0.tar.gz (tarball upstream malicioso) | 0f5c81d545d5269d5d8c7f2447e44ac1d2d52a5bb2d6418dbc44de4204aaa600 |
xz-5.6.1.tar.gz (tarball upstream malicioso) | 2398f4a8e53345325f44bdd9f0cc7401bd9025d736c6d43b372f4dea77bf75b8 |
liblzma.so.5.6.0 (Debian sid amd64) | bf6f4a4f3fb29c5b04c2c8fd6abe2cefa3766fb20bd13c5a1e1c3a3e25e0fc1f |
Versiones limpias confirmadas: 5.4.6-1 (Debian estable), 5.4.5-1ubuntu0.2 (Ubuntu LTS), 5.4.6-3 (Fedora 39).
Regla YARA — detección estática
Regla pública de Binarly:
rule liblzma_xz_backdoor_3094
{
meta:
author = "Binarly + community"
cve = "CVE-2024-3094"
description = "Detecta liblzma 5.6.0/5.6.1 con hook a RSA_public_decrypt"
strings:
$sym_openssl = "RSA_public_decrypt" wide ascii
$ifunc_hook = { 48 83 fa 30 0f 84 ?? ?? ?? ?? 48 83 fa 31 }
$ed448_const = { f3 0f 1e fa 41 57 41 56 41 55 41 54 53 48 83 ec }
condition:
uint32(0) == 0x464c457f and
$sym_openssl and ($ifunc_hook or $ed448_const)
}El símbolo RSA_public_decrypt referenciado desde liblzma no aparece en ninguna versión limpia — la regla tiene falso positivo cero conocido.
Confirmación dinámica de exposición
El backdoor solo se activa si liblzma se carga vía libsystemd (que sólo lo hace sshd en distros que cargan libsystemd para notificación de socket activation):
# ¿libsystemd carga liblzma transitivamente?
ldd $(which sshd) | grep -E 'libsystemd|liblzma'
# El benchmark que disparó el descubrimiento (Andres Freund):
time ssh -i wrongkey user@localhost 2>/dev/null
# Versión limpia: ~50 ms hasta el rechazo
# Versión backdoor: ~500 ms hasta el rechazo (Ed448 verification overhead)Reproducción en lab cerrado
Para análisis estático sin riesgo, snapshot de Debian sid anterior al revert:
docker run --rm -it debian:sid-20240311-slim bash
# Dentro del contenedor:
apt-get update && apt-get install -y xz-utils
xz --version # Debe mostrar 5.6.0 o 5.6.1
strings /lib/x86_64-linux-gnu/liblzma.so.5 | grep -i 'rsa\|openssl'
# Si aparecen símbolos OpenSSL, el binario está modificadoPara análisis del m4/build-to-host.m4 que inyecta el payload durante ./configure, el archivo está disponible en el commit revertido del repo tukaani-project/xz en GitHub.
Mitigación — revert, no parche
La respuesta de distros fue uniforme: revertir a 5.4.x, no parchear sobre 5.6.x. Las razones:
- 5.6.0 y 5.6.1 ya tienen el payload incrustado. Reparar el
m4sin reemplazar los binarios deja el objeto ELF malicioso en la librería. - Las versiones de Jia Tan posteriores a marzo de 2023 (5.4.2 incluida) podrían contener piezas precursoras del payload que aún no se han identificado. La auditoría de las releases firmadas por Jia Tan sigue en curso meses después.
- 5.6.2, ya con Lasse Collin de vuelta al mando, sale en mayo de 2024 con todo lo de Jia Tan revisado y limpio.
Acciones operativas:
- Confirmar versión instalada con los comandos de arriba.
- Si es 5.6.0 o 5.6.1, downgrade inmediato vía el repositorio de la distro. Debian y Ubuntu publican paquetes revertidos el 29-30 de marzo.
- Rotar las host keys de sshd en cualquier servidor que haya tenido 5.6.x corriendo. Aunque no haya señal de activación, el modelo de amenaza incluye que el atacante haya emitido un payload contra esa máquina y se reserve el acceso.
- Revisar logs de sshd del periodo 24-feb a 29-mar buscando conexiones desde IPs anómalas. El backdoor no las marca como aceptadas, pero la actividad de red sí queda.
CISA publica advisory el 29 de marzo. Red Hat asigna el CVE-2024-3094 y publica análisis en su Security Center.
Lo que enseña
Tres cosas concretas, sin sentencia.
Trust en open source es trust en personas, no en código. Lasse Collin mantuvo
xzdurante 14 años. Cuando un actor con tiempo y paciencia llega, ofrece ayuda, mejora el código y monta sockpuppets para presionarte a delegar, el ataque que sigue ya no es técnico, es organizacional. La defensa pasa por modelos de gobernanza que no dependan de un único humano en crisis.El tarball y el repo son artefactos distintos. Las distros suelen construir desde tarball firmado por el mantenedor, no desde
git checkout. Los reviewers de código miran el repo. Esa asimetría es donde Jia Tan metebuild-to-host.m4. Reproducible builds que parten de tarball generan exactamente el mismo binario malicioso; reproducible builds que parten del repo no.liblzmano tenía que terminar ensshd. La cadenasshd → libsystemd → liblzmaes un efecto secundario del patch downstream de systemd-notify. Cada link que añade una distro entre un binario sensible y dependencias indirectas amplía la superficie. Los binarios distroless o las imágenes consshdenlazado desde upstream no eran vulnerables.
Andres Freund no pertenece a un equipo de seguridad. PostgreSQL es un proyecto de bases de datos. La diferencia entre xz-utils 5.6.1 llegando a Ubuntu LTS y siendo descubierto a tiempo es que alguien midió latencia de un benchmark y le hizo preguntas a la herramienta.
Referencias
- Andres Freund, oss-security: https://www.openwall.com/lists/oss-security/2024/03/29/4
- NVD CVE-2024-3094: https://nvd.nist.gov/vuln/detail/CVE-2024-3094
- Russ Cox, Timeline of the xz open source attack: https://research.swtch.com/xz-timeline
- Russ Cox, The xz attack shell script: https://research.swtch.com/xz-script
- Sam J. (thesamesam), xz-utils backdoor situation: https://gist.github.com/thesamesam/223949d5a074ebc3dce9ee78baad9e27
- gynvael.coldwind.pl, xz/liblzma: Bash-stage Obfuscation Explained: https://gynvael.coldwind.pl/?lang=en&id=782
- Red Hat Security advisory: https://access.redhat.com/security/cve/CVE-2024-3094
- Lasse Collin nota oficial: https://tukaani.org/xz-backdoor/
- Filippo Valsorda notas sobre el payload (Bluesky, 30-mar-2024): https://bsky.app/profile/filippo.abyssdomain.expert
- Wikipedia (referencias agregadas): https://en.wikipedia.org/wiki/XZ_Utils_backdoor


