8 Malicious npm Packages Deliver Multi-Layered Chrome Browser Information Stealer

Open-source software repositories have become one of the main entry points for attackers as part of supply chain attacks, with growing waves using typosquatting and masquerading, pretending to be legitimate.

The JFrog Security Research team regularly monitors open-source software repositories using advanced automated tools, in order to detect malicious packages. In cases of potential software supply chain security threats, our research team reports any malicious packages that were discovered to the repository’s maintainers in order to have them removed.

Recently, the JFrog Security Research team uncovered and reported 8 highly sophisticated malicious npm packages, including react-sxt, react-sdk-solana, and more, which exhibit advanced obfuscation techniques and a multi-layer payload delivery mechanism, targeting users of Google Chrome running on Windows. All packages deliver the same final payload, which is a sophisticated Chrome browser information stealer – stealing passwords, credit cards, cryptocurrency funds, and user cookies.

Figure 1: Packages uploaded to the npm repository containing the malicious code

Initial JavaScript Payload

Each package that was uploaded to npm by the user “ruer” contained an obfuscated JavaScript code in the file src/react.js. The package initially employs obfuscator.io-style obfuscation to conceal its malicious intent:


(function(_0x5f0e82,_0x466830){const _0x15fb41=_0x5f0e82();function 
_0x58177b(_0x340223,_0x28bb95,_0x17db4c,_0x507767){return 
_0x27d9(_0x17db4c-0x34e,_0x340223);}function 
_0x2ac9a7(_0x59f78f,_0x4ebbb4,_0x13161e,_0x4fbff2){return _0x27d9(_0x59f78f- 
-0x69,_0x13161e);}while(!![]){try{....
 }

Opening the initial obfuscation revealed a payload that kills any running Python process, then removes any existing Python files from the Windows temp directory. Afterwards, the payload validates that Python is installed on the local machine. If it is not installed, a specific version is downloaded and installed from the official Python releases.

The payload proceeds to install Python libraries that it depends on using the command pip install --quiet --no-warn-script-location: pythonforwindows, pycryptodome and requests. After all setup is done, an obfuscated Python script is written into a temporary file, which is immediately executed silently.

 
async function main() {
    try {
        	await improvedKillPythonProcesses();
        clearOldLockFiles();
       	 cleanupTempDir();        
        const extract_lock = path.join(os.tmpdir(), '.chrome_extract_lock');
       	 fs.writeFileSync(extract_lock, Date.now().toString());
        const _0x88b0b4 = await ensurePythonInstalled();
        await updatePath();
       	 await checkAndInstallLibraries(_0x88b0b4);
        const created_script = await createScript();
        if (!created_script) {
            return;
        }
        const success = await runPythonSilently(_0x88b0b4);
       	 if (!success) {
            await runPythonSilently('python');
       	 }
        cleanupTraces();
	...
}
...
 }

The runPythonSilently() method employs several ways to run the obfuscated Python payload. The script tries to run each one, and on failure proceeds to the other:

 
() => runWithShellApplication(python_path, scriptPath),
() => runWithWMI(python_path, scriptPath),
() => runWithActiveXObject(python_path, scriptPath),
() => executeWithVBS(python_path, [scriptPath]),
() => runWithPowerShell(python_path, scriptPath),
() => runWithScheduledTask(python_path, scriptPath),
 

First Obfuscation Layer – The Python Script

The malicious Python code, embedded within the JavaScript payload, utilizes a more sophisticated obfuscation method than commonly seen. It conceals its payload through an anonymous lambda function that incorporates compression, Base64 encoding, and array order reversal.

 
_ = lambda __ : 
__import__('zlib').decompress(__import__('base64').b64decode(__[::-1]))

exec((_)(b'YNphgEw//+974nrTw/W+uz3EHsGlMqveE4c+L6zJ5/....') 

Decoding the first layer reveals the following code:

 
exec((_)(b'==Q+T7bHB8/vf/O+l6Ey1NvvxZSCXDcj73RWCRrCkpdFI0UOZuoD4IrlW....') 

The inner payload is executed automatically, and it’s decoded to exec((_)(b’...’)). Since the payload is recursively calling the lambda function with another layer of the same obfuscation, it’s harder to recover the final code.

The lambda works like this:

  1. Reversing the input string
  2. Decoding from Base64
  3. Decompressing using zlib
  4. Executing the next payload

Using a custom script, we analyzed the payload to reveal yet another payload hidden inside, under 64 layers, calling the lambda function each time and executing the next layer. Finally, another different obfuscation was revealed.

The Second Obfuscation Layer – Inside the Python Payload

The next layer of obfuscation uses raw bytes to hide the strings used in the code, and hides the usage of string reversing by using ‘-1’ slice reversing with bytes which get parsed as integers (which are also reversed!).

 
exec(__import__(bytes([109, 97, 114, 115, 104, 97, 
108]).decode()).loads(__import__(bytes([98, 122, 
50]).decode()).decompress(__import__(bytes([98, 122, 
50]).decode()).decompress(bytes([66, 90, 104, 57, 49, 65, 
8......128][::-1])[::int(bytes([49, 45][::-1]))])[::int(bytes([49, 
45][::-1]))])))
 

Deobfuscation reveals a twice-compressed Python marshalled code using bz2, with reverse order of bytes in each decompression. A simple “translation” to human readable code would look like this:

 
exec(
    __import__('marshal').loads(
        __import__('bz2').decompress(
            __import__('bz2').decompress(
                bytes([66, 90, 104, 57, 49, 65, 8, ...128][::-1])[::-1)]
            )
        )
    )
) 

The hidden payload in the compressed and ordered reversed bytes was decompiled, and revealed another layer of obfuscated code hidden within it.

Third Obfuscation Layer

After deobfuscation of the previous layer, we end up with code that probably looked like this before compilation:

 
raw_code = __import__('bz2').decompress(Bytes([66,90...128][::-1]))[::-1]
code = (compile(raw_code, 'zeroobf lmao', 'exec'))
exec(code)
 

The bytes of code are compressed using bz2 again. When decompressed, they reveal an obfuscation exactly like the second layer. The term ‘zeroobf lmao’ (the “filename” provided to compile(), which is not used in this context) might mean “zero obfuscation”, as some sort of a joke left by the creator of this obfuscation.

Further Analysis and Final Payload

The next layers of obfuscation use methods already described in this blog. Only after opening them, the final payload was uncovered.

To summarize, the obfuscation used:

  1. JavaScript obfuscated code
  2. Python script obfuscated using zlib, base64 and reversed arrays
    • Inside 64 layers of the same obfuscation
  3. Marshalled Python code, using double bz2 compression with bytes in reverse order
  4. Compiled Python code with bz2 compression and order reversing
  5. Another obfuscation as in layer 3
  6. Another obfuscation as in layer 4

Without requiring user input or approval, each payload executes instantly. This analysis revealed a highly sophisticated mechanism with a total of 70 layers of obfuscated code.

The capabilities of the final payload are extensive and alarming, focusing on data from the Chrome browser, and include:

1. Main Payload – Data Theft:

  • Password extraction
  • Credit card harvesting
  • Cookie stealing
  • Cryptocurrency wallet data exfiltration, targeted extensions:
    • phantom: bfnaelmomeimhlpmgjnjophhpkkoljpa
    • metamask: nkbihfbeogaeaoehlefnkodbefgpgknn
    • trust_wallet: egjidjbpglichdcondbcbdnbeeppgdph
    • solflare: bhhhlbepdkbapadjdnnojkbgioiodbic

Figure 2: A screenshot from the final payload, stealing credit card, cookies and passwords

2. Bypass Techniques:

  • Shadow copy bypass 
    • bypassing lock mechanism to access sensitive Chrome data files
  • LSASS impersonation
    • to decrypt Chrome keys
  • Multiple database access methods
  • File locking circumvention

Figure 3: A screenshot from the final payload, with function declaration of the malware capabilities

3. Stealth Data Exfiltration Techniques:

  • Upload to Railway servers
  • Discord webhook as a fallback mechanism (unused)
  • Local backup storage
  • File splitting to evade detection

Figure 4: A screenshot from the final payload showing data exfiltration implementation

Indicators of Compromise (IOCs)

Malicious npm users:

  • ruer
  • npjun

Malicious npm packages:

  • toolkdvv (versions 1.1.0, 1.0.0)
  • react-sxt (version 2.4.1)
  • react-typex (version 0.1.0)
  • react-typexs (version 0.1.0)
  • react-sdk-solana (version 2.4.1)
  • react-native-control (version 2.4.1)
  • revshare-sdk-api (version 2.4.1)
  • revshare-sdk-apii (version 2.4.1)

Exfiltration URLs:

  • hxxps[:]//vepo-production-013b[.]up[.]railway[.]app/upload
  • hxxps[:]//chrome-data-receiver[.]up[.]railway[.]app/upload
  • hxxps[:]//chrome-extract[.]up[.]railway[.]app/upload

Unused Discord webhook (potential fallback):

  • https://discord.com/api/webhooks/1234567890/your-webhook-token

Conclusion

These 8 npm packages represent a significant threat to developers and organizations relying on open-source components; the packages use multi-layered obfuscation, sophisticated evasion techniques, and possess comprehensive data theft capabilities. JFrog Xray has been updated to detect the malicious packages, providing an added layer of security for our customers.

Check out the JFrog Security Research center for more information about the latest CVEs, vulnerabilities, and fixes to keep your software supply chain secure. For more information about JFrog’s security solutions, feel free to take an online tour, set up a one-on-one demo, or start a free trial at your convenience.