资讯详情

hcaptcha逆向实战:从API解析到参数构造,攻克验证码自动化

发布时间:2026/9/17 17:45:49

500+
企业客户服务经验
120+
行业领域内容覆盖
3000+
原创页面设计沉淀
98%
客户满意度

hcaptcha逆向实战:从API解析到参数构造,攻克验证码自动化

如果你正在做站点自动化、数据采集或者安全评估大概率遇到过这种场景脚本跑到一半页面弹出一个 hcaptcha 的复选框点一下人直接通过但自动化框架拿不到后面的P0_开头 token只能干瞪眼。更难受的是你以为这只是加一个验证码识别的事打开 DevTools 才发现后面是一套完整的请求链路、JS 混淆和 WebAssembly 加固。这篇文章把我从 API 抓包到参数构造的逆向过程完整捋了一遍适合手里有合法授权、却卡在 hcaptcha 这道关卡上的读者。先说结论hcaptcha 的难点从来不是识别图片里的红绿灯而是它把用户行为、浏览器环境、时间戳、甚至计算型 Proof of Work 全部揉进了参数里。你只要漏掉一个服务端直接返回invalid schema这类 400 错误连申诉的余地都没有。下面按我实际排查的顺序来讲。1. 面对hcaptcha先想清楚它到底在哪一层拦你1.1 从一次全军覆没的采集任务说起我最初碰 hcaptcha 是因为负责的一个数据采集任务突然大面积超时。头一天还好好的第二天对方站点把旧验证码换成了 hcaptcha所有并发请求在进入业务页面之前就被挡住了。第一反应是赶紧抓包看它走了哪些接口结果发现每次点击验证码Chrome 网络面板里会瞬间多出七八个请求还夹杂着.wasm文件加载记录根本分不清哪个是核心。后来才明白hcaptcha 的验证不是识别完图片给个答案这么简单。它把前端 SDK 拆成了多个阶段先加载主脚本api.js再注入不可见的挑战 iframe最后在用户点击的瞬间才发起获取挑战和提交答案的请求。这个过程中每一个环节都有参数要做缺一个、顺序错一个都会导致最终拿不到合法 token。1.2 hcaptcha的验证模型拆解我把整个模型分成三层来理解这样后面逆向的时候才不会被细节带偏接入层页面在加载时调用hcaptcha.render(element, {sitekey})此时只是完成了组件渲染还没有任何网络请求但sitekey、host等基础信息已经被记录。挑战层用户点击复选框后SDK 根据当前环境的信任分决定是直接下发 token还是弹出图片挑战。这一层核心是/checkcaptcha/{sitekey}请求响应里会带出挑战 ID、图片数据甚至可能带一个 POW 计算任务。提交层用户完成作答后SDK 把答案、鼠标轨迹、设备信息、挑战 ID 打包再次 POST 到/checkcaptcha/{sitekey}通过后服务端返回一个 token。业务后端拿这个 token 去/siteverify接口做最终校验。这三层里最容易被忽视的是挑战层。很多人以为拿到图片、答对题就完事了实际上 hcaptcha 在挑战层就可以选择不放行——它返回的可能不是图片而是一个你被判定为低风险直接通过的 token也可能是一个需要额外做哈希计算的 POW 任务。1.3 前端能解什么服务端又校验什么理清前端和服务端的边界能帮你省掉大量无用功。前端能控制的无非是请求参数怎么拼、答案怎么提交、行为轨迹怎么构造。但服务端最终校验的是挑战 IDc是否合法、是否过期、是否在限定次数内使用提交的答案是否和挑战图片对应设备指纹相关参数是否可信行为轨迹是否符合真人操作分布返回的 token 是否本身由 hcaptcha 签发。所以逆向的产出不是模拟出前端页面而是构造出一组让服务端认为来自真实浏览器的完整参数。这也是整个链路的核心目标。2. 从Network面板还原完整请求链核心API与参数清单2.1 点击验证后发生的四次关键请求打开 DevTools勾选 Preserve log然后手工点击一次 hcaptcha 复选框正常情况下会看到下面这四类关键请求顺序请求类型地址特征作用1GET/1/api.js加载 SDK 主脚本2GET/POST/checkcaptcha/{sitekey}获取挑战数据或直接放行3静态资源hcaptcha-challenge.html、.wasm加载挑战页面和计算模块4POST/checkcaptcha/{sitekey}提交答案、行为数据换取 token很多文章只盯着第 4 步但实际我在排查时发现第 2 步里返回的c和req字段决定了后续提交体的构造方式。如果挑战接口返回的是{pass: true}之类的结果那么整个挑战 iframe 都不会渲染你直接拿到 token如果返回里带着challenge_image和pow信息就说明要走完整答题流程。判断清楚这一步后面的工作方向就明确了。2.2/checkcaptcha的入参与响应拆分把一次完整的/checkcaptcha请求展开某一版本下的典型请求参数大体如下GET /checkcaptcha/{sitekey} ?v81e5342 sitekey00000000-0000-0000-0000-000000000000 hostexample.com hlzh sw1920 sh1080 sc1 fr0 pd1 nencrypted_browser_fingerprint cchallenge_token_or_empty t1691234567890 motionDataurlencoded_base64_json这里每个参数都不是白给的v是 SDK 的版本指纹它在每次发布后都会变化直接决定服务端用什么版本的规则来解析你的其他参数。sw、sh、sc、fr、pd分别是屏幕宽、高、颜色深度、字体渲染信息、设备像素比。它们不是单纯的统计字段而是用于交叉验证你声称的浏览器环境是否自洽。n是环境指纹的加密结果内容包含 UA、Canvas 指纹、WebGL 信息、浏览器插件列表等。c是挑战阶段拿到的会话标识第一次获取挑战时通常是空字符串或指定值提交答案时必须原样带上。响应体则是另一个结构里面除了一堆c对象外还有图片数据、题目类型、以及可选的 POW 参数。c对象内部通常是{ c: { type: hash, hash: a1b2c3..., req: base64_encoded_json } }这个hash和req是后续构造提交请求的关键不能漏。2.3 token产物与/siteverify的校验关系当你最终通过了挑战前端会在隐藏域#h-captcha-response里填一个长字符串这就是response token。它的格式一般是P0_开头后面跟一段 JWT 风格的 base64 编码内容。拿到之后业务后端会调用 hcaptcha 的官方接口POST https://hcaptcha.com/siteverify Content-Type: application/x-www-form-urlencoded secret你的密钥response用户提交的tokenremoteip用户IP如果返回{success: true}说明验证通过。这里要特别注意token 是一次性的而且和最初的sitekey、host绑定。如果你在本地构造请求时把host填错成localhost即便前端全通过了/siteverify也会失败。这个坑我踩过好几次后面单独讲。3. 参数构造最难啃的骨头motionData、hsw与POW3.1 motionData里到底塞了什么信号motionData 是提交请求里最容易被忽略、但权重极高的参数。它的本质是你在挑战 iframe 内操作时的完整行为记录常见结构类似{ v: 8, st: 1691234567890, mm: [-1, -1, -1], md: [ {type: mousemove, x: 132, y: 245, t: 1691234568100, f: 0}, {type: mousedown, x: 145, y: 260, t: 1691234568300, f: 1}, {type: mouseup, x: 145, y: 260, t: 1691234568350, f: 2} ], topLevel: 0, gestures: [], startTs: 1691234568000, endTs: 1691234568400, dt: 400 }字段含义大概是这样st是开始记录的时间戳md里每一条的t是具体事件时间服务端会计算时间间隔是否合理。x、y是相对 iframe 窗口的坐标不是相对屏幕的坐标。很多人直接拿整页坐标填进去一比对就不对。type对应鼠标移动、按下、抬起、触摸等事件。dt是总交互时长。真人答题通常需要几百毫秒到几秒你秒回反而异常。最关键的是hcaptcha 不只看轨迹坐标它会把md里的时间戳序列做差值分析观察是否存在匀速直线、机械重复等模式。纯脚本生成的固定间隔轨迹在服务端模型面前几乎等于裸奔。3.2 为什么直接生成轨迹容易被拦简单给轨迹加随机数是不够的。真人的鼠标轨迹有几个特征起点和终点之间不是直线而是带有弧度和抖动。速度曲线是加速—减速—再微调的过程不是恒定速度。存在一些无意义的微小移动比如点击前会悬停、会犹豫。总时长和题目难度正相关题越难思考越久。我试过两版方案第一版是每秒固定频率生成 10 个点通过率极低第二版把移动拆成 3 到 5 段每段用不同的速度曲线模拟通过率明显改善。但即便这样也不能保证 100% 通过。因为服务端模型是持续更新的你今天能过的轨迹明天可能就被新的风控策略判为异常。3.3 POW挖矿任务的计算逻辑如果说 motionData 是软校验那 POW 就是硬性计算题。它在某次版本更新后出现得越来越频繁。大致流程是挑战接口返回一个 base64 编码的req字段里面包含pow对象结构类似{ type: request, pow: { key: some_random_string, difficulty: 25, m: 100, expiry: 1691234567890 } }前端需要找到一个字符串nonce使得对key nonce做哈希计算后结果的前若干个二进制位是 0。difficulty就是要求的位数越大越难。找到后的 nonce 会拼进某个参数里和答案一起提交。这个逻辑本身不复杂但 hcaptcha 把它放在了 wasm 模块里执行还做了循环展开和常量混淆直接读 JS 看不到计算入口。我在定位时是先搜索字符串pow然后回溯到 WebAssembly 的导出函数才理清楚调用关系。如果你的场景里req字段突然出现且不处理大概率会得到类似 400 的报错因为服务端知道你根本没有完成工作量证明。3.4 版本更新带来的参数漂移写这篇内容时某版本里还有hsw、n、gx之类的额外参数它们分别从不同的混淆段和 wasm 模块产出。我个人的经验是不要试图在一个静态文档里记住所有参数名而是要维护一套参数来源映射。比如参数来源是否随版本变化vSDK 版本号从加载脚本 URL 或常量池读是c挑战响应中的hash是结构相对稳定n环境指纹加密后生成是算法会变motionData行为记录序列化是字段数会增减hswwasm 模块导出的附加签名是新版本标记answers用户答题选择相对稳定这样当某个版本突然报错时你只需要 diff 新旧版本的请求体就能快速定位新增了哪个参数而不是从头再看一遍混淆代码。4. 实战定位从断点Hook到构造出第一个合法请求4.1 找到token生成入口的方法面对一坨几千行的混淆 JS我一般不会直接通读而是用投毒式断点来找入口。具体做法分三步Hook 请求函数在控制台提前执行const originalFetch window.fetch; window.fetch function(...args) { console.log(fetch, args); return originalFetch.apply(this, args); };这样每次 hcaptcha 发起请求都能看到完整 URL 和参数顺着调用堆栈就能找到发起请求的函数位置。Hook JSON.stringifyhcaptcha 提交前通常会把对象序列化成 JSON改造一下这个函数const originalStringify JSON.stringify; JSON.stringify function(value) { if (value typeof value object JSON.stringify(value).includes(motionData)) { console.trace(hcaptcha payload, value); } return originalStringify.call(this, value); };这样当包含motionData的对象被序列化时会立刻触发堆栈打印直接命中构造参数的代码段。Hook WebAssembly.instantiate如果命中 wasm就在下方下断点查看实例导出的函数名。虽然函数名被混淆过但调用次数和参数类型能帮你判断哪个是计算函数。这三板斧下来基本能在一小时内画出哪个函数产出了哪个参数的调用图。4.2 补环境过程中的典型报错和解决思路定位到关键函数后很多人会尝试把它抠出来直接在 Node.js 里跑这时你会直面补环境这个老生常谈的问题。常见报错我整理了一张表报错信息原因解决方向navigator is not defined缺浏览器全局对象用 jsdom 或手工挂global.navigatorCannot read properties of undefined (reading userAgent)navigator对象不完整按真实浏览器补全完整字段不要只补一个document.createElement is not a functionDOM 方法缺失引入 jsdom 并执行dom.window.document或 mock 最小 DOMWebAssembly.instantiate is not defined缺 wasm 执行环境Node 本身支持 wasm检查是否用了浏览器专属 API补环境最忌讳缺啥补啥地打补丁。hcaptcha 会检查对象属性的顺序和是否存在多余属性你手工拼出来的navigator很可能在Object.keys(navigator)时暴露马脚进而被判定为异常环境。我现在的习惯是优先用 Playwright 拉起一个真实浏览器上下文在页面里执行抠出来的参数构造函数而不是硬搬到 Node。这样环境可信度最高缺点是性能差一些适合验证链路而非大规模调用。4.3 用请求构造脚本验证完整链路当你能拿到所有参数的构造结果后下一步就是脱离浏览器手动组装一次提交请求。我当时的验证脚本逻辑大致是这样的伪码import requests # 第一步获取挑战 challenge_resp requests.get( https://hcaptcha.com/checkcaptcha/{sitekey}, params{ v: sdk_version, sitekey: sitekey, host: example.com, hl: zh, sw: 1920, sh: 1080, c: , t: current_ts, motionData: motion_data_payload, }, headersheaders, ) challenge_data challenge_resp.json() # 解析 c.hash、c.req、还有可能存在的 pow 任务 # 第二步若存在 POW先计算 nonce # 第三步提交答案 submit_resp requests.post( fhttps://hcaptcha.com/checkcaptcha/{sitekey}, json{ answers: answers, c: challenge_data[c], job_mode: default, motionData: motion_data_payload, n: n_param, v: sdk_version, }, headersheaders, ) token submit_resp.json().get(token) # 最后把 token 扔给业务端去 siteverify这一步走通后你的API 解析到参数构造链路就算通了。但请注意这只是第一步。脱离浏览器环境伪造出的参数通过率通常不高因为缺少了n这类强指纹参数的稳定性支撑。5. 实测过程中的坑失败模式与回归清单5.1 高频报错速查表整个链路跑下来我遇到过各种各样的失败高频的基本就这几种现象本质原因处理建议invalid schema for function artifact请求体中某个字段类型或结构不符合服务端定义仔细对比新版本请求体重点看motionData和c对象是否多传、少传字段invalid sitekeysitekey格式或取值不对确认不是测试 sitekey并检查是否与host匹配timeout or duplicate同一挑战 ID 被重复使用或提交超过有效期每次重新获取挑战不要缓存cchallenge expiredc过期缩短从获取挑战到提交答案的时间POW 计算不要耗时太久接口返回 200 但没有 token被判定为低信任风险服务端拒绝签发检查环境指纹和轨迹行为优先用真实浏览器环境验证/siteverify返回falsetoken 与host、sitekey绑定不一致构造时确保host是业务真实域名不能用替换法乱传5.2 行为特征如何影响通过率我做过一组对照实验同一套参数构造逻辑只改 motionData 的生成方式结果差异非常明显。第一种是固定间隔直线移动即每隔 16ms 移动固定像素通过率几乎为 0。第二种是真实浏览器录制回放通过率最高但没法大规模使用。第三种是分阶段生成轨迹先随机提速、再随机减速、末尾加抖动和停顿通过率能到可接受范围但仍存在波动。这背后的原因在于 hcaptcha 的行为模型不只看轨迹还会看整体节奏从挑战加载完成到第一次鼠标事件的时间、点击目标位置是否准确、作答前后的停留时长、甚至 iframe 内部滚动事件。单独优化某一段意义不大必须把整段时间线都模拟得像真人。5.3 合规与授权边界方法论讲完了必须强调一点这套逆向链路只应该用于你自己拥有或获得明确授权的系统。比如给公司内部系统做自动化回归、在授权的渗透测试中评估验证码强度、或者研究自己的站点应该如何加固。未经授权对线上第三方站点做批量绕过既违反服务条款也可能触碰法律边界。我在文章里展示的是技术分析思路不鼓励任何人把它用在未授权场景。6. 一些写在最后的经验hcaptcha 逆向做久了最大的感触是它不像传统 JS 逆向那样扣出一个 md5 函数就完事而是一个持续对抗的过程。SDK 平均一两个月就会更新版本每次更新都可能调整参数结构、加密算法、POW 难度甚至 wasm 模块的加载方式。今天你写好的构造脚本下个月可能就全部失效。所以我最后分享两个实用习惯第一维护一个版本快照库。每遇到一个新版本就把api.js、挑战 iframe 里的 JS、wasm 文件按版本号存档同时标注清楚该版本下请求体的关键差异。下次更新时直接 diff 新旧代码定位速度快很多。第二优先用注入观察而不是通读代码的方式做分析。hcaptcha 前端代码混淆强度很大一段时间不接触再看思路容易断。但只要 Hook 住fetch、JSON.stringify、WebAssembly.instantiate这几个关键点你随时都能快速定位到核心逻辑不需要每次都从零开始读混淆代码。技术链路本身并不复杂真正花时间的全是这些版本对抗和细节还原的功夫。希望这篇从 API 解析到参数构造的完整链路记录能帮你少走一些弯路。
热门专题

