Notes

CVE-2024-3094: The xz/liblzma Backdoor

How a multi-year social engineering campaign embedded a backdoor in xz Utils that hijacked OpenSSH authentication on Linux systems.

Why I wrote this

The xz backdoor is the most sophisticated supply-chain attack against open-source infrastructure to date — understanding its mechanics is essential for anyone building or defending software supply chains.

On March 29, 2024, Andres Freund — a PostgreSQL developer at Microsoft — posted a message to the oss-security mailing list that stopped the open-source world cold. He had traced a 500ms latency increase in SSH logins to a backdoor embedded in xz Utils, one of the most widely used compression libraries in the Linux ecosystem. The backdoor had been planted over the course of two years by a contributor who had patiently earned the trust of the project’s sole maintainer.

This is a teardown of what happened, how the backdoor worked, and what it means for the software supply chain.

What xz/liblzma Does

xz Utils is the reference implementation of the LZMA2 compression algorithm. It provides the xz and lzma command-line tools and, critically, the liblzma shared library. On Linux, liblzma is a foundational dependency — it sits beneath dpkg, rpm, systemd, the kernel’s initramfs tools, and hundreds of other packages.

The connection to OpenSSH is indirect but real. On many Linux distributions, OpenSSH is patched to integrate with systemd for notification and socket activation. That patch links sshd against libsystemd, which in turn links against liblzma. The result is that sshd — the process that handles every SSH login — loads liblzma into its address space at startup.

This dependency chain is what made the backdoor possible. The attacker did not need to compromise OpenSSH or systemd. Compromising the compression library at the bottom of the stack was enough.

The Social Engineering Campaign

The attack began not with code but with patience.

2021: First Contact

A GitHub account named JiaT75 (Jia Tan) began submitting small, legitimate patches to the xz Utils repository. The patches were real improvements — bug fixes, test updates, documentation cleanups. They were the kind of contributions that any project maintainer would welcome.

At this time, xz Utils was maintained by a single person: Lasse Collin. Like many critical open-source projects, xz had no corporate sponsor, no security team, and no second pair of eyes on the code. Collin maintained it in his spare time.

2022: Pressure Campaign

In mid-2022, sockpuppet accounts began appearing on the xz-devel mailing list, pressuring Collin to add a co-maintainer. The messages were carefully crafted to seem like independent community members frustrated with slow patch reviews. One account, “Jigar Kumar,” repeatedly complained about the pace of development and suggested Jia Tan as a trusted contributor who could share the load.

Insight

Maintainer burnout as an attack surface

This is the part of the attack that should concern every organization that depends on open source. Collin was a solo maintainer under real stress, doing unpaid work on infrastructure that millions of systems depend on. The attacker identified that burnout — not a technical flaw — was the weakest point in the system and applied social pressure to exploit it.

Collin, under increasing pressure and dealing with personal health issues, gradually gave Jia Tan more responsibility. By late 2022, Jia Tan had commit access. By early 2023, Jia Tan was effectively the co-maintainer of xz Utils, with the authority to merge code, tag releases, and manage the build infrastructure.

2023-2024: Payload Delivery

With commit access secured, Jia Tan began making changes that would only make sense in retrospect. Test fixture files were added to the repository — binary .xz files ostensibly used by the test suite. The build system’s autotools configuration was modified in subtle ways. Each change was small, plausible, and reviewed in isolation. The full picture only emerged when the pieces were assembled.

In February 2024, Jia Tan tagged the 5.6.0 release of xz Utils. The backdoor was live.

Technical Mechanism

The backdoor’s design is notable for its layered obfuscation. No single file in the repository contained recognizable exploit code. The malicious payload was distributed across multiple components that only combined during the build process.

Stage 1: Obfuscated Payload in Test Fixtures

Two binary files were added to the repository as test fixtures:

tests/files/bad-3-corrupt_lzma2.xz
tests/files/good-large_compressed.lzma

