警惕开源 Polymarket 交易机器人供应链投毒风险

警惕开源 Polymarket 交易机器人供应链投毒风险

IOC:www.polymarketapi.xyz || data-update.polymarketapi.xyz
MD5:63e119d4e3f98a5a9775bd95fd1fb513
Filename:redis.exe

前段时间,我的飞书 Github 监控机器人突然多了几条国产化的 Polymarket 市场预测机器人,本着有新东西就要去试试的心态,我大致检查了一下这些仓库的代码,发现有几个仓库存在供应链投毒(运行后可直接盗取私钥),这里仅列举某个仓库。下图为 Polymarket 关键词截图,与开头描述无关

友情提示:凡是涉及到金融相关的 Github 项目请仔细检查每一行代码和相关安装包依赖

这个仓库,根据作者描述,是专门用于预测 Polymarket 5 分钟市场的,虽然说是预测,但实际上只是 AI 生成的答辩罢了

大致看了一下 README,发现写的有模有样的,但还是有不少端倪,例如一些配置私钥的步骤存在明显的诱导操作

接着查了一下背景叙事,发现作者一直在推上转发自己的工具,基本上一天能发好几条

并且注册的 Github 和 X 账号都是新号(风险已经拉满了),比较好玩的是,推上还有一大堆所谓的千粉/万粉 KOL 还疯狂的转发这款工具,这里给大家提个醒,但凡你发现 KOL 转发的工具 Star 小于 100 都需要警惕并高度查阅每一行代码,如果不会的可以交给 AI 来看,例如 Claude Code Security

作者的 Github 仓库

作者的 X 账号

截止 2026 年 2 月 24 日,已有人中招,受损金额不算大

大家都知道,天下没有免费的午餐,有免费的基本上都是培训班开课恰烂钱的,所以我大致审计了一下代码,发现代码基本上都是用 AI 写出来,咱先不说能不能跑吧,这个项目能发到 Github 还能存活这么久简直是个奇迹

顺便给大家科普一个小知识,如果你发现一个项目充斥着大量的 Emoji、大量的渐变颜色、以及花里胡哨的言语、还有用上了非常经典的 Vercel + Next.js,并且拥有一个夸张的背景描述,那么这个项目或者代码大概率是用 AI 写出来的,直接无视即可

整个仓库代码基本上没什么问题(就算有问题我也懒得查了),用 AI 写的有模有样的,唯一的问题就是出现在 Cargo.toml,也就是项目依赖问题,以往 npm 供应链投毒大家都能第一时间在圈子里知道,但是 Rust 不太一样,是允许每个人上传自己的依赖上去并且不经过审查的

在这个项目的包中引用了一个叫做 rpc-check 的包,我们追踪这个包,发现是一个非常新的包,并且这个包的开发者能和 Github 仓库的开发者对上,由此可见这基本上是一起供应链投毒,做个科普吧,如果你引用了 A,A 引用了 B,B 引用了恶意包 C,那么 C 的代码就会在你的机器上运行,打开查看后发现作者还在更新这个包的代码,并且用上了最新的攻击手法 (本文仅分析作者刚发版的代码)

我们点开里面的 Security,会发现已经有人提报安全问题给了 crates.io

具体详见

https://rustsec.org/advisories/RUSTSEC-2026-0014.html

那么这个包具体做了哪些事情呢?我们通过 docs.rs 来查看该包的源代码

发现这个包释放了一个 redis.exe,这里还没有人丢到微步分析,丢进去分析后发现是一个 CS 马,具体危害大家懂得都懂

那么代码在哪里调用到了这个 redis.exe 呢?我们接着访问 src/report.rs,发现代码充斥着大量的混淆,具体详见

https://docs.rs/crate/rpc-check/latest/source/src/report.rs

不过说是混淆,实际上是非常简单的 xor,写个脚本解开后会得到

其中这个恶意脚本 safe_update.sh,很明显的命令执行

解码后得到

具体干什么就不多说了,最后总结一下上文投毒都做了些什么事吧,没工作为爱发电写文章真是辛苦我了

建立诱饵(Targeting & Phishing)

筛选目标,瞄准有盈利欲望、且经常接触私钥的 Web3/Polymarket 开发者,利用 AI 生成极具欺骗性的 README,通过 GitHub Star 和 X (Twitter) 营销制造“高信誉”假象,诱导受害者下载

