Feed
CriticalMalicious packagePublished 17 Sept 2026Updated 30 Sept 202610 packages · 10 versions

indexed-btree + 9 sibling npm packages: runtime-triggered loader, Ethereum Sepolia smart-contract C2 (Checkmarx disclosure)

Summary

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.

typosquatinfostealercredential-theftcrypto-wallet-drainobfuscationdormant-domain
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

PackageFirst publishedNotes
indexed-btree2026-06-18The headline package; impersonates sorted-btree (a well-known B-tree by David Piepgrass). Peak ~2M weekly downloads, ~1.5M weekly at removal
btree-core2026-06-18>1.9M downloads by removal
ordered-kv-index2026-06-18Hundreds of thousands of downloads
btree-leaderboard2026-06-18Hundreds of thousands of downloads
priority-slot-queue2026-06-18Hundreds of thousands of downloads
btree-range-store2026-06-18Hundreds of thousands of downloads
btree-time-index2026-06-18Hundreds of thousands of downloads
btree-lru-cache2026-06-18Hundreds of thousands of downloads
neighbor-key-map2026-06-18Hundreds of thousands of downloads
sliding-score-window2026-06-18Hundreds 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)

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-scripts does 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 for preinstall/postinstall) reports "clean" while this family is fully weaponised
  • ~2M weekly downloads at peak, ~1.5M near removal: indexed-btree alone 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-core reached >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.js was 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 that charlessadler25 published. 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

  1. 1Grep every package-lock.json, yarn.lock, pnpm-lock.yaml, and package.json for 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; wipe node_modules and lockfile; rebuild against a clean cache; do NOT rely on npm audit (some of these were quarantined without a CVSS-rated advisory)
  2. 2Treat every host that ran ANY of the 10 package versions as fully compromised. indexed-btree was 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 in process.env on that host
  3. 3--ignore-scripts does NOT protect you: the payload fires from BTree.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 for preinstall/postinstall will report the package clean
  4. 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 to api.telegram.org (Telegram Bot API) from CI networks entirely; audit Slack webhook egress
  5. 5Add a lockfile-lint denylist rule for every published package under maintainer charlessadler25 on 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 - the extended/sharedLoad.min.js file 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
  6. 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.org from a developer workstation or CI runner
  7. 7If your dependency graph pulled sorted-btree recently, verify you got the legitimate package (@openbayes/sorted-btree / the well-known sorted-btree package by David Piepgrass) and not the impersonator indexed-btree. Any typosquat-adjacent name in the *-btree, btree-*, *-kv-index, priority-slot-*, neighbor-key-*, sliding-*-window naming families deserves manual review before adoption

References

npm-2026-09-17-indexed-btree-sepolia-etherhiding