Web 前端安全机制分析:以瑞数(RisShu)为例

免责声明: 本文所有分析均基于公开可访问的前端 JS 代码及研究过程中所接触网站的正常访问流量,仅用于安全研究、学习与了解 Web 前端防护机制。请勿将本文技术用于任何未授权的系统,违者后果自负。


一、瑞数是什么

瑞数信息(RisShu)是国内主流的 Web 前端动态安全产品,广泛应用于政务、金融、电商等高安全性场景。其核心产品 Botgate(人机对抗网关) 通过在服务端动态生成 JavaScript 代码来实现客户端行为检测,属于动态安全方案。

与传统静态规则防护不同,瑞数的核心思想是:

每次访问下发的 JS 代码都不相同,使得攻击者无法对固定代码进行逆向和复用。

目前常见版本为瑞数4、瑞数5、瑞数6 ,本文以研究中最常遇到的瑞数6为重点分析对象,采用**补环境(Node.js)**方案处理。


二、瑞数的核心特点

2.1 动态代码下发

瑞数保护的页面,每次请求首页时,HTML 中内嵌的 JavaScript 代码都会动态变化

html 复制代码
<!-- 每次访问,cd 字段内容不同 -->
<script>
;$_ts=window['$_ts'];if(!$_ts)$_ts={};
$_ts.nsd=81449;
$_ts.cd='qEpxrrAlo...(1000字符以上的动态字符串)';
if($_ts.lcd)$_ts.lcd();
</script>

<!-- 带哈希的动态 JS 文件,路径和文件名均为随机字符串 -->
<script src="/fpqQrgG7L6po/eaKJbLE9bqof.e17ed02.js"></script>
  • $_ts.cd:加密的配置数据,每次请求不同
  • 动态 VMP JS 文件:路径和文件名均为随机字符串,每次请求不同

2.2 VMP 虚拟机保护

上述动态 JS 文件是经过 VMP(Virtual Machine Protection) 处理的 JS 文件,其特点:

  • 核心逻辑运行在自定义虚拟机中,无法直接阅读
  • 对环境做大量检测:是否有 DevTools 打开、是否为 Selenium、是否有 Hook 痕迹
  • 函数名、变量名均为随机字符串,每版本不同

2.3 多维度环境指纹采集

瑞数在客户端采集大量浏览器环境信息,用于服务端校验是否为真实浏览器:

指纹类型 采集内容
浏览器属性 navigator.userAgentnavigator.platformnavigator.language
窗口属性 window.screenwindow.innerWidthwindow.outerWidth
Chrome 对象 window.chrome.loadTimeswindow.chrome.csiwindow.chrome.app
存储对象 localStoragesessionStorage 的存在性及读写行为
时序特征 各阶段 JS 执行时间差
CRC 校验 对关键函数的 toString() 做 CRC32,检测是否被 Hook

瑞数的 Cookie 验证分为两个部分,缺一不可:

Cookie 1(服务端下发) :首次 GET 首页时,服务端通过响应头 Set-Cookie 下发第一个 Cookie,名称同样为 13 位随机字母数字,值长度约 80~100 字符。此 Cookie 由服务端生成,客户端只需接收保存即可。值的第一个字符即为瑞数版本号 (如 6xxxx = 瑞数6,5xxxx = 瑞数5)。

Cookie 2(客户端生成) :VMP JS 在浏览器端执行完毕后,通过 document.cookie 写入第二个 Cookie,名称也是 13 位随机字母数字,值长度约 300~500 字符。此 Cookie 包含完整的指纹和校验数据,是补环境方案需要生成的核心目标。

