漏扫动态爬虫实践
这篇文章在前段时间首发于安全客,从正式工作以来,自己没有坚持住经常更新博客的习惯,大部分积累都保存在了个人的笔记,很少有时间整理发出来,果然懒才是原罪。
0x00 简介
动态爬虫作为漏洞扫描的前提,对于web漏洞发现有至关重要的作用,先于攻击者发现脆弱业务的接口将让安全人员占领先机。即使你有再好的payload,如果连入口都发现不了,后续的一切都无法进行。这部分内容是我对之前开发动态爬虫经验的一个总结,在本文将详细介绍实践动态爬虫的过程中需要注意的问题以及解决办法。
在Chrome的Headless模式刚出现不久,我们当时就调研过用作漏洞扫描器爬虫的需求,但由于当时功能不够完善,以及无法达到稳定可靠的要求。举个例子,对于网络请求,无法区分导航请求和其它请求,而本身又不提供navigation lock的功能,所以很难确保页面的处理不被意外跳转中断。同时,不太稳定的CDP经常意外中断和产生Chrome僵尸进程,所以我们之前一直在使用PhantomJS。
但随着前端的框架使用越来越多,网页内容对爬虫越来越不友好,在不考虑进行服务端渲染的情况下,Vue等框架让静态爬虫彻底失效。同时,由于JS的ES6语法的广泛使用,缺乏维护(创始人宣布归档项目暂停开发)的PhantomJS开始变的力不从心。
在去年,puppeteer和Chromium项目在经历了不断迭代后,新增了一些关键功能,Headless模式现在已经能大致胜任扫描器爬虫的任务。所以我们在去年果断更新了扫描器的动态爬虫,采用Chromium的Headless模式作为网页内容解析引擎,以下示例代码都是使用pyppeteer 项目(采用python实现的puppeteer非官方版本),且为相关部分的关键代码段,如需运行请根据情况补全其余必要代码。
0x01 初始化设置
因为Chrome自带XSS Auditor,所以启动浏览器时我们需要进行一些设置,关闭掉这些影响页面内容正常渲染的选项。我们的目的是尽可能的去兼容更多的网页内容,同时在不影响页面渲染的情况下加快速度,所以常见的浏览器启动设置如下:
browser = await launch({
"executablePath": chrome_executable_path,
"args": [
"--disable-gpu",
"--disable-web-security",
"--disable-xss-auditor",# 关闭 XSS Auditor
"--no-sandbox",
"--disable-setuid-sandbox",
"--allow-running-insecure-content",# 允许不安全内容
"--disable-webgl",
"--disable-popup-blocking"
],
"ignoreHTTPSErrors": True # 忽略证书错误
})
接下来,创建隐身模式上下文,打开一个标签页开始请求网页,同样,也需要进行一些定制化设置。比如设置一个常见的正常浏览器UA、开启请求拦截并注入初始的HOOK代码等等:
context = browser.createIncognitoBrowserContext()
page = await context.newPage()
tasks = [
# 设置UA
asyncio.ensure_future(page.setUserAgent("...")),
# 注入初始 hook 代码,具体内容之后介绍
asyncio.ensure_future(page.evaluateOnNewDocument("...")),
# 开启请求拦截
asyncio.ensure_future(page.setRequestInterception(True)),
# 启用JS,不开的话无法执行JS
asyncio.ensure_future(page.setJavaScriptEnabled(True)),
# 关闭缓存
asyncio.ensure_future(page.setCacheEnabled(False)),
# 设置窗口大小
asyncio.ensure_future(page.setViewport({
"width": 1920, "height": 1080}))
]
await asyncio.wait(tasks)
这样,我们就创建了一个适合于动态爬虫的浏览器环境。
0x02 注入代码
这里指的是在网页文档创建且页面加载前注入JS代码,这部分内容是运行一个动态爬虫的基础,主要是Hook关键的函数和事件,毕竟谁先执行代码谁就能控制JS的运行环境。
包含新url的函数
hook History API,许多前端框架都采用此API进行页面路由,记录url并取消操作:
window.history.pushState = function(a, b, url) {
console.log(url);}
window.history.replaceState = function(a, b, url) {
console.log(url);}
Object.defineProperty(window.history,"pushState",{
"writable": false, "configurable": false});
Object.defineProperty(window.history,"replaceState",{
"writable": false, "configurable": false});
监听hash变化,Vue等框架默认使用hash部分进行前端页面路由:
window.addEventListener("hashchange", function() {
console.log(document.location.href);});
监听窗口的打开和关闭,记录新窗口打开的url,并取消实际操作:
window.open = function (url) {
console.log(url);}
Object.defineProperty(window,"open",{
"writable": false, "configurable": false});
window.close = function() {
console.log("trying to close page.");};
Object.defineProperty(window,"close",{
"writable": false, "configura