Anatomy of a Crypto Vulnerability

by alex_gaynor

Before I describe the vulnerability, I want to give huge thanks to Ben Bangert and Alessandro Molina for quickly responding to my report, and to Paul Kehrer for reviewing and confirming my findings.

Sessions are a core part of many web applications. Put an opaque identifier (e.g. a UUID) in a cookie, then in your web app find the session in a database of some sort. The session might contain data like the currently logged in user, whatever.

For a variety of reasons, sometimes you don't want to have the database on your backend. Instead you want to put the session data directly into a cookie. In those cases, generally you want to ensure your cookie has two properties: integrity and confidentiality. Integrity means a user should not be able to change the contents of their session (e.g. to change what user_id they're logged in as), and confidentiality means they should not be able to see the contents of their session1.

Beaker is a popular Python library for managing sessions. I was looking at it for reasons that I've completely forgotten at this point. Specifically, I was looking at its crypto. Beaker offers encrypted sessions, and I wanted to see how they were implemented.

Digging around a bit, I found an aesEncrypt function2:

def aesEncrypt(data, key):
    cipher = AES.new(key, AES.MODE_CTR,
                     counter=Counter.new(128, initial_value=0))

    return cipher.encrypt(data)

Ok, AES in CTR mode. The first thing that jumped out to me is that the nonce (or counter here) is always b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'. CTR mode for block ciphers has one requirement: you must never reuse a (key, nonce) pair. If you reuse a pair, then any data encrypted under that pair becomes decryptable by the attacker. Since our nonce is constant, that means we must have a unique key for every encryption, or confidentiality is totally broken.

Let's look to see where our key comes from.

A quick grep for aesEncrypt(3 turns up:

nonce = b64encode(os.urandom(6))[:8]
encrypt_key = crypto.generateCryptoKeys(self.encrypt_key,
                                        self.validate_key + nonce, 1)
data = util.serialize(session_data, self.data_serializer)
return nonce + b64encode(crypto.aesEncrypt(data, encrypt_key))

What's happening here? First we generate a 6-byte nonce from /dev/urandom and base64 encode it (note that this is different from the nonce in aesEncrypt).

Then we call generateCryptoKeys4. What does it do?

def generateCryptoKeys(master_key, salt, iterations):
    return pbkdf2(master_key, salt, iterations=iterations, dklen=keyLength)

Doing a little substitution, this means that encrypt_key is derived by:

pbkdf2(self.encrypt_key, self.validate_key + nonce, iterations=1, dklen=32)

self.encrypt_key and self.validate_key are constant in this context. This means that if our nonce repeats, then the result of generateCryptoKeys repeats, which is passed to aesEncrypt as key.

If nonce here is 6 bytes, which are base64 encoded, how often does it repeat? We can ignore the base64 encoding since it doesn't affect the entropy. 6 bytes of random data means 48 bits. So 2 ** 48 possible values. The birthday paradox means that we can, with 50% probability, expect a repeat after sqrt(2 ** 48) (or 2 ** 24) values. 2 ** 24 is a little under 17 million.

In short, if we observe 17 million cookies, there's a 50% chance we'll be able to decrypt a pair of them. That may not sound like much, but it's completely unnecessary: we know how to build more secure cryptographic systems. It's particularly bad for a general purpose library, where we have no idea how important the data being protected is.

The immediate-term solution is for Beaker to use a larger nonce size, which decreases the frequency of repeat values. A better long-term solution is to move away from custom cryptographic code and use safer, preprepared recipes, such as NaCL's secret_box or Fernet (available for Python in PyNACL and pyca/cryptography), which have received extensive review from cryptographers and offer safer APIs.

[1]For some applications confidentiality isn't actually important. But if you're going to offer it, you should do it correctly.
[2]https://github.com/bbangert/beaker/blob/8cc7e316df2ac90ea3f75db4052212c192376dec/beaker%2Fcrypto%2Fpycrypto.py#L20-L24
[3]https://github.com/bbangert/beaker/blob/8cc7e316df2ac90ea3f75db4052212c192376dec/beaker/session.py#L259-L270
[4]https://github.com/bbangert/beaker/blob/8cc7e316df2ac90ea3f75db4052212c192376dec/beaker%2Fcrypto%2F__init__.py#L39-L44

Hi, I'm Alex. I'm a software engineer at Mozilla, working on Firefox security. Before that I was a software engineer with the U.S. Digital Service. I'm an avid open source contributor and live in Washington, DC.