雷池 WAF 反爬逆向记录

在一个被雷池 WAF 防护的网站页面上,打开 F12 开发者工具后点击验证按钮,页面提示「当前环境正在被调试」。

查看源码发现,在 challenge.js 中有如下关键逻辑:

if (v().score >= 100) {
    g("on-debug", "");
    return;
}

这段代码会检测当前环境的 score 值:

  • score >= 100 时,说明环境正在被调试(例如打开 F12、或使用 Selenium / Playwright 等自动化工具),会直接中断验证流程。

解决办法:
注释掉这段判断,再点击验证按钮,就可以继续观察验证时发送的请求参数。

验证流程分析

通过抓包调试,大致可以理出验证流程共 4 步:

1.获取 client_id
请求存在雷池 WAF 的目标网页,在返回的源码中能找到类似:

SafeLineChallenge("703108c1058944a0abc02beaf6a10dcc_9")

这里的 "703108c1058944a0abc02beaf6a10dcc_9" 就是 client_id

2.请求 issue 接口
调用 issue 接口,params 携带 client_id,可以得到 issue_id 以及一组验证用参数列表。

返回示例:

{"message":"ok","code":200,"data":{"data":[77,48,60,72,65,50,59,99,3,38,96,32,89,53,67,73,90],"issue_id":"5rDshrUbwr5EMwls"}

3.请求验证接口
将从 issue 返回的数据经过一系列加工(详见后文代码)后提交到验证接口,成功后会返回 JWT 令牌。

请求验证参数:

{'issue_id': '5rDshrUbwr5EMwls', 'result': [29, 13, 10, 40], 'serials': [], 'client': {'userAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0', 'platform': 'Win32', 'language': 'zh-CN', 'vendor': 'Google Inc.', 'screen': [2560, 1440], 'visitorId': '7e7f2db1940534363ff097a0300a48fb', 'score': 100, 'target': ['27']}}

返回示例:

{'message': 'ok', 'code': 200, 'data': {'jwt': 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiIiLCJpYXQiOjE3NTc5OTc5NjEsImxldmVsIjowfQ.7e__t6tCRdQTZx7QT9iBy4CE3rwhPe682riNdImAU_DcdaCCe_rzfDgyIUOgdUY6-WraRBp-5jVUvvSrKEMZjg', 'verified': True}}

4.携带 sl-challenge-jwt 再访问网页
把返回的 JWT 令牌设置到 cookies 中的 sl-challenge-jwt,再次访问检测页面,就可以获取 sl_jwt_session
有了 sl_jwt_session,后续就能持续正常访问网页了。

关键参数说明

  • visitorId
    设备指纹,使用 fingerprintjs2 生成。
  • result
    issue 接口返回的数据,经过一系列算法加工后生成,用于验证接口。

代码

下面是完整验证代码

import json
import requests
import subprocess

import urllib3  # 防止http报错
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)     # 防止http报错

"""
方法一
npm install -g wabt
复制粘贴 https://challenge.rivers.chaitin.cn/challenge/v2/calc.wasm 代码到本地保存为 calc.wat
编译:wat2wasm calc.wat -o calc.wasm
安装生成浏览器指纹需要的库:npm install fingerprintjs2

方法二
pip install subprocess mmh3
"""


# 方法一 通过 subprocess 运行 JS 脚本,调用 calc.wasm 文件转换得到 result 列表
def get_safeline_sl_jwt_session(url):
    """
    获取Safeline系统的JWT令牌

    该函数用于从 Safeline 安全系统中获取 JSON Web Token(JWT),
    用于后续的API调用认证和授权。

    返回值:
        str: JWT令牌字符串,用于添加到 cookies: "sl-challenge-jwt" 中

    异常:
        可能抛出与网络请求、认证失败相关的异常
    """
    headers = {
        'Host': url.split('/')[2],
        'Connection': 'keep-alive',
        'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
        'Upgrade-Insecure-Requests': '1',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
        'Sec-Fetch-Site': 'none',
        'Sec-Fetch-Mode': 'navigate',
        'Sec-Fetch-User': '?1',
        'Sec-Fetch-Dest': 'document',
        'Accept-Language': 'zh-CN,zh;q=0.9',
    }

    ### 请求存在雷池 WAF 的网页,从网页中获取 client_id,示例:SafeLineChallenge("703108c1058944a0abc02beaf6a10dcc_9"
    response = requests.get(url, headers=headers, verify=False)
    # print(response.text)

    client_id = response.text.split('SafeLineChallenge("')[-1].split('"')[0]
    print(f'client_id: {client_id}')

    ### 请求 issue 接口,params 携带 client_id ,获取issue_id和列表
    params = {
        "client_id": client_id,
        "level": 1
    }
    headers['referer'] = url
    headers['Origin'] = url
    headers['Host'] = 'challenge.rivers.chaitin.cn'
    response = requests.post('https://challenge.rivers.chaitin.cn/challenge/v2/api/issue', params=params, headers=headers, verify=False)
    print(response.text)

    ### 调用 node 脚本,执行js获取请求验证必要的参数
    result = subprocess.run(["node", "123.js", str(response.json()['data']['data'])], capture_output=True, text=True).stdout.strip()
    print(result)
    result_li = result.split('\n')

    # 判断返回值,定义 result 和 visitorId
    for i in result_li:
        if '[' in i:
            result = json.loads(i)
        else:
            visitorId = i

    print('result:', result)
    print('visitorId:', visitorId)

    ### 请求验证接口,获取JWT令牌
    json_data = {
        "issue_id": response.json()['data']['issue_id'],
        "result": result,
        "serials": [],
        "client": {
            "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
            "platform": "Win32",
            "language": "zh-CN",
            "vendor": "Google Inc.",
            "screen": [
                2560,
                1440
            ],
            "visitorId": visitorId,
            "score": 100,
            "target": [
                "27"
            ]
        }
    }
    print(json_data)
    response = requests.post('https://challenge.rivers.chaitin.cn/challenge/v2/api/verify', headers=headers, json=json_data, verify=False)
    print(response.json())
    jwt = response.json()['data']['jwt']

    ### cookies 带上 sl-challenge-jwt 再次请求检测网页,获取 sl_jwt_session
    cookies = {
        'sl-challenge-server': 'cloud',
        'sl-challenge-jwt': jwt,
    }
    headers['Host'] = url.split('/')[2]
    response = requests.get(url, headers=headers, cookies=cookies, verify=False)
    print(response.cookies)
    sl_jwt_session = response.cookies.get('sl_jwt_session')
    print(f'sl_jwt_session: {sl_jwt_session}')

    return sl_jwt_session

# 方法二 纯 python 实现,直接通过 f 函数转换得到 result 列表
def get_safeline_sl_jwt_session_2(url):

    host_name = url.split('/')[2]
    def f(e):
        """
        转换 result 列表
        :param e: 需要转换的列表 list
        :return: 转换后的列表 list
        """
        # 计算 e 所有元素之和
        n = sum(e)
        # 初始 t = 1
        t = 1
        # (6 + e长度 + n) % 6 + 6  得到循环次数
        r = (6 + len(e) + n) % 6 + 6

        # 循环 r 次,每次 t *= 6
        for _ in range(r):
            t *= 6

        # 如果 t < 6666 ,乘以 e 长度
        if t < 6666:
            t *= len(e)

        # 如果 t > 0x3f940aa,t = t // len(e)
        if t > 0x3f940aa:
            t = t // len(e)

        # 遍历 e 中的每个元素
        for o in range(len(e)):
            t += e[o] ** 3        # 加立方
            t ^= o                 # 按位异或索引
            t ^= e[o] + o          # 按位异或 (元素+索引)

        # 把 t 转换成 6 位分组(base64-like, 每组6位)
        f_arr = []
        while t > 0:
            f_arr.insert(0, t & 63)  # 63 & t 等价 t % 64
            t >>= 6

        return f_arr

    import mmh3
    def fingerprint_hash(components_values: list[str]) -> str:
        """
        生成组件值列表的指纹哈希值
        该函数接收一个字符串列表,通过对列表中的组件值进行处理和哈希计算,
        生成一个唯一的指纹标识符。这个指纹可以用于识别和比较不同的组件组合。

        :param components_values: 包含组件值的字符串列表,每个元素代表一个组件的值
        :return: 生成的指纹哈希字符串,用于唯一标识输入的组件值组合
        """
        # 拼接所有组件的 value
        joined = ''.join(components_values)
        # 计算 murmurhash3 128bit
        # mmh3.hash128 返回 int,需要转为 16进制
        hash_int = mmh3.hash128(joined, 31, signed=False)  # 31 是 seed
        return f"{hash_int:032x}"  # 补齐32位16进制

    headers = {
        'Host': host_name,
        'Connection': 'keep-alive',
        'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
        'Upgrade-Insecure-Requests': '1',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
        'Sec-Fetch-Site': 'none',
        'Sec-Fetch-Mode': 'navigate',
        'Sec-Fetch-User': '?1',
        'Sec-Fetch-Dest': 'document',
        'Accept-Language': 'zh-CN,zh;q=0.9',
    }

    ### 请求存在雷池 WAF 的网页,从网页中获取 client_id,示例:SafeLineChallenge("703108c1058944a0abc02beaf6a10dcc_9"
    response = requests.get(url, headers=headers, verify=False)
    # print(response.text)

    client_id = response.text.split('SafeLineChallenge("')[-1].split('"')[0]
    print(f'client_id: {client_id}')

    ### 请求 issue 接口,params 携带 client_id ,获取issue_id和列表
    params = {
        "client_id": client_id,
        "level": 1
    }
    headers['referer'] = url
    headers['Origin'] = url
    headers['Host'] = 'challenge.rivers.chaitin.cn'
    response = requests.post('https://challenge.rivers.chaitin.cn/challenge/v2/api/issue', params=params, headers=headers, verify=False)
    print(response.text)

    # 测试
    result = f(response.json()['data']['data'])
    print("🔢 计算结果:", result)

    # 示例:模拟浏览器组件 value 列表
    components = ["userAgentXYZ", "platformWin", "canvasAbc123"]

    visitorId = fingerprint_hash(components)
    print("🧠 指纹哈希:", visitorId)

    ### 请求验证接口,获取JWT令牌
    json_data = {
        "issue_id": response.json()['data']['issue_id'],
        "result": result,
        "serials": [],
        "client": {
            "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
            "platform": "Win32",
            "language": "zh-CN",
            "vendor": "Google Inc.",
            "screen": [
                2560,
                1440
            ],
            "visitorId": visitorId,
            "score": 100,
            "target": [
                "27"
            ]
        }
    }
    print(json_data)
    response = requests.post('https://challenge.rivers.chaitin.cn/challenge/v2/api/verify', headers=headers, json=json_data, verify=False)
    print(response.json())
    jwt = response.json()['data']['jwt']

    ### cookies 带上 sl-challenge-jwt 再次请求检测网页,获取 sl_jwt_session
    cookies = {
        'sl-challenge-server': 'cloud',
        'sl-challenge-jwt': jwt,
    }
    headers['Host'] = host_name
    response = requests.get(url, headers=headers, cookies=cookies, verify=False)
    print(response.cookies)
    sl_jwt_session = response.cookies.get('sl_jwt_session')
    print(f'sl_jwt_session: {sl_jwt_session}')

    return sl_jwt_session

# 存在雷池 WAF 的网址
url = 'https://www.test.com'

# 获取 sl_jwt_session
# sl_jwt_session = get_safeline_sl_jwt_session()
sl_jwt_session = get_safeline_sl_jwt_session_2(url)

headers = {
        'Host': url.split('/')[2],
        'Connection': 'keep-alive',
        'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
        'Upgrade-Insecure-Requests': '1',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
        'Sec-Fetch-Site': 'none',
        'Sec-Fetch-Mode': 'navigate',
        'Sec-Fetch-User': '?1',
        'Sec-Fetch-Dest': 'document',
        'Accept-Language': 'zh-CN,zh;q=0.9',
    }

# 验证带上 sl_jwt_session 请求网页是否正常
cookies = {
    'myannoun': '1',
    'sl_jwt_session': sl_jwt_session,
}
response = requests.get(url, headers=headers, cookies=cookies, verify=False)
print(response.text)

如果使用方法一获取 sl_jwt_session 则需要先安装 Node.js 以及按上面代码注释中的方法创建 calc.wasm

再创建 123.js

const fs = require('fs');
const wasmCode = fs.readFileSync('calc.wasm');
const Fingerprint2 = require('fingerprintjs2');
Fingerprint2.get(components => {
    const values = components.map(component => component.value);
    const murmur = Fingerprint2.x64hash128(values.join(''), 31);
    console.log(murmur);
});
function aa(e) {
    e = JSON.parse(e);
    WebAssembly.instantiate(wasmCode, {
        "env": {},
        "wasi_snapshot_preview1": {}
    }).then(result => {
        const instance = result.instance;
        const reset = instance.exports.reset;
        const arg = instance.exports.arg;
        const calc = instance.exports.calc;
        const ret = instance.exports.ret;
        aaaa = function() {
            return reset(),
            e.map(function(e) { return arg(e); }),
            Array(calc()).fill(-1).map(function() { return ret(); });
        }();
        console.log(JSON.stringify(aaaa));
    });
}
aa(process.argv[2]);

参考文章:

https://blog.csdn.net/m0_64408930/article/details/150054443

https://mp.weixin.qq.com/s/Gp4H0HDHHgbVnobO9kpWhg

https://www.52pojie.cn/thread-1995970-1-1.html

发表评论