两个 Cookie 的名称通常共享前12位字母,仅末位字母不同 (如 UA1L1zGonajvOUA1L1zGonajvP),后续请求必须同时携带两者,缺少任意一个都会返回 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_js(02_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 特征

同时满足以下两点,即可判断目标站点使用了瑞数:

  1. HTML 第一个或第二个 <script> 标签内有 $_ts 赋值,且 $_ts.cd 字段内容超过 1000 字符
  2. 页面中有 src 指向一个带哈希的动态 JS 文件,路径和文件名均为随机字符串,每次请求不同
python 复制代码
import re

# 提取 ts_js($_ts 赋值代码块)
ts_js = re.findall(r'<script.*?>(.*?)</script>', html, re.DOTALL)[1]

# 提取动态 VMP JS 文件路径
js_code_path = re.findall('src=(.*?) ', html)[0][1:-1]

4.2 响应状态码特征

状态码 含义
200 Cookie 有效,正常返回数据
412 缺少或携带无效瑞数 Cookie,需刷新
202 部分站点的验证拒绝状态码,语义与 412 相同
493 / 403 请求频率过高被限流/封禁

每个瑞数站点会同时出现一对 Cookie,名称共享前12位、末位字母不同:

复制代码
# Cookie1(服务端下发,值较短)
# 瑞数6站点:首字符为 6
UA1L1zGonajvO = 60uvxhWj09FTINp9...(约90字符)
# 瑞数5站点:首字符为 5
T0k1m0u5AfREO  = 55ES67vuQ96q_ZPJ...(约90字符)

# Cookie2(客户端VMP JS生成,值较长)
UA1L1zGonajvP = 0VrEWqFthuHnL1on...(约400字符)
Cookie1 Cookie2
来源 服务端 Set-Cookie VMP JS 写入 document.cookie
值首字符 瑞数版本号6=瑞数6, 5=瑞数5) 0
值长度 约 80~100 字符 约 300~500 字符
名称末位 O(字母O)、S PT
是否需要补环境 否,直接接收 是,核心目标

五、补环境方案详解

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 环境中这些对象不存在,需手动补充:

对象/属性 补充方式 说明
window window = global 将 Node.js global 作为 window
window.topwindow.self 同上 同 window 引用
window.innerWidth/Height 固定数值(如 1920/983) 模拟屏幕尺寸
window.outerWidth/Height 固定数值(如 1920/1040) 模拟窗口尺寸
window.chrome 完整 chrome 对象 loadTimescsiapp 等子属性
window.addEventListener 空函数 阻止事件绑定报错
window.setIntervalsetTimeout 空函数 阻止定时器运行
window.name 空字符串 '' 模拟默认 window 名称
window.MutationObserver 返回带 observe 方法的对象 阻止 DOM 变化监听
window.fetchwindow.Request 返回空对象的函数 阻止 fetch 请求
document.cookie 注入 cookie 字符串 VMP JS 可能读取 cookie 参与计算
navigator.userAgent 与请求头 UA 一致的字符串 UA 不一致会导致指纹校验失败
navigator.webdriver false 规避自动化检测
localStoragesessionStorage 实现 Storage getItem/setItem/clear 等方法
XMLHttpRequest 空实现 阻止 JS 内部发起请求
HTMLAnchorElementHTMLFormElement 空函数或 debugger 函数 防止构造函数调用报错
__dirname__filename delete 删除 避免 Node.js 专有变量影响 JS 检测

关键注意window.chromeloadTimes 等方法需要有完整的返回值结构(含 connectionInfonpnNegotiatedProtocol 等字段),空函数可能导致指纹计算异常。

5.3 04_main.js 入口逻辑

javascript 复制代码
// 1. 拦截 XMLHttpRequest.open,捕获 VMP JS 内部传递的动态后缀字符串
let req_url;
function get_suffix() {
    window.XMLHttpRequest = function XMLHttpRequest() {};
    window.XMLHttpRequest.prototype.open = function(method, url) {
        req_url = url;
        return {};
    };
}

// 2. 按序加载
require('./01_env');
get_suffix();
require('./02_ts');       // 写入 $_ts,让 VMP JS 能读到配置数据
require('./03_source');   // 执行 VMP JS,完成 Cookie 计算并写入 document.cookie

// 3. 读取生成的 Cookie2
function gen_cookie() {
    return document.cookie;
}

// 4. 获取请求 URL 后缀(部分站点需要)
function get_suffix_url(_url) {
    const urls = new URL(_url);
    const origin = urls.origin;
    const path = urls.pathname + urls.search;
    const g = new window.XMLHttpRequest();
    g.open("POST", path);   // 触发 open 拦截,req_url 被赋値为带后缀的路径
    return origin + req_url; // 拼接完整 URL
}

5.4 URL 后缀机制(部分站点)

部分瑞数站点在 VMP JS 执行期间,会通过 XMLHttpRequest.open 把一个独立的动态参数 嵌入请求路径中传递出来。该参数以 ?键名=值 的形式附加在请求 URL 后面,与 Cookie1、Cookie2 是完全独立的第三个校验参数。Python 侧发请求时必须使用带后缀的 URL,否则服务端会拒绝。

处理流程:

复制代码
VMP JS 执行时调用 XMLHttpRequest.open("POST", "/path/to/api?key=value...")
    ↓
04_main.js 中 open 被 Hook,将带后缀的路径赋值给 req_url
    ↓
