JavaScript 文件分析与漏洞挖掘指南

5 阅读11分钟

前言

Javascript (.js) 文件一般存储的是客户端代码,Javascript 文件可帮助网站执行某些功能,例如监视单击某个按钮的时间,或者当用户将鼠标移到图像上,甚至代表用户发出请求(例如检索信息)时。有时开发人员可以混淆他们的 javascript 代码,使人无法正常阅读,但是大多数类型的混淆是可以反混淆的;如果你是一个漏洞赏金猎人,你应该花一些时间从中寻找宝藏。如果运气好, 可能会发现严重程度较高的漏洞。


怎么找 JS

1. 手动查找

右键单击所在的页面并单击“查看源代码”,然后开始在 HTML 中查找 .js。如果是手动,那么这是首选方法,因为您会注意到某些 .js 文件仅包含你所在的特定端点的代码,它可能包含你不知道的存在的新的 api 接口。

2. 使用 Burp Suite 专业版

如果使用的是 Burp Suite 专业版,在 Target > sitemap 下,右键点击感兴趣的站点,选择 Engagement tools > Find scripts。使用此特性,你可以导出该应用程序中的所有脚本内容。

3. 使用 waybackurls

这是由英国著名白帽子 tomnomnom 开发的一个工具:github.com/tomnomnom/w…

这个工具的主要逻辑是让你在 archive.org/web/ (这个网站可以理解为数字图书馆, 你可以查找到相对应网站的所有历史网页) 这个网站里面去查找某个目标网站的所有历史数据。

使用方法:

webbackurls target.com | grep "\.js" | uniq | sort

验证 JS 文件

使用 Wayback Machine 可能会导致误报,所以,在收集了 JavaScript 文件的 url 列表之后,我们需要检查这个 js 文件是否真的还存在。

使用 curl 快速检查:

cat js_files_url_list.txt | parallel -j50 -q curl -w 'Status:%{http_code}\t Size:%{size_download}\t %{url_effective}\n' -o /dev/null -sk

使用 hakcheckurl:

# 安装
go install github.com/hakluke/hakcheckurl@latest

# 使用
cat lyftgalactic-js-urls.txt | hakcheckurl

其它相关工具

当然还有其他的一些工具, 这里简单列一下他们的链接, 有兴趣的读者可以自行去研究研究:


JS 美化

大多数时候,我们收集的 JavaScript 文件都是经过压缩、混淆的。

其他的相关工具如下:


JS 文件中需要去找的关键信息

1. JS 危险函数

  1. 将字符串当做代码去执行的三个函数

    eval("alert(1)")
    
    setTimeout("alert(1)",1000)
    
    // 就是下面这么写
    Function("alert(1)")()
    
  2. innerHTML 函数 如果你发现了这个函数,这意味着如果没有进行适当的处理,XSS 漏洞可能在向你招手, 即使经过了处理,试着看能不能绕过。

    • React 中就有一个和 innerHTML 差不多的函数叫做 dangerouslytSetInnerHTML,这个函数也是咱们的重点关注对象。
    • 还有 Angular 中的 bypassSecurityTrustX,还有咱们熟悉的 eval 函数。
  3. Postmessage 函数 最好先去看看它的官方文档。一旦了解了与 postMessage 相关的可能的安全问题,就可以在 JavaScript 文件中查找实现。在消息发送方,寻找 window.postMessage 并在接收方寻找监听器 window.addEventListener

    // Postmessage 有如下几种发送与监听的方式
    
    // 1. 从父窗口往子窗口发送消息
    // 父窗口
    <body>
        <h1>父窗口</h1>
        <iframe id="childFrame" src="getMessage.html" frameborder="0"></iframe> // 目标子窗口
        <button>发送</button> // 点击即可触发发送
        <script>
            const sendPostmessage = document.querySelector('button');
            sendPostmessage.addEventListener('click', function () {
                const iframe = document.getElementById('childFrame');
                iframe.contentWindow.postMessage('<img src=x onerror="alert(1)">', '*'); // 通过iframe往iframe引入的子窗口发送postmessage消息
            });
        </script>
    </body>
    // 子窗口
    <body>
        <h1>子窗口</h1>
        <script>
            window.addEventListener('message', function (event) {
                document.querySelector('h1').innerHTML = event.data; // 通过在window中注册message事件监听postmessage消息,当有postmessage消息往这个子窗口发送消息时,触发事件,执行回调函数,将发送的消息写入到h1标签。如果开发者没有对接收消息做过滤,且用innerHTML等危险的DOM操作方法接收了数据,则有可能造成XSS漏洞
            });
        </script>
    </body>
    
    // 2. 子窗口向父窗口发送消息
    // 父窗口
    <body>
        <h1>父窗口</h1>
        <iframe id="childFrame" src="getMessage.html" frameborder="0"></iframe> // 子窗口
        <script>
            window.addEventListener('message', function(event) { // 开启message监听
                if (event.origin !== window.location.origin) return; // 来源验证,event.origin检查接收到的数据的原始url,window.location.origin获取当前窗口的原始url,event.origin !== window.location.origin的意思便是如果来源不是同源的url,则不接收数据
                document.querySelector('h1').innerHTML = event.data;
            });
        </script>
    </body>
    // 子窗口
    <body>
        <h1>子窗口</h1>
        <script>
            window.parent.postMessage('<img src=x onerror="alert(1)">', "*"); // iframe加载后自动发送数据,如果父窗口没有做好来源校验,则很有可能会造成相关漏洞
        </script>
    </body>
    
    // 3. 父窗口向弹窗发送消息与接收消息
    // 父窗口
    <body>
        <h1>父窗口</h1>
        <button id="openPopup">打开弹窗</button>
        <button id="sendPopup">向弹窗发送消息</button>
        <script>
            let popup;
            function openPopup() { // 打开弹窗
                popup = window.open('getMessage.html', 'popup', 'width=300,height=200');
            }
            function sendToPopup() { // 向弹窗发送消息
                if (popup) {
                    popup.postMessage('<img src=x onerror="alert(1)">', '*');
                }
            }
            let openpopup = document.querySelector('#openPopup');
            let sendpopup = document.querySelector('#openPopup');
            openpopup.addEventListener('click', openPopup);
            sendpopup.addEventListener('click', sendToPopup);
            // 接收从弹窗来的消息
            window.onmessage = function(event) {
                if (event.origin !== window.location.origin) return;
                document.querySelector('h1').innerHTML = event.data;
            };
        </script>
    </body>
    // 弹窗
    <body>
        <h1>弹窗</h1>
        <button>Send to Opener</button>
        <script>
            function sendToOpener() {
                // 通过opener向父窗口发送消息
                window.opener.postMessage('<img src=x onerror="alert(1)">', '*');
            }
            const sendopener = document.querySelector('button');
            sendopener.addEventListener('click', sendToOpener);
            // 接收从opener来的消息,即父窗口向弹窗发送的消息
            window.addEventListener('message', function(event) {
                document.querySelector('h1').innerHTML = event.data;
            });
        </script>
    </body>
    
    // 4. 兄弟iframe间通信
    // 父窗口
    <body>
        <h1>父窗口</h1>
        <iframe id="iframe1" src="sibling1.html" frameborder="0"></iframe>
        <iframe id="iframe2" src="sibling2.html" frameborder="0"></iframe>
        <script>
            const iframe1 = document.querySelector('#iframe1');
            const iframe2 = document.querySelector('#iframe2');
            // 接收来自iframe 的消息,并转发到iframe2
            window.addEventListener('message', function(event) {
                if (event.origin !== window.location.origin) return;
                if (event.data.type === 'toSibling') {
                    iframe2.contentWindow.postMessage(event.data.message, '*');
                } else if (event.data.type === 'toSibling1') {
                    iframe1.contentWindow.postMessage(event.data.message, '*');
                }
            });
        </script>
    </body>
    // 子窗口1
    <body>
        <h1>子窗口1</h1>
        <button>发送到子窗口2</button>
        <script>
            function sendToSibling() {
                // 发送到父窗口,中转到兄弟
                window.parent.postMessage({ type: 'toSibling', message: '<img src=x onerror="alert(1)">' }, '*');
            }
            const btn = document.querySelector('button');
            btn.addEventListener('click', sendToSibling);
            // 接收回复
            window.addEventListener('message', function(event) {
                console.log('Sibling 1 received:', event.data);
            });
        </script>
    </body>
    // 子窗口2
    <body>
        <h1>子窗口2</h1>
        <script>
            // 接收从中转来的消息
            window.addEventListener('message', function(event) {
                if (event.origin !== window.location.origin) return;
                document.querySelector('h1').innerHTML = event.data;
            });
            // 回复回兄弟1
            window.parent.postMessage({ type: 'toSibling1', message: 'Hi Hacker!' }, '*');
        </script>
    </body>
    
    // postmessage存在的漏洞
    
    1. 第一个漏洞在于“postMessage”函数的第二个参数。此参数指定允许哪个源接收消息。使用通配符“*”表示允许任何源接收消息。由于目标窗口位于不同的源,因此发送方窗口在发送消息时无法知道目标窗口是否在目标源。如果目标窗口是另一个源,则另一个源将接收数据。如果发送的数据是敏感信息,则代表着任何网页都有可能通过iframe引入来接收postmessage发送的数据。
    
    2. 第二个漏洞在于接收端。由于侦听器侦听任何消息,攻击者可以通过从攻击者的源发送消息来欺骗应用程序,这将使接收方认为它从发送方的窗口接收了消息。为避免这种情况,接收方必须使用“message.origin”属性验证消息的来源。如使用正则表达式来验证源。
    window.addEventListener("message", function(message){
        if(/^http://www.examplesender.com$/.test(message.origin)){
             console.log(message.data);
       }
    });
    显然,这个正则表达式有缺陷,转义“.”字符很重要,这段代码不仅允许来自“www.examplesender.com”的消息,还允许“wwwaexamplesender.com”、“wwwbexamplesender.com”等消息。
    
    3. 第三个漏洞是DOM XSS,它以应用程序将其视为HTML脚本的方式使用消息,例如:
    window.addEventListener("message", function(message){
       if(/^http://www.examplesender.com$/.test(message.origin)){
          document.getElementById("message").innerHTML = message.data;
       }
    });
    如果发送方发送的数据是<img src=x onerror="alert(1)">,这将触发XSS
    
    // 没有postmessage?
    就算应用程序本身不使用postmessage,但许多第三方脚本使用postMessage与第三方服务通信,因此应用程序可能会在开发者不知情的情况下使用postMessage。我们可以使用Chrome DevtoolsSources -> Global Listeners下检查页面是否有已注册的消息监听器(以及哪个脚本注册了它)
    
  4. String.prototype.search() 一些开发人员使用它来查找一个字符串在另一个字符串中的出现。然而,”.” 在此函数中被视为通配符。

    // String.prototype.search()是javascript中字符串的内置方法,如
    // let i = "hello world!".search("hello");
    // 从javascript中查找子串是否存在,不存在则返回-1,一些开发人员使用它来查找一个字符串在另一个字符串中的出现,而有的在进行安全验证时使用,从而造成了验证的绕过
    if ("https://www.baidu.com".search(target.origin) !== -1) {
        eval(target.data);
    }
    // 根据MDN,search的参数是一个正则表达式对象,如果参数不是RegExp对象,并且不具有Symbol.search方法,则会使用new RegExp(regexp)将其隐式转换为RegExp。在正则表达式中,点(.)被视为通配符。换句话说,任何数字起源的字符都可以替换为点。攻击者可以利用它并使用特殊域而不是官方域来绕过验证,例如 www.bai.u.com
    
  5. location 相关的几个函数

  6. document.cookie

  7. window.name

    window.name是浏览器窗口的一个字符串属性,用于标识或存储窗口的“名称”。它本质上是一个持久化存储机制,类似于sessionStorage,但更简单且跨页面持久。只要窗口不关闭、不刷新,window.name就保持不变。即使导航到新URL,它也不会丢失。如果用window.open打开新窗口,它会继承opener的name。有这几种访问方式:当前窗口window.name,父窗口top.name,iframe中parent.name。
    
    比如有一个页面存在XSS,但利用需要写很多javascript代码,同时该注入点有字符限制。攻击者便可以首先编写钓鱼页:
    var payload = btoa('完整 exploit 代码');
    window.open('https://site.com/users/attacker', payload); // 打开存在XSS的页面
    接着存在XSS的页面使用
    eval(atob(window.name))
    atob对payload进行base64解码,eval执行解码后的JS
    
  8. localStorage 以及 sessionStorage