These files appeared to be standard test cases for the compression library — corrupted and valid archives used to verify error handling. In reality, they contained an obfuscated binary payload. The data was hidden using a combination of xz compression, RC4-like decryption via AWK scripts, and careful byte alignment so that standard analysis tools would see them as ordinary compressed data.

Stage 2: Modified Build Script

The key enabler was a modification to build-to-host.m4, an autotools macro file used during the ./configure step. The modification added a block of shell code that ran during the build process:

# Simplified representation of the extraction logic
if test -f tests/files/bad-3-corrupt_lzma2.xz; then
  # Extract obfuscated script from test fixture
  xz -d < tests/files/bad-3-corrupt_lzma2.xz |
    # Process through tr and AWK to decode
    tr "\t \-_" " \t_\-" |
    xz -d |
    /bin/bash
fi

The actual obfuscation was more complex, involving multiple layers of character substitution, shell variable expansion, and conditional logic designed to evade code review. The script only executed under specific conditions:

  • Building on Linux (not macOS, BSD, or Windows)
  • Building for x86-64 architecture
  • Building with GCC
  • Building in a Debian or RPM packaging context (the dpkg-buildpackage or rpmbuild environment variables were checked)

This meant the backdoor would not trigger during a developer’s local build or in CI systems — only during the official package build process used by distribution maintainers.

Stage 3: IFUNC Hooking

The extracted payload was a shared object that got linked into liblzma.so during the final build step. At runtime, it used the GNU IFUNC (Indirect Function) mechanism to intercept function resolution.

IFUNC is a legitimate ELF feature that allows a library to select different function implementations at load time — for example, choosing an AVX2-optimized code path on hardware that supports it. The backdoor abused this mechanism to redirect the resolution of RSA_public_decrypt — a function provided by OpenSSL and used by OpenSSH during RSA key verification.

When sshd started, it loaded liblzma.so (via the libsystemd dependency chain). The IFUNC resolver in the compromised liblzma ran before main(), patching the Global Offset Table (GOT) to redirect calls to RSA_public_decrypt through the attacker’s code.

Normal sshd startup:
  sshd → libsystemd.so → liblzma.so (clean)
  RSA_public_decrypt → OpenSSL implementation

Compromised sshd startup:
  sshd → libsystemd.so → liblzma.so (backdoored)
  IFUNC resolver hooks RSA_public_decrypt at load time
  RSA_public_decrypt → attacker's shim → OpenSSL (if not triggered)

Warning

Why this is hard to detect at runtime

The IFUNC hook is established during the dynamic linker’s symbol resolution phase, before the process reaches main(). By the time any monitoring tool or intrusion detection system inspects the running sshd process, the GOT entry already points to the attacker’s code. The hook is invisible to ldd, strace on normal operations, and most file integrity monitoring tools because liblzma.so itself is the signed, package-manager-installed binary.

Exploitation Path

The backdoor enabled pre-authentication remote code execution on affected SSH servers where sshd loaded the compromised liblzma path (typically through distribution-specific OpenSSH/systemd integration).

The Trigger

When a client connected to the compromised sshd and presented an SSH certificate during authentication, the hooked RSA_public_decrypt function inspected the certificate’s public key field. If the key contained a specific magic byte sequence — effectively a cryptographic signature that only the attacker could produce — the backdoor extracted a command payload from the certificate data, decrypted it, and executed it.

The flow looked like this:

  1. Attacker connects to target SSH server
  2. Attacker presents a specially crafted SSH certificate containing an encrypted command in the RSA public key field
  3. sshd calls RSA_public_decrypt to verify the certificate
  4. The hooked function intercepts the call, checks for the magic bytes
  5. If the trigger is present: decrypt the embedded command payload using a key derived from the attacker’s Ed448 public key, execute it via system(), and return an authentication failure to the SSH layer (hiding the connection from logs)
  6. If the trigger is absent: pass through to the real RSA_public_decrypt and behave normally

