DOM XSS 模糊测试策略——第 1 部分
XSS 至今仍是迄今为止最常见的漏洞类型,本文介绍自动化查找 XSS 的策略。
跨站脚本(XSS)至今仍是 Web 应用中迄今为止最常见的漏洞,与其他类别的漏洞相比,它们既容易引入,也更容易发现。XSS 分为 3 类:反射型、存储型和基于 DOM 的 XSS。前者是三者中最普遍的一种,也是最难检测的一种。
要查找 DOM XSS,可以采用静态方法:解析 Javascript、对源(source)和汇(sink)进行污点标记、静态传播污点等。由于 Javascript 语言的动态特性,这种方法对 Javascript 来说很困难,容易产生误报,而且复杂且耗费资源。
动态方法似乎更适合这项任务,它们需要对 Javascript 进行插桩,以便对 JavaScript 运行时进行内省。可行的方法包括(以下列举绝非详尽无遗):
动态重写 Javascript 代码以注入插桩代码,这种方法比较脆弱,复杂度中等,而且耗费资源
对浏览器的 Javascript 引擎进行插桩,这是最节省资源的方法,但需要修改浏览器内部机制,这既困难(因为大多数 JavaScript 引擎都包含各种 JIT 魔法),长期维护的成本也很高
使用调试器 API,在适当的位置设置断点、单步执行代码等。事实证明这种方法速度较慢,功能也不如人们期望的那样完整——要在所有 eval 调用上设置断点,祝您好运。
使用覆盖率 API 粗略地窥探正在执行的内容,并结合猴子补丁(monkey patching)和 Proxy 对象注入,这种方法性能良好,也更容易实现,但理论上存在一些局限性
在这篇博文中,我们将构建一个由覆盖率引导的 XSS 模糊测试工具的简单概念验证(PoC)。该模糊测试工具将使用精确的覆盖率信息来识别新执行的代码路径,并利用这些信息生成新的测试载荷。除了某些局限性(参见 eval 带来的麻烦)之外,该模糊测试工具会对 sink 方法进行插桩。
为了限定 PoC 的范围,我们将专注于 postMessage XSS,使用 Chrome 浏览器及其远程调试器 API,用 Python 3 编写 PoC,并使用 Pychrome 库与浏览器进行交互。
介绍就到这里,让我们开始吧。
要与 Chrome 调试器 API 交互,我们需要使用以下命令行启用它:
chrome --verbose --window-size=1200x600 --disable-gpu --remote-debugging-port=9222 --user-data-dir=/tmp/foo --disable-web-security
您还可以选择启用无头模式,这将节省约 20% 的资源;如果您要构建一个功能完备的 XSS 模糊测试工具,这一点很重要。
重要的标志是 remote-debugging-port,其余的可以忽略;disable-web-security 用于禁用 XSS 审计器(我们首先想要找到 XSS,至于如何绕过,可以改天再研究)。
要访问这些 API,我们只需按如下方式实例化:
import pychrome
debug_host = '127.0.0.1'
debug_port = 9222
url = f"http://{debug_host}:{debug_port}"
browser = pychrome.Browser(url=url)
现在舞台已经搭好,让我们来想一想模糊测试工具应该做些什么?
- 创建一个新标签页
- 在页面中启用精确覆盖率收集
- 访问目标页面(看似简单,事实证明并非如此)
- 注入插桩代码
- 注入 XSS 检测方法
- 注入载荷
- 检测代码中执行的路径
- 据此生成新的载荷
转到第 6 步
第一步很简单,这是注入器类的初始化方法,它创建一个新标签页并启动它,然后在 Chrome 调试器中启用一组 API,以收集特定类型的事件:
def __init__(self, browser):
self.browser = browser
self.debugger = browser.new_tab()
self.debugger.start()
self.debugger.Page.enable()
self.debugger.Console.enable()
self.debugger.Runtime.enable()
启用代码覆盖率很容易,但使用其输出则要复杂一些。下面这段代码的作用是将覆盖率信息转化为可利用的内容,它会告诉我们哪段代码被执行了:
class Coverage:
def __init__(self, debugger):
self.debugger = debugger
self.sources = {}
self.coverages = []
self.debugger.Profiler.enable()
self.debugger.Profiler.start()
self.debugger.Debugger.enable()
self.debugger.Debugger.setSkipAllPauses(skip=True)
self.debugger.set_listener('Debugger.scriptParsed', self._on_script_parsed)
self.debugger.Profiler.startPreciseCoverage(callCount=True, detailed=True)
def _on_script_parsed(self, scriptId, **kwargs):
source = self.debugger.Debugger.getScriptSource(scriptId=scriptId)
self.sources[scriptId] = source
访问页面很简单,但并不能涵盖所有情况。例如,如果我们需要发送 POST 请求、设置特定的 Cookie 或向请求添加特定的请求头,这些情况就需要更复杂的代码,在某些情况下还需要在网络层拦截请求。
为了 PoC 的简便,我们假设是简单的情况:
self.debugger.Page.navigate(url=url)
检测页面何时加载完成本身又是一件麻烦事,调试器 API 会发送有助于检测加载完成的事件,但同样为了 PoC 的简便,我们直接等待:
self.debugger.wait(2)
下一步是注入插桩代码,这些代码必须在页面的其余部分开始加载之前执行。例如,如果我们需要对 sink 方法进行猴子补丁,这一点就至关重要。Chrome 调试器为此提供了一个 API:
source = open('instrument.js', 'r').read()
self.debugger.Page.addScriptToEvaluateOnNewDocument(source=source)
让我们回顾一下:我们现在可以控制 Chrome 实例,可以启动新标签页、注入插桩代码,并访问目标页面。为了避免文章篇幅过长,我们将在下一篇博文中介绍剩余的步骤,即:
- 如何检测 XSS?
- 如何使用 Javascript Proxy API 对对象进行插桩?
- 如何注入载荷?
- 如何利用覆盖率数据检测新的分支?
- 如何生成新的载荷?