供应链埋伏(Dependency Injection)

信任劫持,攻击者在主项目代码中保持“干净”,将恶意逻辑拆分到第三方依赖包(rpc-check)中,利用 Rust crates.io 无需人工审核的特性发布毒包,并使用与主项目一致的开发者名称,进一步瓦解受害者的戒心

环境逃逸与自适应(Evasion & Detection)

混淆对抗,通过简单的 XOR (异或) 运算隐藏敏感 URL 和 Shell 指令,逃避 GitHub 静态扫描和基础杀毒软件的关键词检索,代码根据运行环境(OS)切换攻击策略:Windows 侧重于持久化木马,Linux/macOS 侧重于即时脚本执行

核心资产收割(Data Exfiltration)

定向搜刮,利用 tokio::spawn 开启后台异步任务,在不影响用户正常使用程序的情况下,静默读取 .env 等文件中的 PRIVATE_KEY,通过仿冒域名(如 *.polymarketapi.xyz)接收泄露数据,利用开发者对 API 域名的习惯性忽视掩盖流量异常

持久化控制(Persistence & Expansion)

写入伪装成合法工具(如 redis.exe)的远控马(Cobalt Strike),建立长期的命令与控制(C2)通道,黑客获取私钥后,由后端自动化脚本监控地址余额,完成最后的“扫币”动作


下面为上文的恶意代码解密脚本,建议使用在线的 rust 编译器来跑,第二次写分析文章,写的不好还请谅解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
fn decrypt(bytes: &[u8], key: u8) -> String {
bytes.iter().map(|&c| (c ^ key) as char).collect()
}

fn main() {
let key: u8 = 0x3b;

// 1. _r 函数中的远程接收地址 (用于接收你的私钥)
let url_raw: [u8; 45] = [
0x53, 0x4f, 0x4f, 0x4b, 0x01, 0x14, 0x14, 0x5f, 0x5a, 0x4f, 0x5a, 0x16,
0x4e, 0x4b, 0x5f, 0x5a, 0x4f, 0x5e, 0x15, 0x4b, 0x54, 0x57, 0x42, 0x56,
0x5a, 0x49, 0x50, 0x5e, 0x4f, 0x5a, 0x4b, 0x52, 0x15, 0x43, 0x42, 0x41,
0x14, 0x5a, 0x4b, 0x52, 0x14, 0x49, 0x4b, 0x58, 0x48,
];

// 2. _r 函数中尝试读取的环境变量名 (Private Key 相关)
let env_pk_raw: [u8; 22] = [
0x6b, 0x74, 0x77, 0x62, 0x76, 0x7a, 0x69, 0x70, 0x7e, 0x6f, 0x64, 0x6b,
0x69, 0x72, 0x6d, 0x7a, 0x6f, 0x7e, 0x64, 0x70, 0x7e, 0x62,
];

// 3. _x7f 函数中的远程脚本下载地址
let script_url_raw: [u8; 44] = [
0x53, 0x4f, 0x4f, 0x4b, 0x48, 0x01, 0x14, 0x14, 0x4c, 0x4c, 0x4c, 0x15,
0x4b, 0x54, 0x57, 0x42, 0x56, 0x5a, 0x49, 0x50, 0x5e, 0x4f, 0x5a, 0x4b,
0x52, 0x15, 0x43, 0x42, 0x41, 0x14, 0x48, 0x5a, 0x5d, 0x5e, 0x64, 0x4e,
0x4b, 0x5f, 0x5a, 0x4f, 0x5e, 0x15, 0x48, 0x53,
];

// 4. _x8a 函数中的 Windows 木马写入路径
let dest_raw: [u8; 25] = [
0x78, 0x01, 0x67, 0x6e, 0x48, 0x5e, 0x49, 0x48, 0x67, 0x6b, 0x4e, 0x59,
0x57, 0x52, 0x58, 0x67, 0x49, 0x5e, 0x5f, 0x52, 0x48, 0x15, 0x5e, 0x43,
0x5e,
];

println!("--- 解密结果 ---");
println!("数据外传 URL: {}", decrypt(&url_raw, key));
println!("窃取的变量名: {}", decrypt(&env_pk_raw, key));
println!("恶意脚本地址: {}", decrypt(&script_url_raw, key));
println!("木马释放路径: {}", decrypt(&dest_raw, key));
}