ChainDrop Took 444 npm Packages With 2 Billion Monthly Downloads, Signed Them Legitimately, and Reads Its C2 From an Ethereum Contract. We Shipped the Ingest for That Indicator Class Two Days Ago.
- Patrick Duggan
- 3 minutes ago
- 5 min read
On August 4, researchers identified a Shai-Hulud variant — ChainDrop — that had trojanized 444 npm packages across multiple publishers, collectively pulling roughly two billion downloads a month. By 13:20 UTC that day, 2,212 malicious versions had been catalogued.
The interesting parts are not the size. They are the four places this campaign refuses to behave the way defences expect.
It came in through the front door, signed
The attackers compromised the GitHub account of the maintainer behind keyv, cacheable, flat-cache and file-entry-cache. They pushed malicious code to the main branch, and then triggered the maintainer's own GitHub Actions pipelines to build and publish the infected versions to npm.
The releases carry valid digital signatures, because the legitimate pipeline produced them. Provenance attestation says exactly what it should say: this artifact was built by this repository's own CI. That is true. It is also worthless here, because the input to that pipeline was already poisoned.
The headline versions and their weekly download counts:
keyv 6.0.0 at 153.7M · flat-cache 6.1.24 at 149.9M · file-entry-cache 11.1.6 at 147.6M · cacheable-request 13.0.20 at 34.0M · cache-manager 7.2.10 at 4.3M.
Nobody chose these packages. They are caching primitives that arrive four levels deep in a dependency tree nobody reads.
It reads its C2 from a smart contract
This is the part we care most about, and it is the fifth instance of a class we started tracking a month ago.
ChainDrop does not carry a hardcoded C2 domain. The payload calls an Ethereum mainnet contract — 0xE1f2395ee43e45A1556EC6438a88c31B83493103, function selector 0x53ed5143 — trying 75 public RPC endpoints in order, and AES-decodes a list of C2 domains from the response. The technique has a name now: EtherHiding.
Everything we wrote about DeadLock's Polygon contracts on Wednesday applies here. It is an eth_call: read-only, no transaction, no gas, no on-chain trace of the fetch. There is no domain to seize because the address of record is a contract that thousands of nodes replicate, and the operator rotates C2 on-chain without recompiling or redeploying anything.
And the same asymmetry holds. The read is invisible; every write is permanent and public. A defender cannot take that contract down, but can watch it — for free, from any public RPC endpoint — and see the new C2 list the moment the operator publishes it, before it is used.
Trying 75 RPC endpoints in sequence is also, incidentally, loud. A developer workstation walking a list of public Ethereum RPC providers is not a normal thing for a build to do.
We shipped the ingest for this on Wednesday
Two days ago we added chain-address as a first-class IOC type to our ingest path, because we had defined on-chain dead-drops as an indicator class in July, written a standing process to harvest them, published on them three times — and discovered our own feed had been silently rejecting every such write as an invalid type.
We fixed it Wednesday and backfilled the four addresses we already had.
Today ChainDrop's contract went in through that same path, verified by read-back, alongside the exfil domain, three payload hashes and the trojanized package set. That is the fifth on-chain dead-drop in our index and the first to appear inside a self-propagating worm rather than a targeted loader.
We are noting the timing because it is a fair test of whether a capability was real or performative. Built Wednesday, used Friday, on the largest npm compromise of the year.
It brings its own runtime
The dropper downloads the official Bun runtime, version 1.3.13, from GitHub releases, and uses Bun to execute the 727 KB stage-2 payload.
Read that again as a defender. The malicious traffic is a download from github.com/oven-sh/bun/releases — a legitimate release URL for a legitimate, popular JavaScript runtime. Then execution happens under Bun rather than Node, which means every detection rule watching Node process trees for suspicious child_process behaviour is looking at the wrong parent.
Living off the land used to mean PowerShell and certutil. It now means fetching a well-regarded open-source runtime from its official distribution point and running your payload inside it.
It steals the keys that let it spread, and it goes to memory for them
Stage two searches for npm tokens with full write privileges, plus cloud keys and secrets, in shell configuration files, environment variables, and live process memory.
That last location matters. A team that moved secrets out of .env files and into a secrets manager injected at runtime has done the right thing and is still exposed here, because the secret is in memory by definition once the process holds it.
Then it self-propagates with no further attacker action: it enumerates every package the stolen token can publish, and republishes them. One compromised maintainer becomes their entire portfolio. That is the Shai-Hulud worm behaviour, and it is why 444 packages fell out of what began as a single account compromise.
There is also a fallback: if the on-chain lookup fails, the payload finds C2 by searching GitHub commits for the literal marker thebeautifulmarchoftime. We have added that string and its sibling to our hunting signatures, because no legitimate code contains it.
Two things worth saying plainly
Provenance is not integrity. Signed, attested, built-by-CI artifacts were the whole promise of supply-chain hardening, and ChainDrop satisfied all three by compromising the thing upstream of the signature. Attestation proves where an artifact was built. It cannot prove the source that went in was clean. Anyone selling provenance as the answer to supply-chain risk should be asked what it does about this case.
Opening the branch is enough. Reporting indicates that opening an infected Git branch in VS Code or Claude Code can bring a repository under ChainDrop's control — the editor and agent tooling execute project configuration as part of doing their job. We have written this pattern three times now under different names. If your threat model for "I checked out a branch to look at it" is that nothing happens, update it.
What to do
Check whether the versions above are in your lockfiles — including transitively, which is where all of these live. Rotate npm tokens and any cloud credential that has been in the environment of a build that ran in the last two weeks; assume memory-resident secrets were reachable. Look in egress logs for npm-cache.com and for build agents contacting public Ethereum RPC endpoints. And watch that contract address rather than hoping somebody takes it down.
Credit for the analysis goes to StepSecurity, Elastic Security Labs, OPSWAT and the npm community researchers who caught this on the day. We did not find it. What we did was have somewhere to put it — indicators live in our feed now, pullable and blockable, which is the only part of this we consider our job.
Ninety-five percent, as always.
Every indicator in this post is in the feed. Free.
1.58M+ IOCs, STIX 2.1 / TAXII, 88% novel vs ThreatFox, exploited-CVE leads ahead of CISA. No credit card — a free API key in 30 seconds, and you can audit every claim above against the live endpoints.
Was this useful? Thirty seconds, no cookies, no tracking, no third parties, your address hashed and never stored. If the box below does not load, the same question lives at https://analytics.dugganusa.com/nps.html?post=chaindrop-took-444-npm-packages-with-2-billion-monthly-downloads-signed-them-legitimately-and-read




Comments