蓝奏云直链下载协议分析
最近有个需求要批量下载蓝奏云分享的文件,网上搜了一圈都是些过时的接口或者要钱的解析站,干脆自己抓包走一遍。
环境:Chrome + F12,最后用 Python requests 复现。
一、文件夹密码验证,拿文件列表
链接 ****,打开是个密码框,输入 61tx。
切到 Network 看 XHR,密码提交后走了这么个请求:
POST https://wwbwz.lanzoul.com/filemoreajax.php?file=13512492
lx=2&fid=13512492&uid=5173976&pg=1&rep=0&t=1779256017&k=76a1cce0138a64290d289b19f20cdfbf&up=1&ls=1&pwd=61tx
返回的 json:
{
"zt": 1,
"info": "sucess",
"text": [{
"id": "iX9ow3pt9ali?webpage=U2JWNgpgBmtWMlAzVDYGMVI6Um5VdgU1VWo...",
"name_all": "GlassKeep-Setup-2.0.0.exe",
"size": "85.7 M"
}]
}
文件 id 在里面了,拼一下就是详情页地址:https://wwbwz.lanzoul.com/iX9ow3pt9ali?webpage=...
提交参数里的 fid、uid、t、k 怎么来的?右键看页面源码,这些值都在一段 script 里,大概是:
var ib0cv3 = '1779257519'; // t 参数的值
var _gyoww = 'c7d18967cd...'; // k 参数的值
...
data: { 't': ib0cv3, 'k': _gyoww, ... }
这里有个坑:变量名不是固定的。我隔几分钟刷新,ib0cv3 变成了 ixy9pq,再过一会又变了。所以不能直接正则写死变量名。
怎么搞?先正则匹配 't': 变量名, 这段,拿到变量名,再回头去找这个变量的赋值,这样它怎么变都不怕。
t_var = re.search(r"'t'\s*:\s*(\w+)\s*,", html).group(1)
t_val = re.search(rf"{t_var}\s*=\s*'(\d+)'", html).group(1)
二、文件详情页,过反爬
拿到文件 id 之后直接 GET 详情页,结果返回的不是页面,是一大段混淆的 js:
var arg1='BFED7AE7961FBD5E4E5B456DC5E30DEA57A3833D';
(function(a,c){var G=a0j,d=a();while(!![]){...}})(...);
document.cookie='acw_sc__v2='+v+';...';document.location.reload();
一看就知道是阿里云 WAF 的那套 acw_sc 反爬。这套东西逻辑不复杂,就是:
- 服务端给一个 40 位的 hex 串 (
arg1)
- 按一个固定的位置表重新排列字符
- 和一个固定密钥逐字节 XOR
- 结果写到 cookie
acw_sc__v2,然后 location.reload()
反正我只需要结果,直接把这俩常量从 js 里抠出来写进 Python:
ACW_M = [0xF,0x23,0x1D,0x18,0x21,0x10,0x1,0x26,0xA,0x9,0x13,0x1F,
0x28,0x1B,0x16,0x17,0x19,0xD,0x6,0xB,0x27,0x12,0x14,0x8,
0xE,0x15,0x20,0x1A,0x2,0x1E,0x7,0x4,0x11,0x5,0x3,0x1C,
0x22,0x25,0xC,0x24]
ACW_P = "3000176000856006061501533003690027800375"
# 算法还原
q = [''] * 40
for x in range(40):
for z in range(40):
if ACW_M[z] == x + 1:
q[z] = arg1[x]
u = ''.join(q)
v = ''
for i in range(0, 40, 2):
val = int(u[i:i+2], 16) ^ int(ACW_P[i:i+2], 16)
v += f'{val:02x}'
# 写入 session 的 cookie,重试 GET 就能拿到正常页面了
这关过了之后,页面里就能拿到两个关键东西:
fid = 286947778——文件的数字 id
- 一个 iframe 的 src:
/fn?VzEAals0AGxWNlQ7UzNTZVE8BzVRNgAkB...
三、iframe 里的签名,换 CDN 下载地址
这个 iframe 点进去是个单独的页面,里面有一段 js:
var wp_sign = 'AGZRb1prAjNRWAc4BzcHOwBoAz4DagM4ADNVZFI5UGZTa1MiWnM...';
var ajaxdata = 'QcSp';
var kdns = 1;
然后页面加载完自动发了一个 AJAX:
POST https://wwbwz.lanzoul.com/ajaxm.php?file=286947778
action=downprocess&websignkey=QcSp&signs=QcSp&sign={wp_sign}&websign=2&kd=1&ves=1
返回值:
{
"zt": 1,
"dom": "https://developer3.lanrar.com",
"url": "?BGJRbw08VGUIAQY+AzZUOAE+VW1eRgRqAmRQJVZ5WhACZwA...",
"inf": 0
}
直链地址 = dom + "/file/" + url 拼起来就行了。注意这个 inf 字段,0 表示不用验证,直接用。
在同一个 session 里 GET 这个地址,直接返回文件流:
HTTP 200
Content-Type: application/octet-stream
Content-Disposition: attachment; filename*=UTF-8''GlassKeep-Setup-2.0.0.exe
Content-Length: 89825826
Accept-Ranges: bytes
支持 Range,断点续传也没问题。
四、inf ≠ 0 的情况——CDN 有个验证页
偶尔会遇到 inf 不是 0 的情况,这时候 GET 那个 CDN 地址会返回一个验证页面,大概长这样:
系统发现您的网络异常,需要验证后下载文件
[验证并下载]
页面 js 里有三个参数:
'file': 'VDxRcgkgVHBUcQpmUygGKFtkD3JfZQo5Bi9bKgI7UTIHYFdl...',
'el': 2,
'sign': 'ADFRMV40AWwDZwNgAGIGN1Y+UmVfYQ=='
然后 POST 到同域名的 ajax.php,带上这三个参数,返回的 json 里 url 字段就是真实下载地址。
resp = session.post(cdn_domain + "/ajax.php", data={
"file": file_param, "el": el_val, "sign": sign_val
})
real_url = resp.json()["url"]
这个验证页面看起来是 CDN 那边做的人机校验,不过只要把参数提出来直接 POST 就行,几行代码的事。
整个流程理一遍就是:
分享链接
↓ GET 页面 → 正则提 fid/uid/t/k (变量名自适应)
↓ POST filemoreajax.php + 密码
文件列表 [id, name, size]
↓ GET /文件id
↓ 遇到反爬 → 算 acw_sc__v2 cookie → 重试
↓ 从页面提 fid + iframe src
↓ GET iframe → 提 wp_sign
↓ POST ajaxm.php + wp_sign
CDN 下载地址
↓ inf=0 → 直接 GET → 文件流 (支持 Range 断点续传)
↓ inf≠0 → 验证页 → POST ajax.php → 换真实链接 → GET
回头看其实不算复杂,就是反爬那关要稍微绕一下。那套 acw_sc 的映射表和密钥应该是跟域名绑定的,wwbwz.lanzoul.com 这套测试下来没变过,直接写死了用。
整个流程用 Python 跑下来,85.7M 的文件大概 13 秒左右下完,速度还不错。