indexed-btree + 9 sibling npm packages: runtime-triggered loader, Ethereum Sepolia smart-contract C2 (Checkmarx disclosure)
Checkmarx disclosed on 2026-09-17 that npm user charlessadler25 had published indexed-btree (impersonating sorted-btree) plus 9 sibling packages, together reaching ~2M weekly downloads before npm removed them on 2026-09-03. The loader lives inside BTree.prototype.set() - not a lifecycle hook - and resolves its C2 by polling an Ethereum Sepolia smart contract (EtherHiding), exfiltrates via Slack and Telegram, and is tied to a wallet holding 109 ETH.
- Incident type
- Malicious package. A package that was malicious from its first publish under its own name - a fake tool, a lure used in a social-engineering campaign, or a trojanised utility - rather than a copy of an existing name.
- Detected by
- Checkmarx Zero · Socket · Snyk
- Also known as
- indexed-btree campaign · charlessadler25 npm operator · sorted-btree impersonation · Sepolia smart-contract C2 (EtherHiding) npm
- Ecosystems
- npm
- Packages tracked
- 10
What happened
On 2026-09-17 Checkmarx published npm btree malware campaign affects millions of downloads, no need for install script, disclosing that npm user charlessadler25 had operated a 10-package supply-chain campaign for 11 weeks. Follow-on reporting by The Hacker News, SecurityWeek, BleepingComputer, and InfoWorld ran through 2026-09-22. This is the first widely-analysed sample in this corpus of the runtime-triggered loader class: the loader is not in a preinstall, postinstall, or any other lifecycle hook - it lives inside a normal library method (BTree.prototype.set()) that only fires when consumer application code calls it.
The campaign
| Package | First published | Notes |
|---|---|---|
indexed-btree | 2026-06-18 | The headline package; impersonates sorted-btree (a well-known B-tree by David Piepgrass). Peak ~2M weekly downloads, ~1.5M weekly at removal |
btree-core | 2026-06-18 | >1.9M downloads by removal |
ordered-kv-index | 2026-06-18 | Hundreds of thousands of downloads |
btree-leaderboard | 2026-06-18 | Hundreds of thousands of downloads |
priority-slot-queue | 2026-06-18 | Hundreds of thousands of downloads |
btree-range-store | 2026-06-18 | Hundreds of thousands of downloads |
btree-time-index | 2026-06-18 | Hundreds of thousands of downloads |
btree-lru-cache | 2026-06-18 | Hundreds of thousands of downloads |
neighbor-key-map | 2026-06-18 | Hundreds of thousands of downloads |
sliding-score-window | 2026-06-18 | Hundreds of thousands of downloads |
All 10 packages were first uploaded on 2026-06-18 by npm user charlessadler25 (avatar was AI-generated). npm removed them on 2026-09-03 after Checkmarx notified the registry.
Attack mechanics
Runtime-triggered loader: the malicious file extended/sharedLoad.min.js is required into the codebase, and the code inside BTree.prototype.set() calls it when the incoming key matches a specific expected value. Because there is no install-time trigger, npm install --ignore-scripts and every install-script static analyser reports the package clean.
EtherHiding for C2 resolution: the loader does not hardcode a domain. It polls a smart contract deployed on the Ethereum Sepolia testnet via public Sepolia RPC endpoints, calling a getter function that returns the current C2 address. When defenders take down a C2 host, the operator calls the smart-contract setter to publish a new address, and every running installation picks it up on the next poll cycle. Domain-based egress denylists are worthless against this scheme; the only actionable egress block target is the set of Sepolia RPC endpoints themselves, which are legitimate infrastructure that many other applications share.
Exfil via Slack + Telegram: once the C2 endpoint is resolved, the loader fingerprints the host (hostname, username, OS, current working directory, Node.js version, npm user-agent, environment metadata) and posts the details to a hardcoded Slack channel and a Telegram bot. A second-stage payload is unlocked using shared-secret cryptography against material returned from the Sepolia contract; the second stage is opaque to defenders without that on-chain key.
Repository vs registry divergence: the extended/sharedLoad.min.js file that carries the loader was present only in the npm tarballs, not in the public GitHub repository that charlessadler25 maintained. A developer who reviewed the linked source repo before adopting saw benign code; the running package used files that did not exist in the mirror. Any due-diligence workflow that inspected the source repo without checking the actual npm tarball missed the entire attack surface.
Attribution and profits
Checkmarx traced attacker profits to an Ethereum wallet holding roughly 109 ETH (~€230,933 at time of writing). No named threat-actor attribution has been published; the operator profile (11-week patience, EtherHiding, runtime-triggered loader, multi-package staging under one npm user) is consistent with a professional supply-chain group rather than a smash-and-grab operator.
Cross-reference
GHSA-f3xx-wvxc-5vh4 covers indexed-btree with GitHub's CWE-506 boilerplate ("any computer that has this package installed or running should be considered fully compromised"). MAL-2026-15888 in the OSV corpus mirrors it. The remaining 9 sibling packages were removed from npm without individual GHSA advisories at time of writing - Checkmarx' blog post is the authoritative list.
The EtherHiding technique is the same class of blockchain-hosted C2 that Wiz's pyservercheck (2026-09-01 sweep) and the PolinRider Packagist campaigns (visanduma/nova-two-factor, 2026-09-17) use, but this is the first sample this corpus records where the loader sits inside a normal library runtime method rather than in an install or lifecycle hook. Expect other operators to copy the technique in the coming months.
Registry state
All 10 packages are removed from npm at time of writing. Private mirrors (Verdaccio, Artifactory, Nexus) that cached tarballs before 2026-09-03 will still serve the malicious versions to internal consumers. Rebuild lockfiles against a mirror synced after 2026-09-03.
Discovery credits
Checkmarx Zero (Yehuda Gelb / Tzachi Zornstain) is the primary discovering vendor; Socket, npm Security, and Snyk assisted with the takedown.
Affected packages (10)
- npmbtree-core*
- npmbtree-leaderboard*
- npmbtree-lru-cache*
- npmbtree-range-store*
- npmbtree-time-index*
- npmindexed-btree*
- npmneighbor-key-map*
- npmordered-kv-index*
- npmpriority-slot-queue*
- npmsliding-score-window*
These are usually pulled in as transitive dependencies rather than installed directly. Check your whole tree at once - it runs in your browser and nothing is uploaded.
Impact
- Runtime-triggered loader bypasses npm v12 install-script controls: the malicious code is placed inside
BTree.prototype.set(), a normal library method that any consumer application will call in its own hot path.npm install --ignore-scriptsdoes NOT prevent the payload firing - it fires when application code invokes.set(key, value)with a specific key value that the loader looks for. This is the first widely-analysed sample of the runtime-triggered class in this corpus; every install-hook-only defence you have (Snyk CI gates, GitHub Advanced Security dependency review, static tarball scanners looking forpreinstall/postinstall) reports "clean" while this family is fully weaponised - ~2M weekly downloads at peak, ~1.5M near removal:
indexed-btreealone reached nearly 2 million weekly downloads before Checkmarx flagged it on 2026-09-17 (npm removed it on 2026-09-03 after 11 weeks in the registry); Socket telemetry showed ~1.5M weekly at removal.btree-corereached >1.9M downloads. The other siblings had hundreds of thousands each. Any project that added ANY package on the affected list between 2026-06-18 and 2026-09-03 should be treated as installed the family - EtherHiding via Ethereum Sepolia testnet smart contract: the loader does not carry a hardcoded C2 domain. Instead it polls a smart contract deployed on the Sepolia testnet through public Ethereum RPC endpoints, calling a getter function that returns the current C2 address. When one C2 endpoint is taken down, the attacker calls the setter to publish a new one and every running installation picks it up on the next poll. Domain-block-only egress control is ineffective; sinkhole-style disruption is only possible against the RPC endpoints (which are legitimate infrastructure for many other apps)
- Slack + Telegram + Ethereum stack: fingerprints the host (hostname, user, OS, cwd, Node.js version, npm user-agent, working env) and beacons the details to a hardcoded Slack channel and a Telegram bot. A second-stage payload is unlocked using shared-secret cryptography against the material returned from the Sepolia contract - the second stage is opaque without the on-chain key
- Hidden
extended/sharedLoad.min.jswas in the npm tarballs but never in the public GitHub repo: the malicious loader file was shipped only in the packaged tarball, not in the mirror source-control tree thatcharlessadler25published. A developer inspecting the GitHub repo before adopting saw benign code; the running package used a file that did not appear on GitHub. Any due-diligence review process that looked only at the source repo (and not at the actual npm tarball) missed the entire attack surface - ~109 ETH (~€230,933) in attacker wallet: Checkmarx traced attacker profits to an Ethereum wallet holding roughly 109 ETH at the time of disclosure. Whether that represents crypto-drain profits from downstream victims or is the operator's working balance, it confirms this was a professional operation, not opportunistic research
What to do
- 1Grep every
package-lock.json,yarn.lock,pnpm-lock.yaml, andpackage.jsonfor ALL 10 package names:indexed-btree,ordered-kv-index,btree-leaderboard,priority-slot-queue,btree-range-store,btree-core,btree-time-index,btree-lru-cache,neighbor-key-map,sliding-score-window. All versions of all 10 packages are compromised. Uninstall on hit; wipenode_modulesand lockfile; rebuild against a clean cache; do NOT rely onnpm audit(some of these were quarantined without a CVSS-rated advisory) - 2Treat every host that ran ANY of the 10 package versions as fully compromised.
indexed-btreewas in the registry for 11 weeks; the CI pipeline of any project that took it during that window ran the payload every time application code hit.set(<trigger-key>, ...). Rotate credentials the installer / runtime user had access to: npm publish tokens, GitHub tokens (including workflow tokens), AWS/GCP/Azure access keys, npm/nx cache credentials, wallet mnemonics and private keys, any secrets that appeared inprocess.envon that host - 3
--ignore-scriptsdoes NOT protect you: the payload fires fromBTree.prototype.set()at application runtime, not at install time. Any consumer that calls.set()on the imported class triggers the loader. Static supply-chain scanners looking only forpreinstall/postinstallwill report the package clean - 4Block outbound traffic from developer and CI networks to public Ethereum RPC endpoints (
sepolia.infura.io,sepolia.publicnode.com,rpc.sepolia.org,sepolia.blockscout.com,sepolia.drpc.org,sepolia.1rpc.io,sepolia.gateway.tenderly.co,endpoints.omniatech.io/v1/eth/sepolia,rpc-sepolia.rockx.com) - the loader polls a smart contract on Sepolia specifically. Block outbound toapi.telegram.org(Telegram Bot API) from CI networks entirely; audit Slack webhook egress - 5Add a lockfile-lint denylist rule for every published package under maintainer
charlessadler25on npm and any sibling scope you discover. When reviewing new npm packages, look at the actual tarball contents (npm pack <pkg>then extract) and diff against the linked GitHub source - theextended/sharedLoad.min.jsfile in this campaign was present in the tarball but not in the repo. Any file that appears in the tarball but not in the public source is a red flag - 6Sysmon/EDR rule for Node.js processes making outbound HTTPS to Sepolia RPC endpoints without a legitimate application reason. Alert on Node.js processes contacting
api.telegram.orgfrom a developer workstation or CI runner - 7If your dependency graph pulled
sorted-btreerecently, verify you got the legitimate package (@openbayes/sorted-btree/ the well-knownsorted-btreepackage by David Piepgrass) and not the impersonatorindexed-btree. Any typosquat-adjacent name in the*-btree,btree-*,*-kv-index,priority-slot-*,neighbor-key-*,sliding-*-windownaming families deserves manual review before adoption
References
- Checkmarx Zeronpm `btree` malware campaign affects millions of downloads, no need for install scriptcheckmarx.com
- GitHubGHSA-f3xx-wvxc-5vh4 - indexed-btree malicious code (npm)github.com
- OSVMAL-2026-15888 - indexed-btree OSV recordosv.dev
- The Hacker NewsMalicious npm Package indexed-btree Hid Its Loader in Runtime Code Before Removalthehackernews.com
- SecurityWeekMalicious B-tree NPM Package Accumulates Millions of Downloadssecurityweek.com
- BleepingComputerMalicious npm packages evade install-script defenses at runtimebleepingcomputer.com
- InfoWorldNew npm malware finds a way around install script defensesinfoworld.com