Skip to content
pwnsy
threat-inteladvanced#xz-utils#supply-chain#open-source#incident-analysis#case-study

The xz-utils Backdoor: A Two-Year Campaign Against a Maintainer

How CVE-2024-3094 worked: a backdoor hidden in release tarballs rather than the git repo, injected during build, hooking sshd through an IFUNC resolver, after a multi-year campaign for commit access.

Someone spent about two years becoming a trusted maintainer of a compression library in order to put a backdoor into every Linux server that runs OpenSSH.

They almost succeeded. The malicious versions reached the testing branches of several distributions in early 2024 and were weeks away from flowing into the stable releases that run most of the internet's infrastructure. What stopped it was a developer noticing that ssh logins were using about half a second more CPU than they should.

The technical construction is worth studying, and the part that matters most is the part before the code: the campaign that produced commit access in the first place.

Facts here come from the original disclosure on the oss-security mailing list, CISA's alert, and the affected distributions' own advisories. The contributor account operated under a pseudonym and no charges have been brought, so this page refers to the account rather than to any individual.

Timeline

DateEvent
2021 onwardA new contributor account begins submitting patches to xz-utils
2022Pressure appears on the mailing list from other accounts, pushing the original maintainer, who had described his own limited capacity, to hand over more responsibility
2022 to 2023The contributor account gains commit rights and becomes a co-maintainer
February 2024xz 5.6.0 is released, containing the first stage
March 20245.6.1 follows. Both reach rolling and testing distributions
29 March 2024The backdoor is reported publicly. CISA issues an alert. Distributions roll back

The interesting number in that table is the gap between the first contribution and the payload: roughly two years of legitimate work, which is a level of patience that most threat models do not contemplate.

The social engineering

The technical vehicle was xz-utils, a compression library that almost nothing depends on directly and almost everything depends on indirectly. It was maintained by one person, unpaid, who had publicly described struggling with the workload and with his own health.

The publicly visible pattern went like this. A new contributor appeared and submitted useful patches over months. Separately, other accounts appeared on the mailing list complaining about slow releases and unaddressed issues, and suggesting that maintenance should be handed to someone with more time. The named candidate was the helpful contributor. Eventually commit rights were granted.

None of the individual steps is unusual. Projects want new contributors. Users do complain about release cadence. Exhausted maintainers do hand over responsibility, and that is normally a good outcome. Assembled deliberately over two years, the same sequence is an access operation.

That is the uncomfortable finding: the entry point was not a technical control that failed. It was the ordinary social process by which open source projects recruit help, applied to a maintainer who was under-resourced and struggling.

The technical construction

Several layers, each chosen to survive a different kind of review.

The repository and the tarball were different. Distributions build from release tarballs rather than from a git checkout, because tarballs contain generated build scripts. The malicious content was in the tarball and not in the repository. Every reviewer looking at GitHub was looking at clean code that was not the code being built.

The payload looked like test data. The project contains binary test files representing corrupted compressed data, because a compression library needs to handle malformed input. Additional files of that kind were added, and their contents were the stages of the backdoor. Binary test fixtures are the one part of a repository nobody reads.

The build script assembled it. A modification in the build tooling, in a place produced during release preparation, extracted and decoded content from those test files during compilation and linked the result into liblzma. The activation was conditional: it checked that it was building on the right architecture, under the right toolchain, and for a distribution-style package build, so a developer building locally would not trigger it.

It hooked function resolution. The library used an IFUNC resolver, a mechanism where a program decides at load time which implementation of a function to use, typically to select a CPU-optimised variant. Running there means running very early in process startup, before most things are initialised. The backdoor used that position to redirect a function used in the OpenSSH authentication path.

The target was sshd through systemd. OpenSSH does not link liblzma. On distributions that patch sshd to support systemd notification, it links libsystemd, which pulls in liblzma. That indirect path is what put the compression library inside the authentication process on exactly the systems worth attacking.

Access was gated by a key. The implant checked for a payload signed with a specific Ed448 key and ignored anything else, passing execution back to the real function. Without the private key, the backdoor does nothing observable. This is what separates it from a vulnerability: it was an entrance for one actor, not a flaw anyone could use.

How it was found

By a performance anomaly. A developer running benchmarks noticed that ssh logins were consuming noticeably more CPU than expected, on the order of half a second, and saw unexplained errors under a memory checking tool. Following that thread led into liblzma and then into the backdoor.

It is worth being honest about what that means. The construction was designed to survive code review, and it did. It was designed to be invisible in the repository, and it was. What it could not hide was that hooking a hot function costs measurable time, and one person happened to be measuring, happened to care, and happened to keep pulling.

Nobody should build a strategy on that recurring. The next campaign will be slightly more careful about performance.

Why the damage was limited

Timing, mostly. Versions 5.6.0 and 5.6.1 had reached rolling and testing distributions such as Fedora Rawhide, Debian testing and unstable, openSUSE Tumbleweed, Arch and Kali. Most stable enterprise distributions move slowly enough that they had not yet picked the versions up.

Had it gone unnoticed for another few months, the affected versions would have flowed into stable releases and into the base images that most container builds inherit, and the eventual population would have been most Linux servers running OpenSSH.

Response was fast: distributions rolled back to 5.4.x within days, CISA published an alert, and detection scripts circulated for identifying affected binaries.

What generalises

The artefact you ship must be the artefact you reviewed. This is the same structural gap as SolarWinds and SUNBURST, reached by a different route. There the divergence happened on the build server; here it was baked into the release tarball. Reproducible builds and building from version control rather than from generated tarballs both close it, and both remain uncommon.

Trust in open source is trust in people, and people can be worked on. Code review catches mistakes. It does not reliably catch a determined contributor who has spent two years earning the right to commit and who hides the payload where reviewers do not look.

Maintainer burnout is a supply chain vulnerability. A critical library maintained by one unpaid person under stress is a security problem in the same register as an unpatched CVE. The attack used the handover process directly. Funding and staffing maintenance of widely used infrastructure is a security control, which is an unusual sentence and an accurate one.

Binary files in a repository deserve scrutiny. Test fixtures, sample data, images and archives are the parts nobody reads, and they were the hiding place here.

Indirect linking creates surprising surfaces. Nobody thought of a compression library as part of the authentication path. It got there through a distribution patch and a transitive dependency, which is exactly how Log4Shell reached applications that never chose Log4j.

The verdict

A patient adversary spent two years acquiring the right to commit to a small library, hid the payload in files that looked like test data, activated it in the build rather than in the source, and reached the authentication path of every server on the distributions that shipped it. A developer noticed half a second of CPU time and the whole thing unravelled.

The response is structural rather than heroic: build from version control, make builds reproducible so the artefact can be checked against the source, look at the binary blobs, and stop leaving critical infrastructure to one exhausted volunteer.

Sources & further reading