[CONC-648] Client improperly accepts error packets prior to TLS handshake Created: 2023-06-05 Updated: 2024-01-15 |
|
| Status: | In Review |
| Project: | MariaDB Connector/C |
| Component/s: | None |
| Affects Version/s: | 3.3.5 |
| Fix Version/s: | 3.1, 3.3 |
| Type: | Bug | Priority: | Critical |
| Reporter: | Daniel Lenski | Assignee: | Sergei Golubchik |
| Resolution: | Unresolved | Votes: | 0 |
| Labels: | security, ssl, tls | ||
| Attachments: |
|
||||||||||||
| Issue Links: |
|
||||||||||||
| Description |
|
If the MariaDB client is running with --ssl --ssl-verify-server-cert, it should not trust any application-level traffic received prior to the completion of the TLS handshake and the validation of the server's TLS certificate. The MariaDB client (as built from [v3.3.5](https://github.com/mariadb-corporation/mariadb-connector-c/releases/tag/v3.3.5)) violates this expectation, making it trivially susceptible to DOS by untrusted on-path attackers, even when the user has explicitly specified --ssl --ssl-verify-server-cert. Demonstration:
Running tcpdump in the background confirms that the client is improperly accepting the error packet, even though it has been sent in plaintext and without a TLS handshake:
|
| Comments |
| Comment by Daniel Lenski [ 2023-06-05 ] | |||||||||||||||||||||
|
I based demonstration_of_CONC-648_vulnerability against MariaDB server 10.11 and client v3.3.5, but it's trivial to demonstrate that earlier versions are equally vulnerable. | |||||||||||||||||||||
| Comment by Sergei Golubchik [ 2023-06-06 ] | |||||||||||||||||||||
|
Do you think it's a problem? An "untrusted on-path attacker" can send TCP RST flag to abort the connection, wouldn't it basically have the same effect? | |||||||||||||||||||||
| Comment by Daniel Lenski [ 2023-06-06 ] | |||||||||||||||||||||
Yes, I think it's a major vulnerability if a client is intended not to trust unencrypted communications, and yet it turns out to trusts unencrypted communication. I've merely made an extremely simple demo of the vulnerability here, but it's quite likely that it's worse than this, and that the client will also trust other types of traffic before completing the TLS handshake.
An attacker interfering with layers below a TLS-based channel can disrupt communication between the peers, but should not be able to inject traffic that either peer accepts as genuine. A deviation from that (such as CVE-2009-3555 in which an attacker can inject TLS renegotiations along with unauthenticated data) is a vulnerability. Due to this vulnerability in MariaDB, an on-path attacker can signal an arbitrary error to a client, which might (for example) cause the client to alter its retry behavior or to misdiagnose a connection problem. And it can get much worse than that: I discovered this vulnerability in MariaDB because I'm working on an approach to | |||||||||||||||||||||
| Comment by Georg Richter [ 2023-06-10 ] | |||||||||||||||||||||
|
The client always has to accept error packages before TLS handshake is completed, since the handshake itself may fail. Everything which happens before the TLS handshake has completed succesfully should be considered as insecure. In the comments of | |||||||||||||||||||||
| Comment by Daniel Lenski [ 2023-06-12 ] | |||||||||||||||||||||
I completely agree with that, but the MySQL/MariaDB Connector/C libraries fail in this regard, because they do not allow consumers to distinguish errors that happen after the TLS handshake from errors that happen before/during the TLS handshake. Contrast this with basically every other TLS client. Every modern HTTP client clearly distinguishes errors before TLS handshake from errors afterErrors before/during establishment of TLSTry cURLing {expired,untrusted-root,self-signed,revoked}.badssl.com as a useful source of reproducible TLS errors …
Application-level errors after establishment of TLS
or
mariadb CLI does not distinguish errors sent before or after TLS establishmentError that is sent by the server, but clearly should not be trusted by the client: (with my trivially modified server from dlenski/mariadb-server:demonstration_of_CONC-648_vulnerability)
Error that should be trusted because it is sent after the TLS handshake:
As far as I can tell, there is no way for a consumer of MariaDB Connector/C to distinguish these untrustworthy pre-handshake and trustworthy post-handshake errors, so it would be completely unsafe to base any automated redirection mechanism on the interpretation of an error packet. | |||||||||||||||||||||
| Comment by Daniel Lenski [ 2023-06-12 ] | |||||||||||||||||||||
If the client libraries continue to handle untrustworthy pre-TLS-handshake errors in a way that's indistinguishable from trustworthy post-TLS-handshake errors, any flag sent by the server to indicate (only allow redirection for secure connections) will be ineffectual. | |||||||||||||||||||||
| Comment by Daniel Lenski [ 2023-06-12 ] | |||||||||||||||||||||
|
I've submitted mariadb-connector-c PR #223 which mitigates at least one avenue by which a TLS-enabled client can be tricked into improperly trusting an application-layer error packet received before completion of the TLS handshake. (ping otto) | |||||||||||||||||||||
| Comment by Georg Richter [ 2023-06-13 ] | |||||||||||||||||||||
|
Do you really think it's more safe to send another error message and to hide the original one (which makes it even more difficult to find reasons for connection errors) ? Then it would be also logical to change the message when a TLS alert has occurred before the handshake has completed, since an alert could be sent by a MIM. This would make it impossible to analyze possible TLS problems. Any error packet which the client retrieves before the handshake completed (e.g. "No more connections available", "Server can't create new thread", including all critical TLS alerts) forces the client to abort the connection. So I don't really see any vulnerability here. | |||||||||||||||||||||
| Comment by Daniel Lenski [ 2023-06-13 ] | |||||||||||||||||||||
Yes, I do. If the server inadvisably sends such error responses prior to the completion of the TLS handshake, then yes I think it is unsafe for the error to be relayed to the client in a form where the client could vary its behavior based on the contents of the error response. Replacing the untrusted-origin error with a generic one seems like the only safe mitigation. Clearly, a much better approach would be to handshake and switch to TLS handshake immediately upon connection rather than having it happen several packets later. That's how TLS is supposed to be used and how almost every other TLS-secured protocol works.
The fact that the connection is aborted following the error does not in any way mitigate the fact that the error conveys information to the client, which the client should not trust. In https://github.com/mariadb-corporation/mariadb-connector-c/pull/223, I give a brief description of a very plausible scenario in which clients can be induced to modify their behavior by sending such packets. And as I explained above, this vulnerability is going make it completely unsafe to build any automated redirection mechanism based on the handling of error packets immediate following initial connection.
I'm very surprised that you don't see this as a serious vulnerability. Suppose that $SPY_AGENCY or $INTERNET_CENSOR wants to write a tool to degrade and interfere with TLS-secured MariaDB client→server connections. The issue discovered here makes it almost trivial to do so. Just detect the unencrypted pre-handshake MySQL server/client greeting packets and inject whichever application-layer error packets you think will interfere. | |||||||||||||||||||||
| Comment by Andrew Hutchings [ 2023-06-15 ] | |||||||||||||||||||||
|
I was asked to review this by dlenski, I have been through it and here are my comments: This does appear to be a cut and dry MITM attack, and if Right now it is important but I don't think critical. It is possible to make an application behave badly due to it being developed to respond a certain way based on an error code. For example, connect to server B because A returned a max connections error. This may sound benign, but I suspect there are ways to abuse this. All that being said, I'm not sure the current PR is 100% the correct approach. You do want the error codes to remain as-is because this will break applications. I don't think an additional error code / message at the same time is possible, so the library should probably inject a prefix to the message which states that this message came before the handshake. If well documented, this could be something applications can watch for. Even better if it can be some kind of flag or API call to check if the message came prior to encryption. | |||||||||||||||||||||
| Comment by Daniel Lenski [ 2023-06-19 ] | |||||||||||||||||||||
|
TheLinuxJedi wrote:
Disagree. I think that (1) the security concern should trump any backwards-compatibility concern, and (2) there is no evidence that there is any real backwards-compatibility concern. For (2), I don't find any evidence that real/non-malicious servers (MySQL, MariaDB, Percona, etc.) actually send pre-handshake application-level error messages. As far as I can tell:
What you're proposing here is to push the burden of verifying the transport-layer security onto the client application logic.
In practice, applications won't watch for it, and pre-existing applications certainly won't watch for it. This is why when a vulnerability like this one is discovered (supposedly TLS-secured client is vulnerable to incorrectly trust non-TLS-secured inputs), it's generally good practice to err on the side of breaking backwards-compatibility. And anyway, as I describe above, there's no evidence that existing MySQL/MariaDB server implementations will actually send meaningful error packets to TLS-secured clients prior to the TLS handshake, so the backwards compatibility concern appears to be a theoretical one. | |||||||||||||||||||||
| Comment by Daniel Lenski [ 2023-06-19 ] | |||||||||||||||||||||
The only exception I can find to this is that if the client requests an auth plugin which the server can't load, then the server will send this back to the client as an unencrypted error packet even if the client wants TLS/SSL: https://github.com/MariaDB/server/blob/11.0/sql/sql_acl.cc#L14331-L14338 | |||||||||||||||||||||
| Comment by Georg Richter [ 2023-06-20 ] | |||||||||||||||||||||
|
There are 3 possible errors which the server can send before the handshake succeeded:
plugin errors will be sent after the handshake, since the client hello packet will be encrypted. | |||||||||||||||||||||
| Comment by Andrew Hutchings [ 2023-06-20 ] | |||||||||||||||||||||
That is fewer than I thought, which is good to know. I do think it is important to know which one of these has been hit, particularly a differentiator between the first two and the last one. At least logged somewhere for debugging / post-mortem purposes. Even if the original error code is not maintained. | |||||||||||||||||||||
| Comment by Daniel Lenski [ 2023-06-20 ] | |||||||||||||||||||||
|
georg wrote:
These cases need to be distinguished: The "too many connections" and "out of threads" errors are application-level errors: they are sent *by the server to the client. The fact that these are sent by the server before the TLS handshake happens is bad. As I described above, the client may vary its retry behavior based on exactly what error it receives, and this could enable a variety of attacks… which rapidly get much, much more serious if the client starts automatically trying to connect to different servers based on errors it receives.
TheLinuxJedi, perhaps my statements above about this previously were unclear: it's not that the client needs to hide the details of any error which it encounters before the TLS handshake. It's just that the client should hide the details of an application-level error which it appears to have received from the server before the TLS handshake (because that error could actually be from a MITM attacker). And indeed that is what my PR does in its current form: "Do not trust error packets received from the server prior to TLS handshake completion". It does not prevent the client from returning unique error codes for other conditions which are unrelated to application-layer traffic ("hostname not found", "TLS alert", "conflicting connection parameters don't make sense", etc). | |||||||||||||||||||||
| Comment by Sergei Golubchik [ 2023-06-28 ] | |||||||||||||||||||||
|
I've counted 13 errors that a server can legally send before TLS is established. Four of them can only happen before authentication (before TLS, if requested):
Others can happen before or after TLS:
Even if we only consider the first four errors, ER_CANT_CREATE_THREAD is transient and the client might want to retry, ER_HOST_IS_BLOCKED and ER_HOST_NOT_PRIVILEGED are permanent. A client cannot just ignore those errors. And in the MitM case they can be spoofed indeed. I don't see how you can possibly solve this. ER_ACCESS_DENIED can never come before TLS and a client can safely ignore it and all errors that aren't in the list above. But errors from the above list are more than enough for MitM to use. | |||||||||||||||||||||
| Comment by Daniel Lenski [ 2023-06-29 ] | |||||||||||||||||||||
serg, thanks for researching this!
You can solve this by designing the applications (client+server) and the protocol to ensure an appropriate separation of concerns between the transport layer (TLS to ensure authenticity of the server and end-to-end encryption of the communications with it) and the application layer (validating the user's credentials to access the database). If a TCP-based client-server protocol wants to use TLS, then wrapping the TCP socket in a TLS socket should be the very first thing that happens, before any communication over the socket. There shouldn't be any exchange of plaintext packets prior to the TLS handshake. Modern web browsers using TLS (especially TLSv1.3 with ECH), and VoIP apps, and TLS-based VPNs (like those supported by openconnect, which I contribute to) basically get this right, and leak no application-layer information prior to TLS establishment… but MariaDB does this quite badly and leaks a ton of information. This seems to be largely a consequence of the unstructured approach to bolting TLS on to the client and server code. The description of the connection protocol appears to have been written after-the-fact to reflect how MariaDB server and Connector/C use TLS, rather than designed in advance. Even in the happy case where the server doesn't send any pre-handshake error to the client, the server sends a "greeting" packet to the client (containing server version information) and the client sends back a login request packet (with no credentials, but with the client's charset and flags) in plaintext before the TLS handshake starts:
This makes MariaDB client-server connections an exploitable and target-rich environment for pervasive MITM attackers. A government agency could, for example, fingerprint the plaintext client+server greeting packets to determine the exact versions, pull out the ones that appear to be from interesting parts of the world based on the plaintext preferred client charset, and manipulate them in various ways with MITM and downgrade attacks using this vulnerability, as well as the long-known For all I know, the NSA or CSIC or GCHQ or יחידה 8200 or the Chinese/Iranian/Indian/Russian/$COUNTRY equivalents have already figured this out themselves, and have been MITM'ing MariaDB connections on the Internet at massive scale for years. (UPDATE: Spun this off into CONC-654 and MDEV-31585; it turns out that there's a server-side mistake in TLS setup, which makes it impossible to fix the client-side leakage without a server-side fix as well.) I've been studying the client-server protocol and implementations pretty carefully for a couple weeks now, and I'm convinced that these vulnerabilities are entirely solvable and in a backwards-compatible way, but it'd require a concerted effort to prioritize the code and design changes. | |||||||||||||||||||||
| Comment by Daniel Lenski [ 2024-01-15 ] | |||||||||||||||||||||
|
Connector/C PR #223 has now been merged to the 3.3 branch, but not yet tagged in a release. |