当前位置: 首页 > news >正文

JS逆向实战:瑞数412会话还原

声明本文仅记录授权测试环境下的调试过程和工程化分析思路文中的域名、接口路径、Cookie 名称和敏感常量均已做脱敏处理。代码片段只用于学习 JS 逆向、协议分析和问题排查不提供针对真实站点的完整可运行客户端。一、案例介绍本文介绍一个典型的 Web 端瑞数挑战还原案例裸请求访问目标站数据查询页面时返回HTTP 412响应体不是业务 JSON而是一段挑战 HTML在会话状态恢复后业务接口还需要额外的timestamp sign请求签名。这个案例可以拆成两层瑞数挑战层执行挑战页 JS得到当前 UA 绑定的会话 Cookie。业务签名层按前端规则生成timestamp和sign再调用数据接口。最终目标不是用浏览器自动化长期跑业务而是在 Node.js 中完成最小协议复现挑战脚本由可控 DOM 环境执行后续页面、配置和 API 请求全部走undici tough-cookie。二、环境准备本节案例使用 Node.js核心依赖如下{type:module,dependencies:{cheerio:1.2.0,sdenv:1.1.3,tough-cookie:6.0.1,undici:8.3.0}}undici负责纯协议请求tough-cookie负责维护 CookieJarcheerio用来解析真实页面里的标题和脚本列表。sdenv用来在 Node.js 环境里执行挑战页脚本把脚本写入的 Cookie 保存下来。常用验证命令可以拆成三类# 步骤一观察裸请求基线pnpmbaseline# 步骤二验证挑战 Cookie 生成后能否进入真实页面pnpmtest:session# 步骤三验证签名 API 是否返回业务数据pnpmtest:search ----repeat2这三个命令对应三个验收点先确认问题确实是挑战页再确认会话恢复最后确认签名后的接口可用。排查时不要一上来就写签名函数否则很容易把412挑战层和业务参数层混在一起。三、抓包与 412 特征观察第一步先做裸请求基线只保留最基础的请求头// 步骤一裸请求基线观察是否进入挑战页import{request}fromundici;consttargets[https://example.invalid/data/home.html,https://example.invalid/data/search.html,];for(consttargetoftargets){constresponseawaitrequest(target,{headers:{user-agent:curl/8.0.0,accept:*/*,},});constbodyawaitresponse.body.text();console.log({statusCode:response.statusCode,contentType:response.headers[content-type],hasChallenge:body.includes($_ts)||body.includes($_ts.cd),});}这里重点看三个字段状态码、响应类型、响应体特征。若状态码是412content-type是text/html并且响应体存在$_ts一类挑战变量就说明当前拿到的是挑战页不是业务页面。基线输出可以整理成下面这样{statusCode:412,contentType:text/html; charsetutf-8,hasChallenge:true}至此可以确定直接改业务参数、分页参数或Accept头没有意义。当前第一目标是还原“首包挑战 HTML - JS 执行 - Cookie 写入 - 二次请求 200”的会话链路。四、瑞数挑战链路拆解瑞数这类挑战不是一个简单的md5()签名函数。它更像一个浏览器状态生成过程脚本会读取navigator、location、document.cookie、UA 等环境信息然后写入会话 Cookie。可以把链路抽象成第一次请求页面 - 服务端返回 412 挑战 HTML - 挑战脚本在 DOM 环境中执行 - 脚本写入 acw_tc / RS_COOKIE_* 等状态 - 同一 UA CookieJar 再请求真实页面 - 进入业务页面和接口层项目里的会话类可以简化成下面的结构// 步骤二使用 sdenv 执行挑战页并导入 Cookieimport{createRequire}fromnode:module;import{CookieJar}fromtough-cookie;constrequirecreateRequire(import.meta.url);exportclassRuishuSession{constructor(options{}){this.originhttps://example.invalid;this.entryPath/data/search.html;this.userAgentoptions.userAgent;this.cookieJarnewCookieJar();this.domnull;}asyncinit(){constsdenvrequire(sdenv);this.domawaitsdenv.jsdomFromUrl(this.originthis.entryPath,{userAgent:this.userAgent,referrer:this.origin/data/home.html,});awaitthis.waitForChallenge();this.importCookies();this.dom.window.close();}}这段代码的关键不是“打开页面”而是让挑战页在 Node.js 的 DOM 环境中完成执行。执行完成后不从浏览器手动复制 Cookie而是从sdenv的 cookieJar 导入到自己的tough-cookieCookieJar。等待挑战完成时不建议依赖混淆函数名因为函数名经常变化。更稳的做法是监听执行边界// 步骤三监听挑战脚本完成边界waitForChallenge(){returnnewPromise((resolve){letdonefalse;constfinish(){if(done)return;donetrue;setTimeout(resolve,1500);};this.dom.window.addEventListener(sdenv:exit,finish);this.dom.window.addEventListener(sdenv:location.replace,finish);this.dom.window.addEventListener(sdenv:location.assign,finish);setTimeout(finish,12000);});}瑞数脚本执行结束后常见动作是退出、跳转或替换location。监听这些稳定边界比在大段混淆代码里追动态函数名更适合工程落地。Cookie 导入和校验可以这样写// 步骤四导入挑战脚本写入的 Cookie并做名称校验importCookies(){constcookiesthis.dom.cookieJar.getCookiesSync(this.origin);for(constcookieofcookies){this.cookieJar.setCookieSync(cookie.toString(),this.origin);}}assertReady(){constnamesthis.cookieJar.getCookiesSync(this.origin).map((item)item.key);if(!names.includes(acw_tc)||!names.some((name)/^RS_COOKIE_/.test(name))){thrownewError(challenge cookies missing:${names.join(,)});}}这里要注意只看到acw_tc不一定够。实际项目里还会有一组挑战状态 Cookie校验时至少要确认主 Cookie 和挑战状态 Cookie 都存在避免出现“脚本执行了但会话状态没成功”的假阳性。五、签名参数定位会话层解决的是412。进入真实页面后数据 API 还有业务签名。定位时可以优先搜索这些锚点timestamp sign md5 headers searchValue pageNum项目中签名规则可以概括为过滤空值。按keyvalue形式拼接参数。对拼接结果按字符串排序。使用连接。末尾追加脱敏后的业务常量。整体做 URL 编码并修正()!~。对编码后的字符串计算 MD5。对应代码片段如下// 步骤五还原业务接口 sign 生成逻辑importcryptofromnode:crypto;constAPP_SECRETsecret;functionisPresent(value){returnvalue!value!undefinedvalue!null;}exportfunctiongetSignString(params){returnObject.keys(params).filter((key)isPresent(params[key])).map((key)${key}${params[key]}).sort().join();}exportfunctionsignParams(params,secretAPP_SECRET){constraw${getSignString(params)}${secret};constencodedencodeURIComponent(raw).replace(/\(/g,%28).replace(/\)/g,%29).replace(/!/g,%21).replace(/~/g,%7E);returncrypto.createHash(md5).update(encoded).digest(hex);}这里最容易错的是编码顺序。不是分别编码每个字段也不是简单md5(query secret)而是拼接完成后整体编码再做额外字符修正。六、Node.js 协议请求实现业务 API 请求时timestamp既要放在查询参数中也要放在请求头中并且两处必须一致// 步骤六构造带 timestamp 和 sign 的 API 请求asyncfunctionsignedApiRequest(session,path,params){consttimestampDate.now();constsignedParamscleanParams({...params,timestamp});constsignsignParams(signedParams);returnsession.request(path,{retryOn412:false,params:signedParams,headers:{accept:application/json, text/plain, */*,referer:https://example.invalid/data/search.html,token:false,timestamp:String(timestamp),sign,},});}这里的session.request()需要统一复用挑战阶段生成的 UA 和 CookieJar。若生成 Cookie 时使用一个 UA业务请求时换成另一个 UA很容易重新落回412。搜索流程可以拆成两步先用统计接口判断哪些栏目有结果再调用列表接口获取分页数据。// 步骤七先统计再查询列表asyncfunctionsearch(client,keyword,page1,pageSize10){constcountsawaitclient.apiGet(/api/count,{itemIds:item_a,item_b,item_c,searchValue:keyword,isSenior:N,});constselectedcounts.data.find((item)Number(item.nums)0)?.itemId;returnclient.apiGet(/api/search,{itemId:selected,isSenior:N,searchValue:keyword,pageNum:page,pageSize,});}这样做的好处是避免盲目请求所有栏目。实际工程里还可以加载前端配置 JSON把返回的f0/f1/f2字段映射成可读字段名。七、412 恢复与重试策略会话 Cookie 可能过期或者某一次请求中途重新进入挑战页。因此请求层需要识别412并刷新会话// 步骤八底层请求只负责刷新一次asyncrequest(pathOrUrl,options{}){if(!this.ready){awaitthis.init();}constresponseawaitthis.requestOnce(pathOrUrl,options);if(response.statusCode412options.retryOn412!false){awaitthis.refresh();returnthis.requestOnce(pathOrUrl,options);}returnresponse;}底层 session 只做一次刷新避免无限递归。业务 API 层可以再控制最大尝试次数、退避时间和是否重载配置这样挑战层和业务层的职责更清楚。八、验证与测试第一组验证是会话验证重点确认412是否变成200以及真实业务脚本是否加载{ok:true,statusCode:200,cookieNames:[acw_tc,RS_COOKIE_O,RS_COOKIE_S,RS_COOKIE_P],title:某数据查询,scripts:[js/ajax.js,js/api.js,js/index/search-result.js]}看到statusCode: 200只能说明挑战层通过了还不能说明业务接口签名正确。还需要继续验证列表接口是否返回数据。第二组验证是签名 API 查询{ok:true,repeat:2,itemName:样例栏目,total:552,first:{fields:{字段一:样例值一,字段二:样例值二,字段三:样例值三}}}连续运行两次都返回非空列表说明会话 Cookie、UA、timestamp、sign和分页参数之间的关系基本正确。若这里失败要分别检查响应是否重新变成412还是业务 JSON 返回了签名错误。九、踩坑记录UA 必须一致挑战 Cookie 和 UA 有绑定关系。用sdenv生成 Cookie 时是一套 UA后续 API 请求必须继续使用同一套 UA。页面 200 不等于 API 可用真实页面能打开只说明瑞数挑战过了。业务接口如果缺少timestamp、sign或签名编码细节不一致仍然会失败。不要只盯着 md5 搜索瑞数挑战层不是普通签名函数直接搜索md5、sha往往定位不到挑战逻辑。挑战层更适合看首包状态、Cookie 写入和二次请求结果。Cookie 校验要检查一组状态只检查acw_tc容易误判。建议同时检查RS_COOKIE_*这类挑战状态 Cookie并在缺失时直接抛错。编码细节会影响 sign签名中()!~的编码修正很容易漏掉。如果关键词或参数里出现特殊字符默认encodeURIComponent与前端实现可能不完全一致。
http://www.rkmt.cn/news/1297403.html

