9

我想使用 Python 的 smtplib 发送带有 Python 脚本的电子邮件。

如果可以建立与服务器的加密连接,脚本应该只发送电子邮件。要加密与端口 587 的连接,我想使用 STARTTLS。

使用一些示例,我编写了以下代码:

smtp_server = smtplib.SMTP(host, port=port)
context = ssl.create_default_context()    
smtp_server.starttls(context)
smtp_server.login(user, password)
smtp_server.send_message(msg)

msg、主机、端口、用户、密码是我脚本中的变量。我有两个问题:

  • 连接是始终加密还是容易受到 STRIPTLS 攻击(https://en.wikipedia.org/wiki/STARTTLS)。
  • 我应该使用 SMTP 对象的 ehlo() 方法吗?在某些示例中,它在调用 starttls() 之前和之后显式调用。另一方面,在 smptlib 的文档中,如果有必要,sendmail() 将调用它。

[编辑]

@tintin 解释说,这ssl.create_default_context()可能会导致不安全的连接。因此,我通过以下方式使用一些示例更改了代码:

_DEFAULT_CIPHERS = (
'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:'
'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:!aNULL:'
'!eNULL:!MD5')

smtp_server = smtplib.SMTP(host, port=port)

# only TLSv1 or higher
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
context.options |= ssl.OP_NO_SSLv2
context.options |= ssl.OP_NO_SSLv3

context.set_ciphers(_DEFAULT_CIPHERS)
context.set_default_verify_paths()
context.verify_mode = ssl.CERT_REQUIRED

if smtp_server.starttls(context=context)[0] != 220:
    return False # cancel if connection is not encrypted
smtp_server.login(user, password)

对于密码设置,我使用了一些最新版本的代码ssl.create_default_context()。这些设置合适吗?

注意:在我原来的问题的代码中是一个错误。这是相关行的正确版本:

smtp_server.starttls(context=context)

[\编辑]

4

1 回答 1

15

连接是始终加密还是容易受到 STRIPTLS 攻击(https://en.wikipedia.org/wiki/STARTTLS)。

长话短说:如果您不检查响应代码,则可以从 smtplib <=py3.5.1rc1 <=py2.7.10.starttls()

  • .starttls()使用恶意 MitM显式调用支持它的 smtp 服务器,剥离您的STARTTLS命令并伪造非220响应不会协商 ssl,也不会引发异常,因此您的通信未加密 - 因此除非您手动验证对或的响应,否则它很容易受到 striptls 的攻击.starttls()[0]==220内部.sock包装了ssl。

    这是一个 python 2.7.9 smtplib 通信,其示例与您的示例类似,该示例无法通过让服务器或 MitM 回复999 NOSTARTTLS而不是200. 没有明确检查客户端脚本中的 200 响应代码,由于 starttls 尝试失败而没有异常,因此邮件传输未加密:

    220 xx ESMTP
    250-xx
    250-SIZE 20480000
    250-AUTH LOGIN
    250-STARTTLS
    250 HELP
    STARTTLS
    999 NOSTARTTLS
    mail FROM:<a@b.com> size=686
    250 OK
    rcpt TO:<a@b.com>
    250 OK
    data
    
  • 显式调用.starttls()不支持 STARTTLS 的 smtp 服务器 - 或从服务器响应中剥离此功能的 MitM - 将引发SMTPNotSupportedError. 请参阅下面的代码。

  • 一般说明:加密还取决于配置的密码规范,即您的 SSLContext 在您的情况下由ssl.create_default_context(). 请注意,将 SSLContext 配置为允许验证但不加密的密码规范是完全有效的(如果服务器和客户端都提供/允许)。例如TLS_RSA_WITH_NULL_SHA256

    NULL-SHA256 TLSv1.2 Kx=RSA Au=RSA Enc=None Mac=SHA256

  • According to this answer python pre 2.7.9/3.4.3 does NOT attempt to enforce certificate validation for the default ssl context and therefore is vulnerable to ssl interception. Starting with Python 2.7.9/3.4.3 certificate validation is enforced for the default context. This also means, that you'll have to manually enable certificate validation for pre 2.7.9/3.4.3 (by creating a custom sslcontext) otherwise any untrusted certificate might be accepted.

Should I use the ehlo() method of the SMTP object? In some examples it is called explicitly before and after calling starttls(). On the other side in the documentation of smptlib it is written, that sendmail() will call it, if it is necessary.

  • .sendmail(), .send_message and .starttls() will implicitly call .ehlo_or_helo_if_needed() therefore there is no need to explicitly call it again. This is also

see source::smtplib::starttls (cpython, inofficial github) below:

def starttls(self, keyfile=None, certfile=None, context=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 SMTPNotSupportedError(
            "STARTTLS extension not supported by server.")
    (resp, reply) = self.docmd("STARTTLS")
    if resp == 220:
        if not _have_ssl:
            raise RuntimeError("No SSL support included in this Python")
        if context is not None and keyfile is not None:
            raise ValueError("context and keyfile arguments are mutually "
                             "exclusive")
        if context is not None and certfile is not None:
            raise ValueError("context and certfile arguments are mutually "
                             "exclusive")
        if context is None:
            context = ssl._create_stdlib_context(certfile=certfile,
                                                 keyfile=keyfile)
        self.sock = context.wrap_socket(self.sock,
                                        server_hostname=self._host)
        self.file = None
        # 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)
于 2015-11-24T10:45:52.080 回答