0x00 前言
上一篇主要讲了如何通过修改 Chromium 代码为 Web 漏洞扫描器的爬虫打造一个稳定可靠的 headless 浏览器。这一篇我们从浏览器底层走到上层,从 C++ 切换到 JavaScript,讲一下如何通过向浏览器页面注入 JavaScript 代码来尽可能地获取页面上的链接信息。
0x01 注入 JavaScript 的时间点
首先我们要解决的第一个问题是:在什么时间点向浏览器页面注入 JavaScript 代码?
答案非常简单, 在页面加载前,我们希望能够注入一段 JavaScript 代码以便于能够 Hook、备份各种未被污染的函数, 在页面加载后,我们希望能够注入一段 JavaScript 代码以便于能够进行遍历各个元素、触发各种事件、获取链接信息等操作。
那么下一个问题又来了:怎么定义页面加载前、页面加载后?
页面加载前的定义非常简单,只要能在用户代码执行前执行我们注入的 JavaScript 代码即可,也就是在页面创建之后、用户代码执行之前的时间段对于我们来说都算是页面加载前,CDP 刚好提供了这么一个 API Page.addScriptToEvaluateOnNewDocument 能够让我们在页面加载前注入 JavaScript 代码。
接下来考虑一下该如何定义页面加载后。最简单的方法就是不管三七二一,每个页面都加载 30s (即便是空白的页面),随后再注入我们的代码,但很明显这会浪费很多资源,我们需要根据每个页面的复杂度来控制加载时间。可能会有同学说我们可以监听 load 事件,等待页面加载结束之后再注入代码,那我们考虑一个比较常见的场景,在某个页面上刚好有那么一两个图片字体资源加载速度特别慢,导致 load 迟迟未被触发(甚至不触发),但这些资源其实我们并不在乎,完全可以直接注入我们代码,所以只等待 load 事件也并不是一个特别好的选择。
我们先看一下加载一个页面的过程,除了会触发 load 事件之外还会触发什么事件:
<html>
<!-- 在外部 script 之前的 css 会阻塞 DOM 的构建 -->
<link rel="stylesheet" href="http://httpbin.org/delay/3?id=1">
<script src="http://httpbin.org/delay/1?id=2"></script>
<!-- 后面两个 css 并不会阻塞 DOM 的构建 -->
<link rel="stylesheet" href="http://httpbin.org/delay/6?id=3">
<link rel="stylesheet" href="http://httpbin.org/delay/6?id=4">
</html>
import pychrome
import pychrome.exceptions
def lifecycleEvent(**kwargs):
print("{}: {}".format(kwargs['timestamp'], kwargs['name']))
browser = pychrome.Browser()
tab = browser.new_tab()
tab.Page.lifecycleEvent = lifecycleEvent
tab.start()
tab.Page.enable()
try:
tab.Page.setLifecycleEventsEnabled(enabled=True)
except pychrome.exceptions.CallMethodException:
pass
tab.Page.navigate(url="http://localhost/load_event.html")
tab.wait(60)
下面我们简单地介绍一下上面几个我们会用到的事件
| 事件| 解释 |
|DOMContentLoaded|一般表示 DOM 和 CSSOM 均准备就绪的时间点|
| networkAlmostIdle| 当前网络连接数少于 2 后触发|
|networkIdle| 当前没有网络连接后触发|
|load |网页所有资源载入后触发,浏览器上加载转环停止旋转 | |
之前解释过 load 事件可能对我们来说太晚了,但是现在 DOMContentLoaded 事件对我们来说又太早了,因为用户代码也可能会绑定这个事件然后操作 DOM,我们肯定是希望能够在页面稳定之后再注入我们的代码,所以在 load 和 DOMContentLoaded 之间某个时间点对我们来说比较合适,可惜并没有这样一个特别的事件存在,所以我个人觉得比较好的方案是将上面各个事件结合一起使用。
我们先说一下这几个事件的触发顺序,首先这几个事件触发顺序不一定,例如触发时间 load 事件不一定比 DOMContentLoaded 晚,load 也不一定比 networkAlmostIdle 晚。唯一能确定的就是 networkAlmostIdle 一定比 networkIdle 晚。在一般的情况下时间顺序是 DOMContentLoaded -> networkAlmostIdle -> networkIdle -> load。
所以一般的解决方案:
- 等待 load,同时设定等待超时时间,load 超时直接注入代码,同时等待 DOMContentLoaded 事件
- DOMContentLoaded 事件触发,接着等待 networkAlmostIdle,同时设定等待超时时间,超时直接注入代码
- networkAlmostIdle 事件触发,接着等待 networkIdle 同时设定等待超时时间,超时直接注入代码
如果 load 事件在其他事件前触发,那就直接注入代码。
0x02 DOM 构建前
解决了在什么时候注入 JavaScript 代码的问题,接下来我们该开始考虑第一阶段该注入什么代码了。
由于在第一阶段的时间点,DOM 树还未构建,所以我们所注入的代码均不能操作 DOM,能干的事情也就只有 Hook、备份 BOM 中的函数。
basic
我们先把一些会导致页面阻塞、关闭的函数给 Hook 了,例如:
window.alert = function () { return false; };
window.prompt = function (msg, input) { return input; };
window.confirm = function () { return true; };
window.close = function () { return false; };
同时也需要在 CDP 中处理 Page.javascriptDialogOpening 事件,因为还有类似 onbeforeunload 这样的弹窗。
location
还记得我们上一篇通过修改 Chromium 代码将 location 变成可伪造的事情了吗?就是为了能够在这里对 location 直接 Hook,直接看代码:
var oldLocation = window.location;
var fakeLocation = Object();
fakeLocation.replace = fakeLocation.assign = function (value) {
console.log("new link: " + value);
};
fakeLocation.reload = function () {};
fakeLocation.toString = function () {
return oldLocation.toString();
};
Object.defineProperties(fakeLocation, {
'href': {
'get': function () { return oldLocation.href; },
'set': function (value) { console.log("new link: " + value); }
},
// hash, host, hostname ...
});
var replaceLocation = function (obj) {
Object.defineProp