在一个被雷池 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