看我如何分析并渗透WebSocket和Socket.io
Websocket简介
WebSocket是一种允许浏览器和服务器建立单个TCP连接然后进行全双工异步通信的技术。由于它允许实时更新,而浏览器也无需向后台发送数百个新的HTTP polling请求,所以对于web程序来说,WebSocket非常流行。这对于测试者来说是不好的,因为对WebSocket工具的支持不像HTTP那样普遍,有时候会更加复杂。
除了BurpSuite之外,还有一些其他工具可用于处理WebSocket。不过经过测试,它们都不怎么理想。
Zed Attack Proxy (ZAP)
Pappy Proxy
Man-in-the-Middle Proxy (mitmproxy)
WebSocket/Socket.io (WSSiP)
如果你对使用Websocket进行渗透测试感兴趣,那么可以查看这篇文章:
https://www.blackhillsinfosec.com/command-and-control-with-websockets-wsc2/
而在这篇文章中主要会讲socket.io,它是一个很流行的JavaScript WebSockets库。在GitHub上它有多流行呢?—已经有超过41.4的star了。
在NPM上,它在WebSocket中排行第二和第三。
另外,OWASP Juice-Shop这样非常棒的项目也使用了socket.io库,所以本篇文章中将使用websocket.io进行演示。
https://github.com/bkimminich/juice-shop/search?utf8=%E2%9C%93&q=socket.io&type=
在本文中,我们假设你已经熟悉使用BurpSuite测试Web应用程序,所涵盖的所有内容都可以在其社区版本中完成。不用多说,现在开始吧。
如果我们在浏览器中访问Juice-Shop,则可以在后台快速查看WebSocket流量。你也可以在BurpSuite中通过Proxy-> WebSockets历史记录找到。
由于协议的无状态特性,HTTP需要始终发送请求/响应对,而WebSocket是一种有状态协议。这意味着你可以从服务器获得任意数量的传出“请求”和任意数量的传入“响应”。由于底层连接是保持打开的TCP,因此客户端和服务器可以随时发送消息而无需等待对方。这就是为什么WebSocket历史记录与你习惯查看的HTTP历史记录存在差异。
在此界面中,你可以看到发送和接收的单字节消息。但是,当应用程序执行一些有趣的操作时,你就可以看到具有更大负载的消息。
BurpSuite具有测试WebSockets的能力,你可以实时进行拦截和修改,但WebSocket没有Repeater,Scanner或Intruder功能。默认情况下,如果要在BurpSuite中启用WebSocket拦截,你只需要打开主拦截就好了。
这样一来,你就可以通过与HTTP相同的方式获取所截获的WebSocket消息。同时也可以在拦截窗口中编辑它们。
在WebSockets历史记录选项卡中可以查看已编辑的消息。
将WebSocket降级为HTTP
方法一:使用Socket.io的HTTP回退机制
一个非常奇怪的点是,有时在HTTP历史记录中也能看到类似Websocket历史记录中的消息,回想一下,这些比较有趣的WebSocket消息需要解决记分板相关问题,下图显示了来自服务器的相同响应,但这次是在HTTP历史记录中。由此可以看出socket.io能够通过WebSocket或HTTP发送消息。
在所观察的请求中,传递的参数值有些为“websockets”,而有些则是“polling”。那么据推测,可能为了防止WebSockets在应用程序中不受支持或被阻止,才允许使用HTTP。
socket.io文档中解释了“polling”和“websockets”如何作为两个默认传输选项。它还介绍了如何通过将WebSockets指定为唯一传输方式来禁用polling。我认为反过来也是如此,我可以指定polling作为唯一的传输机制。
https://socket.io/docs/client-api/#with-WebSocket-transport-only
通过搜索socket.io.js源代码,我找到了以下内容:
this.transports=n.transports||["polling","WebSocket"]
这行代码会将一个名为transports的内部变量设置为传入的值,如果传入的值为false/empty,则为默认的[“polling”,“websocket”]。这很符合我们对polling和WebSocket的默认传输的推测。现在通过Burp中的Proxy->Options下设置匹配并替换规则来更改这些默认值,看看会发生什么。
成功了!添加规则后,刷新页面(需要启用Burp的内置规则“Require non-cached response”或执行强制刷新),数据不再通过WebSockets进行通信。进展不小,但是如果使用的应用程序已经提供了优先于我们的新默认值的传输选项呢?在这种情况下,我们可以修改匹配和替换规则。以下规则应适用于socket.io库的不同版本,并忽略应用程序开发人员所指定的任何传输方式。
以下是要使用的字符串,务必将其设置为正则表达式匹配:
this\.transports=.*?\.transports\|\|\["polling","websocket"]
this.transports=["polling"]
方法二:中止Websocket升级
方法一只能用于于socket.io,可能会扩展到其他客户端库。但是,以下方法应该更加通用,因为它以WebSockets协议本身为目标。
经过分析,我发现WebSockets首先通过HTTP进行通信,以便与服务器协商并“升级”为WebSocket。其中重要的部分是:
1)客户端通过一些WebSocket特定header发送升级请求。
2)服务器响应状态码为101 Switching Protocols,以及WebSocket header。
3)通信转换到WebSocket,此特定会话不再使用HTTP。
WebSockets RFC文档第4.1节提供了有关如何中断此工作流的各种信息,以下是 https://tools.ietf.org/html/rfc6455#section-4.1的摘录,并附加了观点。
1.如果从服务器收到的状态码不是101,则客户端响应HTTP [RFC2616]。特别情况下,收到401状态码时,客户端可能会执行身份验证;服务器也可能会通过3xx状态码重定向客户端(但客户不需要遵循)等。否则按以下步骤进行。
2.如果响应缺少Upgrade header,或Upgrade header包含的值与“WebSocket”的ASCII不匹配,则客户端必须关闭WebSocket连接。
3.如果响应缺少Connection header,或Connection header包含的值与“WebSocket”的ASCII不匹配,则客户端必须关闭WebSocket连接。
4.如果响应缺少Sec-WebSocket-Accept header,或Sec-WebSocket-Accept header的值并非是由Sec-WebSocket-Key(作为字符串,未经base64解码)与字符串”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″串联起来的字符串(忽略任何前导和尾随空格)的base64编码后的SHA-1值的话,则客户端必须关闭WebSocket连接。
5.如果响应中包括Sec-WebSocket-Extensions header,并且header要求使用的扩展并没有出现在客户端的握手消息中(服务器指示的扩展并非是客户端所请求的),则客户端必须关闭WebSocket连接。(解析header以确定请求哪些扩展的问题,将在 第9.1节中讨论)
考虑到这些“连接必定被关闭”的条件,我想出了以下一套替换规则,这些规则应该包含了所有五个的失败条件。
一旦使用这些规则,所有WebSocket升级请求都会失败。由于socket.io默认情况下无法使用HTTP,因此已经达到所需的效果。其他库的表现可能不同,并导致你正在测试的应用程序出错。但我们的工作就是让软件做一些不应该做的事情!
原始响应看起来像这样,并且会使客户端和服务器转换到WebSocket进行通信。
相反,客户端从服务器收到此修改后的响应,会关闭WebSocket连接。
我在测试中遇到的一件事是,在将这些匹配和替换规则加入后,客户端在重试WebSocket连接时非常持久,并在我的HTTP历史记录中引起了大量不必要的流量。如果你正在处理socket.io库,则最简单的方法是使用上面的方法1。如果你有不同的库或其他情况,则可能需要添加更多规则来使客户端服务器不支持WebSocket。
将Burp Repeater作为Socket.io客户端
由于我们强制通过HTTP而非WebSockets进行通信,所以现在可以添加自定义匹配并替换将应用于已经通过WebSockets流量的规则!接下来,可以使用Repeater,Intruder和Scanner等工具,这些更改将特定于socket.io库。不过现在还有两个问题:
1.每个请求都有一个会话号,任何无效请求都将导致服务器终止该会话
2.每个请求的主体都有一个计算字段,表示消息的长度。如果这不正确,服务器会将其视为无效请求并终止会话。
以下是应用程序中使用的几个示例URL。
/socket.io/?EIO=3&transport=polling&t=MJJR2dr
/socket.io/?EIO=3&transport=polling&t=MJJZbUa&sid=iUTykeQQumxFJgEJAABL
URL中的“sid”参数表示到服务器的单个连接流。如果发送了无效消息(在尝试破解时很常见),那么服务器将关闭整个会话,之后必须重新开始新会话。
给定请求的主体中含有一个字段,其中存放有效载荷的字节数。这类似于“Content-Length”HTTP header,只不过该字段的值近针对socket.io。例如,如果你要发送的有效载荷是“hello”,那么,相应的主体将是“5:hello”,Content-Length头部的值是7。其中,5表示字符串“hello”中的字母数量,而7则表示字符串“hello”中的字母数量以及socket.io添加到主体内的字符串“5:”中的字母数量之和。与往常一样,Burp将替我们更新Content-Length头部,因此,这件事情我们无需担心。但是,我还没有找到能够自动计算和包含有效载荷长度的好方法。更让人头疼的是,我发现socket.io竟然会在同一个HTTP请求中发送多条消息。由于每个消息都是一个封装后的WebSocket有效载荷,并且每个消息都有自己的长度,因此,最终看起来就像这样:“5:hello,4:john,3:doe”(实际的语法可能有所不同,这里只是便于演示)。计算长度时一旦出错,服务器就会将其作为无效消息拒绝,这样,我们就要重新开始了。
这是body的示例。这是Juice-Shop应用程序中的响应,请求的格式相同。注意,这里的“215”表示“:”之后的有效载荷的长度。
215:42[“challenge solved”,{“key”:”zeroStarsChallenge”,”name”:”Zero Stars”,”challenge”:”Zero Stars (Give a devastating zero-star feedback to the store.)”,”flag”:”e958569c4a12e3b97f38bd05cac3f0e5a1b17142″,”hidden”:false}]
宏
使用Burp宏能解决第一个问题。基本上,每次Burp在服务器拒绝消息时匹配,宏将自动建立新会话并用有效的“sid”更新原始请求。通过转到options->Sessions->Macros->Add来创建新宏。
建立新会话的URL只需省略“sid”参数。例如:
/socket.io/?EIO=3&transport=polling&t=MJJJ4Ku
服务器响应包含一个全新的“sid”值以供使用。
接下来,单击“Configure item”按钮,并将参数名称命名为“sid”。然后,选择“Extract from regex group”选项,并使用如下所示的正则表达式。
"sid"\:"(.*?)"
这时,配置窗口应如下所示:
会话处理规则
现在有了一个宏,我们需要一种方法来触发它。这就是Burp会话处理规则的用武之地。通过
Project options->Sessions->Session Handling Rules->Add
为“Check session is valid”创建新的规则动作:
配置新规则操作如下:
按如下方式配blackhillsinfosec置新规则操作:最后,在完成新规则操作后,还需修改规则的范围。你可以在此处决定要应用此规则的位置。建议至少将它用于Repeater,这样就可以手动重复请求。
以下是我配置范围规则的方法。你可以更加具体地了解自己所需范围,但下面的选项应该适用于大多数情况。
这是在没有会话处理规则的情况下发出的请求:
这里是在会话处理规则生效后发出的相同请求:
*参考来源: blackhillsinfosec,FB小编Covfefe编译,转载请注明来自FreeBuf.COM