Python 调用 get_suffix_url(original_url) → 返回拼接后缀的完整 URL
    ↓
Python 用带后缀的 URL 发起请求

实际示例(fangdi):

python 复制代码
original_url = 'https://www.fangdi.com.cn/service/index/getWriteDict.action'
# 通过 JS 获取带后缀的完整 URL
suffix_url = call_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?XJlCTRRM=01MikXalq...'
response = requests.post(suffix_url, cookies=cookies, headers=headers, data=data)

注意get_suffix() 必须在 require('./02_ts') 之前调用,确保在 VMP JS 开始执行前就已完成 Hook。大多数站点没有后缀机制,用 gen_cookie() 读取 document.cookie 即可;当请求接口返回 412 且确认 Cookie 正确时,才需考虑启用后缀机制。

5.5 Python 侧调用代码

python 复制代码
import re
import subprocess
import requests

def call_js_function(js_file_path, function_name):
    result = subprocess.run(
        ['node', js_file_path, function_name],
        capture_output=True, text=True, check=True, encoding='utf-8'
    )
    return result.stdout.strip()

def get_rs_cookie(host, url, ts_js_path, source_js_path, main_js_path):
    # 1. 请求首页,获取动态数据
    res = requests.get(url, headers=headers, verify=False)
    html = res.content.decode('utf-8')

    # 2. 提取 ts_js 和 VMP JS 路径
    ts_js = re.findall(r'<script.*?>(.*?)</script>', html, re.DOTALL)[1]
    js_code_path = re.findall('src=(.*?) ', html)[0][1:-1]

    # 3. 下载 VMP JS
    js_code = requests.get(f"{host}{js_code_path}", headers=headers, verify=False).text

    # 4. 写入本地文件
    with open(ts_js_path, 'w', encoding='utf-8') as f:
        f.write(ts_js)
    with open(source_js_path, 'w', encoding='utf-8') as f:
        f.write(js_code)

    # 4. 同时保存服务端下发的 Cookie1(来自响应头 Set-Cookie)
    cookie1 = res.cookies.get_dict()   # {'UA1L1zGonajvO': '60uvx...'}

    # 5. 调用 Node.js 生成 Cookie2(VMP JS 写入 document.cookie)
    cookie_res = call_js_function(main_js_path, "gen_cookie")
    key, value = cookie_res.split(';')[0].split('=', 1)
    cookie2 = {key: value}             # {'UA1L1zGonajvP': '0VrEW...'}

    # 6. 合并两个 Cookie 一起返回
    return {**cookie1, **cookie2}

def request_with_rs(api_url, cookies, data, headers):
    res = requests.post(api_url, cookies=cookies, headers=headers, data=data, verify=False)
    if res.status_code == 412:
        # Cookie1 和 Cookie2 均需刷新
        cookies.update(get_rs_cookie(...))
        res = requests.post(api_url, cookies=cookies, headers=headers, data=data, verify=False)
    return res

六、关于 content 字段

大多数瑞数站点的首页 HTML 中,除了 $_ts 代码块外,还会同时返回一个 content 字段(通常是一段 JSON 或配置数据,赋值给 window.content)。

html 复制代码
<!-- 示例:页面中的 content 赋值 -->
<meta name="content" content='{"key":"value",...}' />
<!-- 或以 JS 变量形式出现 -->
<script>window.content = {...}</script>

是否需要在补环境中注入 content 大多数站点的 VMP JS 实际上不会 读取 window.content 参与 Cookie 计算,即使不注入该字段,补环境也能正常生成 Cookie。仅在少数站点(VMP JS 内部确实引用了 window.content)时,才需要额外提取并注入:

