客户端JavaScript插桩
有大量代码不值得花费时间和精力去分析。二进制逆向工程师通常使用ltrace、strace或frida直接跳转到重要代码处。对于客户端JavaScript,你也可以仅使用常见的浏览器功能实现相同效果。这将节省时间,让测试更有趣,并帮助你将注意力集中在值得关注的代码上。
本文介绍了我对客户端JavaScript进行插桩的思考过程和实用方法。这些过程帮助我相对轻松地在复杂代码库中发现深度嵌入的错误。我使用这些技巧已经很久,以至于我在名为Eval Villain的浏览器扩展中实现了它们。虽然我会向你介绍Eval Villain的一些全新功能,但也会展示如何在不使用Eval Villain的情况下获得相同结果。
通用方法与思考
测试应用程序常常会引发关于应用程序工作原理的问题。如果应用程序要正常运行,客户端必须知道其中一些问题的答案。考虑以下问题:
- 服务器接受哪些参数?
- 参数是如何编码/加密/序列化的?
- wasm模块如何影响DOM?
- DOM XSS接收器在哪里?正在应用什么清理措施?
- 消息处理程序在哪里?
- 广告之间的跨源通信是如何实现的?
为了使网页工作,它需要知道这些问题的答案。这意味着我们也可以在JavaScript中找到答案。注意,每个问题都暗示了特定JavaScript函数的使用。例如,客户端如何在不调用addEventListener的情况下实现消息处理程序?所以"第一步"是钩住这些有趣的函数,验证用例是否符合我们的兴趣并回溯追踪。在JavaScript中,它看起来像这样:
|
|
如果处理程序尚未注册,只需将上述代码粘贴到控制台即可工作。但是,在函数甚至被使用之前钩住它至关重要。在下一节中,我将展示一个简单实用的方法来始终赢得这场竞赛。
钩住原生JavaScript是"第一步"。这通常帮助你找到有趣的代码。有时你会想要对该代码进行插桩,但它是非原生的。这需要不同的方法,将在"第二步"部分中介绍。
第一步:钩住原生JavaScript
构建自己的扩展
虽然你可以使用许多会将任意JavaScript添加到页面的浏览器扩展之一,但我不推荐这样做。这些扩展通常有错误,存在竞争条件,并且难以开发。在大多数情况下,我发现编写自己的扩展更容易。不要被吓到,这真的很简单。你只需要两个文件,我已经在这里为你准备好了。
要在Firefox中加载代码,请在URL栏中转到about:debugging#/runtime/this-firefox,点击"加载临时附加组件",然后导航到扩展顶级目录中的manifest.json文件。
对于Chrome,转到chrome://extensions/,在右侧启用开发者模式,然后点击"加载已解压的扩展程序"。
扩展应显示在附加组件列表中,你可以在其中快速启用或禁用它。启用后,script.js文件将加载到每个网页中。以下代码行记录所有对document.write的输入:
|
|
用你想要的任何代码替换这些代码行。你的代码将在每个页面和框架中运行,在页面有机会运行自己的代码之前。
工作原理
样板代码使用清单文件注册内容脚本。清单告诉浏览器内容脚本应在每个框架中运行,并在页面加载之前运行。内容脚本无法直接访问它们加载到的页面的作用域,但它们可以直接访问DOM。因此样板代码只是将新脚本添加到页面的DOM中。CSP可能会禁止此操作,因此扩展会检查它是否工作。如果CSP确实阻止了你,只需通过浏览器配置、浏览器扩展或拦截代理禁用CSP。
请注意,插桩代码最终具有与网站相同的权限。因此你的代码将受到与页面相同的限制,例如同源策略。
异步和竞争
快速警告一下。上述内容脚本将让你首先访问唯一的JavaScript线程。网站本身在你放弃该线程之前无法运行任何JavaScript。试试看,看看你能否在样板代码钩住document.write之前运行它。
首先访问是一个巨大的优势,你可以毒化网站即将使用的环境。在完成毒化之前不要放弃你的优势。这意味着要避免使用异步函数。
这就是许多旨在将用户JavaScript注入页面的浏览器扩展有错误的原因。在浏览器扩展中检索用户配置是使用异步调用完成的。在异步查找用户配置时,页面正在运行其代码,并且可能已经执行了你想要钩住的接收器。
这就是Eval Villain仅在Firefox上可用的原因。Firefox有一个独特的API,可以使用用户配置注册内容脚本。
Eval Villain
我很少遇到Eval Villain无法解决的"第一步"情况。Eval Villain只是一个钩住接收器并在输入中搜索源的内容脚本。你可以将几乎任何原生JavaScript功能配置为接收器。源包括用户配置的字符串或正则表达式、URL参数、本地存储、cookie、URL片段和窗口名称。这些源会被递归解码以查找重要的子字符串。让我们看看上面示例的同一页面,这次使用默认配置的Eval Villain。
请注意,此页面是从本地file://加载的。源代码见下文。
|
|
即使页面没有网络请求,Eval Villain仍然成功在页面使用它之前钩住了用户配置的接收器document.write。没有竞争条件。
还要注意,Eval Villain不仅显示document.write的输入。它正确高亮了注入点。URL参数x包含一个编码字符串,命中了接收器document.write。Eval Villain通过递归解码URL参数发现了这一点。由于参数被解码,因此向用户提供了编码函数。你可以右键单击,复制消息并将其粘贴到控制台中。使用编码函数可以让你快速尝试有效负载。下图显示了使用编码函数将marquee标签注入页面。
如果你阅读了前面的部分,你就知道这一切是如何工作的。Eval Villain只是使用内容脚本将其JavaScript注入页面。它做的任何事情,你都可以在自己的内容脚本中完成。此外,你现在可以使用Eval Villain的源代码作为你的样板代码,并为其功能定制以适应你的特定技术挑战。
第一步半:快速提示
假设你使用"第一步"从有趣的原生函数获得了console.trace。也许URL参数命中了你的decodeURI接收器,现在你正在回溯到URL解析函数。在这种情况下我经常犯一个错误,我希望你做得更好。当你获得跟踪时,不要立即开始阅读代码!
现代Web应用程序通常在console.trace的顶部有polyfill和其他杂乱代码。例如,我在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这样的工具能做什么和不能做什么。必要时,当工具不足时,你也可以调整自己的代码。