WebSocket通信协议基础原理与潜在安全威胁

Posted Tr0e

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WebSocket通信协议基础原理与潜在安全威胁相关的知识,希望对你有一定的参考价值。

文章目录

前言

最近在项目上遇到了一个 Web 系统和本地 PC 的 exe 客户端程序采用了 WebSocket 通信,exe 客户端程序做了登录、鉴权机制并采用 HTTPS 协议与后台微服务进行通信。由于原来没有了解过 WebSocket,所以一开始上来也比较懵,接着学习了下 WebSocket 的原理和攻击模式,竟然发现一个严重漏洞——该 Web 系统虽然也做了登录控制,但是与 exe 程序之间的 WebSocket 却没做鉴权机制……这直接导致攻击者可以绕过 Web 系统和 exe 程序本身的鉴权机制,直接通过 Web 系统 WebScoket 的通信方式来实现对后台业务服务器资源数据的未授权访问。

本文来记录学习下 WebSocket 全双工通信的基本原理与鉴权机制,并分析 WebSocket 常见的安全风险。

WebSocket

WebSocket 和 HTTP 一样,也是一种通讯协议,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

协议基础

有很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出 HTTP 请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而 HTTP 请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

WebSocket 协议是从 html5 开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。它能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

WebSocket 是真正意义上的全双工模式,也就是我们俗称的「长连接」。当完成握手连接后,客户端和服务端均可以主动的发起请求,回复响应,并且两边的传输都是相互独立的。

以一个通俗的场景来理解下 HTTP 协议与 WebSocket 协议的差异:

(1)HTTP 小场景(模拟 ajax 轮询)

  • 客户端:啦啦啦,有没有新信息(Request)
  • 服务端:没有(Response)
  • 客户端:啦啦啦,有没有新信息(Request)
  • 服务端:没有。。(Response)
  • 客户端:啦啦啦,有没有新信息(Request)
  • 服务端:你好烦啊,没有啊。。(Response)

(2)WebSocket 小场景(模拟全双工)

  • 客户端:啦啦啦,有没有新信息(Request)
  • 服务端:额。。没有(Response)
  • 客户端:啦啦啦,有没有新信息(Request)
  • 服务端:你个烦人精,有消息的时候我会主动发给你的(Response)
  • 服务端:烦人精,你要的信息来了(Response)……

建立连接

WebSocket 的数据传输,是基于 TCP 协议,但是在传输之前,还有一个握手的过程,双方确认过眼神,才能够正式的传输数据。WebSocket 的握手过程,符合其 “Web” 的特性,是利用 HTTP 本身的 “协议升级” 来实现。

在建立连接前,客户端还需要知道服务端的地址,WebSocket 并没有另辟蹊径,而是沿用了 HTTP 的 URL 格式,但协议标识符变成了 “ws” 或者 “wss”,分别表示明文和加密的 WebSocket 协议,这一点和 HTTP 与 HTTPS 的关系类似。

以下是一些 WebSocket 的 URL 例子:

ws://cxmydev.com/some/path
ws://cxmydev.com:8080/some/path
wss://cxmydev.com:443?uid=xxx

为了创建 Websocket 连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”。

实现步骤:

1、发起请求的浏览器端,发出协商报文:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

具体字段含义:

里面的核心字段:

  • Connection: Upgrade 以及 Upgrade: websocket 这个就是告诉服务器,下一步我要对协议进行升级了,升级到 WebSocket;
  • Sec-WebSocket-Key 是由浏览器随机生成的字符串的 Base64 编码,提供基本的防护,防止恶意或者无意的连接。

2、服务器端响应 101 状态码(即切换到socket通讯方式),其报文:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

一行行来解释上面服务端的响应的含义:

  • 首先,101 状态码表示服务器已经理解了客户端的请求,并将通过 Upgrade 消息头通知客户端采用不同的协议来完成这个请求;
  • 然后,Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key;
  • 最后,Sec-WebSocket-Protocol 则是表示最终使用的协议。

其中 Sec-WebSocket-Accept 的计算方法:

  • 将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;
  • 通过 SHA1 计算出摘要,并转成 base64 字符串,伪代码为:toBase64(sha1(Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ))

注意:Sec-WebSocket-Key/Sec-WebSocket-Accept 的换算,只能带来基本的保障,但连接是否安全、数据是否安全、客户端 / 服务端是否合法的 ws 客户端、ws 服务端,其实并没有实际性的保证

一旦服务器端返回 101 响应,即可完成 WebSocket 协议切换。服务器端可以基于相同端口,将通信协议从 http:// 或 https:// 切换到 ws://或 wss://。协议切换完成后,浏览器和服务器端可以使用 WebSocket API 互相发送和收取文本和二进制消息。

