一道有趣的XSS-Challenge

 

早上刷某特时推送了三上悠ya的动态,猛点双击后却发现是pwnfunction更新了一道xss-challenge的wp(上当了上当了)。看了下题目难度是hard,质量很高,考点也很有趣。官方wp的payload和解题思路看起来不是很复杂,实际上还是隐藏了很多知识点,如果大家复现这个题目,希望这篇文章能够对你有帮助。

 

题目信息

题目名称:WW
题目难度:Hard
题目地址:https://xss.pwnfunction.com/challenges/ww3/
思路:bypass DOMPurify+DOM clobbering

可控的输入的点有两个textimg

let text = new URL(location).searchParams.get('text')
let img = new URL(location).searchParams.get('img')

img作为img标签的src属性被写入,且被过滤了关键符号。

<img class="circle" src="${escape(img)}" onload="memeGen(this, notify)">

const escape = (dirty) => unescape(dirty).replace(/[<>'"=]/g, '');

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)) : ''

 

DOMpurify bypass via Jquery.html()

乍一看经过DOMPurify后的这些交互点都很安全,但是使用html()解析会存在标签逃逸问题。

题目作者在wp中提到了两种解析html的方式:jquery.html&innerhtmlinnerHTML是原生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
/<(?!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)

所以相同的语句<style><style/><script>alert(1337)//则会被解析成如下形式,成功逃逸<script>标签。

<style>
    <style>
</style>
<script>alert(1337)//

我们知道DOMPurify的工作机制是将传入的payload分配给元素的innerHtml属性,让浏览器解释它(但不执行),然后对潜在的XSS进行清理。由于DOMPurify在对其进行innerHtml处理时,script标签被当作style标签的text处理了,所以DOMPurify不会进行清洗(因为认为这是无害的payload),但在其后进入html()时,这个无害payload就能逃逸出来一个有害的script标签从而xss。

 

DOM-clobbering

第二个考点是要覆盖变量notify,只有在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-clobbering创造一个id为notify的变量,但是这种方式不允许覆盖已经存在的变量。

<html>
<img id=notify>
<img src="" onerror="memeGen(notify)">

<script>
const memeGen = (notify) =>{
    consol.log(notify);  //false
}

let notify = false;
</script>
</html>

不过我们依然可以借助标签的name属性值,为document对象创造一个变量document.notify,熟悉dom-clobbing的都很了解这种方式也常用来覆盖document的各种属性/方法。然而这道题不需要覆盖什么,我们就先把它当作一种创造变量的手段,后文再讲。我们先看简单了解一下JS的作用域

 

JS作用域&作用域链

在JS的函数中,一个变量是否可访问要看它的作用域(scope),变量的作用域有全局作用域和局部作用域(函数作用域)两种,关于详细的介绍可以移步之前博客的小记:深入Javascript-作用域&Scope Chain,这里举个最简单的例子如下

function init() {
    var inVariable = "local";
}
init();
console.log(inVariable); //Uncaught ReferenceError: inVariable is not defined

这就是因为函数内部用var声明的inVariaiable属于局部作用域范畴,在全局作用域没有声明。我们可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突

在寻找一个变量可访问性时根据作用域链来查找的,作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

而在Javascript event handler(时间处理程序)中,也就是onxx事件中(这块地盘),scope chain的调用就比较有意思了。它会先去判断当前的scope是否有局部变量notify,若不存在向上查找window.document.notify,仍不存在继续向上到全局执行环境即window.notify为止。

这样说起来可能有点绕,我们来看下面这个例子就明白了

<img src="" onerror="console.log(nickname)"> //pig
<img src="" onerror="var nickname='dog';console.log(nickname)"> //dog

<script>
window.document.nickname = 'pig';
window.nickname = 'cat';
<script>

打印的结果分别为pigdog。原因就是在第二个img标签中,onerror的上下文存在局部作用域的nickname变量,不用再向上查找了。

同时注意到题目触发memeGen函数的方式也恰好是写在event handler中—即onload内。所以污染了document.notify就相当于污染了将要传递的实参notify,这也就是为什么需要之前的dom-clobbing。

<img class="circle" src=url onload="memeGen(this, notify)">

 

思路线&题外话

dom clobbing新建一个document.notify->onload->bypass D0MPurify via html()=>XSS

另外,我们前文提到在event handler的作用域中scope chain是:局部变量->document->global。

但是在普通的局部作用域内,scope chain 没有 document这一链,而是局部作用域变量->global,示例如下

<script>
window.document.nickname = 'pig';
window.nickname = 'cat';
let nickname = 'dog';

function echoNameA(nickname){
    console.log(nickname); // dog
}

window.realname = 'me';
window.document.realname = 'hpdoger';

function echoNameB(){
    console.log(realname); //me
}

echoNameA(nickname);
echoNameB(realname);
</script>
(完)