【作者主页】:吴秋霖
【作者介绍】:python领域优质创作者、阿里云博客专家、华为云享专家。长期致力于Python与爬虫领域研究与开发工作!
【作者推荐】:对JS逆向感兴趣的朋友可以关注《爬虫JS逆向实战》,对分布式爬虫平台感兴趣的朋友可以关注《分布式爬虫平台搭建与开发实战》
还有未来会持续更新的验证码突防、APP逆向、Python领域等一系列文章
1. 写在前面
接上一篇文章,继续完成未完成的内容。截止当前我们已经完成对加密参数的定位与分析、也通过断点调试加代码分析找到了加密方法的入口,剩下的就是对JS代码扣取从而完成X-Nonce、X-Sign参数的加密还原,以保障最终的请求能正常并成功获取到数据
2. 扣JS代码
首先把XHR断点取消,回到之前的普通断点,在参数加密那块我们去分析X-Nonce、X-Sign是如何生成的,进入到NewGuid方法内,如下所示:
在这个方法内可以看到又调用了Guid.NewGuid,先找这个方法,把它先抠出来!断点进入到这个方法,别管三七二十一直接扣就行,扣取代码如下所示:
javascript">Guid.NewGuid = function(){
var g = "";
var i = 32;
while(i--){
g += Math.floor(Math.random()*16.0).toString(16);
}
return new Guid(g);
}
简单看一下代码逻辑,定义g、i两个变量,根据i进行一个循环递减,下面那一行长的代码主要干了个啥呢?不知道我们就试试,毕竟很多新手朋友可能对代码理解能力较弱的可以直接控制台调试看看结果,如下所示:
没错,就是一个随机生成一个值再拼接上面定义的g,最后把g传给Guid的这个方法,直接运行的话肯定是会报错的,因为Guid方法未定义,如下所示:
虽说JS里面一切皆对象,某些时候我们在扣代码补环境的时候,一些缺失可以先补一个空对象测试,但是这个Guid找不到方法我们就需要找到它,将代码扣出来!
在JS同文件内,就能够找到Guid方法,扣取代码如下所示:
function Guid(g){
var arr = new Array();
if (typeof(g) == "string"){
InitByString(arr, g);
}
else{
InitByOther(arr);
}
this.Equals = function(o){
if (o && o.IsGuid){
return this.ToString() == o.ToString();
}
else{
return false;
}
}
this.IsGuid = function(){}
this.ToString = function(format){
if(typeof(format) == "string"){
if (format == "N" || format == "D" || format == "B" || format == "P"){
return ToStringWithFormat(arr, format);
}
else{
return ToStringWithFormat(arr, "D");
}
}
else{
return ToStringWithFormat(arr, "D");
}
}
function InitByString(arr, g){
g = g.replace(/\{|\(|\)|\}|-/g, "");
g = g.toLowerCase();
if (g.length != 32 || g.search(/[^0-9,a-f]/i) != -1){
InitByOther(arr);
}
else{
for (var i = 0; i < g.length; i++){
arr.push(g[i]);
}
}
}
function InitByOther(arr){
var i = 32;
while(i--){
arr.push("0");
}
}
function ToStringWithFormat(arr, format){
switch(format){
case "N":
return arr.toString().replace(/,/g, "");
case "D":
var str = arr.slice(0, 8) + "-" + arr.slice(8, 12) + "-" + arr.slice(12, 16) + "-" + arr.slice(16, 20) + "-" + arr.slice(20,32);
str = str.replace(/,/g, "");
return str;
case "B":
var str = ToStringWithFormat(arr, "D");
str = "{" + str + "}";
return str;
case "P":
var str = ToStringWithFormat(arr, "D");
str = "(" + str + ")";
return str;
default:
return new Guid();
}
}
}
// 打印调用10次测试
let count = 10;
while(count--){
console.log(Guid.NewGuid().ToString());
加上前面扣出来的Guid.NewGuid代码,测试如下如下所示:
至此,X-Nonce参数的加密生成就已经搞定了。接下来,我们需要继续扣取X-Sign的签名参数JS代码,继续回到入口处,跳转进入到sign方法内部,这个在上一篇文章中断点分析的时候已经详细解读过了,如下所示:
既然我们从上一篇文章中知道它是一个标准的md5加密算法,那么我们现在要做的就是补全sign方法接受的5个参数,然后进行加密测试,首先是timestamp参数,这个一看就可能大家都知道是一个10位的时间戳,代码如下所示:
然后nonce参数的话,就是上面我们所扣取的JS加密代码生成的值,application、version固定值,body是请求提交的POST参数,最终参数定义代码如下所示:
let timestamp = Math.floor((new Date()).getTime() / 1000);
let application = 'Pdfreader.Web';
let version = 'V2.2';
let body = '{"SearchKeyword":"考研资料","UniversityCode"'
参数准备好之后我们继续扣md5的加密算法,这里标准算法的话其实是可以直接使用标准库生成的。但是为了带大家掌握扣代码的技巧与乐趣~~
这里作者同样采用扣取JS代码的方式,单步执行到对应的JS文件,如下所示:
一共是600多行,我们把它拿下来放到编辑器内,可以看到它是一个自执行函数
看一下如何调用,简单分析一下代码。发现代码中645行中已经做了导出操作,如下所示:
把自执行函数的头尾去掉,我们打印一下exports,如下所示:
如上hex就是加密方法,现在我们需要模拟一下加密算法进行测试,编写测试代码如下:
// 签名加密方法
function get_sign(body) {
let nonce = Guid.NewGuid().ToString('');
let timestamp = Math.floor((new Date()).getTime() / 1000);
let application = 'Pdfreader.Web';
let version = 'V2.2';
let secret = "SV1dLfFDS32DS97jk32Qkjh34"; // 固定盐值
let str = secret + "&" + timestamp + "&" + nonce + "&" + application + "&" + version + "&" + body;
let sign = exports.hex(str).toUpperCase();
return {'nonce': nonce, 'sign': sign, 'timestamp': timestamp}
}
console.log(get_sign('{"SearchKeyword":"复试资料","UniversityCode":"","MajorCode":"","PageIndex":18,"PageSize":30}'))
上图中定义的参数我们根据浏览器中调试显示的来即可,这样能够保证与浏览器环境一致。最后测试一下完整的加密算法,如下所示:
如上,成功完成加密参数还原!接下来也是最重要的最后一步。我们就是需要对接到Python爬虫,然后调用上面我们扣取出来的JS代码,生成X-Nonce、X-Sign参数值,携带完成最后的请求即可!
作为喂饭系列的一期文章,Python调用JS加密算法发送请求获取数据的完整代码,作者也帮大家编写完了,代码如下:
import execjs
import requests
import json
headers = {
"A***ept": "application/json, text/plain, */*",
"A***ept-Language": "en-US,en;q=0.9,zh-***;q=0.8,zh;q=0.7",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Content-Type": "application/json",
"Origin": "https://kaoyan.docin.***",
"Pragma": "no-cache",
"Referer": "https://kaoyan.docin.***/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"X-Application": "Pdfreader.Web",
"X-Nonce": "be6f01a6-c355-b8a7-d602-d652d5fb33f7",
"X-Sign": "B5998071CFE4500E324971E46C665177",
"X-Timestamp": "1708785902",
"X-Token": "null",
"X-Version": "V2.2",
"sec-ch-ua": "\"Not A(Brand\";v=\"99\", \"Google Chrome\";v=\"121\", \"Chromium\";v=\"121\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"macOS\""
}
url = "https://ky.douding.***//Api/Web/GetDocumentInfos"
data = {"SearchKeyword":"复试资料","UniversityCode":"","MajorCode":"","PageIndex":18,"PageSize":30}
with open("guid.js", "r", encoding="utf-8") as f:
js_code = f.read()
js = execjs.***pile(js_code)
args = js.call('get_sign', json.dumps(data))
headers['X-Nonce'] = args['nonce']
headers['X-Sign'] = args['sign']
headers['X-Timestamp'] = str(args['timestamp'])
response = requests.post(url, headers=headers, json=data)
print(response.json())
好了,到这里又到了跟大家说再见的时候了。创作不易,帮忙点个赞再走吧!你的支持是我创作的动力,希望能带给大家更多优质的文章