Note: This is all written for Python 2.7.3. These details might be different in other versions of Python - especially 3+!
Attempting to escape a sandbox is always a fun challenge. Python sandboxes are no exception. In a static language, this is usually done by analyzing the code to see if certain functions are called, or wrapping the dangerous functions with code that does validation. However, this is a bit more challenging in dynamic languages like Python.
A simple approach to sandboxing is to scan the contents of the script for certain keywords or functions that are dangerous, like eval, exec, execfile, and import. That can easily be bypassed by encoding your script. PEP-0263 goes into the details, but as long as you have # coding:<encoding>
on one of the first two lines in the script, the Python interpreter will interpret that entire script with that encoding.
Obviously, a better method is needed. But before we go into that, some background:
dir
is your first tool for examining python objects. To quote the docs: “Without arguments, return the list of names in the current local scope. With an argument, attempt to return a list of valid attributes for that object.”
It doesn’t claim to be complete, and it’s possible for a class to define a __dir__
method, but for now we can assume it will be accurate.
The second function I often use is type
. Simply enough, with a single parameter, it gives you the type of an object - this can be useful for understanding what something is, once you know about it’s existence. Again, to refer to the docs: “Return the type of an object. The return value is a type object.”
So now let’s start poking.
When execution begins, the following objects are in the local scope (yay dir()
!):
From these, __builtins__
is most interesting.
Hmm, let’s see the Python language reference:
“A module object has a namespace implemented by a dictionary object … Attribute references are translated to lookups in this dictionary, e.g., m.x
is equivalent to m.__dict__["x"]"
Now, we can examine the builtins simply by running dir(__builtins__)
That list is a little long. The entries are all of the builtin types and functions.
So now let’s revisit the previous string-checking method for sandboxing.
Perhaps you don’t have the ability to change the encoding for the entire file.
You can still encode the name of a single function call by accessing the module’s underlying dict, and then using a variable to access the needed function.
So let’s call import os
in a slightly sneakier way, using the builtin function __import__
:
First get the base64 version of the strings "__import__"
and "os"
:
Putting it together:
Hmm, so using text filtering to sandbox code is clearly out.
Perhaps we can take another approach at filtering, based on using __builtins__.__dict__
.
Since __builtins__.__dict__
is a dictionary that represents all of the builtin functions available to our environment, if we modify one of those entries we can change what’s available for the environment.
For example, the abs
function returns the absolute value of a number:
Now, let’s play with it a bit:
The del
statement deletes a reference to an object:
We just removed the ability for the environment to call abs
!
So now we have another approach to Python sandboxing - remove access to many ‘dangerous’ builtins.
Let’s make a small list of dangerous functions:
Hmm, this looks a bit safer, right?
Oh wait!
reload(module)
reloads an imported module, and executes the code - so the module gets imported back into our namespace.
I guess we’ll have to add that to the list.
OK. So now we have a safe approach, right? We prevented anyone in our sandbox from using a dangerous builtin, and we can prevent them from using the eval keyword by not allowing them to encode the entire file and scanning the contents. Hopefully we removed all of the dangerous builtins…
Let’s take a challenge from Hack.lu’s CTF. In their challenge, you need to read the contents of a file, ‘./key’.
They first destroy the builtin functions for opening a file by deleting the references. Then they allow you to execute user input. Take a look at a slightly-modified version of their code:
Encoding tricks won’t work - we don’t have a reference to file
or open
in the __builtins__
. But perhaps we can dig up a reference to file
or open
from another location in the interpreter and use that instead?
Let’s dig a bit.
(For this section I need to give credit to Ned Batchelder. I was reading his blog this past summer, and he has a nice writeup about how eval
is an extremely dangerous statement, and he goes through this method in his code.)
As mentioned earlier, running type
on an object returns a type object
.
For example:
Now, let’s start examining the fields of a tuple:
Most of these are functions. But __class__
is a bit interesting. It returns the class representing the object.
This gets into metaclass and metatype details. That’s all in PEP 0253. Let’s ignore that for now and dig a bit deeper.
According to the docs, new-style classes have some special attributes. Specifically, __bases__
, which contains “the base classes, in the order of their occurrence in the base class list” and class.__subclasses__()
, which returns a list of all of the subclasses.
Let’s examine our tuple.
It inherits directly from object. I wonder what else does:
We have everything we need right here!
Then I was able to find the index of file
in the list with a few simple lines:
We can’t use this code (even rewritten as a list comprehension) in the challenge, because it includes whitespace. But since file
is at index 40, we can hard-code that.
Once we have a reference to the file, all we need to do is create a file object and read it:
So to solve the challenge:
moshe@moshe-desktop:~$ netcat ctf.fluxfingers.net 2045
Go Ahead, Expoit me >;D
().__class__.__bases__[0].__subclasses__()[40]("./key").read()
().__class__.__bases__[0].__subclasses__()[40]("./key").read()
Return Value: FvibLF0eBkCBk
On a side note, we didn’t need to do it in a single statement - exec
runs code in the current context, so we could just as easily maintain state across commands by storing the output of each command in a variable (besides a
).
moshe@moshe-desktop:~$ netcat ctf.fluxfingers.net 2045
Go Ahead, Expoit me >;D
x=23
x=23
Return Value: 23
45
45
Return Value: 45
x
x
Return Value: 23
Further Reading:
-
Another bit of magic that could be useful: http://www.reddit.com/r/Python/comments/hftnp/ask_rpython_recovering_cleared_globals/c1v372r
-
Ned Batchelder’s blog post: http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html