客户端JavaScript插桩
有大量代码不值得花费时间和脑力去分析。二进制逆向工程师通常使用ltrace、strace或frida直接跳转到重要代码部分。对于客户端JavaScript,你也可以仅使用常见的浏览器功能实现相同目的。这将节省时间,使测试更有趣,并帮助你将注意力集中在值得关注的代码上。
这篇博客介绍了我在插桩客户端JavaScript时的思考过程和实用方法。这些过程帮助我相对轻松地在复杂代码库中发现深度嵌入的漏洞。我使用这些技巧已经很久,以至于我在一个名为Eval Villain的Web扩展中实现了它们。虽然我会介绍Eval Villain的一些全新功能,但也会展示如何在不使用Eval Villain的情况下获得相同结果。
通用方法与思考
测试应用程序常常会引发关于应用程序如何工作的问题。如果应用程序要正常运行,客户端必须知道其中一些问题的答案。考虑以下问题:
- 服务器接受哪些参数?
- 参数是如何编码/加密/序列化的?
- wasm模块如何影响DOM?
- DOM XSS接收器在哪里?应用了哪些清理措施?
- 消息后处理程序在哪里?
- 广告之间的跨源通信是如何实现的?
为了使网页工作,它需要知道这些问题的答案。这意味着我们也可以在JavaScript中找到答案。注意,每个问题都暗示了特定JavaScript函数的使用。例如,客户端如何在不调用addEventListener的情况下实现消息后处理程序?因此,“第一步”是钩住这些有趣的函数,验证用例是否符合我们的兴趣,并回溯。在JavaScript中,它看起来像这样:
|
|
如果处理程序尚未注册,只需将上述代码粘贴到控制台即可工作。然而,关键是在函数被使用之前钩住它。在下一节中,我将展示一种简单实用的方法来始终赢得这场竞赛。
钩住原生JavaScript是“第一步”。这通常有助于你找到有趣的代码。有时你会想插桩该代码,但它是非原生的。这需要一种不同的方法,将在“第二步”部分中介绍。
第一步:钩住原生JavaScript
构建自己的扩展
虽然你可以使用许多Web扩展之一将任意JavaScript添加到页面,但我不推荐这样做。这些扩展通常有bug,存在竞争条件,并且难以开发。在大多数情况下,我发现编写自己的扩展更容易。不要被吓倒,这真的很简单。你只需要两个文件,我已经在这里为你准备好了。
要在Firefox中加载代码,请在URL栏中转到about:debugging#/runtime/this-firefox,点击“加载临时附加组件”,并导航到扩展顶级目录中的manifest.json文件。
对于Chrome,转到chrome://extensions/,在右侧启用开发者模式,然后点击“加载已解压的扩展”。
扩展应显示在附加组件列表中,你可以在其中快速启用或禁用它。启用后,script.js文件将在每个网页中加载。以下代码行记录所有输入到document.write的内容。
|
|
用你想要的任何代码替换这些行。你的代码将在每个页面和框架中运行,在页面有机会运行自己的代码之前。
工作原理
样板文件使用清单文件注册内容脚本。清单告诉浏览器内容脚本应在每个框架中运行,并在页面加载之前运行。内容脚本无法直接访问它们加载到的页面作用域,但它们可以直接访问DOM。因此,样板代码只是向页面DOM添加一个新脚本。CSP可以禁止这一点,因此扩展检查它是否工作。如果CSP阻止了你,只需通过浏览器配置、Web扩展或拦截代理禁用CSP。
注意,插桩代码最终具有与网站相同的权限。因此,你的代码将受到与页面相同的限制,例如同源策略。
异步和竞争
快速警告一下。上述内容脚本将让你首先访问唯一的JavaScript线程。网站本身无法运行任何JavaScript,直到你放弃该线程。试试看,看看你能否在样板钩住document.write之前运行它。
首先访问是一个巨大的优势,你可以毒化网站即将使用的环境。在你完成毒化之前,不要放弃你的优势。这意味着避免使用异步函数。
这就是许多旨在将用户JavaScript注入页面的Web扩展有bug的原因。在Web扩展中检索用户配置是使用异步调用完成的。在异步查找用户配置时,页面正在运行其代码,并可能已经执行了你想要钩住的接收器。
这就是Eval Villain仅在Firefox上可用的原因。Firefox有一个独特的API,可以注册带有用户配置的内容脚本。
Eval Villain
我很少遇到无法用Eval Villain解决的“第一步”情况。Eval Villain只是一个内容脚本,它钩住接收器并在输入中搜索源。你可以将几乎任何原生JavaScript功能配置为接收器。源包括用户配置的字符串或正则表达式、URL参数、本地存储、cookie、URL片段和窗口名称。这些源被递归解码以查找重要子字符串。让我们看看上面示例的同一页面,这次使用Eval Villain的默认配置。
注意此页面是从本地file://加载的。源代码见下文。
|
|
即使页面没有Web请求,Eval Villain仍然成功钩住了用户配置的接收器document.write,在页面使用它之前。没有竞争条件。
还要注意,Eval Villain不仅显示document.write的输入。它正确突出了注入点。URL参数x包含一个编码字符串,击中了接收器document.write。Eval Villain通过递归解码URL参数发现了这一点。由于参数被解码,向用户提供了一个编码器函数。你可以右键单击,复制消息并将其粘贴到控制台。使用编码器函数可以快速尝试有效负载。下图显示了使用编码器函数将marquee标签注入页面。
如果你阅读了前面的部分,你就知道这一切是如何工作的。Eval Villain只是使用内容脚本将其JavaScript注入页面。它所做的任何事情,你都可以在自己的内容脚本中完成。此外,你现在可以使用Eval Villain的源代码作为你的样板代码,并为其功能定制以应对特定的技术挑战。
第一步.5:快速提示
假设你使用“第一步”从有趣的原生函数获取console.trace。也许URL参数击中了你的decodeURI接收器,现在你正在回溯到URL解析函数。在这种情况下,我经常犯一个错误,我希望你做得更好。当你得到跟踪时,不要开始阅读代码!
现代Web应用程序通常在console.trace的顶部有polyfills和其他冗余。例如,我在Google搜索结果页面上得到的堆栈跟踪以函数iAa、ka、c、ng、getAll开始。不要陷入隧道视野,开始阅读ka,当getAll显然是你想要的。当你看getAll时,不要阅读源代码!继续扫描,注意getAll是一个方法,它的兄弟是get、set、size、keys、entries以及URLSearchParams文档中列出的所有其他方法。
我们刚刚找到了多个自定义URL解析器,在最小化代码中重新实现,而无需实际阅读代码。“扫描”尽可能多,不要开始深入阅读代码,直到找到正确的位置或扫描失败。
第二步:钩住非原生代码
插桩原生代码没有导致漏洞。现在你想插桩非原生实现本身。让我用一个例子来说明这一点。
假设你发现了一个URL解析器函数,它返回一个名为url_params的对象。该对象具有URL参数的所有键值对。我们想监控对该对象的访问。这样做可以为我们提供一个与URL关联的每个URL参数的清晰列表。我们可能通过这种方式发现新参数,并解锁站点中的隐藏功能。
在JavaScript中这样做并不难。在16行代码中,我们可以有一个组织良好、唯一的URL参数列表,与适当的页面关联,并保存在localStorage中以便轻松访问。我们只需要弄清楚如何将代码直接粘贴到URL解析器中。
|
|
Chrome的开发工具允许你在JavaScript源中键入自己的代码,但我不推荐这样做。至少对我来说,添加的代码会在页面加载时消失。此外,这种方式不容易管理任何插桩点。
我有一个更好的解决方案,它内置于Firefox和Chrome中。将你的插桩代码用括号括起来,在末尾添加&& false。上面的代码变成这样:
|
|
现在右键单击要添加代码的行号,点击“条件断点”。
将你的代码粘贴在那里。由于&& false,条件永远不会为真,因此你永远不会得到断点。浏览器仍将执行我们的代码,并在我们插入断点的函数作用域内。没有竞争条件,断点将继续存在。当你打开开发人员工具时,它将在新选项卡中显示。你可以通过禁用辅助断点来快速禁用单个插桩脚本。或者通过禁用断点或关闭开发人员工具窗口来禁用所有它们。
我使用这个特定示例来展示你可以走多远。插桩代码将按站点将URL参数保存到本地存储条目。在任何给定页面,你可以通过将以下代码粘贴到控制台来自动填充所有已知URL参数到URL栏。
|
|
如果你经常使用这个,你甚至可以将代码放在书签中。
结合原生和非原生插桩
没有什么能阻止我们同时使用原生和非原生函数。你可以使用内容脚本实现大型花哨的代码库。将该功能导出到全局作用域,然后在条件断点中使用它。
这带来了Eval Villain的最新功能。你的条件可以利用Eval Villain的递归解码功能。在弹出菜单中点击“配置”并转到“全局”部分。确保“sourcer”行已启用,然后点击保存。
我发现自己经常启用/禁用此功能,因此在弹出菜单本身有第二个“启用”标志。它在“启用/禁用”菜单中作为“用户源”。这导致Eval Villain将evSourcer函数导出到全局名称作用域。这将向递归解码源列表添加任何任意对象。
可以看到,第一个参数是你为源命名的名称。第二个是你要搜索接收器的实际对象。除非有Eval Villain不理解的自定义编码,否则你可以直接原始放入。有一个可选的第三个参数,会导致sourcer每次调用时都console.debug。此函数返回false,因此你可以在任何地方将其用作条件断点。例如,你可以将其添加为条件断点,仅在感兴趣的消息后处理程序中运行,当从特定源接收消息时,作为查找消息的任何部分是否会击中DOM XSS接收器的手段。在正确的地方使用这可以减轻对你的插桩代码施加的SOP限制。
就像evSourcer一样,还有一个evSinker。我很少使用这个,因此在弹出菜单中没有“启用/禁用”条目。它接受接收器名称和参数列表,并像你自己的接收器一样行动。它也返回false,因此可以轻松地在条件断点中使用。
结论
编写自己的插桩是漏洞研究的一项强大技能。有时,只需几行JavaScript就能驯服一个巨大的杂乱代码库。通过了解这是如何工作的,你可以更好地洞察像Eval Villain和DOM invader这样的工具能做什么和不能做什么。必要时,当工具不足时,你也可以调整自己的代码。