Anatomy of a Crypto Vulnerability
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 session 1.
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
function 2:
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 generateCryptoKeys
4. 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.
-
For some applications confidentiality isn’t actually important. But if you’re going to offer it, you should do it correctly. ↩︎
-
https://github.com/bbangert/beaker/blob/8cc7e316df2ac90ea3f75db4052212c192376dec/beaker%2Fcrypto%2Fpycrypto.py#L20-L24 ↩︎
-
https://github.com/bbangert/beaker/blob/8cc7e316df2ac90ea3f75db4052212c192376dec/beaker/session.py#L259-L270 ↩︎
-
https://github.com/bbangert/beaker/blob/8cc7e316df2ac90ea3f75db4052212c192376dec/beaker%2Fcrypto%2F__init__.py#L39-L44 ↩︎