The use of Ed448 — a less common elliptic curve — as the authentication mechanism for the backdoor trigger meant that only someone possessing the corresponding private key could activate the backdoor. Even if the backdoor’s existence was known, it could not be exploited by third parties without that key.

Tip

Checking if your sshd links liblzma

# Check if sshd dynamically links liblzma
ldd $(which sshd) | grep liblzma

# If you see output like:
#   liblzma.so.5 => /usr/lib/x86_64-linux-gnu/liblzma.so.5
# then sshd loads liblzma and was potentially vulnerable

# Check the installed xz version
xz --version

Not all distributions patch OpenSSH to link against libsystemd. If ldd shows no liblzma dependency, your sshd was not affected regardless of the xz version installed.

Detection

Affected Versions

The backdoor was present in xz Utils versions 5.6.0 and 5.6.1. These versions were released in February and March 2024, respectively. The window of exposure was narrow — roughly five weeks — because Andres Freund’s discovery came before most stable distributions had picked up the new releases.

Distributions that shipped the affected versions in their testing or unstable channels included Fedora 40 (pre-release), Debian Sid, openSUSE Tumbleweed, and Arch Linux. Stable releases of Debian, Ubuntu LTS, RHEL, and CentOS were not affected because they had not yet adopted 5.6.x.

Version and Hash Verification

# Check installed version
xz --version
# Affected: xz (XZ Utils) 5.6.0 or 5.6.1
# Safe: 5.4.x or earlier, 5.6.1+revert builds

# Check package hash (Debian/Ubuntu)
dpkg -l | grep xz-utils
sha256sum /usr/lib/x86_64-linux-gnu/liblzma.so.5

# Check package hash (Fedora/RHEL)
rpm -qa | grep xz
rpm -V xz-libs

Wazuh Detection Rules

If you run Wazuh, you can deploy rules to detect the affected package versions across your fleet:

<rule id="100200" level="14">
  <decoded_as>dpkg</decoded_as>
  <regex>xz-utils.*(5\.6\.0|5\.6\.1)</regex>
  <description>CVE-2024-3094 — Backdoored xz-utils version detected</description>
  <mitre>
    <id>T1195.002</id>
    <tactic>Initial Access</tactic>
  </mitre>
</rule>

<rule id="100201" level="14">
  <decoded_as>rpm</decoded_as>
  <regex>xz-libs.*(5\.6\.0|5\.6\.1)</regex>
  <description>CVE-2024-3094 — Backdoored xz-libs version detected (RPM)</description>
  <mitre>
    <id>T1195.002</id>
    <tactic>Initial Access</tactic>
  </mitre>
</rule>

For broader detection, use Wazuh’s SCA (Security Configuration Assessment) module to check the installed version across all agents:

<check>
  <title>CVE-2024-3094: xz backdoor version check</title>
  <condition>none</condition>
  <rules>
    <rule>c:xz --version -> r:5\.6\.0|5\.6\.1</rule>
  </rules>
</check>

Tip

Detecting the liblzma link in sshd

You can also use Wazuh’s command monitoring to check whether sshd links liblzma:

<localfile>
  <log_format>full_command</log_format>
  <command>ldd $(which sshd) | grep liblzma</command>
  <frequency>86400</frequency>
</localfile>

If the command produces output, the agent’s sshd loads liblzma and should be checked for the affected version.

Remediation

Immediate Actions

Downgrade xz Utils to a version in the 5.4.x series. Distribution maintainers pushed emergency packages within hours of the disclosure:

# Debian/Ubuntu
sudo apt update && sudo apt install xz-utils=5.4.5-0.3

# Fedora
sudo dnf downgrade xz xz-libs

# Arch Linux
sudo pacman -U /var/cache/pacman/pkg/xz-5.4.6-1-x86_64.pkg.tar.zst

Restart sshd after the downgrade. The compromised liblzma is loaded into the running sshd process — replacing the package on disk does not neutralize a running instance:

sudo systemctl restart sshd

Verify the downgrade:

xz --version
ldd $(which sshd) | grep liblzma
# Confirm the linked liblzma version is 5.4.x

