导语:10 月 29 日 17 时 10 分,链上一个从未有过交易的新地址收到第一笔 POL。52 分钟后,它装走 28,636,284 枚 POL,折合约 264 万美元,随即在 10 分钟内把钱分散到三个钱包。整件事没有碰过任何私钥,没有绕过任何合约,甚至没有动用一笔用户看不懂的签名——攻击者只是站在 Polymarket 的页面上,借用了它自己的交易客户端和它自己的中继器。

一、52 分钟的一次收割
预测市场 Polymarket 在 10 月 29 日遭遇了一次前端钱包抽干。PolyChain 研究员 Hudson Rock 两次从 Polymarket 站点引用的脚本地址取到同一份文件,SHA-256 一致,15,187 字节;Ohey 的研究员把它和 Polygon 链上流水一比,拼出了完整时间线。
关键数字摆在这里:受影响钱包 726 个,平台承诺退款约 270 万美元,另加 119 万美元补偿停摆期间亏损的钱包,合计返还 390 万;攻击者收款地址在 17:10:23 收到第一笔 POL,18:11 停止增长,18:28 到 18:38 之间把全部 POL 分三笔转走;峰值 28,636,284 枚 POL,按 DefiLlama 当时 0.0922 美元的价格,约合 264 万美元。

这不是 Polymarket 第一次被抽。5 月约 60 万美元从平台一个休眠的运营钱包被搬走,原因是私钥泄露;6 月第三方供应商把恶意脚本注进前端,损失接近 300 万美元,平台全额赔付。四个月两次前端抽干,第三次安全事件。同一周,另一家预测市场 Predict.fun 也报了约 40 万 USDT 未授权转账,文档站同样托管在第三方。
二、入口:一个 CDN 域名怎么变成攻击者的分发节点
DebugBear 做的是真实用户监控(RUM),往客户页面里塞一段脚本,量访客看到的加载速度。每个客户从 cdn.debugbear[.]com 拿自己独立的一份文件,Polymarket 的那份叫 C7xcKKrjUnXK.js。互联网档案馆存了这个地址从 2025 年 6 月到 10 月 29 日早上的所有副本,每一份都是干净的 RUM 代码,带 snippetKey 和 sendTo 配置。最后一份干净副本存档于当日 12:06:55,来自 Google Cloud Storage,带 MD5 ETag 和 x-goog-* 头。
然后事情变了。
16:54:57,Cloudflare 对这个域名返回 521,源站不响应。攻击者接手了应答权。恶意响应的 ETag 形如 6ae09e23-3b53,是 nginx 的格式,直接编码了文件时间和 15187 这个字节数,而且没有一条 Google Cloud Storage 的头。18:13:44,data.debugbear[.]com 回了 nginx/1.24.0(Ubuntu)的 404 页。换句话说,两台主机名背后站着的已经不是原来那套基础设施。
DebugBear 在 X 上说,自己的域名注册商被攻陷,DNS 记录被改了;初步迹象显示攻击只针对一个客户。它的排查结论还没出,但 10 月 27 日 12:50:43 这个文件日期说明代码存在了至少两天——攻击者备好弹药,等源站一断就接管。
这里最该记住的教训不是”CDN 会被劫持”,而是:拿到一个 CDN 域名的控制权,就等于拿到上千家客户页面的执行权。 每个客户加载的文件互相独立,攻击者只需要挑中一个文件写坏,其余客户毫发无伤。DebugBear 这单损失面窄到只剩 Polymarket,不是因为攻击者心善,而是因为它没有动力扩大。
17:27:45,Cloudflare 芝加哥边缘把这份毒文件缓存下来。这一步是收割速度的关键——没有边缘分发,攻击只能一个访客一个访客地等。
三、抽干器拆解:四步
整个攻击装在一个名叫 startdebugger 的压缩函数里。15,187 字节,纯 JavaScript,没有混淆。
第一步,等一个登录的人。 脚本同时发起三个请求:拉配置、调 Polymarket 的 api/geoblock 判断访客是否被地域限制、等待 1.5 秒。配置端点 /metrics/init 在 18:13:44 返回的是 HTML 404,JSON 解析失败,于是代码回落到写死的默认值:收款地址、最低金额 10 美元、优先收 POL。然后它读 localStorage 里三个键——polymarket.auth.params(登录类型和钱包地址)、polymarket.auth.proxyWallet(装资金的充值钱包)、poly_clob_api_key_map(每个钱包的交易凭证)。没有登录、或者登录了但没有交易凭证的访客,到此为止。剩下的账号被分成两类:自托管钱包(EOA)和邮箱登录。

