[issue43124] [security] smtplib multiple CRLF injection

2021-07-13 Thread Martin Ortner


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

2021-02-04 Thread Martin Ortner

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

2021-02-04 Thread Martin Ortner

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