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 进行渗透测试感兴趣, 那么可以查看这篇文章:
而在这篇文章中主要会讲 socket.io, 它是一个很流行的 JavaScript WebSockets 库. 在 GitHub 上它有多流行呢?- 已经有超过 41.4 的 star 了.
在 NPM 上, 它在 WebSocket 中排行第二和第三.
另外, OWASP Juice-Shop 这样非常棒的项目也使用了 socket.io 库, 所以本篇文章中将使用 websocket.io 进行演示.
在本文中, 我们假设你已经熟悉使用 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 作为唯一的传输机制.
通过搜索 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] https://tools.ietf.org/html/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 节 https://tools.ietf.org/html/rfc6455#section-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 库. 不过现在还有两个问题:
每个请求都有一个会话号, 任何无效请求都将导致服务器终止该会话
每个请求的主体都有一个计算字段, 表示消息的长度. 如果这不正确, 服务器会将其视为无效请求并终止会话.
以下是应用程序中使用的几个示例 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, 这样就可以手动重复请求.
以下是我配置范围规则的方法. 你可以更加具体地了解自己所需范围, 但下面的选项应该适用于大多数情况.
这是在没有会话处理规则的情况下发出的请求:
这里是在会话处理规则生效后发出的相同请求:
来源: http://www.tuicool.com/articles/RvaYZ3R