A few months ago, THC released a
PoC code to DoS servers using TLS. The principle is pretty simple and uses the fact that computing crypto material is done on the server side. If you can force the server to re-compute crypto material over and over, you can overrun it and stop it from answering legitimate requests (and eventually crash it). And yes, TLS does provide a mechanism to just do that: re-negotiation.
What this does is that it forces a re-negotiation/re-generation of the crypto materiel which is much more expensive on the server side. And the pain (or beauty depending on which side you stand) of it, is that this is done inside the already existing TLS session. You do not need to re-negotiate a new TCP session, which should be the good way of doing this. Why? Because that would enable a firewall/device to regulate the number of TCP connections per clients/TLS connections per client in a given time frame... And more generally all the standard arsenal of anti-ddos attack would work (max tcp connections per client, max tcp connections in a given time-frame, volume based threshold, max TLS connections per client, and all you can think of...). But here, re-negotiation happens inside the TLS pipe so nobody sees anything at the network layer. So mitigation can only be done at the application layer. Nice... Oh and yes, there is currently no workaround, only half-crappy mitigations.
Is your server vulnerable?
Try this to see if it is (I just tried a few https servers on the internet):
openssl s_client -connect developer.mozilla.org:443 -tls1CONNECTED(00000003)
depth=1 C = US, O = "GeoTrust, Inc.", CN = GeoTrust SSL CA
verify error:num=20:unable to get local issuer certificate
verify return:0
New, TLSv1/SSLv3, Cipher is RC4-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
SSL-Session:
Protocol : TLSv1
Cipher : RC4-SHA
Session-ID: 757DC6F2F77A04555E3E48EAB41F387392A2024D9D3EF9E2B94C3DAC8CCFF8DB
Session-ID-ctx:
Master-Key: 7047D3D1FA0E1BFAE5453400A638AC4FA14D21475F93522D02F4F57C7055F4897973B80049D90BD9B803866CD4BF3ADC
Key-Arg : None
PSK identity: None
PSK identity hint: None
Start Time: 1324623593
Timeout : 7200 (sec)
Verify return code: 20 (unable to get local issuer certificate)
---
R
RENEGOTIATING
depth=1 C = US, O = "GeoTrust, Inc.", CN = GeoTrust SSL CA
verify error:num=20:unable to get local issuer certificate
verify return:0
R
RENEGOTIATING
depth=1 C = US, O = "GeoTrust, Inc.", CN = GeoTrust SSL CA
verify error:num=20:unable to get local issuer certificate
verify return:0
R
RENEGOTIATING
depth=1 C = US, O = "GeoTrust, Inc.", CN = GeoTrust SSL CA
verify error:num=20:unable to get local issuer certificate
verify return:0
If renegotiation succeeds (here I manually did it 3 times), then your server is vulnerable.
So what are the mitigations?
- Disable TLS re-negocaition on the server side
- Terminate the TLS connection on a load-balancer and inspect TLS at that level. But maybe the load-blancer will die due to load?
- Lower the cipher used on the sever side.
- Network layer (aka, we manage network infrastructure and can't change anything on the server side)
This is were it is nice. Because you can't do anything, no really. All you can try is to mitigate with semi working methods. But go and ask the guys under a DDoS attack to do some changes on there infrastructure and that from a network perspective you can't do anything...
A few ideas:
- Set a per-client number of concurrent connections on the front-end firewall. This will help mitigate the issue once TLS re-negociation is disabled, since the attacker side will have to deploy more clients/connections to achieve the same DDoS result. This is standard mitigation for DDoS attacks, and all configs should have that, right? This implies that you manage to convince the server side team to do some changes.
- The idea is to try and drop the TLS re-negociation packet before it reaches the server. The interesting packets are the Encrypted Handshake Message one. This is the packet which triggers re-negociation.
So I tried to focus on number 2. Why is this idea interesting? Because we are going to statistically try and identify the packet triggering re-negotiation from the network layer (statistically is a big word for taking a vague guess).
But first, why do classic threshold/counter based approaches fail in our case? That is because of the nature of the TLS DoS. A bit like Slowloris, it is a low bandwidth consumption attack, thus the attacker does not generate more noise than a standard user.
Back to our "statistical" solution, let's call it heuristical to show off a bit. We are going to use some of the properties of the TLS header to try and drop the re-negotiation packet. TLS header looks like
this . I'll add the header picture too for clarity:
So we can match on the fields we know should identify the TLS handshake packet, which are Content-Type (=handshake), version (=SSL3.0 -> TLS1.2), 2 bytes of length which we will just ignore, and last but not least the message-types field.
So this is what we are looking for:
- tcp port 443 and tcp flags PUSH/ACK
- Look into the tcp payload for the following signature:
- 0x16 (22) which is the start of a handshake protocol in TLS (Content-Type field)
- 0x0300 (SSL 3.0) or 0x0301 (TLS 1.0) or 0x0302 (TLS 1.1) or 0x0303 (TLS 1.2) (Version field)
- Ignore the length field (2 bytes)
- Look for a Message Type which is undefined. The defined types are (0x00, 0x01, 0x02, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x14)
3. Simply drop the packet
The important part is last point of 2. because an interesting property is that when re-negociation is triggered, no Message-Type is used. That way we can differentiate legitimate TLS traffic from malicious re-negociations. The pattern we are looking for would be something like (for TLS 1.0, . being an unknown byte, and x a not-bute): 160301..x where x should not be (0x00, 0x01, 0x02, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x14).
So in a nutshell we are going to apply a regex to the payload of the PSH/ACK tcp packets with a destination of port 443, hoping we can match our regex and drop the packet. Who talked about a performance hog...
Now let's go and apply this to our network device, in my case a good old Guard module. The Guard is already configured with basic settings (rate-limiting, thresholds, conns per client...) but is still letting some DoS packets through because the traffic is not exceeding the threshold.
The way to do this is to create a flex-content-filter on the ADM, which will just hopefully drop the re-negotiation packets. So we need to write a tcpdump regex for this and apply it to our zone. So the nice surprise is that you cannot do a logical "not" in a regexp (or not that I know of). So I had to generate all non-valid Message Type bytes, which gives us this very lengthy filter:
flex-content-filter 1 enabled drop 6 443 expression "tcp[13] = 24" pattern "\x16(\x0300||\x0301||\x0302||\x0303)..?(\x03||\x04||\x05||\x06||\x07||\x08||\x09||\x0a||\x11||\x12||\x13||\x15||\x16||\x17||\x18||\x19||\x1a||\x1b||\x1c||\x1d||\x1e||\x1f||\x20||\x21||\x22||\x23||\x24||\x25||\x26||\x27||\x28||\x29||\x2a||\x2b||\x2c||\x2d||\x2e||\x2f||\x30||\x31||\x32||\x33||\x34||\x35||\x36||\x37||\x38||\x39||\x3a||\x3b||\x3c||\x3d||\x3e||\x3f||\x40||\x41||\x42||\x43||\x44||\x45||\x46||\x47||\x48||\x49||\x4a||\x4b||\x4c||\x4d||\x4e||\x4f||\x50||\x51||\x52||\x53||\x54||\x55||\x56||\x57||\x58||\x59||\x5a||\x5b||\x5c||\x5d||\x5e||\x5f||\x60||\x61||\x62||\x63||\x64||\x65||\x66||\x67||\x68||\x69||\x6a||\x6b||\x6c||\x6d||\x6e||\x6f||\x70||\x71||\x72||\x73||\x74||\x75||\x76||\x77||\x78||\x79||\x7a||\x7b||\x7c||\x7d||\x7e||\x7f||\x80||\x81||\x82||\x83||\x84||\x85||\x86||\x87||\x88||\x89||\x8a||\x8b||\x8c||\x8d||\x8e||\x8f||\x90||\x91||\x92||\x93||\x94||\x95||\x96||\x97||\x98||\x99||\x9a||\x9b||\x9c||\x9d||\x9e||\x9f||\xa0||\xa1||\xa2||\xa3||\xa4||\xa5||\xa6||\xa7||\xa8||\xa9||\xaa||\xab||\xac||\xad||\xae||\xaf||\xb0||\xb1||\xb2||\xb3||\xb4||\xb5||\xb6||\xb7||\xb8||\xb9||\xba||\xbb||\xbc||\xbd||\xbe||\xbf||\xc0||\xc1||\xc2||\xc3||\xc4||\xc5||\xc6||\xc7||\xc8||\xc9||\xca||\xcb||\xcc||\xcd||\xce||\xcf||\xd0||\xd1||\xd2||\xd3||\xd4||\xd5||\xd6||\xd7||\xd8||\xd9||\xda||\xdb||\xdc||\xdd||\xde||\xdf||\xe0||\xe1||\xe2||\xe3||\xe4||\xe5||\xe6||\xe7||\xe8||\xe9||\xea||\xeb||\xec||\xed||\xee||\xef||\xf0||\xf1||\xf2||\xf3||\xf4||\xf5||\xf6||\xf7||\xf8||\xf9||\xfa||\xfb||\xfc||\xfd||\xfe||\xff)"
The important parts are:
- the tcpdump expression "6 443 expression "tcp[13] = 24"" (look only at tcp (6) PSH/ACK to port 443). Thanks to this great resource for clarifying and explaining in depth the tcpdump filters.
- the regexp which was discussed earlier
So know, does this work? To be honest, I'm not 100% sure, because the script kiddies (aka Columbian ransom guys) backed off before we were able to test this. I tried the regexp against some traffic we had captured and I got matches for what I was expecting, but that is no real world scenario. For example, this filter is rendered useless if attacker uses any form of obfuscation through fragmentation. So I still need to test this in the lab, but I thought it could be worthwhile sharing this.
I expect these attacks to increase in the future since the THC PoC tool is publicly available to anyone.
This post is more or less a rehash of what has been written
here, but Vincent goes in to much more detail and explains it much better than I do. Go read that for some real technical details.