Closed Bug 1622640 (CVE-2020-15685) Opened 5 years ago Closed 4 years ago

IMAP Response Injection when using STARTTLS

Categories

(Thunderbird :: Security, defect, P1)

defect

Tracking

(thunderbird_esr78+ fixed, thunderbird85 fixed)

RESOLVED FIXED
86 Branch
Tracking Status
thunderbird_esr78 + fixed
thunderbird85 --- fixed

People

(Reporter: poddebniak, Assigned: gds)

References

Details

(Keywords: sec-moderate, Whiteboard: [TM:78.7.0])

Attachments

(3 files, 8 obsolete files)

User Agent: Mozilla/5.0 (X11; Linux x86_64; rv:74.0) Gecko/20100101 Firefox/74.0

Steps to reproduce:

Thunderbird has connected to an IMAP server and issued the starttls command. The server responds with "okay, let's start TLS now". However, the server (or some malicious party) also appends extra data after the starttls server response.

Example trace:

S: * OK [CAPABILITY IMAP4REV1 STARTTLS LOGINDISABLED]\r\n
C: 1 STARTTLS\r\n
S: 1 OK start TLS now\r\n* OK [ALERT] Huch?\r\n2 OK ...\r\n4 OK ...\r\n
----- <TLS handshake> --- everything after this line is encrypted -----
C: 2 capability\r\n
// Thunderbird proceeds with login, as "2 OK" was already received/buffered
C: 4 login \"alice\" \"password\"\r\n
// Thunderbird proceeds with list, as "4 OK" was already received/buffered
C: 5 list \"\" \"*\"\r\n
...

Actual results:

Thunderbird shows the alert, and also interprets both results even though they were provided via plaintext (w/o TLS.)

1 OK start TLS now\r\n* OK [ALERT] Huch?\r\n2 OK ...\r\n4 OK ...\r\n

It seems that when the server answers with multiple results attached directly after "1 OK start TLS now\r\n" response, all following responses are written to an internal buffer first. Only after that, all sockets are switched to speak TLS. However, the old responses are kept and thus end up being interpreted as if they were read via TLS.

This issue was first described by Wietse Venema (www.postfix.org/CVE-2011-0411.html). Theoretically, any other protocols supporting STARTTLS (SMTP, LDAP, ...) could also be affected.

Expected results:

Thunderbird must not interpret the additional data. Arguably, everything after "1 OK start TLS now\r\n" should be interpreted as the SERVER_HELLO TLS message. An easier to implement, but potentially breaking alternative is to discard the data.

Using the additional data allows an attacker to inject malicious responses, which are interpreted as if they came via a secure TLS connection.
I guess that this issue could in one way or another be exploited to obtain sensitive data or be very useful to mount a successful attack on STARTTLS.

Please see the attached test program for a demonstration of the issue.
You will need to provide a a certificate chain and a private key for the leaf certificate. I used the mkcert tool [1] to create a local root CA and issued a certificate for 127.0.0.1.

Thunderbird is configured as follows:
IMAP: 127.0.0.1:10143 (STARTTLS)
SMTP: 127.0.0.1:10587 (STARTTLS)

To make testing easier, the provided example server also implements the minimum SMTP functionality to pass thunderbirds account wizard. Thus, you can setup a fresh thunderbird profile (-ProfileManager) for testing.

[1] https://github.com/FiloSottile/mkcert

Summary:
Attacker injects protocol commands during the plaintext phase, which will get evaluated within the encrypted session.

Status: UNCONFIRMED → NEW
Ever confirmed: true

(In reply to Damian Poddebniak from comment #0)

Thunderbird shows the alert

How is the alert shown? WIth an interactive dialog/prompt?

Yes, I see a notification in my gnome shell. I only used the alert, because it is easier to see the issue. 2 OK and 4 OK are probably more important as they are used to answer the capability and login commands TB will send in the future. However, alerts do generally work pre-TLS and I think they maybe shouldn't. Shall I open another issue?

How did Postfix patch this? As you mentioned there are two options discussed, error out or ignore the extra data. Erroring would be clearer to everyone, but might cause compat problems. I guess either could cause compat problems, so at least the error approach would make the failure easier to diagnose.

Setting severity to "moderate" based on the CVSS I see on the old advisories, but that was 9 years ago and we might have higher standards now?

Keywords: sec-moderate

I installed mkcert using the "linuxbrew" method and made the certificate and key file for localhost, 127.0.0.1 and ::1. Then built your server using "cargo build". I then run the server like this:

response-injection-release]$ target/debug/response-injection ~/.linuxbrew/localhost+2.pem ~/.linuxbrew/localhost+2-key.pem