而在连接建立后,WebSocket 采用二进制帧的形式传输数据,其中常用的包括用于数据传输的数据帧 MESSAGE 以及 3 个控制帧:

  • PING:主动保活的 PING 帧;
  • PONG:收到 PING 帧后回复;
  • CLOSE:主动关闭 WebSocket 连接。

示例程序

下面使用 Python 来编写 WebSocket 通信的示例程序。Python websockets是用于在 Python 中构建 WebSocket 服务器和客户端的库,它基于 asyncio 异步 IO 建立,提供基于协程的 API。

1、服务端 Server.py

用于构建 websocket 服务器,在本地 8765 端口启动,会将接收到的消息加上 I got your message: 返回回去。

import asyncio
import websockets


async def echo(websocket, path):
    async for message in websocket:
        message = "I got your message: ".format(message)
        await websocket.send(message)

if __name__ == '__main__':
    asyncio.get_event_loop().run_until_complete(websockets.serve(echo, 'localhost', 8765))
    asyncio.get_event_loop().run_forever()

2、客户端Client.py

跟指定 url 建立 websocket 连接,并发送消息,然后等待接收消息,并将消息打印出来。

import asyncio
import websockets


async def hello(uri):
    async with websockets.connect(uri) as websocket:
        await websocket.send("Hello world!")
        recv_text = await websocket.recv()
        print(recv_text)


if __name__ == '__main__':
    asyncio.get_event_loop().run_until_complete(hello('ws://localhost:8765'))

先执行 Server.py,再执行 Client.py,客户端的输出结果如下:

3、服务端主动发送消息

上面的示例未实现服务端主动给客户端发送消息的全双工通信,下面来完善下代码,当建立连接之后,客户端可以随时接收服务器发来的消息。服务器可以依据逻辑,给客户端推送指定消息。服务器和客户端代码会有一点变化,在服务器回完第一条消息之后,开始轮询时间,当秒数达到0的时候,会主动给客户端回一条消息。

Server.py:

import asyncio
import websockets
import time


async def echo(websocket, path):
    async for message in websocket:
        message = "I got your message: ".format(message)
        await websocket.send(message)
        while True:
            t = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
            if str(t).endswith("0"):
                await websocket.send(t)
                break

if __name__ == '__main__':
    asyncio.get_event_loop().run_until_complete(websockets.serve(echo, 'localhost', 8765))
    asyncio.get_event_loop().run_forever()

Client.py:

import asyncio
import websockets


async def hello(uri):
    async with websockets.connect(uri) as websocket:
        await websocket.send("Hello world!")
        print("< Hello world!")
        while True:
            recv_text = await websocket.recv()
            print("> ".format(recv_text))

if __name__ == '__main__':
    asyncio.get_event_loop().run_until_complete(hello('ws://localhost:8765'))

先执行 Server.py,再执行 Client.py,客户端的输出结果如下:

最后一条消息则是服务端主动给客户端发送的。

Burp抓包

1、在浏览器上使用 WebSocket

如何在前端发送 Websocket 请求呢?看这段代码 Client.html,先建立连接,然后向服务端发送 Hello world,然后把接收到的所有消息依次展示出来。

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <title>websocket通信客户端</title>
    <script src="jquery-3.5.0.min.js"></script>
    <script type="text/javascript">
        function WebSocketTest() 
            if ("WebSocket" in window) 
                // 打开一个 web socket
                var ws = new WebSocket("ws://127.0.0.1:8765");
                // 连接建立后的回调函数
                ws.onopen = function () 
                    // Web Socket 已连接上,使用 send() 方法发送数据
                    ws.send("Hello world!");
                    $("#main").append("<p>" + "<=" + "Hello world!" + "</p>")
                ;

                // 接收到服务器消息后的回调函数
                ws.onmessage = function (evt) 
                    var received_msg = evt.data;
                    if (received_msg.indexOf("sorry") == -1) 
                        $("#main").append("<p>" + "=>" + received_msg + "</p>")
                    
                ;
                // 连接关闭后的回调函数
                ws.onclose = function () 
                    // 关闭 websocket
                    alert("连接已关闭...");
                ;
             else 
                // 浏览器不支持 WebSocket
                alert("您的浏览器不支持 WebSocket!");
            
        
    </script>
</head>
<body onload="WebSocketTest()">
<div id="main">
</div>
</body>
</html>

点击 PyCharm 提供的如下按钮在浏览器打开上述 HTML 文件:

程序运行效果如下图:

