
免责声明本文所有分析均基于公开可访问的前端 JS 代码及研究过程中所接触网站的正常访问流量仅用于安全研究、学习与了解 Web 前端防护机制。请勿将本文技术用于任何未授权的系统违者后果自负。一、瑞数是什么瑞数信息是国内主流的 Web 前端动态安全产品广泛应用于政务、金融、电商等高安全性场景。其核心产品Botgate人机对抗网关通过在服务端动态生成 JavaScript 代码来实现客户端行为检测属于动态安全方案。与传统静态规则防护不同瑞数的核心思想是每次访问下发的 JS 代码都不相同使得攻击者无法对固定代码进行逆向和复用。目前常见版本为瑞数4、瑞数5、瑞数6本文以研究中最常遇到的瑞数6为重点分析对象采用**补环境Node.js**方案处理。二、瑞数的核心特点2.1 动态代码下发瑞数保护的页面每次请求首页时HTML 中内嵌的 JavaScript 代码都会动态变化!-- 每次访问cd 字段内容不同 --script;$_tswindow[$_ts];if(!$_ts)$_ts{};$_ts.nsd81449;$_ts.cdqEpxrrAlo...1000字符以上的动态字符串;if($_ts.lcd)$_ts.lcd();/script!-- 带哈希的动态 JS 文件路径和文件名均为随机字符串 --scriptsrc/fpqQrgG7L6po/eaKJbLE9bqof.e17ed02.js/script$_ts.cd加密的配置数据每次请求不同动态 VMP JS 文件路径和文件名均为随机字符串每次请求不同2.2 VMP 虚拟机保护上述动态 JS 文件是经过VMPVirtual Machine Protection处理的 JS 文件其特点核心逻辑运行在自定义虚拟机中无法直接阅读对环境做大量检测是否有 DevTools 打开、是否为 Selenium、是否有 Hook 痕迹函数名、变量名均为随机字符串每版本不同2.3 多维度环境指纹采集瑞数在客户端采集大量浏览器环境信息用于服务端校验是否为真实浏览器指纹类型采集内容浏览器属性navigator.userAgent、navigator.platform、navigator.language等窗口属性window.screen、window.innerWidth、window.outerWidth等Chrome 对象window.chrome.loadTimes、window.chrome.csi、window.chrome.app等存储对象localStorage、sessionStorage的存在性及读写行为时序特征各阶段 JS 执行时间差CRC 校验对关键函数的toString()做 CRC32检测是否被 Hook2.4 Cookie 令牌机制瑞数的 Cookie 验证分为两个部分缺一不可Cookie 1服务端下发首次 GET 首页时服务端通过响应头Set-Cookie下发第一个 Cookie名称同样为 13 位随机字母数字值长度约 80~100 字符。此 Cookie 由服务端生成客户端只需接收保存即可。值的第一个字符即为瑞数版本号如6xxxx 瑞数65xxxx 瑞数5。Cookie 2客户端生成VMP JS 在浏览器端执行完毕后通过document.cookie写入第二个 Cookie名称也是 13 位随机字母数字值长度约 300~500 字符。此 Cookie 包含完整的指纹和校验数据是补环境方案需要生成的核心目标。两个 Cookie 的名称通常共享前12位字母仅末位字母不同如UA1L1zGonajvO和UA1L1zGonajvP后续请求必须同时携带两者缺少任意一个都会返回412。三、瑞数6 整体工作流程客户端首次访问页面GET 首页 ↓ 服务端返回 ① 响应头 Set-Cookie → Cookie1服务端生成值较短约90字符**首字符为瑞数版本号** ② HTML 体含 $_ts.cd 动态数据 动态 VMP JS 文件 URL ↓ Python保存 Cookie1提取 $_ts 代码块ts_js和 VMP JS 文件 URL ↓ 下载 VMP JS 文件03_source.js保存到本地 ↓ Node.js 加载补环境文件01_env.js→ 加载 ts_js02_ts.js→ 加载 03_source.js ↓ VMP JS 在补好的浏览器环境中执行采集指纹计算 Cookie 值 通过 document.cookie 写入 Cookie2客户端生成值较长~400字符 ↓ Node.js 读取 document.cookie 并输出 ↓ Python 接收 Cookie2与 Cookie1 合并组成完整 Cookie 字典 ↓ 后续请求同时携带 Cookie1 Cookie2 → 服务端校验通过 → 返回正常数据200 ↓ 若 Cookie 失效→ 服务端返回 412 → 重新触发上述流程Cookie1 和 Cookie2 均需刷新四、抓包特征识别4.1 HTML 特征同时满足以下两点即可判断目标站点使用了瑞数HTML 第一个或第二个script标签内有$_ts赋值且$_ts.cd字段内容超过 1000 字符页面中有src指向一个带哈希的动态 JS 文件路径和文件名均为随机字符串每次请求不同importre# 提取 ts_js$_ts 赋值代码块ts_jsre.findall(rscript.*?(.*?)/script,html,re.DOTALL)[1]# 提取动态 VMP JS 文件路径js_code_pathre.findall(src(.*?) ,html)[0][1:-1]4.2 响应状态码特征状态码含义200Cookie 有效正常返回数据412缺少或携带无效瑞数 Cookie需刷新202部分站点的验证拒绝状态码语义与 412 相同493 / 403请求频率过高被限流/封禁4.3 Cookie 命名特征每个瑞数站点会同时出现一对Cookie名称共享前12位、末位字母不同# Cookie1服务端下发值较短 # 瑞数6站点首字符为 6 UA1L1zGonajvO 60uvxhWj09FTINp9...约90字符 # 瑞数5站点首字符为 5 T0k1m0u5AfREO 55ES67vuQ96q_ZPJ...约90字符 # Cookie2客户端VMP JS生成值较长 UA1L1zGonajvP 0VrEWqFthuHnL1on...约400字符Cookie1Cookie2来源服务端 Set-CookieVMP JS 写入 document.cookie值首字符瑞数版本号6瑞数6,5瑞数50等值长度约 80~100 字符约 300~500 字符名称末位O字母O、S等P、T等是否需要补环境否直接接收是核心目标五、补环境方案详解5.1 整体架构Python (xxx.py) └─ subprocess.run([node, 04_main.js, gen_cookie]) └─ 04_main.js入口按序加载 ├─ require(./01_env.js) # 浏览器环境模拟 ├─ require(./02_ts.js) # 动态 $_ts 代码块每次从页面提取 └─ require(./03_source.js) # 动态 VMP JS每次从页面下载5.2 01_env.js 需补充的浏览器对象瑞数 VMP JS 在执行时会访问大量浏览器 API在 Node.js 环境中这些对象不存在需手动补充对象/属性补充方式说明windowwindow global将 Node.jsglobal作为 windowwindow.top、window.self同上同 window 引用window.innerWidth/Height固定数值如 1920/983模拟屏幕尺寸window.outerWidth/Height固定数值如 1920/1040模拟窗口尺寸window.chrome完整 chrome 对象含loadTimes、csi、app等子属性window.addEventListener空函数阻止事件绑定报错window.setInterval、setTimeout空函数阻止定时器运行window.name空字符串模拟默认 window 名称window.MutationObserver返回带observe方法的对象阻止 DOM 变化监听window.fetch、window.Request返回空对象的函数阻止 fetch 请求document.cookie注入 cookie 字符串VMP JS 可能读取 cookie 参与计算navigator.userAgent与请求头 UA 一致的字符串UA 不一致会导致指纹校验失败navigator.webdriverfalse规避自动化检测localStorage、sessionStorage实现Storage类含getItem/setItem/clear等方法XMLHttpRequest空实现阻止 JS 内部发起请求HTMLAnchorElement、HTMLFormElement等空函数或debugger函数防止构造函数调用报错__dirname、__filenamedelete删除避免 Node.js 专有变量影响 JS 检测关键注意window.chrome的loadTimes等方法需要有完整的返回值结构含connectionInfo、npnNegotiatedProtocol等字段空函数可能导致指纹计算异常。5.3 04_main.js 入口逻辑// 1. 拦截 XMLHttpRequest.open捕获 VMP JS 内部传递的动态后缀字符串letreq_url;functionget_suffix(){window.XMLHttpRequestfunctionXMLHttpRequest(){};window.XMLHttpRequest.prototype.openfunction(method,url){req_urlurl;return{};};}// 2. 按序加载require(./01_env);get_suffix();require(./02_ts);// 写入 $_ts让 VMP JS 能读到配置数据require(./03_source);// 执行 VMP JS完成 Cookie 计算并写入 document.cookie// 3. 读取生成的 Cookie2functiongen_cookie(){returndocument.cookie;}// 4. 获取请求 URL 后缀部分站点需要functionget_suffix_url(_url){consturlsnewURL(_url);constoriginurls.origin;constpathurls.pathnameurls.search;constgnewwindow.XMLHttpRequest();g.open(POST,path);// 触发 open 拦截req_url 被赋値为带后缀的路径returnoriginreq_url;// 拼接完整 URL}5.4 URL 后缀机制部分站点部分瑞数站点在 VMP JS 执行期间会通过XMLHttpRequest.open把一个独立的动态参数嵌入请求路径中传递出来。该参数以?键名值的形式附加在请求 URL 后面与 Cookie1、Cookie2 是完全独立的第三个校验参数。Python 侧发请求时必须使用带后缀的 URL否则服务端会拒绝。处理流程VMP JS 执行时调用 XMLHttpRequest.open(POST, /path/to/api?keyvalue...) ↓ 04_main.js 中 open 被 Hook将带后缀的路径赋值给 req_url ↓ Python 调用 get_suffix_url(original_url) → 返回拼接后缀的完整 URL ↓ Python 用带后缀的 URL 发起请求实际示例fangdioriginal_urlhttps://www.fangdi.com.cn/service/index/getWriteDict.action# 通过 JS 获取带后缀的完整 URLsuffix_urlcall_js_function(os.path.join(_DIR,04_main.js),get_suffix_url,original_url)# suffix_url 形如# https://www.fangdi.com.cn/service/index/getWriteDict.action?XJlCTRRM01MikXalq...responserequests.post(suffix_url,cookiescookies,headersheaders,datadata)注意get_suffix()必须在require(./02_ts)之前调用确保在 VMP JS 开始执行前就已完成 Hook。大多数站点没有后缀机制用gen_cookie()读取document.cookie即可当请求接口返回 412 且确认 Cookie 正确时才需考虑启用后缀机制。5.5 Python 侧调用代码importreimportsubprocessimportrequestsdefcall_js_function(js_file_path,function_name):resultsubprocess.run([node,js_file_path,function_name],capture_outputTrue,textTrue,checkTrue,encodingutf-8)returnresult.stdout.strip()defget_rs_cookie(host,url,ts_js_path,source_js_path,main_js_path):# 1. 请求首页获取动态数据resrequests.get(url,headersheaders,verifyFalse)htmlres.content.decode(utf-8)# 2. 提取 ts_js 和 VMP JS 路径ts_jsre.findall(rscript.*?(.*?)/script,html,re.DOTALL)[1]js_code_pathre.findall(src(.*?) ,html)[0][1:-1]# 3. 下载 VMP JSjs_coderequests.get(f{host}{js_code_path},headersheaders,verifyFalse).text# 4. 写入本地文件withopen(ts_js_path,w,encodingutf-8)asf:f.write(ts_js)withopen(source_js_path,w,encodingutf-8)asf:f.write(js_code)# 4. 同时保存服务端下发的 Cookie1来自响应头 Set-Cookiecookie1res.cookies.get_dict()# {UA1L1zGonajvO: 60uvx...}# 5. 调用 Node.js 生成 Cookie2VMP JS 写入 document.cookiecookie_rescall_js_function(main_js_path,gen_cookie)key,valuecookie_res.split(;)[0].split(,1)cookie2{key:value}# {UA1L1zGonajvP: 0VrEW...}# 6. 合并两个 Cookie 一起返回return{**cookie1,**cookie2}defrequest_with_rs(api_url,cookies,data,headers):resrequests.post(api_url,cookiescookies,headersheaders,datadata,verifyFalse)ifres.status_code412:# Cookie1 和 Cookie2 均需刷新cookies.update(get_rs_cookie(...))resrequests.post(api_url,cookiescookies,headersheaders,datadata,verifyFalse)returnres六、关于 content 字段大多数瑞数站点的首页 HTML 中除了$_ts代码块外还会同时返回一个content字段通常是一段 JSON 或配置数据赋值给window.content。!-- 示例页面中的 content 赋值 --metanamecontentcontent{key:value,...}/!-- 或以 JS 变量形式出现 --scriptwindow.content{...}/script是否需要在补环境中注入content大多数站点的 VMP JS 实际上不会读取window.content参与 Cookie 计算即使不注入该字段补环境也能正常生成 Cookie。仅在少数站点VMP JS 内部确实引用了window.content时才需要额外提取并注入# 仅在确认 VMP JS 依赖 content 字段时才需要此步骤content_textre.findall(rcontent(.*?) ,html,re.DOTALL)[1]withopen(./06_content.js,w,encodingutf-8)asf:f.write(fwindow.content{content_text})# 加载顺序01_env.js → 06_content.js → 02_ts.js → 03_source.js调试建议若不确定是否需要注入可先不注入直接跑Cookie 生成成功则无需处理若 Node.js 执行报错提示window.content相关异常再按需注入。七、动态 VMP JS 文件缓存策略VMP JS 文件的 URL 路径是完全随机的字符串如/fpqQrgG7L6po/eaKJbLE9bqof.e17ed02.js服务端会定期更新。建议以完整 URL 路径作为缓存键URL 变化时重新下载importosimporthashlibdefget_js_with_cache(host,js_url_path,cache_dir./cache): 以 URL 路径的 MD5 为缓存键文件存在则复用URL 变化时重新下载 # 用 URL 路径的 MD5 作为缓存文件名避免文件名中含特殊字符url_hashhashlib.md5(js_url_path.encode()).hexdigest()[:12]cache_pathos.path.join(cache_dir,fsource_{url_hash}.js)ifos.path.exists(cache_path):# 缓存命中直接复用withopen(cache_path,r,encodingutf-8)asf:returnf.read()else:# 下载新版本写入缓存js_coderequests.get(f{host}{js_url_path}).text os.makedirs(cache_dir,exist_okTrue)withopen(cache_path,w,encodingutf-8)asf:f.write(js_code)returnjs_code八、本项目涉及的瑞数站点汇总以下是本次研究中实际接入瑞数处理的站点清单8.1 政务监管类站点域名说明湖北市场监管scjg.hubei.gov.cn瑞数6标准补环境方案8.2 学术资源类站点域名说明维普期刊期刊频道qikan.cqvip.com瑞数6标准补环境方案维普期刊图书馆入口lib.cqvip.com瑞数6与 qikan 使用相同 VMP JS8.3 政务数据类站点域名说明海关统计数据在线stats.customs.gov.cn瑞数6标准补环境方案上海房地产交易中心www.fangdi.com.cn瑞数6另有 URL 后缀改写逻辑国家知识产权局专利检索epub.cnipa.gov.cn瑞数6需注入 content 字段从 meta 标签提取国家药品监督管理局www.nmpa.gov.cn瑞数6标准补环境另有 MD5 签名sign timestamp 参数8.4 商业平台类站点域名说明欧冶云商钢铁电商www.ouyeel.com瑞数6标准补环境方案九、常见问题与调试Q1Node.js 执行 03_source.js 时崩溃报错通常是补环境不完整。建议在01_env.js中临时开启 Proxy 拦截打印所有被访问的属性// 调试用对 window 开启 Proxy打印所有 get/set 访问windownewProxy(global,{get(target,property){console.log([GET],property);returnReflect.get(target,property);},set(target,property,value){console.log([SET],property,,value);returnReflect.set(target,property,value);}});根据打印结果逐一补充缺失的属性。Q2Cookie 生成成功但请求仍返回 412可能原因Cookie 已过期瑞数 Cookie 有效期较短需在请求前重新生成Cookie 键名错误Cookie 名称由 VMP JS 动态计算需从document.cookie输出中正确提取UA 不一致01_env.js中的navigator.userAgent必须与请求头中的User-Agent完全一致Q3VMP JS 文件更新后 Cookie 失效VMP JS 文件的 URL 哈希变化时旧版生成的 Cookie 可能被服务端拒绝。解决方案每次请求首页时重新提取最新的 VMP JS URL 并下载检测到 URL 哈希变化时清除旧缓存文件Q4确认已有两个 Cookie 但仍然返回 412请检查两个 Cookie 是否都是最新的。瑞数的 Cookie1 和 Cookie2 是配套的同一次首页请求产生如果只刷新了其中一个而另一个使用了旧值服务端仍会拒绝。刷新时必须同时重新获取两者。十、小结瑞数的核心防护思路可以概括为四点动态性每次下发的 JS 代码和配置数据都不同使固定逆向结果失效VMP 保护核心逻辑运行在自定义虚拟机中难以静态分析多维指纹从浏览器属性、窗口尺寸、Chrome 对象等多维度综合判断是否为真实浏览器站点隔离每个站点的关键参数独立配置防止通用破解补环境方案的优势直接调用原始 VMP JS无需手动还原算法适配性强对大量相似结构的站点可复用同一套补环境框架仅需按站点调整关键配置。十一、依赖安装pipinstallrequestsnode--version# 需要 Node.js 环境建议 v16本文技术仅供安全研究与学习切勿用于任何未授权系统违者后果自负。