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 libsystemd → liblzma 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 libsystemd → liblzma 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:
- env/Dockerfile
- env/docker-compose.yml
- env/config/entrypoint.sh
- env/config/patch_ed448.py
- env/config/sshd_config
Bring up the environment:
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:
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):
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:
The client prints the crafted RSA certificate as a hex dump and then prints:
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:
What proves it worked. Success is confirmed by three independent checks performed via a privileged docker exec shell that the exploit does not control:
- The marker exists at the expected path (it was absent in the baseline check in Step 3).
- Its content equals the per-run nonce chosen in Step 2, ruling out any pre-planted file.
- The file is owned by
root:root, proving the command ran insidesshd'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:
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:
Mitigations and detection indicators¶
The following indicators and mitigations apply during the upgrade window:
- Kill switch: setting the environment variable
yolAbejyiejuvnup=Evjtgvsh5okmkAvjinsshd'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
TERMis set,LD_DEBUGorLD_PROFILEis set,LANGis unset,argv[0]is not exactly/usr/sbin/sshd, or a software breakpoint is present on an entry point. - Honeypot detection (
openssh.patch): thexzbotrepository 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 -efinside the sshd container may reveal a transientsh -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
sshdthat 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,nowsignificance, 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.patchhoneypot showing the exactRSA_public_decryptcall site inopenssh_RSA_verify. - Kaspersky Securelist: XZ Backdoor Story Part 1: rtld-audit hook mechanism,
_rtld_global_ro._dl_auditmodification, 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_decryptPLT patching logic.