继续阅读更多专题内容

围绕企业服务、数字化转型与官网运营的常青话题,持续输出深度内容

企业官网建设指南 企业托管服务模式 财税政策与解读 企业数字化转型 官网SEO与获客 网站安全与运维
配套服务

读完这篇文章,了解更多服务

从整站搭建到SEO布局,17项核心服务助您打造高转化的企业官网

01

企业托管整站搭建

从信息架构到栏目预留,搭建可生长的企业站点骨架,每个页面独立原创设计。...

了解详情
02

规整可信网页设计

雪地靴温暖风原创设计,金属铜线条贯穿全页,拒绝通用模板与AI流水线。...

了解详情
03

企业服务SEO布局

关键词体系与语义化结构,从建站源头为搜索排名而生。...

了解详情
04

业务预约咨询表单

多场景表单与线索收集体系,把访问流量转化为可追踪的销售线索。...

了解详情
05

企业服务站点运维

安全巡检、数据备份与内容更新支持,全年守护网站稳定运行。...

了解详情
06

全终端商务适配

电脑、平板、手机一致呈现,移动端体验与转化同样出色。...

了解详情
需要专业建议?

让专业顾问为您解读行业趋势

关于企业官网建设、SEO获客与数字化转型的任何疑问,欢迎一对一咨询我们的专业顾问。