2、BurpSuite 观察 WebSocket 报文

这上面的示例程序运行过程中,使用 BurpSuite 抓包观察,可以在 HTTP history 看到建立 WebSocket 的握手过程:


同时可以在 WebSocket history 看到客户端与服务端的通信过程:


可以将第一个会话 Hello World 发送到 Repeater 进行重放,效果如下:

同时上述重放会触发浏览器也同步更新:

安全威胁

上文讨论完 WebSocket 的基本概念和用法后,下面来讨论下 WebSocket 所面临的安全威胁。

鉴权缺失

从上面的示例程序中,读者应该发现了上述程序的服务端和客户端并未存在鉴权机制,谁都可以向服务端发起连接,如果服务端提供的接口包含敏感数据或业务功能,那么后果可想而知……

WebSocket 协议没有规定服务器在握手阶段应该如何认证客户端身份。服务器可以采用任何 HTTP 服务器的客户端身份认证机制,如 cookie 认证,HTTP 基础认证,TLS 身份认证等。在 WebSocket 应用认证实现上面临的安全问题和传统的 Web 应用认证是相同的,如:

  1. CVE-2015-0201:Spring 框架的 Java SockJS 客户端生成可预测的会话ID,攻击者可利用该漏洞向其他会话发送消息;;
  2. CVE-2015-1482:Ansible Tower 未对用户身份进行认证,远程攻击者通过 Websocket 连接获取敏感信息。

所谓鉴权,其实就是为了安全考虑,避免服务端启动 WebSocket 的连接服务后,任谁都可以连接,这肯定会引发一些安全问题。其次,服务端还需要将 WebSocket 的连接实体与一个真实的用户对应起来,否则业务就无法保证了。

前文提到,WebSocket 在握手阶段,使用的是 HTTP 的 “协议升级”,它本质上还是 HTTP 的报文头发送一些特殊的头数据,来完成协议升级。那么实际我们在 WebSocket 握手阶段,也可以通过 Header 传输一些鉴权的数据,例如 uid、token 之类,具体方法:

  1. 方案A:在握手阶段返回 Response 响应包的时候,为其 Header 增加鉴权字段传递给客户端 ;
  2. 方案B:部分业务功能集成了 WebSocket 协议的 Web 系统已单独有授权机制并已颁发 Token,那么在触发 WeSocket 业务功能时携带该 Token 进行后台校验。

随后的 WebSocket 通信的的 URL 需要携带鉴权参数来防着未授权访问,比如如下的鉴权参数 token:

wss://example.com?uid=xxx&token=xxx

与此同时,同鉴权缺失的风险一样,WebSocket 协议没有指定任何授权方式,应用程序中用户资源访问等的授权策略由服务端或开发者实现。WebSocket 应用也会存在和传统 Web 应用相同的安全风险,如:垂直权限提升和水平权限提升

最后附上一个用于建立 WSS 协议链接且携带 JSON 格式数据的攻击测试脚本:

import asyncio
import websockets
import json

msg = 
    "method": "SUBSCRIBE",
    "id": 1,
    "params": ["!bookTicker"]



async def call_api(msg):
    async with websockets.connect('wss://127.0.0.1:8765/ws') as websocket:
        await websocket.send(msg)
        while websocket.open:
            response = await websocket.recv()
            # do something with the response...
            print(response)


if __name__ == '__main__':
    asyncio.get_event_loop().run_until_complete(call_api(json.dumps(msg)))

劫持漏洞

WebSocket 使用基于源的安全模型,在发起 WebSocket 握手请求时,浏览器会在请求中添加一个名为 Origin 的 HTTP 头,Oringin 字段表示发起请求的源,以此来防止未经授权的跨站点访问请求。WebSocket 的客户端不仅仅局限于浏览器,因此 WebSocket 规范没有强制规定握手阶段的 Origin 头是必需的,并且 WebSocket 不受浏览器同源策略的限制。

如果服务端没有针对 Origin 头部进行验证可能会导致跨站点 WebSocket 劫持攻击。该漏洞最早在 2013 年被Christian Schneider 发现并公开,Christian 将之命名为跨站点 WebSocket 劫持 (Cross Site WebSocket Hijacking)(CSWSH)。跨站点 WebSocket 劫持危害大,但容易被开发人员忽视。相关案例可以参考: IPython Notebook(CVE-2014-3429)、OpenStack Compute(CVE-2015-0259)、Zeppelin WebSocket 服务器等跨站 WebSocket 劫持。

