CVE-2024-3094 — xz-utils backdoor (Jia Tan / liblzma)

CVE-2024-3094 — xz-utils backdoor (Jia Tan / liblzma)

TL;DR: A multi-year social-engineering and supply-chain operation embedded a covert SSH-server backdoor in liblzma (part of xz-utils). The build script for the tarball releases (not the git tree) injected the malicious payload via test fixtures during build, then redirected the RSA_public_decrypt@@… symbol resolution at runtime to attacker-controlled code. Discovered by Andres Freund noticing 500 ms of extra latency in SSH logins on Debian unstable. The defining supply-chain incident of 2024. Companion to npm-postinstall-and-typosquat-audit and python-pypi-supply-chain-audit.

Why this matters

  • It’s the closest a backdoor has come to mass-deploy in a base distro in recent memory.
  • The technique is social-engineering-first, not vuln-first: a maintainer was groomed for two years.
  • The build artifact differed from the git tree — a generic class of supply-chain risk no git audit catches.
  • Detection was incidental (latency); without that observation, the rollout would have hit stable distros.

The kill chain at a glance

  1. Social grooming — “Jia Tan” appeared in 2021, contributed quality patches, built reputation. Eventually took over maintenance from the burnt-out original maintainer.
  2. Innocuous-looking build infra changes — adjustments to m4/build-to-host.m4 and binary test fixtures.
  3. Backdoor lives in test fixtures — payload bytes shipped as tests/files/bad-3-corrupt_lzma2.xz and good-large_compressed.lzma blobs.
  4. Build script extracts and links — only in tarball releases, only on x86_64 Linux with glibc + systemd, only when building a Debian/Fedora-style RPM/deb.
  5. Runtime hook — modifies the GNU IFUNC resolver for RSA_public_decrypt. When sshd (linked against libsystemd which links liblzma) handles SSH pubkey auth, the hook intercepts.
  6. Authentication backdoor — supplies an attacker-controlled certificate signing key; valid signatures from attacker bypass SSH authentication.

The bug class: build-time supply chain

This is not a code vulnerability in the conventional sense. The published source on GitHub is benign. The tarball release that distros pulled differed. Build scripts assembled the payload from blob fixtures.

Class: release-tarball / build-script supply chain.

Related: see npm-postinstall-and-typosquat-audit for the npm equivalent (postinstall scripts assemble malware) and python-pypi-supply-chain-audit for PyPI.

Why it almost worked

  • The payload was conditional: only deployed when build matched expected target. Researchers / fuzzers running on different distros wouldn’t see it.
  • The payload was encrypted: the test fixtures decompress to garbage unless the extractor logic runs.
  • The trigger was passive: only activated when an attacker connected with the right certificate. No process visibility unless you knew what to look for.
  • The maintainer was trusted: the GitHub project had legitimate contributors and a real release cadence.
  • Tooling like reproducible-builds and SBOM was rolled out but not universally applied at distro level.

What detection caught it

Andres Freund (a Postgres maintainer at Microsoft) noticed sshd taking 500 ms longer per connection than expected. He traced the latency to liblzma, then to the tarball-only build script discrepancy. Disclosed quietly to distros, then publicly.

This is one engineer being curious about a 500 ms latency. It nearly didn’t happen.

What’s left in the codebase

The payload was caught before reaching most stable distros:

  • Hit unstable / testing in Debian, Fedora Rawhide, Kali, openSUSE Tumbleweed, Arch.
  • Did not reach Debian stable, Ubuntu LTS, RHEL.

But many derived rolling distros pulled it transiently. Operators still need to verify their build pipelines did not consume affected versions (5.6.0, 5.6.1).

Defensive lessons

  1. Reproducible builds at distro level. If the tarball doesn’t match the git tree’s deterministic build, fail.
  2. Bus-factor on critical OSS. xz-utils had one maintainer.
  3. Sigstore / SLSA / in-toto provenance attestation. The attacker still needs to produce signed provenance.
  4. Behavioural baselining of long-running services for latency / CPU changes after patches.
  5. Don’t ship binaries in tests. The vector relied on opaque test blobs that auditors had no reason to inspect.
  6. Audit GNU IFUNC resolver usage in security-critical libraries.

Reproducing in a lab

Pre-built VMs and reproductions exist (Akamai, JFrog, Binarly, Trail of Bits). Do not run on a production host.

Method:

  1. Pull a copy of the 5.6.1 tarball (mirrors).
  2. Build under the conditions the payload checks for (x86_64 + glibc + deb/rpm build).
  3. Strace sshd startup and observe the runtime hook installation.
  4. Connect with the published reproduction certificate (community-derived).

This is purely educational. The cert+payload pair was reverse-engineered after disclosure.

How a similar attack would look elsewhere

Other plausible vectors of the same shape:

  • A widely-included npm package with a maintainer takeover and obfuscated postinstall.
  • A PyPI package shipping a .whl that differs from the source sdist.
  • A Go module proxy serving a tarball that differs from the public git tag.
  • A Maven artifact with a malicious META-INF not in the source.

All of these have happened at small scale. xz was the warning shot for the scale-out version.

References