CVE-2021-22890: TLS 1.3 session ticket proxy host mixup
Low
C
curl
Submitted None
Actions:
Reported by
mingtao
Vulnerability Details
Technical details and impact analysis
## Summary:
(I don't think that this can be easily exploitable, but I am submitting it as a security issue for precaution. I am not looking for a bounty.)
Commit [549310e907e82e44c59548351d4c6ac4aaada114](https://github.com/curl/curl/commit/549310e907e82e44c59548351d4c6ac4aaada114) enables session resumption with TLS 1.3. Curl connections maintain two SSL contexts, one for the proxy and one for the destination. However, curl incorrectly stores session tickets issued by an TLS 1.3 HTTPS proxy under the non proxy context.
The issue is that the logic inside `Curl_ssl_addsessionid` that chooses which context to store the tickets under is incorrect under TLS 1.3.
```
const bool isProxy = CONNECT_PROXY_SSL();
struct ssl_primary_config * const ssl_config = isProxy ?
&conn->proxy_ssl_config :
&conn->ssl_config;
const char *hostname = isProxy ? conn->http_proxy.host.name :
conn->host.name;
```
```
#define CONNECT_PROXY_SSL()\
(conn->http_proxy.proxytype == CURLPROXY_HTTPS &&\
!conn->bits.proxy_ssl_connected[sockindex])
```
One of the major differences between how TLS session tickets are issued between TLS 1.3 and prior versions of TLS is that TLS 1.3 issues session tickets in a *post* handshake message. What this means in practice is that TLS 1.3 tickets are delivered in the first call to `SSL_read()`, rather than being issued as part of `SSL_connect()`. Consequently, `CONNECT_PROXY_SSL()` will see that the proxy has already been connected (since the call to `SSL_connect()` to the proxy was completed), so the call to `Curl_ssl_addsessionid` believes the `isProxy` is `false`, and it stores the ticket under the non proxy context.
After the `CONNECT` call returns successfully, a connection to the original destination will be made through the established TCP tunnel. If the original destination uses https, another TLS handshake will be made. During this TLS handshake, the curl client offers the session ticket of the *proxy* to the destination.
If the proxy is malicious, at this point it could decide to terminate the TLS handshake to the upstream. Since the proxy has the corresponding session ticket key (it was the entity that issued the ticket, after all), it can complete the client -> destination TLS handshake through a resumption. Normally, this would result in a full man in the middle, as TLS certificates are not exchanged as part of a resumed connection. However, curl already performs some of its own certificate validation outside of OpenSSL in `ossl_connect_step3`, which largely mitigates this vulnerability.
The certificate validation that curl performs includes steps such as (1) checking if the certificate was self signed and (2) ensuring that the certificate contains a subject that matches the destination. The certificate of the proxy is stored in the `SSL_SESSION` that was used for resumption, so curl will attempt to perform these validations against the proxy certificate.
## Steps To Reproduce:
I've attached a reproducer in this report.
* `server_that_fails_on_ticket.c` is a simple TLS server (listening on port 12345) that will send an alert if it receives a session resumption attempt. Under normal circumstances, curl should never be sending a ticket when connecting through a proxy, since it has never connected to this destination before. With this bug, you should be able to observe that the server receives a ticket on the first connection regardless.
* `https_proxy.c` is a extremely rudimentary implementation of a HTTPS proxy (listening on port 12346), that only uses TLS 1.3. If a special proxy header `Mitm: 1` is passed, then the proxy will attempt to terminate the TLS connection itself, acting as a man in the middle.
* `proxy_ca.pem` is the CA file that signs the proxy cert, `haxx.se.pem`
* `haxx.se.pem` is the TLS certificate that the proxy uses. Notice that it has the identities: `localhost` and`haxx.se`.
## Demonstrating that curl sends the proxy ticket to the original destination.
1. Run `server_that_fails_on_ticket`. This will listen on port 12345
2. Run `https_proxy`. This will listen on port 12346
3. Run `curl --proxy-cacert proxy_ca.pem -x 'https://localhost:12346' 'https://localhost:12345'`
4. Notice that the curl client receives a TLS alert, and that "Received a TLS 1.3 ticket resumption attempt" is printed on the server.
## Demonstrating the very limited MiTM possibility.
1. Run `https_proxy`. This will listen on port 12346
2. Run `curl --proxy-cacert proxy_ca.pem --proxy-header 'Mitm: 1' -x 'https://localhost:12346' 'https://haxx.se'`
3. Notice that "MITM" is returned, and no certificate error is thrown.
The MITM is only possible because `haxx.se` is listed as one of the subjects in the proxy certificate. Curl's certificate validation passes: (1) the proxy cert is not self signed and (2) the name haxx.se is present in the certificate is "presented" by the original destination.
## Impact
In a very specific environment (perhaps a corporate environment where all access to the internet requires going through an HTTPS proxy), an attacker that can issue a trusted proxy certificate may be able to man in the middle connections established with libcurl, even if curl explicitly does not include the proxy CA in the trust store for normal destinations.
Report Details
Additional information and metadata
State
Closed
Substate
Resolved
Submitted
Weakness
Man-in-the-Middle