2. 过时的依赖和框架

从 JS 代码中查找过时的依赖项,可以直接使用 retire.js 来扫描漏洞。可以在以下链接中找到该项目:

3. 敏感信息

有时,开发人员会在客户端 JS 代码中留下大量信息,例如密码、API 密钥等硬编码。从 JS 代码中找到这些信息。

例如,AWS 密钥正则表达式可能如下所示:

(?i)aws(.{0,20})?(?-i)['\”][0–9a-zA-Z\/+]{40}['\”]

更多参考工具:

相关的漏洞报告:

4. 隐蔽的接口

有时,不是对接口进行模糊测试,而是从 Javascript 文件中查找隐蔽接口会更有效。这些接口可能是一些废弃的服务(开发人员有时忘记删除它)或用户不应该访问的服务(如隐蔽的 API)。如果你找到了,这些隐蔽接口通常比主要的 Web 应用程序更容易受到攻击。

有一些很方便的工具帮助我们从 js 文件中提取这些接口:

5. 开发人员的注释

开发人员注释(例如 // 这是一个开发评论/* 这是一个多行开发评论 */)有时可以包含诸如代码何时发布或发生的任何更新之类的信息(有时候还会注释关于 XSS 过滤,这有助于我了解他们如何修复它并去绕过)。如果代码是旧版本的,那么你发现问题的机会就更大。

6. js.map 文件

还有一个比较特殊的文件, 是以 js.map 为后缀的文件, 非常多 Webpack 打包的站点都会存在 js.map 文件。通过还原前端代码找到 API, 间接性获取未授权访问漏洞。

简单说,Source map 就是一个信息文件,里面储存着位置信息。转换后的代码的每一个位置,所对应的转换前的位置。有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码, 这无疑给开发者带来了很大方便。

相关的处理工具有 (可以使用它们将代码还原):


在 React.js 上读取 .js 文件的示例

在查看一个 React.js 的网站的时候, 发现一个 settings.js 文件: app.js 是我们的重点。我们要找什么?新的端点、参数,也许还有 api key。打开它时,我们会看到似乎是“乱七八糟”的东西。

第一步:美化。 美化会将我们的代码变成可读的代码。复制代码内容并使用 Javascript 美化器,例如:beautifier.io/

第二步:搜索关键字。 现在我们的代码可读了,让我们开始寻找新的端点、参数,也许还有 api key。通过搜索关键字 pathname,我能够找到网站上使用的所有端点,这些端点扩展了攻击面以查找错误,并开始查找有关其 API 的信息。

要查找的其他常见关键字:

  • url:
  • POST
  • api
  • GET
  • setRequestHeader
  • send( (注意: 只有一个 (,因为它在发出 Ajax 请求时使用!)
  • headers
  • onreadystatechange
  • var {xyz} =
  • getParameter()
  • parameter
  • .theirdomain.com
  • apiKey
  • postMessage
  • messageListenger
  • .innerHTML
  • document.write(
  • document.cookie
  • location.href
  • redirectUrl
  • window.hash

自动化读取工具

如果是自动读取的话,可以利用上面的三个工具:

JS 混淆处理

如果 js 被混淆了,可以使用下面的工具:

大多数类型的混淆都可以解决。