如何攻破WebSockets和Socket.io
Ethan Robish //
WebSockets概述
WebSockets是一种技术,允许浏览器和服务器建立单个TCP连接,然后异步双向通信。这对Web应用非常有用,因为它允许实时更新,而无需浏览器在后台发送数百个新的HTTP轮询请求。但对渗透测试人员来说,这很糟糕,因为对WebSockets的工具支持远不如HTTP普遍或复杂。
除了Burp Suite,还有一些其他工具可用于处理WebSockets。我尝试使用每一个,但都没有按我想要的方式工作。
- Zed Attack Proxy (ZAP)
- Pappy Proxy
- Man-in-the-Middle Proxy (mitmproxy)
- WebSocket/Socket.io (WSSiP)
如果您有兴趣在攻击端使用WebSockets来规避检测,请查看这篇文章。
在本文中,我将主要关注socket.io,一个流行的JavaScript WebSockets库。然而,这里的一些想法也可以应用于其他库或一般性的WebSockets协议。
socket.io有多流行?它在Github上有超过41,000颗星。
它还在NPM上占据了第二和第三最流行的WebSockets包的位置。
事实证明,优秀的OWASP Juice-Shop项目使用了socket.io库,因此我将用它进行演示。
本文假设您已经对使用Burp Suite测试Web应用有所了解,并且所有内容都可以在社区版中完成。事不宜迟,让我们开始吧。
如果我们在浏览器中访问Juice-Shop,可以快速看到后台的WebSocket流量。您可以在Burp中通过转到Proxy->WebSockets history找到这个。
与HTTP由于协议的无状态性质总是有请求/响应对不同,WebSockets是一个有状态协议。这意味着您可以有任何数量的传出“请求”和任何数量的来自服务器的传入“响应”。由于底层连接是保持打开的TCP,客户端和服务器都可以随时发送消息,而无需等待对方。这解释了WebSockets历史视图与您可能习惯查看的HTTP历史的不同之处。
在这个视图中,您会主要看到发送和接收的单字节消息。但当应用程序做一些有趣的事情时,您会看到具有更大负载的消息。
Burp有一些测试WebSockets的能力。您可以实时拦截和修改它们,但没有用于WebSockets的Repeater、Scanner或Intruder功能。WebSocket拦截在Burp中默认启用,您只需要打开主拦截。
您会以与HTTP相同的方式获得拦截的WebSocket消息。您也可以在拦截窗口中编辑它们。
并在WebSockets历史选项卡中查看编辑后的消息。
将WebSockets降级为HTTP
方法1:滥用Socket.io的HTTP回退机制
我很快注意到的一个奇怪之处是,有时我会在HTTP历史中看到与WebSockets历史中类似的消息。如果您从上面回忆,我指出的有趣WebSocket消息与解决记分板挑战有关。下面显示了来自服务器的相同响应,但这次是在HTTP历史中。所以我知道socket.io能够通过WebSockets或HTTP发送消息。
我猜测HTTP可用是为了在WebSockets不被支持或以某种方式在应用程序中被阻止时回退。传输参数引起了我的注意,其值在我观察到的请求中为“websockets”和“polling”。
socket.io文档中的这一部分讨论了“polling”和“websockets”是两个默认的传输选项。它还显示了如何通过指定WebSockets作为唯一传输来禁用轮询。我认为反向也是成立的,我可以指定轮询作为唯一的传输机制。
通过搜索socket.io.js源代码,我遇到了以下内容,这看起来很有希望。
|
|
这行代码将一个名为“transports”的内部变量设置为某个传入的值,或者如果传入的值为false/empty,则默认为[“polling”,”websocket”]。这肯定符合我们到目前为止对默认传输为轮询和WebSockets的理解。让我们看看如果我们在Burp的Proxy->Options下设置一个匹配和替换规则来更改这些默认值会发生什么。
成功!添加规则后,刷新页面(我还必须启用Burp的内置规则“Require non-cached response”或执行强制刷新),并且不再有通信通过WebSockets发送。
这很好,但如果您使用的应用程序已经提供了传输选项,这些选项将优先于我们的新默认值怎么办?在这种情况下,我们可以修改我们的匹配和替换规则。以下规则应该适用于不同版本的socket.io库,并忽略应用程序开发人员指定的任何传输。
为方便复制粘贴,以下是使用的字符串:
|
|
确保将其设置为正则表达式匹配。
方法2:中断WebSockets升级
方法1特定于socket.io,并可能扩展到其他客户端库。但以下方法应该更通用,因为它针对WebSockets协议本身。
经过一些调查,我发现WebSockets首先通过HTTP通信,以便与服务器协商并将连接“升级”到WebSocket。其中重要的部分是:
-
客户端发送一个升级请求,带有一些WebSocket特定的头部。
-
服务器响应状态码101 Switching Protocols,也带有一些WebSocket特定的头部。
-
通信过渡到WebSockets,我们不再看到此特定对话的任何更多HTTP请求。
WebSockets RFC第4.1节提供了各种关于我们如何中断此工作流的线索。
以下是来自[https://tools.ietf.org/html/rfc6455#section-4.1]的摘录,带有我自己的强调。
如果从服务器接收的状态码不是101,客户端按照HTTP [RFC2616]程序处理响应。特别是,如果收到401状态码,客户端可能执行认证;服务器可能使用3xx状态码重定向客户端(但客户端不需要遵循它们),等等。否则,按如下进行。
如果响应缺少|Upgrade|头部字段或|Upgrade|头部字段包含的值不是与值“WebSocket”的ASCII不区分大小写匹配,客户端必须_失败WebSocket连接_。
如果响应缺少|Connection|头部字段或|Connection|头部字段不包含与值“Upgrade”的ASCII不区分大小写匹配的令牌,客户端必须_失败WebSocket连接_。
如果响应缺少|Sec-WebSocket-Accept|头部字段或|Sec-WebSocket-Accept|包含的值不是|Sec-WebSocket-Key|(作为字符串,非base64解码)与字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”连接后的base64编码SHA-1,但忽略任何前导和尾随空白,客户端必须_失败WebSocket连接_。
如果响应包括|Sec-WebSocket-Extensions|头部字段,并且此头部字段指示使用客户端握手(服务器指示了客户端未请求的扩展)中不存在的扩展,客户端必须_失败WebSocket连接_。(解析此头部字段以确定请求哪些扩展在第9.1节讨论。)
考虑到这些“必须失败”的条件,我想出了以下一组替换规则,应该在所有五个条件上失败。
一旦这些规则就位,所有WebSocket升级请求都失败了。由于socket.io默认会静默失败到HTTP,这具有所需的效果。特定的实现或其他库可能行为不同,并在您测试的应用程序中导致错误。但我们的工作是让软件做它不打算做的事情!
原始响应看起来像这样,并会导致客户端和服务器过渡到WebSockets进行通信。
相反,客户端从服务器接收到这个修改后的响应,并且根据RFC应该失败WebSockets尝试。
我在测试中遇到的一件事是,在放入这些匹配和替换规则后,客户端异常持久地重试WebSockets连接,并在我的HTTP历史中产生了大量不需要的流量。如果您正在处理socket.io库,可能最容易使用上面的方法1。如果您有不同的库或情况,您可能需要添加更多规则来说服客户端服务器不支持WebSockets,甚至削弱客户端库中的WebSockets功能。
使用Burp Repeater作为Socket.io客户端
由于我们已经强制通信通过HTTP而不是WebSockets进行,您现在可以添加自定义匹配和替换规则,这些规则将应用于本应通过WebSockets进行的流量!
接下来,我们可以更进一步,为使用Repeater、Intruder和Scanner等工具铺平道路。这些更改将特定于socket.io库。
有几个问题阻止我们重复socket.io使用的HTTP请求。
- 每个请求都有一个会话号,任何无效的请求都会导致服务器终止该会话。
- 每个请求的主体都有一个计算的消息长度字段。如果这不正确,服务器会将其视为无效请求并终止会话。
以下是应用程序中使用的几个示例URL。
|
|
URL中的“sid”参数表示到服务器的单个连接流。如果发送了无效消息(这在尝试破坏事物时很常见),那么服务器将关闭整个会话,我必须重新开始一个新会话。
给定请求的主体包含一个字段,其中包含负载的字节数。这类似于“Content-Length”HTTP头部,只不过它是特定于socket.io负载的。例如,如果您想发送的负载是“hello”,那么主体将是“5:hello”,而Content-Length头部将是7。那是5代表“hello”中的字母,7代表“hello”中的字母以及socket.io添加到主体的“5:”。一如既往,Burp将为我们更新Content-Length头部,所以我们不需要担心那个。但我找不到一个好的方法来自动计算并包括负载的长度。更复杂的是,我目睹了socket.io在同一个HTTP请求中发送多个消息。由于每个都是封装的WebSockets负载,每个都有自己的长度,最终看起来像这样:“5:hello,4:john,3:doe”(实际语法可能不同,但您明白意思)。任何计算长度的错误,服务器都会拒绝它作为无效消息,并将我们带回到问题#1。
这是一个消息体的例子。这是来自Juice-Shop应用程序的响应,但请求的格式相同。请注意,这里的“215”代表跟随“:”的负载的长度。
|
|
宏
我能够使用Burp宏解决第一个问题。基本上,每次Burp匹配到服务器拒绝消息时,宏会自动建立一个新会话并用有效的“sid”更新原始请求。通过转到Project options->Sessions->Macros->Add创建一个新宏。
建立新会话的URL只需通过省略“sid”参数来构建。例如:
|
|
我发现“t”的值并不重要,所以我保持不动。
服务器响应包括一个全新的“sid”值供我们使用。
接下来,单击“Configure item”按钮,并将参数名称填写为“sid”。使用“Extract from regex group”选项和以下正则表达式。
|
|
您的配置窗口应该看起来像这样:
会话处理规则
现在我们有了一个宏,我们需要一种方法来触发它。这就是Burp会话处理规则的用武之地。通过转到Project options->Sessions->Session Handling Rules->Add创建一个新规则。
为“Check session is valid”创建一个新的规则操作。
按如下配置新规则操作:
最后,在完成新规则操作后,修改规则的范围。在这里,您可以决定希望此规则应用于何处。我建议至少将其用于Repeater,以便您可以手动重复请求。
以下是我配置范围规则的方式。您可以更具体地设置范围,但以下选项应该适用于大多数情况。
以下是没有会话处理规则时发出的请求。
以下是有了规则后发出的相同请求。请注意,会话处理规则透明地为您更新了请求中的cookie和“sid”值。
最终想法
最终,我被上面讨论的问题#2阻止使用Burp Scanner和Intruder。我修改了一个现有的Burp插件,这似乎完成了工作,但服务器由于某种原因不喜欢它。如果有人有兴趣进一步研究,请随时与我联系。
准备好了解更多吗? 通过Antisyphon的实惠课程提升您的技能! Pay-Forward-What-You-Can培训 提供实时/虚拟和点播