第二步,借页面自己的交易客户端。 React 会把每个组件的 props 和 hook 状态挂在 DOM 元素上的 __reactFiber$ 属性里。脚本收集页面上最多 400 个按钮、链接和容器,沿着 fiber 树往下走,最长 5 秒、最多 6 万个节点,凡是同时具备 postOrder 和 createOrder(或 createMarketOrder)方法的对象就留下——那是 Polymarket 的订单客户端,签名器就挂在它旁边。找不到就每 250 毫秒重试一次,一共 15 次;还是不行,就把 location.pathname 改成钱包地址,让页面重新加载,自己再来一遍。
第三步,分叉搬钱。 脚本通过 Polygon RPC 读余额,从 data-api.polymarket.com 读最多 200 个持仓,组合价值不足 10 美元就放过。现金优先处理:先问 api.fun.xyz 要一份把余额换成 POL 的报价,把兑换和转账打包进一个 EIP-712 DepositWallet 批次,用页面上的签名器签好,POST 到 relayer-v2.polymarket.com/submit,deadline 一小时。持仓随后按账号类型分叉(下一节详说)。
第四步,假遥测回传。 三个端点挂在 data.debugbear[.]com 下面:/metrics/submit 交出组合全景(现金、每个持仓的市场标题、价值和结果)、/metrics/update 报每次现金归集、/metrics/handle 报每次持仓转移。每条报告都带着充值钱包、登录钱包、登录类型、操作系统、追踪 ID 4c6b64a0-a7ef-4c0f-9c70-3a4e6f89065c,以及从 geoblock API 拿到的 IP 和国家。
关键在于抽干器调用的其他每一个主机都在 Polymarket 的内容安全策略(Content Security Policy)白名单里:Polymarket 自己的 API 和 relayer、Alchemy 的 RPC、fun.xyz 的报价。策略放行了每一个请求,因为每一个都去了平台自己在用的主机。
四、它没偷你的私钥,这才是最难防的地方
抽干器读完那串 API 凭证,并没有把它发回自己的服务器。静态分析也没找到任何外传密码、助记词或私钥的代码。这是这段代码最值得研究的地方:它不偷权限,它就地使用权限。
Polymarket 的文档把两件事分得很清楚——CLOB API 凭证用来认证私有请求,下单还需要钱包签名。凭证单独不够。于是脚本去借那个”还在页面里、已经授权好的签名器”。
分叉就发生在这里:

邮箱登录的用户,钱包是内嵌在页面里的 Magic 钱包,会话期内可以静默签名,没有弹窗。抽干器用抢到的订单客户端发起 fill-and-kill 卖单:先按现价的 97% 挂,再 50%,最后压到市场的最小 tick,能成交多少算多少,成交完的货款再走现金流程。已经关闭、不活跃或者可赎回的市场跳过。用户全程零感知,钱是在成交之后才消失的——交易者 @CarOnPolymarket 17:45 发帖说”我刚被抽了 80 万”,十分钟后补一句”我所有持仓都被卖掉了”。
外部钱包用户,签名器在用户自己的扩展里。抽干器走 ERC-1155 safeBatchTransferFrom,在条件代币合约 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045 上一次性把所有价值超过 10 美元的 outcome token 搬走。这一批会弹出签名请求,X 上有人截图说”这明显是诈骗”,然后拒了。这就是这次唯一的一道人肉防线。
两条分支的共同点是,签名和提交都走 Polymarket 自己的体系:EIP-712 结构化签名的 DepositWallet 批次是平台授权体系里的正常机制,中继器(relayer)替用户付 gas 提交交易。同一套中继器,这次替攻击者付了 gas。
边界得说明白:签名请求不等于用户点了确认——钱包提供方可能弹窗、可能拒绝、也可能按会话规则直接放行。Hudson Rock 没有连钱包也没执行样本,静态分析证明不了每条路径都成功,但链上的钱确实搬走了。
五、资金流:52 分钟,33 个钱包
收款地址 0x3F99139F1C5854bb628c2e576Aaf638e99dFfc18 在 17:10:23 之前余额为零、没有发过任何交易。第一笔进账 1,957 枚 POL,随后 17:25 涨到 81 万,17:40 冲到 1120 万,17:55 破 2200 万——大部分钱是在 17:25 之后的 37 分钟里进来的,正好卡在边缘缓存投毒之后。18:02:56 最后一笔转账落地,18:11 停止增长,比生产构建时间戳晚 12 分钟。
总共收进 28,636,284.45 枚 POL,来自 33 个充值钱包的 36 笔持仓转移,外加 15 笔 10,383 枚 USDC.e 和 14 笔 6,320 枚 USDC。钱随后分成四路:稳定币在 17:32 和 18:02 两笔换掉,25 笔持仓代币在 17:45 到 17:56 之间转到另一个地址,POL 在 18:28:32、18:30:41、18:38:16 三笔转给三个钱包。到 20:50,这三个钱包分别还剩 1533 万、429 万、423 万枚 POL。
链上分析师 @TheFatMbappe 追出 721 个源钱包共 272.4 万 pUSD。Polymarket 自己没公布数字。
六、48 分 57 秒,和一个救不回来的洞
Polymarket 17:38 发了第一条状态更新,17:59:20 产出生产构建 chore(polymarket): remove debugbear rum script (#15337)(commit f908fe564ea73067b09ab1f0c5d683970020fbca),18:05 交易所进入仅可撤单模式、中继动作暂停,18:33 宣布第三方供应商被攻破、已报警、受影响用户全额赔付,19:00 交易恢复。从首笔 POL 到账到构建时间戳,48 分 57 秒;从用户发帖到构建,13 分 23 秒。DebugBear 的公开警告比构建晚 98 秒。
响应速度不慢,但止血不等于修好。新 bundle 只对重新加载的页面生效,已经开着的标签页还在跑旧代码——18:02:56 最后一笔转账正好落在构建时间戳之后 3 分半,那正是还开着的旧会话。
平台后续的动作是清缓存、重置所有登录会话、暂停交易,并把第三方脚本加固:更严的内容安全策略、锁版本、沙箱化。DebugBear 则要求所有 RUM 客户立刻从页面里移除它的嵌入代码;到 11 月 1 日,cdn.debugbear[.]com 已经没有 DNS 记录。
七、我的判断
一、真正的攻击面是”第三方脚本等于页面全部权限”这个默认设定。 RUM 脚本要的是加载速度,它不需要订单簿客户端,也不需要签名器。可这两样东西就在同一个标签页里,而浏览器不会把同源下不同供应商的脚本区别对待。内容安全策略只管”哪个主机”,不管”这段代码想干什么”——而抽干器调的每一个主机都在白名单里,所以每一个请求都合法通过。
二、四个前提缺一不可,每一个单独看都是合理的产品决策。 任何脚本都能摸到交易客户端;邮箱会话能无人值守签名;订单簿愿意一次吃掉一串 fill-and-kill 卖单;中继器愿意替它付 gas。四条凑齐,就凑出了一个抽干器。DefiPrime 那句问得最狠:RUM 脚本为什么要跑在签名会话的同一个页面里?性能监控不需要交易客户端,也不该拿到钱包。
三、”让用户更小心”在这里是废话。 外部钱包用户至少能看到签名弹窗,邮箱登录用户连提示都没有,没有可拒的东西。治本只能让危险动作不再依赖人肉确认——瞬间卖光持仓再把余额扫向一个从未打款过的地址,中继器完全有能力识别,只是它有识别能力,没有拒绝能力。
四、回传通道选在供应商自己的域名下,是这次最聪明的动作。 /metrics/submit、/metrics/update、/metrics/handle——全是 RUM 产品本来就在用的路径和命名空间。抓包的人看到的,就是一条正常遥测。攻击者不需要搭新的 C2 基础设施,供应商的域名就是最好的藏身处。
五、代码里有业余的地方,但不影响收割。 卖出后轮询余额最多 50 秒的检查,把新余额赋给旧余额再比较,第一次读必然通过——攻击者自己都不确定钱到没到账。它在意的是”把请求发出去”,不是”确认成交”。自托管钱包那条分支只要签名被拒就断了。业余归业余,264 万美元照单全收。
六、六月已经预演过一次,这次缺的不是检测,是架构。 六月出事之后最该加的一条控制措施,是给第三方脚本加子资源完整性(Subresource Integrity)校验或者干脆自托管——供应商被拿下时,脚本会坏掉,而不是变成武器。这条至今没加上。
七、退款口径要想清楚。 邮箱用户全程没看到任何提示,自托管用户看到了但很可能没读。既然”会话期静默签名”是平台自己的产品设计带来的后果,按”有没有看到提示”划线说不通。我的意见是全赔。
八、站方和用户的清单
给平台:
- 用子资源完整性校验锁死第三方脚本的哈希,或者干脆自托管。放行一个可信 CDN 不等于检查了它返回的内容。
- 交易、登录、签名页面不加载任何不必要的第三方脚本。放进隔离 iframe 也比放在同层强。
- 审计登录态页面上哪些代码能调用签名器,尤其是能遍历 React fiber 的那种。
- 中继器加风控:瞬间卖光持仓 + 余额扫向陌生地址,应该拦得住。
- 保留投递证据:CDN 响应字节、哈希、HTTP 头、加载点引用、浏览器请求历史。响应一换,窗口就没了。
给用户:
- 拒绝任何你没主动发起的签名请求。页面开着不代表请求是你要的。
- 关掉已打开的 Polymarket 标签页,重新加载再交易。新版本只对新加载生效。
- 外部钱包用户要知道,那次 outcome token 转移是一次性签名,不是长期授权,撤销授权撤不回已经完成的转账。
- 忽略任何主动找上门的”退款””认领””V2 迁移”链接,这类事件后面通常跟着一波借名钓鱼。
附录:失陷指标
恶意文件 SHA-256:0755e767504d8103f6c27b9e765a1026507a17c04513242adb503df7777879e5(15,187 字节);同地址下的干净 RUM 文件 SHA-256:0525f046c83daa429e0f676d36650d51d9e283ba5cacfa56097c80bad586128b(60,493 字节)。
涉及域名与路径:cdn.debugbear[.]com/C7xcKKrjUnXK.js、data.debugbear[.]com/metrics/init、/metrics/submit、/metrics/update、/metrics/handle。
Polygon 链上地址:收款 0x3F99139F1C5854bb628c2e576Aaf638e99dFfc18;持仓接收 0x5a2764fbbe5d139ef0ed988ec5f95320ce730c0b;POL 分流 0xe4c4c39c1733e1c7c06b79f97ed442b68df41ec6、0x5392587814ce8c6af6789764f88e5e9ab0b9962c、0xc91ee0574a14446de200923729c349b9cae19b60。
其他:追踪 ID 4c6b64a0-a7ef-4c0f-9c70-3a4e6f89065c;Polymarket 移除脚本的构建 chore(polymarket): remove debugbear rum script (#15337),commit f908fe564ea73067b09ab1f0c5d683970020fbca。
版权声明:本文由华盟网原创发布,保留所有权利。配图由华盟网授权使用。














暂无评论内容