python 复制代码
# 仅在确认 VMP JS 依赖 content 字段时才需要此步骤
content_text = re.findall(r'content=(.*?) ', html, re.DOTALL)[1]
with open('./06_content.js', 'w', encoding='utf-8') as f:
    f.write(f"window.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 变化时重新下载:

python 复制代码
import os
import hashlib

def get_js_with_cache(host, js_url_path, cache_dir='./cache'):
    """
    以 URL 路径的 MD5 为缓存键,文件存在则复用,URL 变化时重新下载
    """
    # 用 URL 路径的 MD5 作为缓存文件名,避免文件名中含特殊字符
    url_hash = hashlib.md5(js_url_path.encode()).hexdigest()[:12]
    cache_path = os.path.join(cache_dir, f"source_{url_hash}.js")

    if os.path.exists(cache_path):
        # 缓存命中,直接复用
        with open(cache_path, 'r', encoding='utf-8') as f:
            return f.read()
    else:
        # 下载新版本,写入缓存
        js_code = requests.get(f"{host}{js_url_path}").text
        os.makedirs(cache_dir, exist_ok=True)
        with open(cache_path, 'w', encoding='utf-8') as f:
            f.write(js_code)
        return js_code

八、本项目涉及的瑞数站点汇总

以下是本次研究中实际接入瑞数处理的站点清单:

8.1 政务监管类

站点 域名 说明
湖北市场监管 scjg.hubei.gov.cn 瑞数6,标准补环境方案

8.2 学术资源类

站点 域名 说明
维普期刊(期刊频道) qikan.cqvip.com 瑞数6,标准补环境方案
维普期刊(图书馆入口) lib.cqvip.com 瑞数6,与 qikan 使用相同 VMP JS

8.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,标准补环境方案

九、常见问题与调试

Q1:Node.js 执行 03_source.js 时崩溃报错

通常是补环境不完整。建议在 01_env.js 中临时开启 Proxy 拦截,打印所有被访问的属性:

javascript 复制代码
// 调试用:对 window 开启 Proxy,打印所有 get/set 访问
window = new Proxy(global, {
    get(target, property) {
        console.log('[GET]', property);
        return Reflect.get(target, property);
    },
    set(target, property, value) {
        console.log('[SET]', property, '=', value);
        return Reflect.set(target, property, value);
    }
});

根据打印结果,逐一补充缺失的属性。

Q2:Cookie 生成成功但请求仍返回 412

可能原因:

  1. Cookie 已过期:瑞数 Cookie 有效期较短,需在请求前重新生成
  2. Cookie 键名错误 :Cookie 名称由 VMP JS 动态计算,需从 document.cookie 输出中正确提取
  3. UA 不一致01_env.js 中的 navigator.userAgent 必须与请求头中的 User-Agent 完全一致

VMP JS 文件的 URL 哈希变化时,旧版生成的 Cookie 可能被服务端拒绝。解决方案:

  • 每次请求首页时重新提取最新的 VMP JS URL 并下载
  • 检测到 URL 哈希变化时,清除旧缓存文件

请检查两个 Cookie 是否都是最新的。瑞数的 Cookie1 和 Cookie2 是配套的(同一次首页请求产生),如果只刷新了其中一个而另一个使用了旧值,服务端仍会拒绝。刷新时必须同时重新获取两者。


十、小结

瑞数的核心防护思路可以概括为四点:

  1. 动态性:每次下发的 JS 代码和配置数据都不同,使固定逆向结果失效
  2. VMP 保护:核心逻辑运行在自定义虚拟机中,难以静态分析
  3. 多维指纹:从浏览器属性、窗口尺寸、Chrome 对象等多维度综合判断是否为真实浏览器
  4. 站点隔离:每个站点的关键参数独立配置,防止通用破解

补环境方案的优势:直接调用原始 VMP JS,无需手动还原算法,适配性强,对大量相似结构的站点可复用同一套补环境框架,仅需按站点调整关键配置。


十一、依赖安装

bash 复制代码
pip install requests
node --version  # 需要 Node.js 环境(建议 v16+)

本文技术仅供安全研究与学习,切勿用于任何未授权系统,违者后果自负。

相关推荐
freewlt2 小时前
前端安全新范式:2026年防护实战
前端·安全
无忧智库2 小时前
新能源场站无人值守革命:构建高效、安全、智能的下一代运维体系(PPT)
运维·安全
包子源2 小时前
React-PDF 详解:API 要点与在线简历项目中的落地
前端·react.js·pdf
Bruce_Liuxiaowei2 小时前
2026年4月第3周网络安全形势周报
安全·web安全
Bigger2 小时前
第九章:我是如何剖析 Claude Code 的 CLI 里的安全沙盒与指令拦截机制的
前端·claude·源码阅读
黎阳之光2 小时前
黎阳之光受邀出席上海口岸联合会2026智慧口岸研讨班 无感通关方案获盛赞
大数据·人工智能·算法·安全·数字孪生
Jason_zhao_MR2 小时前
STM32MP135F安全芯引入!米尔MYD-YF13X系统、安全、功能三重升级
stm32·嵌入式硬件·安全·嵌入式
得想办法娶到那个女人2 小时前
Vue3 组合式API 标准写法(通俗易懂,可直接复制)
前端·javascript·vue.js
kang0x02 小时前
Divide and Conquer - Writeup by AI
安全