作者:Fr1day@0keeTeam
0x00 前言
在 浅谈动态爬虫与去重 中,分享了动态爬虫中触发事件、监控节点变动、URL去重等的实现方法。在接近一年的线上运行与迭代更新过程中,处理了很多bug,也遇到一些有趣的漏抓案例。
本文将详细分析几个有代表性的案例,希望能对各位的coding大业有所帮助。
0x01 一个都不能少
上图为被抓取页面的源码。两个 <a>
标签点击后会分别跳转到 /test4.php
和 /test5.php
(抓取时页面已锁定,实际不会跳转,但可以监控到跳转的目标URL)。
但爬虫并未抓取到 http://localhost/test4.php。检查下phantomjs模块的具体调用日志:
可以看到 id=test4 节点对应的事件确实被触发了,而且还把锚点的变化都记录下来了。但就是没抓到 /test4.php 跳转的请求。
原本以为是因为锚点的问题,锚点阻止了后续的事件执行(一个不负责任的脑洞)。但经过一番调试,发现是触发事件的时候出问题了。
window.location.href 重复执行的时候,浏览器只会执行后面的一个,比如这段代码,可以粘贴到 Console 执行下,页面会跳转到 /456。
解决方案如下图,由于 Javascript 的异步非阻塞的特性,还加了个闭包来实现 sleep。
如果不想这么做的话,也可以通过 Hook location对象来解决。
一句话总结:触发事件一定要有时间间隔!
0x02 猝不及防的关闭
<script type="text/javascript" language="javascript">
function BtnPsw_onclick() {
window.open("../Login/UpdatePass")
}
</script>
<form name="form1" method="post" action="" id="form1">
用户名:<input name="UserName" type="text" id="UserName" class="logintext"><br/>
密码: <input name="Password" type="password" id="Password" class="logintext"><br/>
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="1222">
<input name="LoginButton" type="button" id="LoginButton" value="登 录" class="lbotton">
<input name="login" type="button" value="退 出" class="lbotton" onclick="window.close()">
<input name="UpdatePwd" type="button" value="修改密码" class="lbotton" onclick="BtnPsw_onclick()">
</form>
phantomjs解析的时候,超时严重导致漏抓。通过伟大的注释调试法,可以发现问题在这行代码里:
<input name="login" type="button" value="退 出" class="lbotton" onclick="window.close()">
动态分析时会主动去执行行内绑定的代码,即:window.close()。关闭了页面之后,PhantomJS后续绑定的事件都会失效,比如page.evaluate、page.onCallback、phantom.exit。没有执行exit函数,一直阻塞导致触发python的超时——狗带。
修复方案,在执行关闭页面的时候,PhantomJS的onClosing事件可以收到通知,示例代码如下:
还可以通过Hook来解决这个问题:
0x03 永远触发不完的事件
案例URL: https://lvyou.baidu.com/main/event/rank
动态分析超时导致没有结果返回。动态爬虫里触发行内绑定事件的代码如下:
逻辑是遍历所有的节点的所有属性,执行以on开头的属性值,即 onclick=alert(1) 这种。
但是抓取上面案例的时候,发现一直没有返回结果,使用伟大的print调试法打印了触发的具体内容后,发现页面一直在不停的触发同一个事件。
仔细看下页面源码:
登录应该是用JSONP来实现的,每次点击登录都会生成一个script标签,而且这个标签恰好还插入在了登录标签前面。
遍历数组的过程中,也在不断扩展这个数组。这就是问题的关键。
那应该怎么解决呢?用了个不太优雅的方法来实现JS深拷贝:
0x04 Hook是个哲学问题
<a onclick=show()>test</a>
<script type="text/javascript">
function show(){
window.showModalDialog("another_page.html");
}
</script>
如上为漏抓页面的部分代码。爬虫进程超时,没有返回任何数据。
window.showModalDialog 是早期浏览器使用比较频繁的函数,用来弹出一个新页面,并且是阻塞执行的(所以造成爬虫超时被强行杀进程)。后来被 window.open 函数替代。替换的原因有:
1. showModalDialog 没有导航栏,无法进行后退、前进、收藏等操作
2. showModalDialog debug非常复杂(只能用alert调试法 2333)
3. 名字又长又难记(迷之猜测)
下图为正常打开的页面与 showModalDialog 打开的页面比较:
目前Chrome最新版已经不支持这个函数了,但Firefox、Safari、IE仍然支持。毫无意外的 PhantomJS 也支持。解决方案很简单,直接 Hook 函数就可以了:
这样的话,加上最开始就被 Hook 的 alert/prompt/confirm,现在已经 Hook 了四个可能会引起阻塞的函数了,是不是还有其他隐藏的存在呢?
写个脚本来检查下:
var page = require('webpage').create();
page.onConsoleMessage = function(msg) {
console.log('> ' + msg );
return true;
};
page.open("http://127.0.0.1:8082", "GET", "", function (status) {
console.log(status);
page.evaluateAsync(function(){
for(var i in window){
try {
if (typeof eval("window." + i) != "function") {
continue
}
}catch (e){
}
// if(i in {"showModalDialog": "1"}){
// continue
// }
try{
console.log(i)
eval("(function(){" + i + "();})()");
}
catch (e){
// console.log(e)
}
}
}, 10)
});
用 PhantomJS 加载任意页面,然后遍历 window 对象。首先出现阻塞卡顿的函数是 showModalDialog,再次运行脚本,跳过 showModalDialog 函数,然后….
顺畅的运行完成,说好的 alert/prompt/confirm 函数导致的阻塞呢?
复制脚本到浏览器中运行,倒是成功复现了 alert/prompt/confirm/print 导致的阻塞:
分析原因,应该是PhantomJS在封装onAlert、onPrompt、onConfirm接口的时候就对这几个可能产生阻塞的函数做了处理。
同样的原理,可以套用在其他的动态解析器上。举个栗子,在Chrome Headless里需要 Hook 哪些接口,你现在知道了吗?
0x05 总结
虽然在 Chrome Headless
出来之后,PhantomJS
变得索然无味。但是同样都基于Webkit内核,所遇到的问题和解决方案也大多相通,不必拘泥于形式。
如果有动态分析和爬虫方面的问题/想法,欢迎微博私信我 @吃瓜群众-Fr1day
注:水印图片来自于“安全小黄鸭”,是我个人公众号,不涉及版权问题。