Build1 publisher3 min readPublished
Malicious npm package fires its loader from inside BTree.prototype.set()
Checkmarx found that indexed-btree declares no lifecycle hooks and starts its loader when application code calls set() with key 100. The loader reads its C2 address from a smart contract on Ethereum Sepolia.
The Engineer · Build desk

What happened
- Checkmarx Zero reported on 17 September 2026 that indexed-btree, an npm package named to resemble the legitimate sorted-btree, reached nearly 2 million weekly downloads.
- The loader sends host details to hardcoded Slack and Telegram destinations, then reads a C2 address and two ciphertexts from an Ethereum Sepolia contract to assemble a second stage.
- Checkmarx says the GitHub repository backing the package, complete with commit history and a developer account, did not contain the malicious code shipped in the npm tarball.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Install-script blocking cannot reach a payload that runs from a library method, so the detection point moves to process and egress telemetry on developer machines, CI runners and build hosts.
- decision Domain and IP blocklists do not help here; teams have to decide whether to deny public Sepolia RPC endpoints outright from build and production networks.
- exposure Whether an installing project is actually exposed depends on its own call pattern into set(), so the same dependency can be inert in one service and live in the next.
- cost Because the loader can delete its files and strip the prototype trigger, incident responders pay for retained process and network logs or they get no answer at all.
`npm install --ignore-scripts`, or the org-wide equivalent, stops lifecycle hooks from running. Checkmarx's analysis says indexed-btree declares no `preinstall`, `install` or `postinstall` hook at all, so installation by itself runs nothing and a scanner that reads only lifecycle scripts clears the package [4].
The trigger lives in `BTree.prototype.set(key, value, overwrite)` [5], the method every user of a B-tree library calls. Two conditions gate it: `extended/sharedLoad.min.js` must exist, and the call must arrive with `key == 100` [6]. When both hold, `set()` calls `child_process.spawn()` and starts `node` on that file as a detached child [6]. A dependency that behaves correctly for the first ninety-nine keys passes most review standards. That file is obfuscated with string-array encoding and self-checksumming array rotation [16].
The second stage does not arrive from a hardcoded domain. The loader polls a smart contract on Ethereum Sepolia for the C2 destination, and the operator can switch that destination by updating the contract value even after domains or IPs are blocked [8]. It then generates an X25519 key pair, pulls the attacker's public key from the same contract, derives an AES key from the ECDH shared secret, and decrypts and combines two ciphertexts held in the contract into the payload [9]. Cutting that at the network means treating Sepolia RPC endpoints as denied egress, alongside Slack and Telegram API traffic [13].
Nearly 2 million weekly downloads, the figure Checkmarx gives for indexed-btree [2], is a distribution number. Checkmarx says the download count is not the count of systems where the logic actually ran [12], and it has not disclosed how many hosts executed the loader or what the second stage does [11]. Executions cannot exceed installs, so the 2 million is a ceiling and not a measurement [19]. For the two figures to converge, every installing project would have to load the package in a live Node.js process, call `set()` with a key equal to 100 while the extended file is present, and reach Slack, Telegram, the Sepolia RPC and the C2 that the contract names [14].
Host-side evidence decays. The loader carries cleanup code that deletes its files and removes the trigger from the prototype method [10]. What survives is the lockfile entry, plus whatever your telemetry retained of a detached Node child process invoked with `extended/sharedLoad.min.js` as an argument [13].
The distribution side used a plausible-looking GitHub repository with commit history and a developer account, and according to Checkmarx that repository did not contain the malicious code found in the npm package [15]. So the review habit of reading the upstream source shows clean code. The check that catches this is a diff of the published tarball against the repository tag.
What to watch
- Publication of the second-stage contents, which would say what runs after the contract resolves a C2 address.
- Whether the Sepolia contract address is released as an indicator, so egress rules can name it rather than the whole RPC endpoint.
- Whether further packages surface using the same in-method trigger instead of install hooks.