跨站WebSocket漏洞的原理、检测和修复。
WebSocket 是 HTML5 的新特性之一,它引起了开发人员的注意,因为它使客户端(主要是浏览器)能够提供对套接字的支持,从而在客户端和服务器之间提供基于单个 TCP 连接的双向通道。
适用于实时性要求高的应用,如***游戏、不同设备之间的信息同步等。 信息的实时同步一直是一个技术挑战,在WebSockets出现之前,常见的解决方案是轮询和Comet技术,但这些技术增加了设计复杂性,给网络和服务器带来了额外的负担,并且在重负载下效率相对较低,导致应用程序的可扩展性有限。 对于此类应用程序的开发人员来说,WebSocket 技术是读者登录 WebSocket 的强大工具组织**的特色案例,以及它提供的websocket和comet性能的比较分析。
近年来,WebSocket技术在各种实际应用中被开发人员广泛使用。 不幸的是,与 WebSocket 相关的安全漏洞也被揭露出来,其中最有可能的是跨站 WebSocket 劫持漏洞。 本文将简单介绍跨站WebSocket漏洞的原理、检测方法和修复方法,希望能帮助读者在实际工作中规避这个已知的安全漏洞。
为了说明跨站WebSocket劫持漏洞的原理,本文将简要介绍一下WebSocket协议的握手和切换过程。 建议有兴趣的读者阅读参考文献中提供的 RRFC 6455 规范,以深入研究 WebSocket 协议。
任何了解 websocket 技术的人都知道它ws://
跟http://
那么 websockets 和 http 之间是什么关系呢? 笔者对这个问题的理解是,WebSocket 是 HTML5 引入的一种新协议,与 HTTP 协议本身的内容无关。 WebSocket 是一种持久性协议,而 HTTP 是一种非持久性连接。
如前所述,WebSocket 提供全双工通信,通常称为与 Web 的 TCP 连接,但 TCP 通常处理字节流(独立于消息),而 WebSocket 则基于 TCP 实现消息流。 WebSocket 也类似于用于握手连接的 TCP,与 TCP 不同,WebSocket 基于 HTTP 协议。 作者使用 Chrome 开发者工具收集 WebSocketECHO 测试服务的协议握手请求和响应如清单 1 和 2 所示。
清单 1WebSocket 协议升级请求。
get ws: http/1.1host: echo.websocket.orgconnection: upgradepragma: no-cachecache-control: no-cacheupgrade: websocketorigin: 13user-agent: mozilla/5.0 (macintosh; intel mac os x 10_11_4) chrome/49.0.2623.110accept-encoding: gzip, deflate, sdchaccept-language: en-us,en;q=0.8,zh-cn;q=0.6cookie: _gat=1; _ga=ga1.2.2904372.1459647651; jsessionid=1a9431cf043f851e0356f5837845b2ecsec-websocket-key: 7arps0ajshn8bx5dci1kkq==sec-websocket-extensions: permessage-deflate; client_max_window_bits
熟悉HTTP的人可以发现WebSocket的核心,是的,这是两条连接线:upgrade和upgrade:websocket。 这两行相当于告诉服务器:我想请求切换到 websocket 协议。
清单 2WebSocket 协议升级响应。
http/1.1 101 web socket protocol handshakeaccess-control-allow-credentials: trueaccess-control-allow-headers: content-typeaccess-control-allow-headers: authorizationaccess-control-allow-headers: x-websocket-extensionsaccess-control-allow-headers: x-websocket-versionaccess-control-allow-headers: x-websocket-protocolaccess-control-allow-origin: upgradedate: sun, 03 apr 2016 03:09:21 gmtsec-websocket-accept: ww9bl95vtfjdbphdfivy7csoado=server: kaazing gatewayupgrade: websocket
一旦服务器端返回 101 响应,websocket 协议切换就完成了。 然后,服务器端可以使用相同的端口从http://
或https://
切换到ws://
或wss://
。协议切换完成后,浏览器和服务器可以使用 WebSocket API 相互发送和接收文本和二进制消息。
以下是一些与安全相关的重要标头参数,sec-websocket-key 和 sec-websocket-accept。 这涉及 websocket 安全功能,其中客户端负责生成一个 base64 编码的随机数作为 sec-websocket-key,服务器将生成一个 GUID 以及客户端的随机数,以生成一个哈希密钥,该哈希密钥作为 sec-websocket-accept 返回给客户端。 这可用于避免缓存代理和请求重播。
细心的读者可能还会注意到许多其他以“sec”开头的与 websocket 相关的标头。 其实这也是 websocket 设计器出于安全考虑的特殊设计,以 “sec-” 开头的 header 可以避免被浏览器脚本读取,这样攻击者就无法使用 xmlhttprequest 伪造 websocket 请求来执行跨协议攻击,因为 xmlhttprequest 接口不允许设置以 sec- 开头的 header。
虽然 WebSocket 协议在设计时就考虑到了安全性,但随着 WebSocket 技术的普及,安全工作者也慢慢发现了一些与 WebSocket 相关的安全漏洞,比如 Wireshark 漏洞 CVE-2013-3562 (Wireshark 18.7 之前 18.EPAN 在 X 版本的 WebSocket 解析器中分析 packet-websocket。C 语言的 'tvb unmasked' 函数存在多个整数符号错误,远端攻击者可利用这些错误通过恶意数据包造成拒绝服务。
Asterisk WebSocket Server 中的 DOS 漏洞 CVE-2014-9374(WebSocket Server 模块中存在一个双重释放漏洞,远程攻击者可以利用该漏洞发送长度为零的帧,从而造成拒绝服务)。 这两个 DDoS 漏洞与 WebSocket 协议本身和 WebSocket 应用程序关系不大。 然而,在 2015 年,Cisco 的 Brian Manifold 和 Nebula 的 Paul McMillan 报告了 OpenStack Nova 控制台中的一个 WebSocket 漏洞 (CVE-2015-0259),该漏洞受到广泛关注,并在许多 WebSocket 应用程序中被发现。
事实上,该漏洞是在 2013 年由德国白帽黑客 Christian Schneider 发现并公开的,他将其命名为跨站点 WebSocket 劫持 (CSWSH)。 跨站点 websocket 劫持相对有害,更容易被开发人员忽视。
什么是跨站WebSocket劫持漏洞,如前所述,为了创建全双工通信,客户端需要基于HTTP握手切换到WebSocket协议,而这种协议升级的过程是潜在的致命弱点。 如果你仔细观察上面的握手获取请求,你可以看到cookie标头将域名下的所有cookie都发送到了服务器。
如果你有机会阅读 websocket 协议 (10第 5 章 客户端身份验证)发现 WebSocket 协议没有指定服务器在握手阶段应如何对客户端进行身份验证。服务器可以使用任何HTTP服务器的客户端认证机制,如cookie、HTTP基本认证、TLS认证等。 因此,对于大多数 Web 应用程序,客户端身份验证应为 Cookie,例如 sessionid 或 HTTP 身份验证标头参数。 熟悉跨站请求伪造(CSRF)的朋友应该能够想到黑客可能会伪造握手请求以绕过身份验证。
因为 websocket 的客户端不局限于浏览器,所以 websocket 规范的来源不必相同(有兴趣的读者可以阅读规范 10。2 章用于 Origin 规范)。所有浏览器都会发送源头,如果服务器不针对源头进行验证,则可能导致跨站点 websocket 劫持攻击。
例如,如果用户登录到一个应用程序,如果他被诱骗访问社交**的恶意网页,则该恶意网页将在元素中植入一个 websocket 握手请求,以请求 websocket 连接到目标应用程序。 打开恶意网页后,会自动发起以下请求: 请注意,origin 和 sec-websocket-key 都是由浏览器自动生成的,Cookie 等认证参数由浏览器自动上传到目标应用的服务器端。 如果服务器检查来源失败,请求会成功切换到 WebSocket 协议,恶意网页可以成功绕过身份认证连接到 WebSocket 服务器,然后窃取服务器发送的信息,或者向服务器发送伪造的信息来篡改服务器的数据。
有兴趣的读者可以将此漏洞与CSRF进行对比,CSRF主要通过恶意网页悄悄发起数据修改请求,不会造成信息泄露问题,而跨站WebSocket伪造攻击不仅可以修改服务器数据,还可以控制整个双向通信通道进行读取和修改。 这就是为什么克里斯蒂安将漏洞命名为劫持,而不是请求伪造。
清单 3被篡改的 websocket 协议升级请求。
get ws: http/1.1host: echo.websocket.orgconnection: upgradepragma: no-cachecache-control: no-cacheupgrade: websocketorigin: 13accept-encoding: gzip, deflate, sdchaccept-language: en-us,en;q=0.8,zh-cn;q=0.6cookie: _gat=1; _ga=ga1.2.290430972.14547651; jsessionid=1a9431cf043f851e0356f5837845b2ecsec-websocket-key: 7arps0ajshn8bx5dci1kkq==sec-websocket-extensions: permessage-deflate; client_max_window_bits
在这一点上,熟悉 j**ascript 跨域资源访问的读者可能会持怀疑态度。 如果 HTTP 响应没有指定 “access-control-allow-origin”,浏览器脚本将无法访问跨域资源,没错,这就是大家熟知的跨域资源共享(CORS),确实是 HTML5 带来的新特性之一。 不幸的是,跨域资源共享不适用于 websocket,它没有指定如何跨域处理它们。
了解了跨站WebSocket劫持漏洞的原理后,很容易想到这个漏洞的检测方法,重点是重放WebSocket协议升级请求。 简单来说,就是使用拦截 websocket 握手请求的工具,修改请求中的源头,然后重新发送请求,看看服务器是否能成功返回 101 响应。
如果连接失败,则 websocket 是安全的,因为它会正确拒绝来自不同源的连接请求。 如果连接成功,通常证明是服务器没有进行源头检查,为了严谨起见,最好进一步测试一下 websocket 消息是否可以发送,如果 websocket 连接能够发送和接受消息,则充分证明跨站 websocket 劫持漏洞的存在。
为了演示如何测试和修复这个漏洞,笔者编写了一个简单的 WebSocket 应用,实现了基于 Jaas 的 HTTP Basic 认证,读者可以将这个程序部署到 Tomcat 进行测试。 打开客户端网页后,先登录,然后点击“连接”按钮通过j**ascript建立websocket连接,然后点击“发送”按钮将问题提交到服务器,服务器实时确认收到查询请求,5秒后将结果推送到客户端。
在测试工具方面有很多选择,出于许可原因,我使用开源 OWASP ZAP v24.3。这里简单提一下,测试过程主要是基于测试工具的**,拦截 websocket 握手请求和 websocket 消息通信,然后通过工具修改源站后重新发送请求,连接成功后重新发送 websocket 客户端消息。 上述所有功能都可以通过任何商业安全测试工具完成。
1. 首先在 Firefox 中配置 Zap,然后浏览整个 WebSocket 应用程序。 如下图所示,请求头中会出现 HTTP 基本授权信息,表示登录成功。
图1WebSocket 协议升级请求。
2. 右键单击并选择重新发送 WebSocket 协议升级请求,将源修改为任何其他**,然后单击发送。
图2篡改 WebSocket 协议升级请求。
3. 点击响应选项卡,可以看到服务器端返回101,即协议握手成功。
图3WebSocket 协议握手成功。
4. 进一步测试 websocket 消息是否可以重传。 如下图所示,右键点击客户端发送的第一条websocket消息,选择Retransmit,输入测试消息“www”,点击发送,可以看到ZAP已经依次收到了服务器返回的两条消息。 这充分证明了测试应用站点存在跨站WebSocket劫持漏洞。
图4重新发送客户端 websocket 消息。
上面已经介绍了跨站websocket劫持漏洞的原理和检测方法,相信读者已经了解了它的危害,那么我们来谈谈如何防止这个漏洞。 这个漏洞的原理听起来有点复杂,但幸运的是它测试起来相对简单,所以很容易修复。 很多读者会认为,这不仅仅是检查服务器中的 origin 参数**。 是的,检查原产地是必要的,但还不够。 如果客户端发送的源信息来自其他域,建议服务器拒绝请求并发回 403 错误响应以拒绝连接。
作者使用 J**A EE 技术编写了一个 WebSocket 测试应用,J**A EE WebSocket API 提供了一个配置器,允许开发者覆盖配置来拦截和检查协议握手过程。 作者在文章附录中已经将这部分内容收录在了源代码**中,下面对一些核心类和配置进行了简要介绍。 如果您不熟悉 J**A EE WebSocket API,建议您先查看相关规格。
1. 首先,为 websocket 服务器终端编写一个配置器,继承并重写 checkorigin 方法,如清单 4 所示。 注意作者忽略了没有源头的场景,这取决于每个应用的实际情况,如果有非浏览器客户端,则需要添加此检查。 还建议非浏览器客户端查看下面的令牌机制。
清单 4WebSocket 源检查配置器。
public class customconfigurator extends serverendpointconfig.configurator }
2. 然后将配置器关联到 websocket 服务器。
清单 5配置 websocket 源检查。
@serverendpoint(value = "/query", configurator = customconfigurator.class)public class websockettestserver }
3. 重新打包并发布 websocket 应用程序。
有兴趣的读者可以自己试试,如果补上以上**后重播被篡改的websocket握手协议请求,会收到403错误。
以上看起来不错,但仅仅检查来源是不够的,别忘了,如果 websocket 客户端不是浏览器,来自非浏览器客户端的请求根本没有来源。 除此之外,我们还需要记住,恶意网页可以伪造源头信息。 一个更激进的解决方案是借鉴CSRF的代币机制。
由于篇幅关系,笔者不会详细发布整个设计,但建议读者参考以下总结设计,以提高 websocket 应用的安全性。
服务端为每个 websocket 客户端生成一个唯一的一次性令牌; 客户端使用令牌作为 WebSocket 连接 URL 的参数(例如,WS:发送到服务器进行 WebSocket 握手连接; 服务器验证令牌是否正确,如果正确,则标记丢弃,不再重复使用,websocket握手连接成功。 如果令牌认证失败或认证失败,则返回 403 错误。 该方案中的令牌设计是关键,笔者推荐的方案是为登录用户生成一个安全随机并存储在会话中,然后使用对称加密(如AES GCM)将这个安全随机值加密为令牌,并将加密后的令牌发送给客户端进行连接。 这允许每个会话有一个唯一的随机数,每个随机数可以通过对称加密生成多个一次性令牌。 即使用户通过不同的终端通过 websocket 连接到服务器,服务器仍然可以在保证 Token 唯一且使用一次的前提下,将来自不同渠道的信息与同一用户关联。
可能还有另外一种设计思路,就是给 websocket 消息添加 token 和身份信息,但我觉得这种设计与 websockets 的设计思路背道而驰,增加了不必要的网络负载。 欢迎读者提供更好的设计方案。
在本文中,笔者与读者分享了对WebSocket协议握手的理解,并在此基础上解释了WebSocket跨站劫持漏洞的原理。 如本文所述,这是 Web 应用程序中唯一一个广为人知的 websocket 漏洞。 同时,笔者还分享了跨站WebSocket劫持漏洞的检测方法,并介绍了基于J**A EE技术的漏洞修复,以及基于token机制的更全面的安全解决方案。