In tb I create the account but don't know what username and password I should use. Otherwise it seems to work. I see the STARTTLS command and response and the alert popup occurs. It tries to use "login" authentication and asks for password and I give it my usual user password and it fails and nothing more happens. Should I be able to complete the login?
(I'm on fedora 31 linux.)

Edit: I see this in the server console:

IMAP: 127.0.0.1:57544 --> 127.0.0.1:10143
S: * OK [CAPABILITY IMAP4REV1 STARTTLS LOGINDISABLED]\r\n
C: 1 STARTTLS\r\n
S: 1 OK start TLS now\r\n* OK [ALERT] This is plaintext, which should not have been interpreted. We can inject more plaintext and e.g. already answer Thunderbird's upcoming capability command.\r\n2 OK [CAPABILITY IMAP4REV1 AUTH=X1337METHOD]\r\n4 OK login seems legit, I guess?\r\n
thread '<unnamed>' panicked at 'called Result::unwrap() on an Err value: Custom { kind: InvalidData, error: AlertReceived(UnknownCA) }', src/libcore/result.rs:1188:5

I suppose the panic means I don't have somthing set up right?

Edit2: Had to import ~/.local/share/mkcert/rootCA-key.pem. Now I see:

IMAP: 127.0.0.1:57636 --> 127.0.0.1:10143
S: * OK [CAPABILITY IMAP4REV1 STARTTLS LOGINDISABLED]\r\n
C: 1 STARTTLS\r\n
S: 1 OK start TLS now\r\n* OK [ALERT] This is plaintext, which should not have been interpreted. We can inject more plaintext and e.g. already answer Thunderbird's upcoming capability command.\r\n2 OK [CAPABILITY IMAP4REV1 AUTH=X1337METHOD]\r\n4 OK login seems legit, I guess?\r\n
C: 2 capability\r\n
C: 4 login "gene@127.0.0.1" "mypassword"\r\n
C: 5 capability\r\n
thread '<unnamed>' panicked at 'called Result::unwrap() on an Err value: Custom { kind: ConnectionAborted, error: "CloseNotify alert received" }', src/libcore/result.rs:1188:5

At least now tb tries to send password but not sure why "capability" is sent twice with no response and then the server still seems to give up and no response to login.

Oh, sorry. Two things first:

  • I should't have used 127.0.0.1... please use only "localhost" to avoid other TLS errors.
  • you need to use "mkcert -install" to install the local CA in the system trust store.

If Thunderbird prompts for a password, just type anything. There is no authentication going on whatsoever.

Your trace looks like expected.

S: * OK [CAPABILITY IMAP4REV1 STARTTLS LOGINDISABLED]\r\n
C: 1 STARTTLS\r\n

until here everything is normal.

S: 1 OK start TLS now\r\n* OK [ALERT] This is plaintext, which should not have been interpreted. We can inject more plaintext and e.g. already answer Thunderbird's upcoming capability command.\r\n2 OK [CAPABILITY IMAP4REV1 AUTH=X1337METHOD]\r\n4 OK login seems legit, I guess?\r\n

The server responds with multiple responses now. Only "1 OK start TLS now" is properly defined in the spec. After this line, everything what the server send should be TLS. However, there is trailing plaintext:

  • the Alert (which is what you see in the dialog)
  • the "2 OK" response, which should only be send after TB issued the "2 capability" command and
  • the "4 OK" response, which should also only be issued after TB sends the login command.

Note: I hardcoded the values after observing that TB uses deterministic tags.

Then TB sends the following line

C: 2 capability\r\n

Normally, TB will wait for the "2 OK" response. However, it was already received before. This is why TB directly proceeds with

C: 4 login "gene@127.0.0.1" "mypassword"\r\n

For some reason my setup does not send the "5 capability", but continues with the "list" command. I used "STARTTLS/Normal password".

S: * OK [CAPABILITY IMAP4REV1 STARTTLS LOGINDISABLED]\r\n
C: 1 STARTTLS\r\n
S: 1 OK start TLS now\r\n* OK [ALERT] ...\r\n2 OK [CAPABILITY IMAP4REV1 AUTH=X1337METHOD]\r\n4 OK ...?\r\n
C: 2 capability\r\n
C: 4 login \"alice\" \"alice\"\r\n
C: 5 list \"\" \"*\"\r\n

However, I think this is not super important. The important bit is, that TB has accepted plaintext answers during an encrypted session. You can test it by removing everything after "* OK [ALERT] ...\r\n" and see that TB will wait for the capability response via TLS.

Sorry, I hope that makes sense?

I also forgot to mention how I would propose to fix this. The easiest thing you could do is to just call buffer.clear() after the client parsed the starttls and before the client continues with TLS (e.g. CLIENT_HELLO.) This solution might break in case the buffer already contains e.g. the SERVER_HELLO. But the server will not send it until the client sends its CLIENT_HELLO. So this might be easy to deploy and do not cause interoperability issues.

A better solution would be to pass any remaining data in the buffer to the TLS socket as server data. However, common TLS stream implementations likely do not have this functionaility.

Btw, I am not 100% certain, that the buffering is the core issue here. This is only an educated guess.

Also a note on severity:

We believe that we can build a PoC to obtain user credentials as MitM with this bug. However, in order to work, the server needs to behave in a certain way. (We have already reported this to a larger email provider, but most providers are luckily not affected.)

The Postfix bug is nearly a decade old and I am also not sure how critical a response injection really is in practice. What is the worst an attacker can do when injecting responses in TB? The best idea I came up with is expunging the inbox with "a begin starttls\r\n* expunge\r\n* expunge\r\n* expunge\r\n* expunge\r\n...."

Ok, I now understand that your server doesn't respond to anything after it enters the loop to receive encrypted commands (just prints the commands with "C:" prefix). I added responses in that loop to "capability", "login" and "logout" and now get all the way to the list command which is not supported in the loop.

For some reason my setup does not send the "5 capability", but continues with the "list" command. I used "STARTTLS/Normal password".

That's because of a patch for another problem that I have in my tb code right now that sends the extra capability. So without the patch I should see the same as you see, even without making a change to your server code.

I also made a change to your server to respond NO to the login command I added. This should prevent login from tb. But as you have pointed out, even with the real NO response now sent by the server, the fake "4 OK ...?" response inserted with the STARTTLS response causes the login to succeed with a completely bogus userid and password and the real NO response is ignored. This seems kind of bad.

(In reply to Damian Poddebniak from comment #8)

Also a note on severity:

We believe that we can build a PoC to obtain user credentials as MitM with this bug. However, in order to work, the server needs to behave in a certain way. (We have already reported this to a larger email provider, but most providers are luckily not affected

The Postfix bug is nearly a decade old and I am also not sure how critical a response injection really is in practice. What is the worst an attacker can do when injecting responses in TB? The best idea I came up with is expunging the inbox with "a begin starttls\r\n* expunge\r\n* expunge\r\n* expunge\r\n* expunge\r\n...."

Postfix is smtp, right? Or maybe POP3? I don't see many recent imap servers using starttls. The only one I see defaulting to it is maybe Courier.
Not sure if I understand how the expunge would work. Looks like you show it as an untagged response which would be ignored in tb I think. Oh, but maybe not if there is also a leading sequence number. This may cause the message to disappear from view in tb, but they would probably come back when the folder re-syncs with the server. Seems like it would have to appear as an imap command to actually expunge anything from the server.

Yup, Postfix is SMTP. Dovecot is POP3 and IMAP. But generally, I think that this issue can pretty much exist in any protocol with STARTTLS support, e.g. ftp, xmpp, telnet, irc, mysql, postgres, lmtp, nntp, sieve, ldap, ... Tbh, I still have to test TBs SMTP implementation for the same problem, but I have no time currently. Does TB support one of the other protocols?

I think that it is not super important if a server understands STARTTLS, as long as some client tries to speak STARTTLS. In the MitM scenario this is trivial, because a MitM attacker can just add the STARTTLS capability and maybe even block port 993. AFAIK, TB will also never automatically choose implicit TLS (port 993) via probing, even if it is available. At least the tests I conducted showed this. Maybe via Autoconfiguration (https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration)?

Anyway, as soon as the client starts to accept STARTTLS, a MitM can simply do the whole pre-TLS plaintext protocol dance and than just forward the TLS traffic (starting with CLIENT_HELLO) to imap.example.org:993. This is also something we tested and what works. (ALPN could have helped here.) So if a server supports STARTTLS is not that important.

Attached patch starttls-resp-insertion.diff (obsolete) — Splinter Review

Here's a possible fix for the STARTTLS response insertions. It does two things after TLS mode entered and before the first imap command, CAPABILITY, is sent encrypted.

  1. It drains the socket input buffer of any application data that is has not yet been read out and not yet stored in the application buffer (where IMAP parsing occurs).
  2. It clears the application buffer of any data that was read and stored after the STARTTLS OK response but before TLS mode was entered.

An assumption is that no new plaintext data can be received into the socket buffer after TLS mode is entered. (I have minimal knowledge of how the transition to TLS actually occurs in tb/moz code.)

In my initial tests using the rust IMAP server, only step 2 was necessary to fix the problem. However, I did some temporary modifications to the TB code that reads IMAP lines from the socket so that not all the socket data is read out before the CAPABILITY in TLS mode is sent and, even with step 2 code in place, the "inserted" responses were received by tb and caused problems. So both steps 1 and 2 are needed to make sure all the plaintext data sent after the STARTTLS "OK" response is discarded and ignored.

Assignee: nobody → gds
Status: NEW → ASSIGNED
Priority: -- → P1
Attachment #9136652 - Flags: feedback?(benc)

Looking at RFC 3501 it seems that everything after the server response (<tag> OK Begin TLS negotiation now\r\n) would be considered part of the StartTLS negotiation:

A [TLS] negotiation begins immediately after the CRLF at the end
of the tagged OK response from the server. Once a client issues a
STARTTLS command, it MUST NOT issue further commands until a
server response is seen and the [TLS] negotiation is complete.

So I'd guess a server which sends extra plaintext should result in a failed negotiation.

In an ideal world the TB IMAP parser would stop after the "\r\n", call StartTLS() and then, once the negotiation was complete, continue reading on the newly-secured socket. In practice, the IMAP parser will be greedy and slurp in as much data as it can buffer (otherwise you're just reading single bytes, looking for \r\n, which would suck). Because the StartTLS upgrade is handled at a lower level, the IMAP parser needs a way to feed the remaining buffered data back into the stream for the negotiation.

Back In the real world:
After the server sends it's <tag> OK Begin TLS negotiation now\r\n response, it must wait upon the client for the next step (which is, I think, the client saying which versions of SSL/TLS and ciphers it talks).
So, with that in mind, it seems perfectly fine for the client to completely discard everything after the server's OK Begin... line.

Gene's approach in comment 11 seems to be the right approach... although technically I think it wants to drain the socket and clear the TB IMAP-parser-incoming-data buffer before StartTLS() is called, rather than afterwards. But the main thing is that any dodgy data buffered up in the IMAP parser is ditched rather than used.

We should decide how we want to proceed here.

Just so you know: We have disclosed this issue to other email client developers now and most people decided to just clear an internal buffer after STARTTLS. Have you decided what to do about this? gene smiths patch looks slightly more complicated to what I expected (maybe due to how streaming is implemented in TB?), but I guess this should prohibit the injection.

(In reply to Ben Campbell from comment #12)

Looking at RFC 3501 it seems that everything after the server response (<tag> OK Begin TLS negotiation now\r\n) would be considered part of the StartTLS negotiation:

A [TLS] negotiation begins immediately after the CRLF at the end
of the tagged OK response from the server. Once a client issues a
STARTTLS command, it MUST NOT issue further commands until a
server response is seen and the [TLS] negotiation is complete.

So I'd guess a server which sends extra plaintext should result in a failed negotiation.

I think this mean that after the client (TB) has sent STARTTLS it should be quiet, IMAP wise, until negotation is complete. I don't think any extra injected data the server sends after the STARTTLS Ok response will affect the negotiations.

In an ideal world the TB IMAP parser would stop after the "\r\n", call StartTLS() and then, once the negotiation was complete, continue reading on the newly-secured socket. In practice, the IMAP parser will be greedy and slurp in as much data as it can buffer (otherwise you're just reading single bytes, looking for \r\n, which would suck). Because the StartTLS upgrade is handled at a lower level, the IMAP parser needs a way to feed the remaining buffered data back into the stream for the negotiation.

Back In the real world:
After the server sends it's <tag> OK Begin TLS negotiation now\r\n response, it must wait upon the client for the next step (which is, I think, the client saying which versions of SSL/TLS and ciphers it talks).

Yes, that's in the "Hello Client" packet. See my attached diagrams starttls-sequence-bad.jpg.

So, with that in mind, it seems perfectly fine for the client to completely discard everything after the server's OK Begin... line.

Yes, that's shown in my starttls-sequence-bad diagram. However, a malicious server or MitM could ignore the spec and send "More injections" after sending the "OK STARTTLS + Injection" and before starting negotiations. Depending on timing, this 2nd injection would never get cleared from the application buffers.

Gene's approach in comment 11 seems to be the right approach... although technically I think it wants to drain the socket and clear the TB IMAP-parser-incoming-data buffer before StartTLS() is called, rather than afterwards. But the main thing is that any dodgy data buffered up in the IMAP parser is ditched rather than used.

I think you mean the function called sslControl->StartTLS(). This starts up the TLS handshakes. The other function, which is called first and also named just StartTLS() sends the imap STARTTLS to the server. By itself this imap transaction doesn't cause any TLS transactions to occur from what I can tell. The call to sslControl->StartTLS() triggers the low level code to send the "Hello Client" to the server to begin the negotiation. So if bufferers are cleared before sslControl->StartTLS() is called, there is a small window where more injections that are never cleared can be sent by the server. I show this in the "bad" diagram.

My diagram starttls-sequence-good.jpg show my proposed approach. To avoid the problem of delayed injections, I wait until TLS mode is entered before clearing the application buffers. My assumptions are

  1. Once the client sends "Hello Client" any injections will will break the negotiations.
  2. A server that sucessfully negotiates and enters TLS mode with the client is not malicious.
  3. Clearing the application buffers of any injection after entry to TLS mode is effective.

This is why in the original diff above in comment 11 I do the clear after the sslControl->StartTLS() call.

However, looking at the sslControl->StartTLS() code, it is not clear to me exactly how to know when TLS mode is reached. Stepping with the debugger I don't see anything occur on the network when stepping over sslControl->StartTLS. I only see it when run at full speed. What I observed is that after ssl->StartTLS is called the subsequent IMAP transactions are encryped unless negotiations failed. If nego fails, nothing is sent on the network. (I forced nego fails by tweaking the tls version prefs.) But clearing the buffers right after ssl->StartTLS does not guarantee that TLS mode is yet entered since it sends nothing on the network. So in my next proposed code I do an IMAP noop before clearing buffers to ensure that TLS is entered.

I have also added a loop that will wait up to 5 seconds after the NOOP is sent (actually, just the tag is initailly send) that polls the connection security status. I'm not sure if this is really needed but it is semi-interesting. I did notice that with nothing sent before this loop that often secure state was never detected after 5 seconds or longer. With something send, it is always detected.

Just the tag "a " for the noop is initially sent so that a response from the server is not solicited. Then after the buffers are cleared the remaining noop command is sent "noop\r\n" and the response received in the normal manner and the usual imap commands continue on in TLS mode.

This works with the rust server, both Damian's original version and my modified version with more injections. I also tested this with one real imap server that uses STARTTLS connection security. I thinks it's located in Germany but not sure; it's a Dovecot server. I'm trying to locate some other test servers supporting STARTTLS but they are kind of rare.

Attached image starttls-sequence-good.jpg (obsolete) —
Attached patch starttls-resp-insertion-v2.diff (obsolete) — Splinter Review

There are a couple small N/A changes in this diff. The main change is in nsImapProtocol.cpp and is described in my previous long comment.

Attachment #9136652 - Attachment is obsolete: true
Attachment #9136652 - Flags: feedback?(benc)
Attachment #9178906 - Flags: feedback?(mkmelin+mozilla)
Attachment #9178906 - Flags: feedback?(benc)

Comment on attachment 9178905 [details]
starttls-sequence-good.jpg

I left out from the picture the "a " that is sent right before the ClearBuffer() client activity. This is need to ensure that TLS mode (secure mode) is entered before clearing the possible injected stuff. I also left out the send of "noop\r\n" right before Imap Capability is sent to complete the noop transaction.

Comment on attachment 9178906 [details] [diff] [review] starttls-resp-insertion-v2.diff Review of attachment 9178906 [details] [diff] [review]: ----------------------------------------------------------------- I'll let Ben handle reviewing this
Attachment #9178906 - Flags: feedback?(mkmelin+mozilla)

Can we register a CVE for this to have it documented? I can do that in case you do not prefer to do it yourself.

Tom, could you please assign a CVE?

Flags: needinfo?(tom)
Alias: CVE-2020-15685
Flags: needinfo?(tom)
Comment on attachment 9178906 [details] [diff] [review] starttls-resp-insertion-v2.diff Review of attachment 9178906 [details] [diff] [review]: ----------------------------------------------------------------- Sorry about the slow reply and the rambling comments below, but I don't quite have all this stuff clear in my head, so there's a degree of muddling about to clarify things! Quick summary: In general, this seems like it's heading in the right direction. I think just draining the buffer after the sslControl->StartTLS() is enough. I think that automatically provides the boundary you were intending with the NOOP. I don't think the check to see we've gone into secure mode is required (and could even be dangerous, as the negotiation is deferred until the next read/write). ::: mailnews/imap/src/nsImapProtocol.cpp @@ +1715,4 @@ > nsCOMPtr<nsISSLSocketControl> sslControl = > do_QueryInterface(secInfo, &rv); > > if (NS_SUCCEEDED(rv) && sslControl) { OK, at this point we've already sent the IMAP STARTTLS command to the server and parsed the response it sent back (and I _think_ if there's anything extra the naughty server sent, it'll be sitting in the socket receive buffer. And of course the server might also have sent more data before we call sslControl->StartTLS()... @@ +1715,5 @@ > nsCOMPtr<nsISSLSocketControl> sslControl = > do_QueryInterface(secInfo, &rv); > > if (NS_SUCCEEDED(rv) && sslControl) { > rv = sslControl->StartTLS(); Calling this tells the socket to start negotiating TLS. It doesn't necessarily happen immediately - it's usually deferred to the next read or write operation... presumably the socket keeps track of any buffered pre-TLS read/write data and starts the negotiation at an appropriate time. We never see the negotiation data, of course. @@ +1726,5 @@ > + // and stored injected responses is deleted and ignored below > + // after secure state (TLS) is verfied. This ensures that any > + // malicious injected text received before secure state is > + // reached is deleted and ignored in later imap command responses. > + SendData("a "); // Tag + Space for noop, continued below I _think_ this is the point where the negotiation is likely to actually occur. The send will block until the negotiation is performed, then the data will be sent over the new encrypted transport. I'm pretty sure we'd need to check the error code here. If the negotiation fails (eg bad certificate, self-signed, whatever), this is where we'll first hear about it. @@ +1745,5 @@ > + } > + NS_ASSERTION( > + (secState & nsIWebProgressListener::STATE_IS_SECURE) == > + nsIWebProgressListener::STATE_IS_SECURE, "tls not entered"); > + } I don't think this block is needed. The sslControl->StartTLS() marks the socket as secure, so any subsequent reads or writes will fail if the negotiation didn't produce a secure connection. The probable exception is any data waiting to be read that was received _before_ the sslControl->StartTLS() call. And the next block should drain that nicely. @@ +1770,5 @@ > + kungFuGrip = nullptr; > + > + // Discard any data previously read from socket buffer. > + m_inputStreamBuffer->ClearBuffer(); > + I'm not too sure of the exact details - I don't know if there's the possibility of the nsImapServerResponseParser still having some unprocessed pre-tls data stashed away at this point. But the general idea seems right to me: flush everything anything that might be waiting to be read! In fact, I'd probably say the NOOP isn't need - this block would be enough to trigger the negotiation, which provides the boundary you were after by issuing the NOOP. And if the read buffer was already empty, that'd be fine - that means the server didn't send any sneaky extra plain-text. In that case negotiation would probably be delayed until the next read/write: the capability command below, which would happen over the now-secure transport. @@ +1773,5 @@ > + m_inputStreamBuffer->ClearBuffer(); > + > + // Finish the noop. Tag already sent above. > + SendData("noop\r\n"); > + ParseIMAPandCheckForNewMail(); Hmm. The SendData() method stashes the string away in a member variable... I'm not sure what the implications are of a two-part Send are.
Attachment #9178906 - Flags: feedback?(benc) → feedback+

Ben, I think I understand your points. You must be right that TLS negotiation doesn't start after sslControl->StartTLS() until a send is requested. That would explain why I never saw the bit that indicated secure state unless I attempted a send of something, e.g., the "a" tag.

I think you are also saying that once sslControl->StartTLS() returns that no additional malicious injected data can arrive in the socket buffer, even though SSL negotiation may not yet have started. So we just clear the buffers and do the next normal send and I don't need to send the dummy tag and NOOP.

If that's what you mean, that's basically what I proposed in my original (now marked obsolete) diff: attachment 9136652 [details] [diff] [review].

I think if something bad did arrive between the return of sslControl->StartTLS() and the start of the negotiation handshakes (a very small window), it would cause the negotiation to fail, which is probably OK. So just clearing the application buffer (m_inputStreamBuffer->ClearBuffer()) is probably sufficient and don't need the "kungFuGrip" loop? But maybe keep it just to be safe, don't know.

I did some more testing with reporter Damian's server code. I added a sleep with a small delay after the write of the original insertion and did another insertion. Depending on the delay, this causes the additional insertion to occur either before, during or after the call to to sslControl->StartTLS(). If it occurs before or during, the inserted data looks just like the original insertion data and doesn't affect the negotiation. If it occurs after sslControl->StartTLS() call, it causes the negotiation to fail.

So it is OK to just clear the socket/stream buffer and the application buffer after sslControl->StartTLS() call as Ben recommended and it will get rid of any pre-TLS inserted data that is probably harmful.

I also noticed that when the negotiation failed due to socket data arriving during negotiation that there is a crash. It was caused by a null m_transport when calling m_transport->Close() here: https://searchfox.org/comm-central/rev/de8ef99bb6d07bdf76201be94c02f2b64cd8fbca/mailnews/imap/src/nsImapProtocol.cpp#1752. So I'll change this to check for m_transport as is done here: https://searchfox.org/comm-central/rev/de8ef99bb6d07bdf76201be94c02f2b64cd8fbca/mailnews/imap/src/nsImapProtocol.cpp#1758.

(In reply to gene smith from comment #23)

I think if something bad did arrive between the return of sslControl->StartTLS() and the start of the negotiation handshakes (a very small window), it would cause the negotiation to fail, which is probably OK.

I've been pondering this for the last hour or so and arguing myself into loops :-)
I did get a bit worried, but after trying to think through it, I'm happy again. Here's my train of thought:

The sslControl->StartTLS() is deferred - negotiation doesn't actually happen until later.
Writing is easy to reason about - if we've called StartTLS() then the new data must be sent securely.
For reading, I originally worried that the server (or MITM) could still be sending out plaintext data which our transport layer would cheerfully receive and buffer up, with no way to tell if it was secure or not. This would suck.
But now I'm less worried:

https://tools.ietf.org/html/rfc2595#section-3.1
"A TLS negotiation begins immediately after the CRLF at the end of the tagged OK response from the server."

This makes total sense, and implies that the negotiation starts for both ends at a really well defined time. And the only way for that to be sensible is if the client can treat everything after the sslControl->StartTLS() as encrypted - both reading and writing. Even though the negotiation is deferred, from a logical point of view, all read/write calls after the sslControl->StartTLS() can safely be assumed encrypted.

So, the security issue is when the server (or the MITM) sends more data directly after it's "OK" response, before the client calls sslControl->StartTLS(). Technically, this should probably fail the negotiation, but I think the way our transport layer is implemented means that it'll be likely already be sitting in our input buffer. So calling ClearBuffer() after sslControl->StartTLS() should do the trick nicely.

So just clearing the application buffer (m_inputStreamBuffer->ClearBuffer()) is probably sufficient and don't need the "kungFuGrip" loop? But maybe keep it just to be safe, don't know.

So all that about was a roundabout way to say yes, I agree with you :-) I think the ClearBuffer() on it's own will be enough. Anything new that arrives can (I think!) safely be assumed secure, because we've already called sslControl->StartTLS().

(In reply to gene smith from comment #24)

I also noticed that when the negotiation failed due to socket data arriving during negotiation that there is a crash. It was caused by a null m_transport when calling m_transport->Close() here: https://searchfox.org/comm-central/rev/de8ef99bb6d07bdf76201be94c02f2b64cd8fbca/mailnews/imap/src/nsImapProtocol.cpp#1752. So I'll change this to check for m_transport as is done here: https://searchfox.org/comm-central/rev/de8ef99bb6d07bdf76201be94c02f2b64cd8fbca/mailnews/imap/src/nsImapProtocol.cpp#1758.

Oh, beware that the m_transport in IMAP hides an m_transport in the base class! (Bug 1660714). If you've got any renaming suggestions, I'm all ears!
(I'm dithering about renaming because I don't want to change the base class one, and I don't want to use a longer name for the IMAP one!)

I only saw the error due to null m_transport when I was setting an error value on "rv" when I still was testing with my proposed code that checked the bit indicating "secure state" and it never got set due to later injected data. I'm not seeing the error now since the original code and my new proposed code only sets "rv" to an error if rv = sslControl->StartTLS() fails. I've never seen StartTLS() fail .

But I'll add the check for null m_transport in my proposed diff. (Not sure I understand the name override issues.)

Just to be clear, the point where rv is checked is here: https://searchfox.org/comm-central/rev/de8ef99bb6d07bdf76201be94c02f2b64cd8fbca/mailnews/imap/src/nsImapProtocol.cpp#1746. "rv" could also be set bad if GetSecurityInfo() fails but that has nothing to do with TLS handshakes it appears.

So all that about was a roundabout way to say yes, I agree with you :-) I think the ClearBuffer() on it's own will be enough. Anything new that arrives can (I think!) safely be assumed secure, because we've already called sslControl->StartTLS().

To see what's really going on I had to look with wireshark. When I send my additional injection after sslControl->StartTLS() returns, I'm seeing tb still send the "Client Hello" packet. The server then sends, in cleartext, my additional injection string "Inject stuff\r\n". This causes tb to respond with "Alert (Level: Fatal, Description: Record overflow)". This is handled in moz code it seems. Tb imap then goes ahead and tries to send the CAPABILITY but sees an error, per IMAP:5 log, that the connection is dropped with error code 0x805a2fe7 (SSL_ERROR_RX_RECORD_TOO_LONG according to https://james-ross.co.uk/mozilla/misc/nserror?0x805A2FE7).

I'm not sure where the cleartext "Inject stuff" string is stored when received by tb and it probably doesn't matter since it breaks the TLS negotiation. Even a single injected character kills the negotiation and drops the connection (but with a different error code).

I never see anything left in the stream/socket buffer when I attempt to "drain" it. This is because when the imap STARTTLS OK response line is parsed, nsMsgLineStreamBuffer::ReadNextLine() pulls everything out of the socket buffer and copies it to the application buffer. The only way I ever see anything to drain from the socket buffer is if I limit the number of bytes Read() to a low number like 22, the number of bytes in the server's STARTTLS OK response. So, just to be sure, I'd feel better going ahead and draining this too since there may still exist a small window for receiving additional injected data that I haven't been able to hit that stores to the normal socket buffer and doesn't break negotiations.

Attached patch starttls-resp-insertion-v3.diff (obsolete) — Splinter Review

This still includes the drain of the stream/socket buffer. Also, I still have some printf to show what/where stuff gets drained. Also, includes the check for null m_transport. I'll submit a formal patch if you think this is OK.

Attachment #9178906 - Attachment is obsolete: true
Attachment #9188744 - Flags: feedback?(benc)
Attached image starttls-sequence-good-v3.png (obsolete) —

Here's an updated sequence diagram corresponding to starttls-resp-insertion-v3.diff.

The dashed lines show the case where the inserted data arrives right after sslControl->StartTLS() takes effect. This causes the negotiation to fail and is indicated by failure due to dropped transport connection while attempting to do the imap capability request. This will, of course, inhibit entry to TLS mode and subsequent imap activity on the account.

The solid lines show the successful path where possible stored injections are deleted and subsequent encrypted imap activity occurs.

Attachment #9178905 - Attachment is obsolete: true

In ...v3.pgn, all lines appear "dashed". Maybe this looks some better. Also, corrected an error and added a few more details.

Attachment #9188767 - Attachment is obsolete: true

I made one more change to the v3 diff inspired by Ben's comment:

  •            SendData("a ");  // Tag + Space for noop, continued below
    

I think this is the point where the negotiation is likely to actually occur. The send will block until the negotiation is performed, then the data will be sent over the new encrypted transport.

I'm pretty sure we'd need to check the error code here. If the negotiation fails (eg bad certificate, self-signed, whatever), this is where we'll first hear about it.

Since I'm no longer sending the tag, the negotiation fail will now be detected on the Capability(). So to detected it I had to add a return value to Capability() instead of returning void. So if Capability() fails it will now set rv which will log a negotiation failed message below. However, a normal user will still not be notified and tb will just fail to fetch anything. So maybe some kind of alert message should pop up too if STARTTLS nego failure detected?

Attachment #9188744 - Attachment is obsolete: true
Attachment #9188744 - Flags: feedback?(benc)
Attachment #9189053 - Flags: feedback?(benc)

(In reply to gene smith from comment #31)

Created attachment 9189053 [details] [diff] [review]
starttls-resp-insertion-v3.1.diff

That all looks pretty sane to me!

Since I'm no longer sending the tag, the negotiation fail will now be detected on the Capability(). So to detected it I had to add a return value to Capability() instead of returning void. So if Capability() fails it will now set rv which will log a negotiation failed message below. However, a normal user will still not be notified and tb will just fail to fetch anything. So maybe some kind of alert message should pop up too if STARTTLS nego failure detected?

The existing StartTLS() error handling is in: nsImapProtocol::CreateNewLineFromSocket(), which the imap state parser uses to feed itself.
The CreateNewLineFromSocket() checks for read failures, and if one occurs it:

  1. pops up an alert box, for some kinds of errors (It really shouldn't!)
  2. grabs the securityInfo from the transport and stashes it on the running Url (ugh, but required), so that the onStopRunningURL() callback can access it.
  3. calls SetConnectionStatus() to set the error code on the connection (again, to pass out to onStopRunningURL()).

The onStopRunningURL() callback is where the outside code can detect and handle IMAP errors. So, for example, it can check to see if the status code is a recoverable NSS cert error (eg a server using a self-signed cert, say) and show an add-a-certificate-exception dialog box to the user. It's also where the GUI _should be popping up the alert boxes in part 1) above, but hey. One day :-)

So, to get back on topic... it's likely that correct error handling will already be performed by virtue of the Capabilty() using the standard read->parse mechanisms, which end up in nsImapProtocol::CreateNewLineFromSocket(). Off the top of my head I can't remember if the GUI-side onStopRunningURL handler pops up an alert for other unhandled errors... I suspect it might not because of 1). But that's probably a different bug.

eg a server using a self-signed cert, say)

Maybe a bit off-topic but I still have my changes for this bug present and I'm not seeing an override dialog for a self-signed cert. See bug 1678792 comment 2.

(In reply to gene smith from comment #33)

eg a server using a self-signed cert, say)

Maybe a bit off-topic but I still have my changes for this bug present and I'm not seeing an override dialog for a self-signed cert. See bug 1678792 comment 2.

Yes, that was the problem. The new return value I put on Capability() used down below caused the error to be logged and a m_transport->Close(). This must have inhibited the self-signed cert override dialog from popping up, I assume because I closed the connection.

I'm also attaching starttls-resp-insertion-v4.diff. This removes the previously added return on Capability() to avoid the problem of comment 33 above.

One thing I've noticed is that for imap when there is a non-overridable TLS error there is just complete silence. So I added an new "alert" to handle TLS errors that don't trigger an override dialog. It does work ok and pops up a general error about the problem but is not specific. I noticed that for SMTP the non-overridable error messages are very specific, e.g., "Your server has an old TLS version so fix it" etc. I guess this is part of the problem with "alerts" you talk about in comment 32. So, if what I did is not OK, I need a bit of help to fix it (if a fix is possible). Also, I'm leaving this in this diff for now but probably this alert stuff should go into a new bug.

Attached patch starttls-resp-insertion-v4.diff (obsolete) — Splinter Review
Attachment #9189053 - Attachment is obsolete: true
Attachment #9189053 - Flags: feedback?(benc)
Attachment #9189801 - Flags: feedback?(benc)
Comment on attachment 9189801 [details] [diff] [review] starttls-resp-insertion-v4.diff Review of attachment 9189801 [details] [diff] [review]: ----------------------------------------------------------------- Looks good! ::: mailnews/imap/src/nsImapProtocol.cpp @@ +4875,5 @@ > // need to do that here, NSSErrorsService::GetErrorClass() could be > // reimplemented here using easily enough using other > // publicly-accessible macros and functions. > + if (((uint32_t)rv & 0x805A0000) == 0x805A0000) { > + // It's a class 21 error (SSL), i.e., 0x45 + 0x15 = 0x5A ) I'd probably aim to use the NS_ERROR_ macros here, which I _think_ are publicly available via nsErrors.h? You should be able cut&paste the code from nsINSSErrorsService.getErrorClass implementation: https://searchfox.org/mozilla-central/source/security/manager/ssl/NSSErrorsService.cpp#112 @@ +4894,5 @@ > + case SEC_ERROR_EXPIRED_CERTIFICATE: > + case SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE: > + case SEC_ERROR_INVALID_TIME: > + case SEC_ERROR_UNKNOWN_ISSUER: > + case SSL_ERROR_BAD_CERT_DOMAIN: And for this check, even if it's not obvious `ErrorIsOverridable()` is actually available as a standalone function (mozilla::psm::ErrorIsOverridable()): https://searchfox.org/mozilla-central/source/security/manager/ssl/NSSErrorsService.h#46 @@ +4898,5 @@ > + case SSL_ERROR_BAD_CERT_DOMAIN: > + // An override dialog should occur so don't pop up an alert. > + break; > + default: > + AlertUserEventUsingName("imapTlsError"); Ugh. But yes - the Right Thing(tm) in these circumstances! <aside> My objection to these alerts in IMAP is just that it couples the IMAP code more tightly to the GUI implementation. We're already passing error codes back out via other methods (the OnStopRunningUrl(), so I think the user notification error should ultimately be moved out to there, leaving IMAP just doing IMAP stuff. The same applies for POP and other protocols. But that's all out of scope for here. </aside>
Attachment #9189801 - Flags: feedback?(benc) → feedback+

https://searchfox.org/mozilla-central/source/security/manager/ssl/NSSErrorsService.h#46

Excellent! For some reason I assumed it was private. So I'll just call it directly.

Note: The fix for TLS error notification (not security issue) is handled in Bug 1676141

This patch only addresses the security related issue identified for STARTTLS and last discussed in comment 32 and earlier in this bug report.

Attachment #9189801 - Attachment is obsolete: true
Attachment #9192880 - Flags: review?(benc)
See Also: → 1676141
Comment on attachment 9192880 [details] [diff] [review] Bug1622640-precludes-starttls-response-injection.patch Review of attachment 9192880 [details] [diff] [review]: ----------------------------------------------------------------- Looks good to me! ::: mailnews/imap/src/nsImapProtocol.cpp @@ +1739,5 @@ > + uint64_t numBytesInStream = 0; > + uint32_t numBytesRead; > + bool discarded; > + rv = m_inputStream->Available(&numBytesInStream); > + discarded = numBytesInStream != 0; Looks like `discarded` is never used.
Attachment #9192880 - Flags: review?(benc) → review+

Looks like discarded is never used.

I'll fix that. In the previous diff attachment I passed it to a printf and should have removed it too when I removed the printf.

Removed unused "discarded" variable.

Attachment #9192880 - Attachment is obsolete: true
Attachment #9193628 - Flags: review?(benc)
Comment on attachment 9193628 [details] [diff] [review] Bug1622640-precludes-starttls-response-injection-v2.patch Review of attachment 9193628 [details] [diff] [review]: ----------------------------------------------------------------- Great! If you're happy for it to be landed, just set the "checkin-needed-tb" keyword and it'll get picked up.
Attachment #9193628 - Flags: review?(benc) → review+
Status: ASSIGNED → RESOLVED
Closed: 4 years ago
Resolution: --- → FIXED
Target Milestone: --- → 86 Branch

Comment on attachment 9193628 [details] [diff] [review]
Bug1622640-precludes-starttls-response-injection-v2.patch

[Approval Request Comment]
Sec issue. Could need beta baking.

Attachment #9193628 - Flags: approval-comm-esr78?
Attachment #9193628 - Flags: approval-comm-beta?

Comment on attachment 9193628 [details] [diff] [review]
Bug1622640-precludes-starttls-response-injection-v2.patch

[Triage Comment]
Approved for beta

Attachment #9193628 - Flags: approval-comm-beta? → approval-comm-beta+

I suspect we will want more than 1 week on beta, so TM:78.7.0

Whiteboard: [TM:78.7.0]

reconfirmed, deferring to 78.7.0

Comment on attachment 9193628 [details] [diff] [review]
Bug1622640-precludes-starttls-response-injection-v2.patch

[Triage Comment]
Approved for esr78

Kaie, Needs text for CVE ?

Flags: needinfo?(kaie)
Attachment #9193628 - Flags: approval-comm-esr78? → approval-comm-esr78+

For text we could use:
During the plaintext phase of the STARTTLS connection setup, protocol commands could be injected and get evaluated within the encrypted session.

Sorry I forget what the procedure is. Do we just put it in the bug like this?
Kai's away until the end of the month.

Flags: needinfo?(kaie)

Hanno Böck send me the following information, which I'm relaying FYI:

This bug could become unrestricted soon. A bigger disclosure will be made in the next few days.

Damian, if you have worked with Hanno on this bug, would it make sense to add Hanno to CC?

Hey, could you ask Mitre to

(In reply to Damian Poddebniak from comment #53)

Hey, could you ask Mitre to

Sorry, I added Hanno to this bug. Seems like Bugzilla published the message draft I was about to post when clicking on the "Add" button in the CC field? Nevermind for now :-)

The bug has been fixed for over half a year and the paper has been released to the public.
Any objections to opening this bug up for public consumption?

Flags: needinfo?(kaie)

no objections

Flags: needinfo?(kaie)
Group: mail-core-security
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: