逆向 Cloudflare 5s 盾
这篇只做记录
TL;DR
在 Node 里用 node:vm + node-tls-client 走完了 Cloudflare Managed Challenge 的前两步:
- GET 原站拿到 interstitial HTML +
_cf_chl_opt - GET orchestrate 脚本,在 VM 里执行
- 等 orchestrate 自己发出 flow 的 POST(VM 内部搞定)
- 卡在 Turnstile widget 不回 token 没 token 就没法 POST
/cv/result,拿不到cf_clearance
链路结构
真实链路就这四步:
1 | |
tier 从 _cf_chl_opt.cFPWv(或 OwLPw9)读,一般是 g;ray 从 _cf_chl_opt.cRay(或 XVCKH0)读;hash 是 orchestrate URL 路径最后一段。
flow 的 body 到底是什么格式我没有分析出来,也不打算分析 因为 VM 内的 orchestrate 能自己把这条请求发出去,我只需要把环境补到让它不报错即可。
仓库结构
打包后就这么几个文件:
1 | |
主脚本三千多行,主要在做”给 VM 装一个够逼真的 window/document/navigator/Event”这件事。
踩过的坑(这些坑踩完了)
1. Node fetch 的 TLS 指纹发不出 /flow
Node 自带 fetch(undici)或 https 模块的 ClientHello 跟 Chrome 完全不一样。Cloudflare 的 /cdn-cgi/challenge-platform/h/g/flow/ov* 对 JA4+ 做了校验,Node 默认指纹直接拒。
解:用 node-tls-client(bogdanfinn 那套的 Node binding),选 ClientIdentifier.chrome_131 profile,配上固定的 HTTP/2 header order:
1 | |
Chrome 142 在当前 Cloudflare 规则下 chrome_131 够用,更接近的 profile 可能需要自己 fork tls-client 库加。
2. Node 没有 PointerEvent/MouseEvent
VM 里的 orchestrate 和 Turnstile SDK 都要 new PointerEvent(...)。Node 原生 Event 只到 Event 基类,往下全缺。
解:手写一套 EnvEventBase / EnvUIEvent / EnvMouseEvent / EnvPointerEvent / EnvTouchEvent / EnvKeyboardEvent / EnvFocusEvent / EnvCustomEvent,每个类完整实现字段和 preventDefault / stopPropagation / composedPath。isTrusted 默认给 true。
这一步没技术含量但很费劲,每发现一个字段报错就补一个。
3. screenX / clientX 的 chrome offset
CDP 派发合成事件时有个经典 bug:screenX = clientX,真浏览器里 screenX 应该比 clientX 多一段浏览器 chrome 的偏移(标题栏+tab条),Chrome 上约 80~140px。orchestrate 拿到 screenX - clientX === 0 就能判定”合成事件”。
解:Pointer/Mouse 事件构造器里强制加偏移:
1 | |
chromeOffset 用 { x: 0, y: 87 }(典型 Chrome on Win10)。
4. Universal callable proxy
orchestrate 里大量动态探测:mY[someMinifiedKey].apply(mY, args)。随机生成 key,我不可能穷举补齐。直接 mY[key] === undefined 就 throw。
解:给宿主对象挂一层 Proxy,未知 key 返回一个 callable proxy 可以当函数调也可以继续取属性,apply/construct 都返回自己:
1 | |
这个 trick 救了我一堆 “X is not a function” 错误。但它也是个双刃剑 把真实错误也屏蔽掉了,后面排查要反复 toggle。
5. Bezier 人机轨迹 + 可复现 PRNG
Turnstile 阶段需要向 widget 容器派发一连串指针事件。直线移动太假,用 Bezier + 抖动:
1 | |
每次 run 记录 seed,出问题时拿同一个 seed 复现。28~42 个中间点、ease-in-out 的时间分布、pointerover pointerenter pointermove pointerdown pointerup click 的完整序列。这一套跟真浏览器的宏观形状差不多。
6. Turnstile api.js 的 currentScript 校验
Turnstile SDK 执行时会做:
1 | |
我的 document.createElement 返回的不是真的 HTMLScriptElement 实例,会死在这。
解:执行 api.js 前临时重写 HTMLScriptElement[Symbol.hasInstance] 让它认 tagName === 'SCRIPT' 的对象,执行完再还原:
1 | |
7. cv/result 的 body 格式
社区有帖子说 body 是 JSON,有人说 form-urlencoded。我实测 form-urlencoded 能走(至少服务器接受请求进到下一步判断),JSON 直接 400。字段 wp 和 cf-turnstile-response 同时塞同一个 token 最稳:
1 | |
返回 200 + Set-Cookie: cf_clearance=... 才算真的过了。前提是 token 必须是真的。
卡死的地方:Turnstile 不出 token
前面六个坑填完,在真实 run 里看到的最终状态:
1 | |
分开看:
- api.js 成功拉下来(61KB 左右,Turnstile 公共 SDK)
- SDK 在 VM 里跑完了,调用了我的 turnstile shim 的
render(),我抓到了 widget - 合成事件全派发完(91 个 pointer/mouse 事件,seed 可复现)
- widget 的 callback 从来没回过 token
到这一步,VM 本身的环境”看上去”是让 api.js 跑通了的。但 Turnstile 判定”你不是真浏览器”这件事,是在 SDK 内部和 challenges.cloudflare.com 之间的黑盒里完成的。它可能做了但不限于:
- WASM 模块里采样 GPU/layout 时序:纯 Node VM 没有 GPU、没有 layout,这里大概率直接失败
- AudioContext 指纹:我的 AudioContext 是空 stub,分析音频指纹的路径一看就是假的
- WebGL 指纹:同上,
document.createElement('canvas').getContext('webgl')我只返回了个 noop - 性能指纹:
performance.now()的分布、Promise/microtask 队列的时序,跟 V8 in Chrome 不是一个抖动特征 - 真浏览器主动信号:像
user-activation、isInputPending()、Permission API 的 state,都是 SDK 可以读到但合成事件不会触发的东西
上面这些我没有单独验证过是哪一条在拦,只是根据 Turnstile 公开资料 + 常见反自动化检测套路的合理怀疑。
这一步就是我过不去的。
为什么补不全
前面六个坑都是 “缺什么补什么” 的力气活,能做到 100% 过检吗?不能。原因:
orchestrate 本身还在偶发报错
即使 universal callable proxy 兜底了一大堆 “X is not a function”,实际跑完还是会看到类似:
1 | |
这说明混淆 dispatcher 里还有某个槽位探测到 native 方法缺失。换 ray 换版本后错误信息会换一个函数名,但”探测到缺失”这件事是稳定的。
missingProps 日志里常年有残留
跑完一次,capture 对象里的 missingProps 数组里总有:
1 | |
以及一些 tagName 为空、prop 是 Symbol 的访问。这些 probe 不致命,但每一个都会留下案底
为什么不上真浏览器
这是自我约束的选择:
- 目标本来就是想摆脱浏览器依赖。如果上 Playwright,整个项目没意义,直接
context.storageState()把 cookie 序列化就完事了。 - 想理解这玩意是怎么工作的。黑盒调真浏览器学不到东西。
- 想看看纯 JS 到底能走到哪一步。现在答案是:能走到 Turnstile 门口,但推不开门。
仓库没开源,不提供现成的”过墙”实现。这篇文章的目的是复盘,不是发布绕过工具。
参考
- bogdanfinn/tls-client JA4+ 指纹的核心
- node-tls-client 上面那个的 Node binding
- Cloudflare 官方 Turnstile 文档(只写了接入,没写内部)
本文仅记录方法论和接口形态,不提供任何可直接运行的绕过实现。具体字段的加密算法、VM 字节码映射、符号逻辑一律略过。
技术点适用于合法场景:E2E 测试、SDK 审计、自家站点监控、前端混淆代码的逆向分析。