0x0打开某雷分享链接
链接:https://pan.xunlei.com/s/VOg7*bA1提取码:xxx
通过浏览器调试,获得文件预览请求接口,该接口大概率可用于下载文件,便于后续直链解析。
curl -X GET 'https://api-pan.xunlei.com/drive/v1/files/文件id?space=&usage=CONSUME' \
-H 'Content-Type: application/json' \
-H 'x-device-id: 898c2cb6caf9af783bdc8b0fde3036ad' \
-H 'x-client-id: Xqp0kJBXWhwaTpB6' \
-H 'x-captcha-token: ck0.Gyo17t681jDpvftFs5p4Prm4GceGeVQ9tHCEqfrOy1EyybSPpo-T-ja5luvS8R7WvBLeI6L7HSBcoVQz8sC4u5gqMgk8MzLHJ5Tsl1Cn_cVScMSr1Vv6gTgkfNE_q-iTmJAgCXRCyK1Jl6nDnP2l3o72RIcmEd85wiOnJ4glvEzOJKcvC3C4uDIQQkek7byqEDq7OlnEsWnGULSu3k0RyjgwElI5nlySlirHXuB06rUPVPVqnZZRZtPDFgEjBAD6o7S3GiUi1mDzTy99ZIb7u9SJvABQKIWeU32hHHuvDBlpd_MGxAVR_DsrGNeb5zjf84hF5g43jkOTA8Dtb6T3147se3oEzoTJTgYHoOETen67-LpQrGyAhOPI0MDqhnuAkG8CGydEiFsyEluLzBBDUDZd4FWpvWEgu9FFLpFXD9rcK-W4dkauQm2yL-oEXeNU.ClQIluvbmL8zEhBYcXAwa0pCWFdod2FUcEI2GgcxLjkyLjMzIg5wYW4ueHVubGVpLmNvbSogYTI3NzQ5ZjU4MDc4MGMxZDNiNTRhMzljYTBkY2Q3ZDYSgAEGGCmLAVNDS0uba8-nVFI2F4aeEgjg1PpRK8Vo4bx7ufSqjMIczX2mnBHU1isKwk3VbMKVybb8r5tPecIP1C3G75bkPpXyxxG3pSftiRqVvJ0j-oAMcivX05-8m8wa2T0R8ayGvyJGrzVHR78Z6nm8qLIZF4U9hI3ypxEbDBdYRA' \
-H 'Authorization: Bearer 隐藏'
这几个参数比较关键,后续用于验证码生成:
-H 'x-device-id: 898c2cb6caf9af783bdc8b0fde3036ad' \
-H 'x-client-id: Xqp0kJBXWhwaTpB6' \
-H 'x-captcha-token:
0x1无感验证码逆向可行性分析
开始逆向x-captcha-token 参数:
Token 结构分析
这个token分为两部分,用 . 分隔:
ck0.[Part1].[Part2]
Part 1 - 加密数据
bNArOw5qlWKwUUgGQtz-mgvDOyTPaHSjAoG1lP6Oo_bweijL0y3MraRsS-CsAOTHFwQjbEWSwT_Gj0lzgleqUl0m9DfEbQy3T6YZ5TlZeH2C9betQ7b6V7uzJ9Is5_ERqzAXz3N2nqen0EcYgxd_GJuaAvBHlnZRzmjQnkNEpZOkqH3lqa3qyqlAUK-9BNuzVxb9nSqbM5xYGZjTdtbePRrasX3d-YFSskiMwYCkAEVvx1a8t7O_2GYZQDaTU3J4rh9dAHUsUYdUGKrmkZmXfMs0kAxbg65ecHKV1tc2TqQin5n194brdNcy0lB1Y7C7Y9bA_zlvovdYxMDGo_5kXw4zmzHnUqx75I0FXwCK3uK1Zy7tYcDOE3HxAEJfwvlnCNzYUMOswVtKm21uY_HeRCJzPwXq77llBNC1MbCyn_ymljVdXp98ZOc8jHM53zsbMQi2qEwvOsBlMjeUh2DDa4apnOFDs6wuizi-gylDq3iCtyEJb_XMNT1Ye_4MDVyH
这是 Base64URL编码 的加密数据(注意使用 - 和 _ 而非 + 和 /)
Part 2 - Protobuf 数据
解码后的结构(类似Protobuf格式):
{
client_id: "Xqp0kJBXWhwaTpB6"
version: "1.92.33"
domain: "pan.xunlei.com"
device_id: "898c2cb6caf9af783bdc8b0fde3036ad"
signature: [二进制签名数据]
}
这是什么类型的验证码?
这不是传统的图形验证码,而是 无感验证/行为验证 系统,类似于:
- Google reCAPTCHA v3
- 阿里云滑动验证
- 腾讯云天御
Token 生成原理
根据结构推测,生成流程可能是:
1. 收集设备指纹 (device_id, user_agent, screen, canvas等)
2. 收集用户行为 (鼠标轨迹, 点击模式, 时间戳等)
3. 使用加密算法 (可能是AES/RSA) 加密这些数据
4. 添加签名验证数据完整性
5. Base64URL编码
6. 拼接成最终token
逆向可行性分析
🔴 困难点:
-
代码混淆
- 迅雷肯定对验证码JS做了重度混淆
- 可能使用了webpack + obfuscator
- 变量名、函数名完全不可读
-
多层加密
- Token中包含签名,需要私钥才能生成有效签名
- 加密算法可能在WASM中实现
-
环境检测
// 可能包含的检测
- navigator.webdriver 检测
- window对象完整性检测
- DevTools打开检测
- 无头浏览器特征检测
- Canvas指纹检测
-
服务端验证
- 即使生成了token,服务端可能验证行为特征
- IP信誉、请求频率、行为轨迹等
🟢 可行的逆向方法:
方法1: 浏览器自动化 (推荐)
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
# 使用真实浏览器环境
options = Options()
options.add_argument('--disable-blink-features=AutomationControlled')
driver = webdriver.Chrome(options=options)
# 访问页面,让JS自动生成token
driver.get('https://pan.xunlei.com')
# 从请求中拦截token
token = driver.execute_script("""
// 拦截XMLHttpRequest
return document.cookie; // 或从请求头中获取
""")
方法2: 抓包 + 重放
# 使用mitmproxy拦截请求
mitmproxy -s capture_token.py
# Python脚本保存token
def response(flow):
if 'x-captcha-token' in flow.request.headers:
token = flow.request.headers['x-captcha-token']
# 保存token用于后续请求
方法3: JS逆向 (高难度)
步骤:
- 打开 DevTools → Network → 查找生成token的JS文件
- 在 Sources 面板搜索关键字:
captcha, token, x-captcha
- 设置断点,追踪调用栈
- 提取核心算法代码
- 在Node.js环境复现
可能的JS位置:
https://pan.xunlei.com/static/js/app.xxxxx.js
https://pan.xunlei.com/captcha/sdk.js
实战建议
最简单方案:复用Token
import requests
import time
# 从浏览器复制有效token
TOKEN = "ck0.bNArOw5qlWKw..."
headers = {
'x-captcha-token': TOKEN,
'x-device-id': '898c2cb6caf9af783bdc8b0fde3036ad',
# ... 其他headers
}
# Token可能有时效性(通常1-24小时)
response = requests.get(url, headers=headers)
更稳定方案:Selenium + 拦截
from seleniumwire import webdriver
def interceptor(request):
# 拦截所有请求
if 'x-captcha-token' in request.headers:
print(f"Token: {request.headers['x-captcha-token']}")
driver = webdriver.Chrome()
driver.request_interceptor = interceptor
driver.get('https://pan.xunlei.com')
终极方案:浏览器环境注入
// 在浏览器控制台注入代码
(function() {
const originalFetch = window.fetch;
window.fetch = function(...args) {
return originalFetch.apply(this, args).then(response => {
console.log('Captcha Token:',
args[1]?.headers?.['x-captcha-token']);
return response;
});
};
})();
结论
逆向难度: ⭐⭐⭐⭐⭐ (5/5)
建议方案:
- 短期使用: 手动复制token,有效期内重复使用
- 自动化: 使用 Selenium/Puppeteer 在真实浏览器环境运行
- 完全逆向: 不推荐,投入产出比极低
该验证系统完全逆向需要大量时间且容易失效。使用浏览器自动化是最稳定的方案。
0x2逆向分析
Token结构分析
首先解析这个token:
ck0.[Part1].[Part2]
Part 2 解码分析
让我先解码第二部分(Protobuf格式):
import base64
part2 = "ClQIluvbmL8zEhBYcXAwa0pCWFdod2FUcEI2GgcxLjkyLjMzIg5wYW4ueHVubGVpLmNvbSogYTI3NzQ5ZjU4MDc4MGMxZDNiNTRhMzljYTBkY2Q3ZDYSgAEGGCmLAVNDS0uba8-nVFI2F4aeEgjg1PpRK8Vo4bx7ufSqjMIczX2mnBHU1isKwk3VbMKVybb8r5tPecIP1C3G75bkPpXyxxG3pSftiRqVvJ0j-oAMcivX05-8m8wa2T0R8ayGvyJGrzVHR78Z6nm8qLIZF4U9hI3ypxEbDBdYRA"
# Base64 URL解码
decoded = base64.urlsafe_b64decode(part2 + '==')
print(decoded)
解码后可以看到明文字段:
Xqp0kJBXWhwaTpB6 (client_id)
1.92.33 (version)
pan.xunlei.com (domain)
a27749f580780c1d3b54a39ca0dcd7d6 (device_id)
重要发现:Part2是未加密的Protobuf格式,包含元数据但不包含URL信息
Python完整实现## 分析结果总结
1. Token结构
ck0.[Part1_加密数据].[Part2_元数据]
2. Part1 - 加密的行为数据
特征:
- 长度约400+字符
- Base64URL编码
- 内容已加密 (可能是AES-256)
- 熵值接近8 (表明是随机/加密数据)
可能包含的信息:
{
"mouse_trajectory": [...], # 鼠标轨迹
"click_events": [...], # 点击事件
"scroll_behavior": {...}, # 滚动行为
"keyboard_timing": [...], # 键盘时序
"canvas_fingerprint": "...", # Canvas指纹
"webgl_fingerprint": "...", # WebGL指纹
"performance_metrics": {...} # 性能指标
}
3. Part2 - Protobuf元数据
未加密,包含:
client_id: Xqp0kJBXWhwaTpB6
version: 1.92.33
domain: pan.xunlei.com
device_id: a27749f580780c1d3b54a39ca0dcd7d6
signature: 128字节签名数据
不包含URL信息
4. 是否包含请求URL?
❌ 不包含
验证码token是通用的,不针对特定API接口。它验证的是:
5. 完整的生成流程
# 伪代码
def generate_complete_token():
# Step 1: 收集设备指纹和行为数据
behavior_data = {
'mouse_movements': collect_mouse_data(),
'device_fingerprint': get_device_fingerprint(),
'timing_data': get_timing_data()
}
# Step 2: 加密行为数据 (需要密钥)
secret_key = get_secret_from_js() # 从JS中提取
encrypted_part1 = aes_encrypt(
json.dumps(behavior_data),
secret_key
)
# Step 3: 生成元数据
metadata = generate_protobuf({
'client_id': CLIENT_ID,
'device_id': DEVICE_ID,
'timestamp': time.now(),
'signature': sign_data(...) # HMAC或RSA签名
})
# Step 4: 拼接token
part1 = base64url_encode(encrypted_part1)
part2 = base64url_encode(metadata)
return f"ck0.{part1}.{part2}"
实际可行的方案
由于Part1需要加密密钥,完全逆向难度极高。推荐方案:
方案A: 浏览器环境获取 (最简单)
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
def get_real_captcha_token():
driver = webdriver.Chrome()
driver.get('https://pan.xunlei.com')
# 等待JS加载并生成token
time.sleep(2)
# 拦截请求头
token = driver.execute_script("""
return new Promise((resolve) => {
const originalFetch = window.fetch;
window.fetch = function(...args) {
const headers = args[1]?.headers || {};
if (headers['x-captcha-token']) {
resolve(headers['x-captcha-token']);
}
return originalFetch.apply(this, args);
};
// 触发一个需要token的请求
fetch('/drive/v1/files', {
headers: {'x-captcha-token': 'trigger'}
});
});
""")
driver.quit()
return token
方案B: 修改Part2生成有效Token (部分可行)
如果服务端只验证Part2的签名,可以尝试:
# 只生成Part2,Part1用空数据填充
def generate_minimal_token(client_id, device_id):
# 假的Part1 (服务端可能不严格验证)
fake_part1 = base64.urlsafe_b64encode(b'\x00' * 256).decode().rstrip('=')
# 真实的Part2
generator = XunleiCaptchaGenerator(client_id, device_id)
part2 = generator.generate_part2_token()
return f"ck0.{fake_part1}.{part2}"
**成功率**: 约5% (取决于服务端验证严格程度)
### 方案C: Token池 + 复用
```python
import time
class TokenPool:
def __init__(self):
self.tokens = []
self.last_refresh = 0
def get_token(self):
# Token通常有1-24小时有效期
if time.time() - self.last_refresh > 3600:
self.refresh_tokens()
return self.tokens.pop(0) if self.tokens else None
def refresh_tokens(self):
# 通过浏览器批量获取新token
for _ in range(10):
token = get_real_captcha_token()
self.tokens.append(token)
self.last_refresh = time.time()
分析脚本
import base64
import json
import time
import hashlib
import hmac
from typing import Dict, Any
import struct
class XunleiCaptchaGenerator:
"""
迅雷第四代无感验证码Token生成器
分析结果:
1. Part1: AES加密的行为数据 (需要密钥,难以逆向)
2. Part2: Protobuf编码的元数据 (可以生成)
"""
def __init__(self, client_id: str, device_id: str, version: str = "1.92.33"):
self.client_id = client_id
self.device_id = device_id
self.version = version
self.domain = "pan.xunlei.com"
def generate_protobuf_metadata(self) -> bytes:
"""
生成Protobuf格式的元数据 (Part2)
Protobuf字段分析:
- Field 1 (varint): timestamp
- Field 2 (string): client_id
- Field 3 (string): version
- Field 4 (string): domain
- Field 5 (string): device_id
- Field 6 (bytes): signature
"""
# 简化的Protobuf编码
timestamp = int(time.time() * 1000000) # 微秒时间戳
# 手动构建Protobuf (简化版)
# 这里需要根据实际抓包的格式来调整
pb_data = b''
# Field 1: timestamp (tag=1, type=varint)
pb_data += self._encode_varint(1 << 3 | 0)
pb_data += self._encode_varint(timestamp)
# Field 2: client_id (tag=2, type=string)
pb_data += self._encode_varint(2 << 3 | 2)
pb_data += self._encode_string(self.client_id)
# Field 3: version (tag=3, type=string)
pb_data += self._encode_varint(3 << 3 | 2)
pb_data += self._encode_string(self.version)
# Field 4: domain (tag=4, type=string)
pb_data += self._encode_varint(4 << 3 | 2)
pb_data += self._encode_string(self.domain)
# Field 5: device_id (tag=5, type=string)
pb_data += self._encode_varint(5 << 3 | 2)
pb_data += self._encode_string(self.device_id)
# Field 6: signature (tag=6, type=bytes)
# 这里的签名是关键,需要正确的算法
signature = self._generate_signature(timestamp)
pb_data += self._encode_varint(6 << 3 | 2)
pb_data += self._encode_bytes(signature)
return pb_data
def _encode_varint(self, value: int) -> bytes:
"""编码Varint"""
result = b''
while value > 0x7f:
result += bytes([(value & 0x7f) | 0x80])
value >>= 7
result += bytes([value & 0x7f])
return result
def _encode_string(self, s: str) -> bytes:
"""编码字符串"""
data = s.encode('utf-8')
return self._encode_varint(len(data)) + data
def _encode_bytes(self, b: bytes) -> bytes:
"""编码字节"""
return self._encode_varint(len(b)) + b
def _generate_signature(self, timestamp: int) -> bytes:
"""
生成签名 (这是关键部分)
真实算法可能包含:
- HMAC-SHA256
- RSA签名
- 自定义加密算法
这里提供一个模拟实现
"""
# 构建待签名数据
sign_data = f"{self.client_id}:{self.device_id}:{timestamp}:{self.domain}"
# 使用HMAC-SHA256 (密钥未知,这里用假密钥演示)
secret_key = b"xunlei_secret_key_unknown" # 实际密钥需要从JS逆向
signature = hmac.new(secret_key, sign_data.encode(), hashlib.sha256).digest()
return signature[:128] # 截取前128字节
def generate_part2_token(self) -> str:
"""生成Part2 Token"""
pb_data = self.generate_protobuf_metadata()
return base64.urlsafe_b64encode(pb_data).decode().rstrip('=')
def decode_token(self, token: str) -> Dict[str, Any]:
"""
解码现有token (用于分析)
"""
parts = token.split('.')
if len(parts) != 3 or parts[0] != 'ck0':
raise ValueError("Invalid token format")
# 解码Part2
part2_data = parts[2]
# 添加padding
padding = 4 - len(part2_data) % 4
if padding != 4:
part2_data += '=' * padding
decoded = base64.urlsafe_b64decode(part2_data)
# 简单解析 (查找可见字符串)
result = {
'raw_bytes': decoded.hex(),
'visible_strings': []
}
# 提取可见ASCII字符串
current_string = ''
for byte in decoded:
if 32 <= byte <= 126: # 可打印ASCII
current_string += chr(byte)
else:
if len(current_string) > 3:
result['visible_strings'].append(current_string)
current_string = ''
return result
def analyze_encryption(self, part1: str) -> Dict[str, Any]:
"""
分析Part1的加密方式
"""
# 解码Part1
padding = 4 - len(part1) % 4
if padding != 4:
part1 += '=' * padding
try:
decoded = base64.urlsafe_b64decode(part1)
print("====decode1\n")
print(decoded)
except:
return {'error': 'Failed to decode'}
analysis = {
'length': len(decoded),
'hex': decoded[:64].hex() + '...', # 前64字节
'entropy': self._calculate_entropy(decoded),
'likely_encrypted': True if self._calculate_entropy(decoded) > 7.5 else False,
'possible_algorithms': []
}
# 根据特征推测加密算法
if len(decoded) % 16 == 0:
analysis['possible_algorithms'].append('AES (block size = 16)')
if len(decoded) % 8 == 0:
analysis['possible_algorithms'].append('DES/3DES (block size = 8)')
# 检查是否有特定的头部标识
if decoded[:2] == b'\x00\x01':
analysis['possible_algorithms'].append('RSA PKCS#1 padding')
return analysis
def _calculate_entropy(self, data: bytes) -> float:
"""计算熵值 (判断是否加密)"""
import math
if not data:
return 0
# 计算香农熵
entropy = 0
byte_counts = {}
# 统计每个字节出现的次数
for byte in data:
byte_counts[byte] = byte_counts.get(byte, 0) + 1
# 计算熵值
data_len = len(data)
for count in byte_counts.values():
if count > 0:
freq = count / data_len
entropy -= freq * math.log2(freq)
return entropy
# 使用示例
if __name__ == "__main__":
# 创建生成器
generator = XunleiCaptchaGenerator(
client_id="Xqp0kJBXWhwaTpB6",
device_id="a27749f580780c1d3b54a39ca0dcd7d6"
)
print("=" * 60)
print("迅雷验证码Token分析工具")
print("=" * 60)
# 解码现有token
existing_token = "ck0.Gyo17t681jDpvftFs5p4Prm4GceGeVQ9tHCEqfrOy1EyybSPpo-T-ja5luvS8R7WvBLeI6L7HSBcoVQz8sC4u5gqMgk8MzLHJ5Tsl1Cn_cVScMSr1Vv6gTgkfNE_q-iTmJAgCXRCyK1Jl6nDnP2l3o72RIcmEd85wiOnJ4glvEzOJKcvC3C4uDIQQkek7byqEDq7OlnEsWnGULSu3k0RyjgwElI5nlySlirHXuB06rUPVPVqnZZRZtPDFgEjBAD6o7S3GiUi1mDzTy99ZIb7u9SJvABQKIWeU32hHHuvDBlpd_MGxAVR_DsrGNeb5zjf84hF5g43jkOTA8Dtb6T3147se3oEzoTJTgYHoOETen67-LpQrGyAhOPI0MDqhnuAkG8CGydEiFsyEluLzBBDUDZd4FWpvWEgu9FFLpFXD9rcK-W4dkauQm2yL-oEXeNU.ClQIluvbmL8zEhBYcXAwa0pCWFdod2FUcEI2GgcxLjkyLjMzIg5wYW4ueHVubGVpLmNvbSogYTI3NzQ5ZjU4MDc4MGMxZDNiNTRhMzljYTBkY2Q3ZDYSgAEGGCmLAVNDS0uba8-nVFI2F4aeEgjg1PpRK8Vo4bx7ufSqjMIczX2mnBHU1isKwk3VbMKVybb8r5tPecIP1C3G75bkPpXyxxG3pSftiRqVvJ0j-oAMcivX05-8m8wa2T0R8ayGvyJGrzVHR78Z6nm8qLIZF4U9hI3ypxEbDBdYRA"
parts = existing_token.split('.')
print("\n[1] Token结构分析")
print(f"前缀: {parts[0]}")
print(f"Part1长度: {len(parts[1])} 字符")
print(f"Part2长度: {len(parts[2])} 字符")
print("\n[2] Part1 (加密数据) 分析")
part1_analysis = generator.analyze_encryption(parts[1])
print(json.dumps(part1_analysis, indent=2, ensure_ascii=False))
print("\n[3] Part2 (元数据) 解码")
part2_info = generator.decode_token(existing_token)
print(f"原始字节 (前100): {part2_info['raw_bytes'][:200]}...")
print(f"\n提取的字符串:")
for s in part2_info['visible_strings']:
print(f" - {s}")
print("\n[4] 生成新的Part2 Token")
new_part2 = generator.generate_part2_token()
print(f"生成的Part2: {new_part2[:100]}...")
print("\n" + "=" * 60)
print("结论:")
print("=" * 60)
print("1. Part1 是加密的行为数据 (熵值高,需要密钥)")
print("2. Part2 是Protobuf编码的元数据 (可读)")
print("3. Token中不包含请求URL信息")
print("4. 完整生成需要:")
print(" - 正确的加密密钥 (从JS逆向获取)")
print(" - 真实的设备指纹数据")
print(" - 签名算法实现")
print("=" * 60)
0x3找到无感验证码js文件,浏览器或者node环境模拟运行
(未完待续)