相关文章:

  • DynamicData:革命性响应式集合库,简化.NET动态数据管理
  • AI-Aimbot硬件要求解析:如何为你的游戏选择最佳的AI瞄准方案
  • 盒马鲜生礼品卡用不完?回收变现只需3步,亲测靠谱 - 京顺回收
  • hostyoself实战案例:从零搭建个人博客托管系统
  • Ardb运维实战:备份恢复、监控告警与性能调优全攻略
  • 蘑菇品种识别及可食用检测-目标检测数据集
  • 终极指南:5分钟学会用FanControl免费掌控Windows风扇转速
  • 3DS文件传输终极解决方案:告别命令行,轻松无线推送游戏文件
  • 打造你的终极AI桌面助手:UI-TARS-desktop实战指南
  • 保姆级避坑指南:在米联客FPGA开发板上搞定Xilinx MIG核驱动DDR3(附完整UCF引脚配置)
  • 工控一体机与普通平板电脑的本质区别:从设计哲学到工业应用
  • 提高人类活动识别准确性的新方法:空间注意力与遗传算法的结合
  • Purple Pi OH开源鸿蒙开发板高校培训实战:从环境搭建到物联网应用开发
  • 5步精通VTube Studio API:从零构建智能虚拟主播插件
  • Steam-Economy-Enhancer多货币支持:全球交易定价策略
  • 为什么7-Zip-zstd让我的压缩效率提升了3倍?
  • Word里MathType插件报错?别慌,手把手教你搞定MathPage.wll文件丢失问题
  • PyTorch KernelAgent 源码解读 ---(3)--- orchestrator
  • PIC微控制器入门:从Hello World到LED呼吸灯实战
  • msphpsql连接恢复机制揭秘:如何在网络中断时保持应用稳定性
  • 3D打印螺纹强度提升实战指南:Fusion 360 FDM螺纹优化完整方案
  • H3C HCL模拟器实战:IS-IS单区域基础配置与排错指南
  • 如何在PUBG中实现90%的压枪稳定性提升?揭秘罗技鼠标宏的隐藏技巧
  • 边缘计算在结构健康监测中的实践与优化
  • 借助Taotoken的用量看板与审计日志精细化管控API访问权限
  • TokenCost:终极LLM成本计算工具 - 轻松估算OpenAI、Anthropic等API费用
  • X2BOT轮式机器人室内路径规划算法【附程序】
  • facebook-wda异常处理终极指南:如何优雅应对WDAError和元素不存在问题
  • msphpsql与现代化PHP框架集成指南:Laravel、Symfony等主流框架的完整配置方案
  • ABAP 生态圈里有没有类似 Spring MVC 的技术,答案不是一个名字,而是一条演进路线