# 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.

- Published: 2026-09-17
- Last updated: 2026-09-30
- Severity: critical
- Kind: 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.
- Ecosystems: npm
- Scope: 10 packages, 10 compromised versions
- Tags: typosquat, infostealer, credential-theft, crypto-wallet-drain, obfuscation, dormant-domain
- Also known as: indexed-btree campaign, charlessadler25 npm operator, sorted-btree impersonation, Sepolia smart-contract C2 (EtherHiding) npm
- Detected by: Checkmarx Zero, Socket, Snyk
- Incident ID: npm-2026-09-17-indexed-btree-sepolia-etherhiding

## Affected packages (10)

Only the exact versions listed were named by the source advisories. Other versions are not implicated.

| Package | Ecosystem | Compromised versions |
|---|---|---|
| [btree-core](https://dependencywatch.io/package/npm/btree-core) | npm | * |
| [btree-leaderboard](https://dependencywatch.io/package/npm/btree-leaderboard) | npm | * |
| [btree-lru-cache](https://dependencywatch.io/package/npm/btree-lru-cache) | npm | * |
| [btree-range-store](https://dependencywatch.io/package/npm/btree-range-store) | npm | * |
| [btree-time-index](https://dependencywatch.io/package/npm/btree-time-index) | npm | * |
| [indexed-btree](https://dependencywatch.io/package/npm/indexed-btree) | npm | * |
| [neighbor-key-map](https://dependencywatch.io/package/npm/neighbor-key-map) | npm | * |
| [ordered-kv-index](https://dependencywatch.io/package/npm/ordered-kv-index) | npm | * |
| [priority-slot-queue](https://dependencywatch.io/package/npm/priority-slot-queue) | npm | * |
| [sliding-score-window](https://dependencywatch.io/package/npm/sliding-score-window) | npm | * |

## 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.

## 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. Grep 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. Treat 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. **`--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. Block 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. Add 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. Sysmon/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. If 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

## Check your own dependencies

- Check a package-lock.json against this incident: https://dependencywatch.io/check/package-lock-json

The scan runs entirely in the browser; lockfile contents are never uploaded.

## References

- [npm `btree` malware campaign affects millions of downloads, no need for install script](https://checkmarx.com/zero-post/npm-btree-malware-campaign-affects-millions-of-downloads-no-need-for-install-script/) - Checkmarx Zero
- [GHSA-f3xx-wvxc-5vh4 - indexed-btree malicious code (npm)](https://github.com/advisories/GHSA-f3xx-wvxc-5vh4) - GitHub
- [MAL-2026-15888 - indexed-btree OSV record](https://osv.dev/vulnerability/MAL-2026-15888) - OSV
- [Malicious npm Package indexed-btree Hid Its Loader in Runtime Code Before Removal](https://thehackernews.com/2026/09/malicious-npm-package-indexed-btree-hid.html) - The Hacker News
- [Malicious B-tree NPM Package Accumulates Millions of Downloads](https://www.securityweek.com/malicious-b-tree-npm-package-accumulates-millions-of-downloads/) - SecurityWeek
- [Malicious npm packages evade install-script defenses at runtime](https://www.bleepingcomputer.com/news/security/malicious-npm-packages-evade-install-script-defenses-at-runtime/) - BleepingComputer
- [New npm malware finds a way around install script defenses](https://www.infoworld.com/article/4224395/new-npm-malware-finds-a-way-around-install-script-defenses.html) - InfoWorld

---

Canonical page: https://dependencywatch.io/incident/npm-2026-09-17-indexed-btree-sepolia-etherhiding
Markdown version: https://dependencywatch.io/incident/npm-2026-09-17-indexed-btree-sepolia-etherhiding.md
Site index for agents: https://dependencywatch.io/llms.txt
DependencyWatch.io is operated by Precursor Security (https://precursorsecurity.com).