上图展示了跨站 WebSocket 劫持的过程,某个用户已经登录了 WebSocket 应用程序,如果他被诱骗访问了某个恶意网页,而恶意网页中植入了一段 js 代码,自动发起 WebSocket 握手请求跟目标应用建立 WebSocket 连接。注意到,Origin 和 Sec-WebSocket-Key 都是由浏览器自动生成的,浏览器再次发起请求访问目标服务器会自动带上Cookie 等身份认证参数。如果服务器端没有检查 Origin头,则该请求会成功握手切换到 WebSocket 协议,恶意网页就可以成功绕过身份认证连接到 WebSocket 服务器,进而窃取到服务器端发来的信息,或者发送伪造信息到服务器端篡改服务器端数据。与传统跨站请求伪造(CSRF)攻击相比,CSRF 主要是通过恶意网页悄悄发起数据修改请求,而跨站 WebSocket 伪造攻击不仅可以修改服务器数据,还可以控制整个双向通信通道。也正是因为这个原因,Christian 将这个漏洞命名为劫持(Hijacking),而不是请求伪造(Request Forgery)。

理解了跨站 WebSocket 劫持攻击的原理和过程,那么如何防范这种攻击呢?处理也比较简单,在服务器端的代码中增加对 Origin 头的检查,如果客户端发来的 Origin 信息来自不同域,服务器端可以拒绝该请求。但是仅仅检查 Origin 仍然是不够安全的,恶意网页可以伪造 Origin 头信息,绕过服务端对 Origin 头的检查,更完善的解决方案可以借鉴 CSRF 的解决方案-令牌机制。

拒绝服务

WebSocket 设计为面向连接的协议,可被利用引起客户端和服务器端拒绝服务攻击,相关案例可参考: F5 BIG-IP 远程拒绝服务漏洞(CVE-2016-9253)。

1、客户端拒绝服务

WebSocket 连接限制不同于 HTTP 连接限制,和 HTTP 相比,WebSocket 有一个更高的连接限制,不同的浏览器有自己特定的最大连接数,如:火狐浏览器默认最大连接数为 200。通过发送恶意内容,用尽允许的所有 Websocket 连接耗尽浏览器资源,引起拒绝服务。

2、服务器端拒绝服务

WebSocket 建立的是持久连接,只有客户端或服务端其中一发提出关闭连接的请求,WebSocket 连接才关闭,因此攻击者可以向服务器发起大量的申请建立 WebSocket 连接的请求,建立持久连接,耗尽服务器资源,引发拒绝服务。针对这种攻击,可以通过设置单 IP 可建立连接的最大连接数的方式防范。

注入漏洞

WebSocket 应用和传统 Web 应用一样,都需要对输入进行校验,来防范来客户端的 XSS 攻击,服务端的 SQL 注入,代码注入等攻击。

来看一个靶场,点开靶场,我们移步到 Live Chat 的界面。

先抓包,接着发送一条数据,并在 WebSockets History 中查看数据,修改包,构造成 XSS 的 POC。

发包,再回到 Web 界面的时候成功实现 XSS。

总结

WebSocket 是一个基于 TCP 的 HTML5 的新协议,可以实现浏览器和服务器之间的全双工通讯。在即时通讯等应用中,WebSocket 具有很大的性能优势,并且非常适合全双工通信,但是和任何其他技术一样,开发 WebSocket 应用也需要考虑潜在的安全风险。

基于 WebSocket 的一系列漏洞的防御措施:

  1. 使用 wss 协议(WebSockets over TLS),防止中间人攻击;
  2. 通过设置 Cookie、Token 等鉴权字段来对 WebSockets 进行鉴权,纺防止未授权漏洞;
  3. 校验 Origin 字段来保护 WebSocket 握手消息免受 CSRF 的攻击,以避免跨站点 WebSockets 劫持漏洞;
  4. 将通过 WebSocket 接收的数据视为在两个方向上都不受信任,在服务器端和客户端安全地处理数据,以防止基于输入的漏洞,如 SQL 注入和 CSRF。

本文参考文章:

  1. WebSocket协议:5分钟从入门到精通
  2. 使用Python创建websocket服务和客户端请求
  3. WebSocket 劫持漏洞
  4. WebSocket应用安全问题分析
  5. PortSwigger 基于 WebSocket 的漏洞讲解

以上是关于WebSocket通信协议基础原理与潜在安全威胁的主要内容,如果未能解决你的问题,请参考以下文章

WebSocket通信协议基础原理与潜在安全威胁

理解即时通讯WebSocket的通信原理协议格式安全性

WebSocket介绍与原理

多用户的春天WebSocket安全问题,怎么解决

Websocket 原理

HTTPS原理浅析