Longer-Term Actions

  • Rebuild from known-good source. If you build xz from source (e.g., for embedded systems or containers), pin to the 5.4.x branch and verify the Git commit signature. The malicious code was present in the release tarballs but was partially obscured in the Git repository — the build-to-host.m4 modifications were injected into the tarball generation process and did not appear in the version-controlled source in their final form.
  • Verify package signatures. This backdoor was distributed through the official release tarballs, which were signed by Jia Tan’s GPG key. In this case, the signature verified correctly — the attacker controlled the signing key. This is a fundamental limitation of signature-based trust: signatures prove authorship, not intent. Post-incident, Lasse Collin revoked Jia Tan’s key and re-signed subsequent releases.
  • Audit the dependency chain. Review whether your sshd links liblzma at all. If your distribution does not patch OpenSSH for systemd integration, you were never exposed. Consider whether that patch is necessary in your environment.

How It Was Caught

Andres Freund was benchmarking PostgreSQL when he noticed SSH logins to his Debian Sid test machine were taking roughly 500ms longer than expected. That is an unusual amount of latency for a local connection. Most people would dismiss it as network noise or a systemd service starting slowly. Freund investigated.

He used perf to profile the sshd process and found that a disproportionate amount of CPU time was being spent in liblzma during SSH authentication — a library that should have no role in that code path. He traced the execution, examined the library, and found the IFUNC hook.

Insight

Performance regressions as a detection signal

The backdoor was not caught by code review, dependency scanning, signature verification, or any automated security tool. It was caught because a developer with deep systems knowledge noticed a half-second anomaly and had the curiosity and skill to trace it to its source. This is both inspiring and sobering. The entire security of the Linux supply chain, in this instance, depended on one person’s attention to performance details.

Freund’s disclosure to the oss-security mailing list triggered an immediate, coordinated response across the Linux ecosystem. Within 24 hours, every major distribution had reverted to safe versions of xz Utils, and CISA issued an emergency advisory.

Lessons for the Supply Chain

Maintainer burnout is a systemic risk

The xz backdoor was possible because a critical piece of infrastructure was maintained by one person, unpaid, with no organizational support. The attacker identified this as the weakest link and spent two years exploiting it. Any serious approach to supply chain security must address maintainer sustainability — not just code signing and SBOMs.

Reproducible builds would have caught this

The backdoor was injected during the tarball generation process, meaning the release tarball did not match what a clean build from the Git source would produce. Reproducible builds — where anyone can rebuild a package from source and get a byte-identical result — would have flagged this discrepancy immediately. Few distributions enforce reproducible builds today. That needs to change.

SBOMs are necessary but not sufficient

A Software Bill of Materials would have told you that your system included xz Utils 5.6.0. That is useful for incident response — you can quickly identify affected systems. But an SBOM would not have prevented the backdoor from being included. SBOMs describe what is present; they do not attest to its integrity.

Dependency depth is attack surface

The fact that sshd loaded liblzma at all — through two levels of indirection via libsystemd — is a design issue. Every transitive dependency in a security-critical process is an attack surface. Minimizing the dependency tree of privileged services reduces the number of libraries an attacker can target. Some distributions have since reconsidered the systemd-notify patch for OpenSSH.

Trust is earned slowly and exploited quickly

Jia Tan spent two years building credibility. The contributions were real, the code reviews were thorough, and the persona was consistent. No automated tool would have flagged anything suspicious. The attack exploited the social dynamics of open-source development — the same dynamics that make open source work in the first place. There is no easy technical fix for this. The best defenses are institutional: multiple maintainers, mandatory multi-party review for releases, and organizational investment in the projects that underpin the ecosystem.


The xz backdoor was caught by luck and skill, five weeks before it would have reached stable distributions used by millions of servers. The next supply-chain attack may not be caught at all. The lesson is not that open source is broken — it is that the infrastructure supporting open source is under-resourced, and the consequences of that are now visible.