今天受一老师朋友所托,帮其看了下企查查高级搜索页请求的哈希校验,页面地址:
YUhSMGNITTZMeTkzZDNjdWNXTmpMbU52YlM5M1pXSXZjMlZoY21Ob0wyRmtkbUZ1WTJVPQ==
先随便交互触发两个请求看看构造,以 Node.js fetch 格式显示如下:
fetch("https://www.qcc.com/api/search/searchCount", {
headers: {
b3e1a8edd33330140573: "e4b1fd60aa53b5a7686492e38b0e7d52b7b2a44db8fa85ba73c2d1b9093bcb9e61a20c5a01fbf931709dbaee324fc524806f2c26fa1dbc435d0c56547c7eeeb5",
"x-pid": "aa22357a6b17df78bc1b6e1ea32cb8b7",
cookie: "QCCSESSID=3f5f0d761ed9eee3f696bd9d2a; qcc_did=82697083-6a9e-4ab7-9059-d7df6f670e61; UM_distinctid=18474def4aaadc-08393462a90e57-18525635-1ea000-18474def4abc1b; acw_tc=db90651616696334990788317ef781a882b454ed444237917218746ed5; CNZZDATA1254842228=241614328-1668405992-https%253A%252F%252Fwww.baidu.com%252F%7C1669631222",
},
body: "{\"count\":true,\"filter\":\"{\\\"i\\\":[\\\"A\\\"]}\"}",
method: "POST"
});
fetch("https://www.qcc.com/api/search/searchCount", {
headers: {
d6c819c35478cea0a8d9: "67ef5dc179bae97c4543bf419a3965081119e12b36a04b8063ad22438b61f241212b92d1c3830cd7217b92a56cc2aee123fd6370d2db1a7b98a476b27f9596f0",
"x-pid": "aa22357a6b17df78bc1b6e1ea32cb8b7",
cookie: "QCCSESSID=3f5f0d761ed9eee3f696bd9d2a; qcc_did=82697083-6a9e-4ab7-9059-d7df6f670e61; UM_distinctid=18474def4aaadc-08393462a90e57-18525635-1ea000-18474def4abc1b; acw_tc=db90651616696334990788317ef781a882b454ed444237917218746ed5; CNZZDATA1254842228=241614328-1668405992-https%253A%252F%252Fwww.baidu.com%252F%7C1669631222",
},
body: "{\"count\":true,\"filter\":\"{\\\"i\\\":[\\\"A\\\"],\\\"r\\\":[{\\\"pr\\\":\\\"CQ\\\"}]}\"}",
method: "POST"
});
可以发现随着请求而变化的数据仅有 headers 里的一对键值,且后台也仅对该变动键值做了二次校验。由于其看起来很像 Hash,索性就叫 hashKey:hashValue。
通过网络请求调用堆栈并没有发现相关的赋值代码,于是便想从代码搜索入手。例如此场景在 js 中的赋值语句通常为 headers[key] = value,搜索 headers[ 即可。
遂查看其网页源代码,发现 JavaScript 脚本文件都集中在路径 //qcc-static.qichacha.com/qcc/pc-web/prod-22.11.515/ 中,搜索后发现此处最为可疑:
在此断点调试看一下:
很明显可以发现,其临时变量 i 就是 hashKey,l 则是 hashValue:
// 源代码
var i = (0,a.default)(t, e.data),
l = (0,r.default)(t, e.data, (0,s.default)());
e.headers[i] = l;
// 其中
t = '/api/search/searchcount';
e.data = {
count: true,
filter: "{\"i\":[\"A\"],\"r\":[{\"pr\":\"GD\"},{\"pr\":\"CQ\"}]}"
};
(0,s.default)() = window.tid;
由此,我们只需要查看 a.default 与 r.default 的函数即可知道其哈希值的生成方法。
首先是生成 hashKey 的函数 a.default,通过多次断点,发现其调用堆栈如下:
// 第一层 a.default
function() {
// t=请求URL,e=请求对象,n=请求对象json
var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}
, t = (arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "/").toLowerCase()
, n = JSON.stringify(e).toLowerCase();
// o.default 加密函数在最后提到
return (0, o.default)(t + n, (0, a.default)(t))
.toLowerCase().substr(8, 20)
};
// 传入加密的两个字符分别为
// t + n = /api/search/searchcount{"count":true,"filter":"{\"i\":[\"a\"],\"r\":[{\"pr\":\"gd\"},{\"pr\":\"cq\"}]}"}
// (0, a.default)(t) = iLAgiklLN8QiklLN8Q86Lv4iLAgiklLN8QiklLN8Q86Lv4
// 第二层 a.default
function() {
for (var e = (arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "/").toLowerCase(), t = e + e, n = "", i = 0; i < t.length; ++i) {
var a = t[i].charCodeAt() % o.default.n;
// 并非最后的加密函数 o.default
n += o.default.codes[a]
}
return n
};
// return "iLAgiklLN8QiklLN8Q86Lv4iLAgiklLN8QiklLN8Q86Lv4"
// 第二层 a.default 中的 o.default
o.default = {
"n": 20,
"codes": {
"0": "W",
"1": "l",
"2": "k",
"3": "B",
"4": "Q",
"5": "g",
"6": "f",
"7": "i",
"8": "i",
"9": "r",
"10": "v",
"11": "6",
"12": "A",
"13": "K",
"14": "N",
"15": "k",
"16": "4",
"17": "L",
"18": "1",
"19": "8"
}
};
其次是 r.default:
// 第一层 r.default
function() {
// n=请求URL,e=请求对象,t=tid,i=请求对象json
var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}
, t = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : ""
, n = (arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "/").toLowerCase()
, i = JSON.stringify(e).toLowerCase();
// o.default 加密函数在最后提到
// a.default 即为刚提到的第二层函数
return (0, o.default)(n + "pathString" + i + t, (0, a.default)(n))
};
// 传入加密的两个字符分别为
// /api/search/searchcountpathString{"count":true,"filter":"{\"i\":[\"a\"],\"r\":[{\"pr\":\"gd\"},{\"pr\":\"cq\"}]}"}8b4ebc1e4a1b8c21235f34bf9db8f1a8
// iLAgiklLN8QiklLN8Q86Lv4iLAgiklLN8QiklLN8Q86Lv4
// 则 return 返回值为
// 9396c85dc08d8c5be05644a07dda6539a2c2ec4b742f0b412eb58d71b3d41574e01a1d2b0982861253aa9a14aa08bafb59dd615ea3c36a9716a203620c5bfec1
其中,两个函数都调用了同一个 o.default,其堆栈如下:
// 第一层 o.default
function(e, t) {
return (0, o.default)(e, t).toString()
};
// 第二层 o.default
return function(e, n) {
// h 对象就是一个库,使用的 HMAC SHA512 加密算法
// input:[e],key:[n]
// 在线加密:https://www.idcd.com/tool/encrypt/hmac
return new h.HMAC.init(t,n).finalize(e)
}
至此,顺带做个 python 的解构版本:GitHub 地址
文中所有内容仅供学习交流,严禁用于商业或非法用途,否则由此产生的一切后果均与作者无关。若有侵权,请与小蔗联系,将在第一时间处理。