用你的浏览器来静态分析网站源码——初级漏洞赏金猎人指南

原文链接:https://medium.com/@_bl4de/how-to-perform-the-static-analysis-of-website-source-code-with-the-browser-the-beginners-bug-d674828c8d9a

本文将教你如何用浏览器自带工具分析web应用的客户端源代码。这听上去有点怪,因为浏览器并不是做这活最好的选择。但在你开始用burpsuite抓包和到处注入alert(1)来测试XSS之前,好好研究下你的目标总是好的。

本文主要面向初级漏洞赏金猎人或者对HTML、Js代码分析缺乏经验的人,但我同样希望老鸟们能在这里有所受益。

近期我的一条介绍小技巧的推文在社区引来不少关注,于是我决定写本文:

twiiterpng

这条推文只是小技巧的冰山一角,我想与其发一堆容易错过的类似推文,不如发一篇汇总型的博文,希望对你们有用。那就开始吧!

工具集

每个现代浏览器都有一组内建工具集。要打开它们,你只需敲Ctrl+Shift+I、CMD+Option+I(macOS系统)、F12或在浏览器右边的菜单栏中找到。本文中我将使用最新版的Chromium,你们也可以使用Firefox、Safari、Chrome、Edge等,随你便啦,但我觉得Chrome的开发者工具是最强大的(该工具集集成在Chrome、Chromium、Brave、Opera和其他基于Chromium的浏览器中)。

另一样需要安装的工具是IDE(集成开发环境)或者任意带HTML、Js语法高亮的代码编辑器。随你喜好,我个人推荐Visual Studio Code,VSCode也是我日常工作中唯一使用的IDE。你可以在这里下载到https://code.visualstudio.com/

NodeJS也值得安装(并熟练使用它——英特网上有好几千的资源包),在这里下载https://nodejs.org/en/

Python解释器也是必装神器(如果你在用*nix系统,它已将预装了。如果是windows用户,还是要自己装一下)。会写Python是一个宝贵的技艺,我也建议没写过一行代码的人去学习一下。

在终端下用NodeJS测试JavaScript代码是很方便的(浏览器也能办到,我们随后讨论这一方法的优缺点)。Python是个好东西,你可以用它开发工具、写PoC和利用代码,下文将展示一些自写工具。如果你更熟悉其他解释型语言(Ruby、PHP、Perl、Bash等),你也可以使用它们。这类语言的优势在于无需编译、类似于命令行、跨平台、大量内建库和第三方模块。

分析HTML源代码

先回到我先前发的那条推特,你也许注意到了,截图里的网站看上去空白一片,但你若查看它的源代码,就会发现大量代码(没办法,我不能提供带网站URL的截图,因为这是个私人众测项目)。为什么浏览器上看不见这些元素?

重点是页面中的部分HTML标签没有渲染任何内容。最常见的是<html>,<head>,<body>,<style><script>。CSS同样能隐藏元素(例如,设置它们的宽、高属性为0或显示为none),例如:

<html>
<head>
    <title>Move along, nothing to see here!</title>
    <style>
    /* note to myself: add CSS from Bob's repo: https://verysecurecompany.com/__internal__/repo/bob/specs.git */
    * {
        font-size:16px;
        color: #c0c0c0;
    }
    </style>
</head>
<body>
    <iframe src="https://verysecurecompany.com/__internal__/loginframe.html" style="width:0;height:0" frameborder="0" id="you-cant-see-me"></iframe>
    <script>
        // a hidden feature
        console.log('Diagnostic message: username is admin and password is password :)');    
    </script>
</body>
</html>

如果你在浏览器中打开这个html页面,它不会渲染任何内容,你也不会看到任何内容。但当你查看源代码,就会发现一些有意思的东西:

在这里插入图片描述

里面有多出信息泄露:访问内部资源的URL、隐藏的包含登录表单的iframe、在控制台输出的凭据信息。这些信息不会显示在页面中,当然,别指望在每个网站里都发现这些信息,但带注解的Js代码很常见,有时它们会暴露一些app的服务端仍可用的API。

仅仅使用“View Source”功能无法揭露所有的内容,因为它只展示HTML文档。更有趣的内容通常由<iframe><script>等标签揭示,你可以在Chrome开发者工具的“Source”标签栏中看到它们。

在这里插入图片描述

左侧的目录树最底端的(index)节点就是你在“View Source”中看到的HTML主文档,其他资源保存在文件夹和文件树中。如果你点击这些文件,就会在右侧看到它们的内容。截图中是jquery.min.js文件的内容,它是所有Js文件的通用压缩版(从性能上说这么做很不错)。但如果你点击底部的“{}”小按钮,工具集会展开这些代码来方便阅读。

在这里插入图片描述

有些网站使用sourcemap特性(source map用来映射压缩版函数、变量、对象名称到它们在源代码中的真实名称及位置,更多详情)。sourcemap使用对象有意义的名称来增加格式化代码的可读性,而不是Js压缩软件产生的标识符。

另一个有用特性是全局搜索。假设你已注意到一些有趣的函数并想知道是否存在于源码中其他地方。也许它是个eval()函数参数来自url(这很可能造成Js代码执行)。为了对“Source”标签中文件做全部搜索,你可以用CTRL+Shift+F快捷键(MacOS中:CMD+Option+F)。下例中,我试着在AppMeasurement.js中查找getAccount()的所有引用。如你所见,该函数在同一文件中调用了一次。但若在其他文件中有出现搜索关键词,就会被列出:

在这里插入图片描述

有时你会发现搜索结果是一条非常长的字串(尤其是在压缩过的Js文件中)。点击字串,工具集就会打开它,点击“{}”按钮就会在右侧演示扩展版的Js版本(有时会有几千行),搜索结果也会出现。

另一个查看源代码的标签叫“Elements”。“Sources”标签的(index)文件(或“View Source”功能)与它看似相同,实则差别不小。前者展示的来自服务器的HTML文档,后者展示的当前DOM树,所有的节点有Js代码添加。为了明白这一区别,我先介绍些原理,然后展示个小示例。

DOM(文档对象模型)代表真个网页的HTML节点,它由单个根节点(<html>)和两个主要子节点(<head><body>)构成。所有其他节点都是二者的子节点,如:<title><meta><head>的子节点,<div><p><img>等是<body>的子节点。

当你在浏览器打开一条URL,首先会加载HTML文件,代码会由浏览器引擎执行。当浏览器发现<script><style>标签(或其他带有src属性的标签,如image、video),它会停止解析并加载src所指文件。如果该文件是可执行的Js,它就会被执行。如果是样式表,CSS规则就会被CSS解析器解析应用。整个过程如下图示:

在这里插入图片描述

但“Elements”和“Sources”标签又不同在哪里呢?考虑一下情形,Js添加元素到DOM中:

<html>
<head>
    <title>Dynamic P Application</title>
    <style>
    * {
        font-size:18px;
        font-weight:bold;
        color: #2e2e2e;
    }
    </style>
</head>
<body>
    <div id="container">

    </div>
    <script>
        const el = document.getElementById('container')
        const dynamic_paragraph = document.createElement('p')
        const dp_content = document.createTextNode('Hello from dynamically added <P>aragraph!')

        dynamic_paragraph.appendChild(dp_content)
        el.appendChild(dynamic_paragraph)
    </script>
</body>
</html>

当你打开这一页面并查看源码,你会看到如下的内容:

在这里插入图片描述

这是Js添加DOM新元素最简单的示例,当使用“Elements”标签时会看到:

在这里插入图片描述

当你比较“Elements”版本和“View source”版本的区别,你会轻易看到区别:前者有个可见的<p>标签,作为<div id="container">的子节点。之前的“Source”中并无该节点,因为它并不是源码的一部分。

如果你用AngularJs,React,Vue.js,Ember.js等处理过单网页应用。你会在“Elements”中看到大量动态生成的内容。这些内容各有不同,但在表单、动态分页排序列表、搜索项等大量元素中,很容易发现DOM型XSS漏洞,用户输入也常在模板表达式中解析(如:AngularJS的{{}})。

原因在于这类应用经常用GET、POST请求,或Cookies、浏览器Storage的内容来构建网页。当然,它们自己会生成大量内容。这里也容易发现一些薄弱点。

在查看JavaScript之前值得一说的是:一定要读一读HTML注解,你会发现不少没有渲染进页面的元素,里面常有意外之喜。

分析cookies和浏览器Storage

DevTools也可以用来检查网站保存在客户端的信息。有几处可被web应用利用。最常见的就是cookies——它是一段以名称定义的数据(你可以认为它的结构是键值对)并在服务器和浏览器的HTTP交互中交换。

浏览器Storage是另一处你能发现有用信息的地方。有两种Storage:本地Storage和会话Storage,区别在于会话Storage在关闭应用时(关掉浏览器标签页或浏览器时)会注销掉。本地Storage将留存很长时间,除非被手动清理(数据没有过期一说)。

想看到所有的存储信息,你只需使用工具集的“Application”功能:

在这里插入图片描述

使用“Application”功能不仅能看到内容,还能修改、删除和添加自己的键值对,这能引起意想不到的响应甚至发现漏洞。这也是检查水平越权最容易的办法,你可以替换会话token或更改cookie中保存身份信息的值来伪装成其他用户(这只是个例子,现在的web应用大多用多种方法认证用户,在多数情况下,更改一个cookie的值不足以劫持其他用户的会话):

在这里插入图片描述

这里还有另一处可以查看与web应用关联的Js源码的地方:“Service Workers”。在网上可以找到该功能更多的介绍https://developers.google.com/web/fundamentals/primers/service-workers/。该功能比较新,没有很多的web应用使用它。但它们的有些信息还是能揭示应用的工作流程,特别是下线后的行为。

分析JavaScript

现在让我们转到整个Web应用程序实际运行的这一部分代码(HTML和CSS只负责可视部分,因为它们不能包含任何业务逻辑。除了CSS表达式这类例外,它们能够运行JavaScript代码,因此可能产生XSS,特别是将用户的输入数据投入使用——仅有HTML和CSS做不了太多事。

有几种方法可以执行JavaScript代码分析。让我们先坚持使用浏览器。我们已经发布了Sources选项卡以及如何使用“{}”功能来读取未经编辑的源代码。但是你可以用DevTools做更多的事情,其中最好的就是JavaScript调试器。

使用开发者工具调试器

如果您不熟悉调试的知识,通常它可以在代码的特定特定行时暂停执行。这允许您查看实际的变量值、实际执行的函数以及此函数的调用方式(这可以通过查看堆栈获得——调试器能够显示函数调用的确切顺序,如函数a()被调用通过函数b(),而函数b()被另一个函数c()调用)。此外,调试器允许你逐步运行代码(一次只有一条指令),这样可以跟踪程序及其状态的每次变化。最后一点,调试器允许“动态”修改程序,这意味着您可以看到修改变量值、程序逻辑时会发生什么。利用好调试器是每个程序员应该具备的最重要的技能之一。

从bug赏金猎人角度看,调试可以让您更好地了解应用程序的工作原理,并直接在可以注入的位置测试攻击代码。您可以轻易地分离程序中易受攻击的部分,并专注于使用调试器的特性对其进行测试。

例如,假设您发现了有漏洞的重定向功能,但每次您尝试查看确切的内容时,此函数都会被执行并且浏览器会重定向到外部资源,并且您无法从上一页看到JavaScript,因为现在看到的是重定向后页面的源代码。在重定向功能开始时设置断点可以阻止重定向,现在您可以阅读函数源代码以了解它的工作原理,查查能否注入,编码功能写的对不对,等等。

以上都是理论,是时候进行一些练习了。

以下是重定向功能的示例实现:

<html>
<head>
    <title>Redirection</title>
</head>
<body>
    </div>
    <script>
        
        // imagine that read url from  GET parameter routine goes here...
        // but we just hardcode it for now :)
        const url = 'https://hackerone.com'
        

        function redirect() {
            // I will redirect you! Now!
            if (url) {
                location.href = url
            } else {
                location.href = 'https://company.com/__internal__/supersecretadminpanel'
            }
        }


        setTimeout( redirect, 10000 )
    </script>
</body>
</html>

当您在浏览器中打开此网站时,它会在10秒后将您重定向到HackerOne网站,您将无法再看到原网站的源代码,因此无法看到具体过程。

在浏览器中打开“开发人员工具”并切换到“Source”选项卡,然后打开上面的示例HTML文件。现在你有10秒钟在第16行设置断点( if (url) { )。要执行此操作,只需单击左侧栏上的16行号。10秒钟后,浏览器将调用redirect()并在设置断点的行停止执行:
在这里插入图片描述
蓝色那行是程序即将执行的实际行(它尚未执行!这一点非常重要)。在左侧,您可以看到调试器面板 —— 它显示您现在在哪里(调用堆栈),如果您展开脚本节点,将看到在当前执行域中定义的所有变量的值(在我们的示例中它只是url)。

现在,您可以不着急阅读和理解源代码了。就像之前所说的,我们将被重定向到HackerOne网站,但前提是第16行中的条件将返回true

让我们修改url的值。将把它改为在JavaScript中返回false以使条件失败的东西(它可以是空字符串,0,布尔值false或任何错误表达式)。这里选择输入false( 要想修改变量的值,只需单击它并输入自己的值):

在这里插入图片描述

现在来看看有什么变化。为了更进一步,看一下调试器面板顶部的图标:
在这里插入图片描述

第一个图标将继续运行程序,第二个图标允许逐步执行代码(我们将在一段时间内使用此代码)。下一个图标允许您跳转到特定行中调用的函数(如果没有设置断点,调试器自己不会进入函数,它只是执行有函数调用那行并移动到下一行),然后有一个图标从当前执行函数中“跳出”并返回到调用它的位置。

现在,单击第二个图标(确保已将url变量的值更改为false),您将注意到要执行的下一行是19行(这是由第16行中的错误条件结果引起的):
在这里插入图片描述
如果您按下调试器工具栏上的第一个图标(“播放”图标),您会注意到这次应用程序将尝试重定向到company.com中的某个内部URL。

这种情况url参数很可能存在漏洞,因此您现在可以尝试查找开放重定向、反射性 XSS,或者你怀疑url的值会被存储在服务器端的某个位置从而深入挖掘(您可以通过进一步研究程序逻辑来确定它是否属实)。

使用Snippets执行JavaScript

有时,您可能只想执行应用程序代码的特定片段。当你想做些工作来找到有趣的点时,通常会很难并且耗时很多。在这种情况下,您可以使用Snippets并仅运行您想要的代码。但请记住,这并不是万能的,例如当您尝试运行具有某些依赖性的代码的一小段时,例如从其他部分传递的变量,或者当前代码片段需要使用其他文件中定义的函数时。

但是,假设您已经确定了一个函数,该函数用以检查值是否正确,并且您只想关注其逻辑。

在“Source”选项卡中,您可以找到名为Snippets的面板:

在这里插入图片描述

当您单击它时,您将看到一个片段列表(如果您已经创建了任何片段)以及创建新片段的选项。单击此选项,在中间面板中,您将找到一个带语法高亮的简单代码编辑器。您放在那里的每个JavaScript代码都可以运行,结果会立即打印在控制台中,只要您运行代码段就会出现在下面,您可以使用此面板左下角的“播放”图标,在macOS按CMD+Enter,其他系统输入CTRL+其他按键):

您可以根据需要修改代码并运行它,但正如我在本文的开头部分所提到的,这里有一些缺点。

Snippets运行在选项卡所加载页面的上下文中,您可以在其中打开DevTools并创建和运行您的代码段。 此外,每次运行代码段时,都会使用相同的上下文运行,也就是之前定义的所有变量都没有变化,并保留着它们的最后一个值。

这里为什么是重点?

考虑这个例子:
在这里插入图片描述
在JavaScript中,当使用const关键字定义常量时,必须对它赋值初始化,并且以后不能更改其值。如你所见,代码段按预期运行,但如果您现在想更改SOME_CONST并重新运行代码段的值,则会爆出语法错误:
450d9e8aad47e42b1ac0eeee36b46ff2.png
此错误是因为在此上下文中,SOME_CONST已被初始化。DevTools“认为”你还在同一个执行环境中执行代码,你有没有改过代码不重要。

因此,如果您用调试器暂停程序(应用程序代码定义的所有变量、对象和函数现在都将存在于执行上下文中)并尝试在同一选项卡中创建代码段,如果你使用现有标识符——你会覆盖原始Web应用程序代码的变量值;如果您想初始化无法重新初始化的标识符(如常量)时,会被报错。为了能够重新运行代码片段,首先需要在浏览器中重新加载页面以提供全新的空执行空间(浏览不缓存重新加载后Web应用程序的状态,因为重新加载会引起整个资源的加载、构建DOM树等)。

为避免此类问题,您可以使用NodeJS环境运行代码,而不是使用Snippets(但它仍然是非常方便的工具,因为您只需使用DevTools打开新选项卡并在其中创建代码段)。

要执行此操作,只需将要运行的代码放在新的JavaScript文件中,然后在终端中运行NodeJS(确保已经安装有它):
9f85b842f9164cd32f2fb932756df73c.png
如图示,我已运行了三次代码,每次都更改SOME_CONST的值。如您所见,没有错误,每次执行都成功并打印出正确的结果。

这种现象是由NodeJS的特性引起的—— 每次用它运行JavaScript代码时,它都会创建新的执行上下文,因此不可能在相同的执行上下文中运行相同的代码两次。

源头和执行池(原文:Sources and execution sinks)

当您查看JavaScript代码时,首先应该主要关注两方面内容。

第一个是源头,这个术语描述了用户提供的输入映射到应用程序代码的每个点。这可以是通过URL中GET传递的参数,应用程序读取的cookie或业务逻辑中使用的本地Storage的内容。

第二个称为执行池。此术语描述的所有会将JavaScript语法元素或HTML API功能作为传递的参数当做代码去执行的地方。这里一个明显的例子是JavaScript函数eval(code_to_evaluate),它以代码的方式去评估(执行)传入参数。另一个例子是setTimeout(function_to_execute,timeout_in_miliseconds),第一个参数传递函数要执行的函数,第二个参数传递时间,在时间到期后开始执行函数。

在Web应用程序中发现漏洞的过程就是寻找源头和执行池之间的联系,这里指真正处理源头的过程。在我介绍如何使用调试器的示例中,作为参数(源头)传递的url直接在location.href(执行池)中使用。这里的另一个例子,是一个会评估用户在HTML表单输入字段中填入的数据的 函数(JavaScript可以使用DOM API读取它,例如document.getElementById('input_id').value并将其值赋给变量 ——这就是此时的源头)然后将此值传递给另一个元素innerHTML()函数,在浏览器窗口中更新实际DOM(那将是一个执行池)。

@LiveOverflow在他的YouTube频道上提供了关于此主题的精彩视频。我建议现在停止阅读这篇文章,然后去看这个视频,熟悉这个概念(长度约为8分钟)

Youtube_LiveOverflow

由于其业务逻辑的不同,Web应用程序中存在许多的源头和执行池(比如说表单字段、URL参数、cookie、浏览器存储、WebSockets等)。但最重要的是它们真正被用在执行池里。有很多这样的执行池,包括像hrefhash这样的位置属性,window.open()document.write()或者像innerHTMLappendChild这样的DOM方法。它们都可用于执行任意代码,重定向或进行其他类型的注入。

为了便于识别这样的代码模式,我写了一个工具,nodestructor。它只是检查JavaScript文件(单个文件或目录参数中所有的JavaScript文件)并根据模式匹配广为人知的执行池(或源头)。别指望nodestructor标识的每一行代码都能被立即或轻松地利用—— 所有这些都取决于源头到执行池之间的过程(数据清洗,编码,解析,将数据转换为对象,字符串操作等)。此工具的主要目的是提供一种在大型代码库中查找此类模式的好用且快速的方法。

让我向您展示一个快速使用示例。首先,我们需要一个JavaScript文件来检查。我要检查的文件是之前看过的GM.com网站上的AppMeasurement.js。我从浏览器中复制了这个文件(首先将它扩展)并粘贴到代码编辑器中并在本地保存在/tmp文件夹中。

在终端中,我在AppMeasurement.js文件上运行nodestructor(使用-H选项包括搜索各种HTML5 API模式):

c3e34ab658eb29dae636e6854fe184ff.png

您可能会注意到,该工具确定了几个可能的执行池。其中大多数是误报,但为了演示目的,让我们看看第二项,它写的是用location.hostname.toLowerCase()函数结果对domain变量初始化。

最好能在全文中跟踪此变量出现的地方以判断是否会在后面的执行池中被用到。你可以使用Visual Studio Code内置的功能,例如查找所有引用或直接搜索字符串domain

虽然前段时间我刚开始使用自己的工具来做这种事—— 对JavaScript文件进行简单的静态代码分析,但这里我想多讲一点。它目前还没有个好听的名字:),现在仍然处于开发的早期阶段(说实话,它更像是PoC,而不是实际能用的工具…),但是为了给你展示个大概,我将针对domain变量运行这个工具(这是迄今为止唯一可用的选项,但正如我所提到的,这只是这个工具的十分早期的功能,所以如果要输入的文件名是被硬编码的进去的:P)

de0c4276b6d6c6bb8dc09ddf5a7d07e6.png

如您所见,工具找到了变量定义的位置以及何时何地被使用。我希望这个工具能做更复杂的分析,比如在不同作用域内查找变量的用法(例如,它作为参数传递给某个函数,或者它是否真正被用作某个执行池的参数)。

总结

Web浏览器是一个非常强大的工具。有时,它是您阅读源代码并完全理解应用程序工作流程,识别应用漏洞,进行一些测试或只是查看其工作原理以及学习新内容所需的唯一工具

我希望我的帖子能帮助您了解如何使用开发人员工具这一非常强大的功能。您可以在此网址找到有关充分发挥其全部功能的资源。
https://developers.google.com/web/tools/chrome-devtools/

如果您有任何问题或只是吐槽我的帖子很糟糕:D—— 请不要犹豫,在Twitter上与我联系

谢谢你的阅读,我希望你能找到很多很棒的bug ?

黑得愉快!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章