爬虫瑞数5.5案例:某钢材交易官网(面向对象补环境)
声明:
该文章为学习使用,严禁用于商业用途和非法用途,违者后果自负,由此产生的一切后果均与作者无关
一、瑞数简介
瑞数动态安全 Botgate(机器人防火墙)以“动态安全”技术为核心,通过动态封装、动态验证、动态混淆、动态令牌等技术对服务器网页底层代码持续动态变换,增加服务器行为的“不可预测性”,实现了从用户端到服务器端的全方位“主动防护”,为各类 Web、HTML5 提供强大的安全保护。
由于之前某通信的瑞数已经调整,这里再出一篇文章,瑞数特点以及请求执行的流程已在之前的文章中分析过,这里不再特别讲解;该爬虫案例和之前瑞数5.5不同就是cookie名称、请求中多了一个K5nOZLud参数、第一次请求状态码是202、非静态页面,瑞数5.5具体特点可参考该文章:https://blog.csdn.net/randy521520/article/details/135304568,这次补环境用的方法和上次不一样,多了补load事件的环境,而且采用了面向对象的方法补环境,在逆向中通常会遇到removeChild、addEventListener、getAttribute这些方法,其实每个标签的这些方法逻辑是相同的,所以这次补环境采用了面向对象继承的写法而不是传统的json,随着逆向的网站增加逐渐完善该js,方便自己之后逆向重新写这些相同的代码,虽然有工具可以自己补环境,但是补出来的很多代码是无用的。如果不想用工具补环境,可以借鉴该案例,虽然比不上工具补的齐全,但是会减少自己的工作量
二、瑞数cookie分析
1.js运行atob(‘aHR0cHM6Ly93d3cub3V5ZWVsLmNvbS9zZWFyY2gtbmcvcXVlcnlSZXNvdXJjZS9pbmRleA==’)拿到网址,F12打开调试工具查看cookies,此时cookies中有T0k1m0u5AfREO、T0k1m0u5AfREP,T0k1m0u5AfREO的httpOnly打勾说明是第一次请求设置cookies。而T0k1m0u5AfREP得第一个字符是代表是瑞数5代,且httpOnly未打勾说明是js生成;由于之前案例已经讲解过瑞书5.5的特点,这里就不通过油猴hook找cookie的生成位置了,有兴趣的可以看之前的案例找一下:https://blog.csdn.net/randy521520/article/details/135304568(该网站第一次请求202)
2.调试工具查看sources,找到事件监听勾选script,script会在脚本加载时候断点,如果安装的插件较多建议使用无痕窗口打开网站
3.清除cookies,刷新页面会进入脚本断点,第一次脚本断点是一段js代码用于生成虚拟js的字符串;第二次断点用于把第一次断点的js代码转换成js并执行生成虚拟js文件,搜索.call即可找到执行的方法(在call处断点);第三次断点是第一次queryResource/index请求结果(请求状态是202),主要包含补环境时用到的meta中的content、第一次脚本断点中的js代码、第二次脚本断点的外链js
4.由于每次清除cookie后,生成的虚拟文件会不一样,所以这里需要把queryResource/index本地替换一下方便逆向时调试
5.新建oy.js,把之前第一次断点生成的js代码和第二次断点生成的js文件代码拷贝到该文件
6.新建oy.py,在network中找到queryResource/index请求,鼠标右击请求找到Copy>Copy as cUrl(cmd)
7.打开网站:https://spidertools.cn/#/curl2Request,把拷贝好的curl转成python代码,新建 oy.py,把代码复制到该文件
三、有效cookie
1.由于cookie会生成多次需要确认哪次cookie是有效的,确认有效cookie时不能使用替代的index文件,因为meta中的content、ts是有时效性的,所以需要通过新的index请求,通过sources>script断点确认cookie是否有效
2.修改oy.py,由于是需要验证第一次请求的cookie是否有效,所以需要找到queryResource/index第二次的200请求,将curl转成python代码,因为存在多个接口的调用,所以采用requests.session请求
3.清除cookie并刷新页面,点击跳过断点,直到之前的call断点,在左侧找到index页面,并在最后的script中的函数调用中断点,此时虚拟文件已经执行完毕会生成第一次cookie
4.点击跳过断点,会进入script标签中的断点,此时查看cookie,把cookie中的T0k1m0u5AfREO、T0k1m0u5AfREP、cookiesession1替换到oy.py文件,运行oy.py文件,会发现请求成功且请求结果已经返回,说明此时的cookie是有效的。还是挺幸运的,之后会执行该函数、一些定时任务、load事件等代码还会生成几次cookie
5.既然确定了有效cookie生成的是在script标签中函数调用前面,此时需要找到本地替换的文件,在相同的地方断点,并取消script监听,开启本地文件替换,开始补环境
四、补环境
1.首先在代码顶部补上window、document、location、navigator环境,并用代理自动把需要补的环境吐出来,新建jsProxy.js把下面代码拷贝过去
// 代理器封装
function getEnv(proxy_array) {
for(var i=0; i<proxy_array.length; i++){
handler = `{\n
get: function(target, property, receiver) {\n
console.log('方法:get',' 对象:${proxy_array[i]}',' 属性:',property,' 属性类型:',typeof property,' 属性值类型:',typeof target[property]);
return target[property];
},
set: function(target, property, value, receiver){\n
console.log('方法:set',' 对象:${proxy_array[i]}',' 属性:',property,' 属性类型:',typeof property,' 属性值类型:',typeof target[property]);
return Reflect.set(...arguments);
}
}`;
eval(`
try{\n
${proxy_array[i]};\n
${proxy_array[i]} = new Proxy(${proxy_array[i]},${handler});
}catch(e){\n
${proxy_array[i]}={};\n
${proxy_array[i]} = new Proxy(${proxy_array[i]},${handler});
}
`)
}
}
// proxy_array = ['window', 'document', 'location', 'navigator', 'history','screen','target' ]
// getEnv(proxy_array)
module.exports = getEnv
2.补location、navigator环境,使用下面脚本,把location、navigator拷贝到oy.py,并分别添加到location、navigator属性上;下图会发现location、navigator添加属性的方法不一样,location是在函数内部通过this添加,navigator是在函数外部通过prototype添加,这是因为location那些属性是对象上的,navigator的属性是原型上的,prototype是构造函数访问原型的属性;上述代码中window.proto = new Window();是对象访问原型
function copyObj(obj) {
var newObject = {};
for (var key in obj) {
var result = obj[key];
if (['string', 'boolean', 'number'].indexOf(typeof result) !== -1 || Object.prototype.toString.call(result) === '[object Array]') {
newObject[key] = result;
}
}
copy(newObject);
};
3.运行oy.js,会发现在回去window的top值时报错,在调试工具控制台输出window.top会发现是个window对象,且Reflect.ownKeys(window).includes(‘top’)返回true说明top是对象属性,直接补上window.top = window(后面就不再说明属性是对象属性还是原型属性,这里只是提供检测属性是否是对象属性的另一种方法)
4.运行oy.js,会发现获取window下的clearInterval时不再打印其他信息;仔细看上面打印信息先把document缺少的补上
5.运行oy.js,这时会发现doucmenr.createElement创建div时报错,把div补上,可以看出此时的div原型指向了Document对象,new Div()将拥有createElement、appendChild、removeChild,这是原型继承,其实每个标签的这些方法逻辑都差不多,采用原型继承的方式可以避免重复的代码
6.运行oy.js,还是doucmenr.createElement创建div时报错,这时看最后的报错信息会发现详细的报错代码_KaTeX parse error: Expected group after '_' at position 4: fA[_̲j8[85]] is not a function,清除cookie并刷新网站,会进入call断点,点击进入函数调用,会进入虚拟文件。在虚拟文件内搜索_KaTeX parse error: Expected group after '_' at position 4: fA[_̲j8[85]](,在搜到的结果中打断点,点击跳过断点,会进入该断点,在控制台输出对应的信息,会发现在创建div标签后,又通过getElementsByTagName获取了div下的i标签,且返回的是空数组,根据分析补上Document.prototype.getElementsByTagName即可,其实这样补i也不太对,由于原型继承,调用getElementsByTagName的对象除了div还可能是其他标签,如果是其他标签获取i时是有值的,那么就和div获取i冲突了,不过这里这样补就够用了
7.运行oy.js,会发现document.getElementsByTagName获取script时报错,在控制台输出document.getElementsByTagName(‘script’),是两个script的数组,其中第一个script具有type、r属性,第二个具有type、r、charset、src属性,根据分析补上script
8.运行oy.js,会发现获取script时仍然报错,这时看最后的报错信息会发现详细的报错代码_KaTeX parse error: Expected group after '_' at position 4: gM[_̲jN] is not a function,虚拟文件内搜索_KaTeX parse error: Expected group after '_' at position 4: gM[_̲jN](,在搜到的地方断点,并点击一直点击跳过断点,看见作用域中的_$gM是script对象时停下,并在控制台输出相关信息,会发现此时获取的是通过getAttribute获取最后一个script中的r属性,根据分析补上Document.prototype.getAttribute
9.运行oy.js,会发现获取script中的r属性后,依然再script处报错;此时把两个script对象全局命名script1、script2,并把script1、script2,加入代理
10.运行oy.js,会发现依然是script2.parentElement报错,在控制台输出parentElement会发现是个head标签,根据分析补上script.parentElement
11.运行oy.js,这时会报错window.attachEvent,这时要注意上一个打印信息window.addEventListener把它补上即可;这里就不做断点儿调试了,如果想要验证可查看最后的报错信息是_KaTeX parse error: Expected group after '_' at position 1: _̲_[_u[4]] is not a function,在虚拟文件搜索KaTeX parse error: Expected group after '_' at position 1: _̲_[__u[4]](断点儿调试会发现调用的是window.addEventListener而不是window.attachEvent
12.运行oy.js,会发现docuemnt.getElementsByTagName获取meta时报错,补meta的方法与script一样,这里就不说过程了,
13.运行oy.js,会发现document.getElementsByTagName获取base时报错,在控制台输出document.getElementsByTagName(‘base’),是空数组,根据分析补上环境即可
14.运行oy.js,获取docuemnt.getElementById时报错,补上getElementById即可
15.运行oy.js,会发现不再打印其他信息,这是定时器造成的,重写定时器为空函数即可;重写定时器之后,再运行文件会发现无报错信息
16.写一个获取cookie的函数,会发现cookie可以正常获取
五、验证结果
1.另建test.js,把oy.js复制到test.js备份,并把oy.js中的meta的content属性用meta_content代替,ts、js代码分别用’ts_code’、'js_code’代替
2.修改oy.js,请求第一次search-ng/queryResource/index拿到meta_content、ts_code、js_code。分别替换从oy.js中的文本,然后再次请求search-ng/queryResource/index会发现虽然请求状态码为200,但是并未返回结果,说明环境有问题
3.瑞数会检测__dirname、__filename。修改oy.js,顶部添加delete __filename、delete __dirname,运行oy.py后,会发现虽然第二次请求返回了结果,但是状态码为202,显然不对,说明环境还有问题
六、补全剩余的环境
1.运行test.js,控制台搜索createElement,会发现创建了form标签,补上form
2.运行test.js,控制台搜索createElement,会发现创建了input标签,补上input
3.运行test.js,控制台搜索input,会发现input创建了3次,调整test.js增加input1、input2、input3,并把form、input1、input2、input3加入代理
4.运行test.js,查看控制台打印信息,搜索input1,会发现每个input都增加了一些属性,且调用了form.appendChild参数分别是三个input,最后还获取了form的action属性,这里有个巨坑:获取form属性时,如果form中有表单属性且表单属性中的name、id的值由对用的form属性名,则获取的是该表单元素
5.验证获取form元素,在代码最后面打印出form、input1、input2、input3,运行test.js,会看到他们的属性值;在浏览器控制台中模拟从form创建到获取form的action的操作
6.根据上述分析,重复运行test.js,查看form获取的属性,在appendChild中修改form属性
7.运行test.js,分别搜索createElement、appendChild、getElementsByTagName 、getElementById会发现此时与标签祥光的已补完,再搜索document,找其中获取某个属性为undefined的,在浏览器控制台输出属性补上即可,大概需要补document.visibilityState、document.documentElement、document.body、document.all、Document.prototype.addEventListener,其中document.all比较特殊,可以参考文章 web逆向中奇葩的document.all
8.把补好的document环境更新到oy.js,运行oy.py,会发现请求还是200未返回结果,此时在test.js搜索window、navigator,找其中获取某个属性为undefined的,在浏览器控制台输出属性补上即可,补的时候如果是方法就补个空方法,如果是个对象就像document、location一样搞个函数new个对象就行。如果window、navigator相关属性补完还是200未返回结果,继续阅读文章第七部分,否则跳过第七部分
七、如何在虚拟文件内调试代码
1.清除cookie刷新页面,进入虚拟文件在虚拟文件顶部断点,复制
t
s
替换
t
e
s
t
.
j
s
中的
_ts替换test.js中的
ts替换test.js中的ts,复制虚拟文件代码替换test.js中的js部分
2.找到关键函数入口,启动之前的hook cookie,点击跳过断点,在调试工具右侧的对战中找到类似下图中的代码,其中有三个变量KaTeX parse error: Expected group after '_' at position 4: _z、_̲c、$jY特别关键,只要这三个变量和虚拟文件中的变量每次循环对应上就性,只需分别在web端循环开始打上日志断点,再在test.js中找到相对位置输出这三个值对比就行,注意:index不同,生成虚拟文件的代码也会不同。只需找出对应位置的变量即可
3.在web日志断点上可以看到大部分环境,如:document.all、navigator.webdriver、__filename、__dirname等环境,补环境时有两个属性需特别注意document.all、navigator.webdriver
4.几个关键的地方补完基本就能拿到请求结果了,这时可以优化下创建form部分的代码,form、input是在createElement外部创建的,优化后可以去掉switch在createElement内部创建
八、补获取商品的环境
1.在network中搜索商品名,可以找到获取商品请求commoditySearch/queryCommodityResult,鼠标右击请求找到Copy>Copy as cUrl(cmd),把拷贝好的curl转成python代码,分析代码会发现多了K5nOZLud参数,cookie也有一些参数,不过用的是requests.session请求,会缓存上一个请求的cookie,除了T0k1m0u5AfREP其他值可以暂时不用管
2.找到K5nOZLud参数生成,同样的先在source中script断点,把请求成功的页面本地替换,和之前流程一样,别忘了开启之前关闭的本地替换和外链js中的.call断点
3.刷新页面,找到任意请求查看堆栈,找到jq的send点击找到该方法,分析此处代码会发现在发送请求时还执行了一个open方法
4.因为不确定是在open还是send增加K5nOZLud参数 ,所以写一个hook,重写send和open方法
(function () {
var open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url, async) {
console.log(url)
if(url.includes('K5nOZLud')) debugger;
return open.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (data) {
console.log(this)
if(url.includes('K5nOZLud')) debugger;
send.apply(this, arguments);
};
})();
5.刷新页面,会进入.call断点,运行hook
6.运行hook,点击跳过断点,此时会有一个请求进入open断点,其中url中包含K5nOZLud,说明只要把open方法补上就可以拿到K5nOZLud参数
7.修改test.js,删除文件中除了补环境的js代码,把第二次请求中的ts和外链js代码拷贝到该文件,meta中的content修改为第二次请求中的content,然后补上XMLHttpRequest.prototype.open方法,open方法补上后,运行oy.py在打印信息中会发现url上并没有K5nOZLud,说明环境有问题
8.这时按照之前的方法按照document、window、navigator依次补环境,其中特别注意的是createElement创建a元素,当补上a元素,看打印信息会发现a元素设置href属性,还获取了很多属性,且在后面打印出a元素会发现只有href,说明此时的a元素还缺少环境,当a设置href属性后会吧href查看分别设置prot、protocol、hostname。而且href值是请求的url,说明href跟着请求的url变化的,这时可使用Object.defineProperty设置href并处理href的值。这个a元素特别重要,如果补错始终拿不到K5nOZLud值或者返回错误的K5nOZLud值,这样补比较通用;也可以写死属性值,这样只能一个请求
九、结语
从以上补环境来看就可以看出来使用面向对象补的重要性,如:navigator.webdriver,如果像平常一样只补个json,webdriver作为json的某一属性,如果是新手找到问题所在就要费一番功夫了,如果直接使用面向对象补环境,那么可以很好的避免这个问题。还有window instanceof Window检测,如果只是简单的把window = global,那么这个环境始终是错的,虽然这个检测没有__filename、__dirname环境那么重要。不过上面也只是简单的面向对象补环境,如果复杂点就要考虑原型链、可枚举属性、不可枚举属性、函数返回值、函数长度、toString等问题。
采用复杂的面向对象补环境,最好还是给封装起来,便于二次使用,其实环境中的大部分值、函数、属性都是相同的,可能会存在某些值不同,如果封装成一个自己的jsDom以后逆向会简单更多。下面图1是我自己封装的,可以看到封装之后同样的网站只补了location.href和一些dom操作。还可以写一个迭代代理,上面的代理输出太不清晰,而且还不能迭代代理实在有点儿low,可以看到下图的日志信息,原比上面的日志清晰,还能生成日志文件。如果有能力的还可以写个chrome插件,日志输出和代理的一样,这样调试起来会很方便,可以看图2日志对比,日志格式完全一样,日志输出会存在差别,比较在浏览器端一些对象是不能代理的,只能代理其属性。不过也可以采用jsDom插件,貌似jsDom内部也没解决document.all的问题。还有市面上一些浏览器插件检测环境,插件甚至可以自动生成环境,不过生成环境的代码太乱,而且还不一定能成功,比起自己补环境感觉还是差些;我自己写的这个插件和封装的环境结合使用还是可以提高逆向的效率和成功率的,由于还在测试阶段,暂时不给大家分享了,后续会通过文章介绍怎么使用分享给大家,也会另出一篇文章介绍怎么封装环境。图3、图4、图5是浏览器插件大致的界面,几乎包括了对象的所有属性,而且还有每个对象的介绍,不过目前的环境并不全,只是一些常用的,打算后面会加上hook、websocket、一些简单的ast解析转换。