Python沙盒逃逸实战:从CISCN 2024 Web题看Sanic框架的安全隐患
Python沙盒逃逸实战:从CISCN 2024 Web题看Sanic框架的安全隐患
最近在复盘一些CTF比赛的Web题目时,CISCN 2024国赛的一道涉及Sanic框架的题目让我对Python沙盒逃逸有了新的认识。这道题不仅考察了基础的代码审计能力,更深入到了Python对象属性污染、框架内部路由机制以及利用生成器(Generator)进行堆栈回溯的巧妙技巧。对于从事Python开发,尤其是对应用安全、代码审计感兴趣的朋友来说,这类题目提供了一个绝佳的“显微镜”,让我们能窥见在看似安全的封装之下,那些可能被利用的脆弱链条。今天,我们就抛开题解本身,深入聊聊这道题背后折射出的Python沙盒逃逸通用思路,以及Sanic这类异步框架在特定场景下可能引入的安全风险。
1. 理解战场:什么是Python沙盒与逃逸?
在开始分析具体案例前,我们得先统一语境。所谓“沙盒”(Sandbox),在安全领域指的是一种隔离的运行环境,用于执行不受信任的代码,限制其访问系统资源,防止其造成破坏。而“沙盒逃逸”(Sandbox Escape),顾名思义,就是被限制的代码通过各种手段突破隔离,获取更高的权限或访问沙盒外的资源。
在Web开发中,沙盒逃逸场景并不少见。例如,一个允许用户提交并执行部分Python代码的在线评测系统、一个支持自定义模板或插件功能的Web应用,都可能需要构建一个Python沙盒。常见的限制手段包括:
- 使用
ast(抽象语法树)模块进行危险函数/关键字的静态检查。 - 利用
restrictedpython等库创建受限的执行环境。 - 重写
__builtins__字典,移除或替换危险的函数(如open,os.system,eval,exec)。 - 设置资源限制(如运行时间、内存)。
然而,Python语言的动态性和丰富的内省(Introspection)机制,使得构建一个绝对安全的沙盒变得异常困难。攻击者的目标,往往就是寻找沙盒限制的“缝隙”,重新获取被禁用的能力。
注意:沙盒逃逸的核心思路并非“无中生有”,而是“曲线救国”。即利用沙盒内允许的、看似无害的操作,通过一系列链式调用,最终触达被禁止的危险函数或敏感数据。
2. 案例剖析:CISCN 2024 Sanic题目的攻击链拆解
虽然我们不能直接复现题目环境,但可以提炼其核心逻辑,这本身就是一个微型沙盒逃逸的绝佳样本。题目大致提供了以下场景:
一个基于Sanic的Web应用,存在一个 /admin 接口,当会话验证通过后,可以接收JSON格式的 key 和 value 参数。代码会使用 pydash.set_() 函数,根据 key 指定的路径,将一个 Pollute 类的实例的某个属性设置为 value。key 路径中不允许出现 _.。
# 简化后的核心逻辑
from sanic import Sanic
from sanic.response import text
import pydash
class Pollute:
def __init__(self):
pass
app = Sanic(__name__)
@app.route("/admin", methods=['POST'])
async def admin(request):
if is_admin(request): # 假设的权限检查
key = request.json['key']
value = request.json['value']
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value) # 关键操作
return text("success")
return text("forbidden")
攻击者的目标是通过操控 key 和 value,最终实现任意文件读取(获取Flag)。整个攻击链可以分为三个精妙的阶段。
2.1 第一阶段:属性污染与全局对象访问
pydash.set_() 是一个功能强大的工具函数,它可以根据点分路径字符串(如 'a.b.c')来设置嵌套对象的属性。题目限制 key 中不能出现 _.,这主要是为了防止直接访问 __ 开头的魔术方法(如 __class__)。但攻击者可以通过转义绕过,例如使用 \\. 来表示一个真正的点号。
攻击的起点是 Pollute 类的实例 pollute。在Python中,任何一个对象都通过 __class__ 属性指向其类对象。通过 __class__,我们可以访问类的 __init__ 方法,进而通过 __globals__ 属性访问到该方法定义时所在的模块的全局命名空间。
# 攻击payload思路
# key: "__class__\\.__init__\\\\.__globals__"
# value: 任意值,先建立访问链
这一步的目的是将 pollute 对象的 __class__.__init__.__globals__ 属性指向一个我们可控或已知的模块全局字典。但这里更常见的是利用它作为跳板,去访问应用本身(app)的全局变量。
2.2 第二阶段:深入框架内部:Sanic的路由与静态文件处理器
Sanic框架的路由系统维护着一个路由字典。攻击者通过精心构造的 key,可以遍历到这个字典,并定位到用于处理静态文件请求的 StaticRoute 对象。这个对象内部有一个 handler,而 handler 又包含 keywords 等属性,其中可能引用了处理静态目录的视图函数及其参数。
# 更深入的payload示例(路径已简化示意)
# key: "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index...static.handler.keywords.directory"
# value: "/" # 将静态文件处理器的目录改为根目录
通过污染这些内部属性,攻击者可以改变Sanic静态文件处理器的行为,例如将其服务的根目录从 ./static 更改为系统根目录 /。这就为后续的文件读取打开了大门。这种攻击方式本质上是一种“对象属性污染”(Object Property Pollution),通过修改程序内部状态来达到非预期的效果。
下表概括了这一阶段利用的关键对象和属性:
| 攻击目标 | 属性路径(示例) | 污染目的 |
|---|---|---|
| 静态文件处理器目录 | ...directory_handler.directory |
将静态文件服务的基础目录修改为系统根目录 /,从而可以访问任意文件。 |
| 目录视图开关 | ...directory_handler.directory_view |
启用目录列表功能,方便浏览目录结构,寻找目标文件。 |
| 文件/目录判断逻辑 | ...file_or_directory |
可能影响路由匹配逻辑,辅助攻击。 |
2.3 第三阶段:利用生成器(Generator)进行堆栈回溯
这是本题最精彩的部分。在完成静态目录篡改后,理论上可以通过 /static/../../etc/passwd 这类路径遍历来读取文件。但题目可能还有别的限制,或者Flag文件路径未知。此时,攻击者需要一种在沙盒限制下获取更多信息(甚至是执行代码)的能力。
他们巧妙地利用了Python的生成器(Generator)。生成器函数在 yield 时会暂停,其对应的帧对象(Frame Object)会被保留。通过生成器的 gi_frame 属性,可以访问到这个帧对象,进而通过 f_back 属性回溯调用栈。
def backdoor():
# 这是一个生成器函数
yield backdoor.gi_frame.f_back # 返回自己的帧对象的上一级调用帧
g = backdoor()
frame = next(g) # 此时frame是调用next(g)的那个作用域的帧
通过多次调用 frame.f_back,攻击者可以像爬梯子一样,回溯到更早的调用上下文,最终可能到达一个包含丰富全局变量(如 __builtins__)的帧。一旦获取到 __builtins__,就相当于拿到了Python的“万能钥匙”,可以调用 eval, exec, open, __import__ 等所有内置函数。
# 利用生成器回溯获取builtins的简化exp
def exp():
def gen():
yield gen.gi_frame.f_back
g = gen()
frame = next(g)
# 假设通过若干次f_back回溯到了目标帧
target_frame = frame.f_back.f_back.f_back
builtins = target_frame.f_globals.get('__builtins__')
if builtins:
# 现在可以执行任意代码了
builtins.eval("__import__('os').system('id')")
在实际的题目利用中,攻击者将这段利用代码作为 value 传入,通过属性污染链将其写入某个可被后续请求触发执行的上下文(例如一个路由处理函数的默认参数),从而完成最终的逃逸和命令执行。
3. 从CTF到实战:Python沙盒逃逸的常见模式
这道CTF题目像是一个“教科书式”的复合利用案例。从中我们可以总结出Python沙盒逃逸的几种常见模式,这些模式在真实世界的漏洞利用中同样有效。
3.1 模式一:利用内省与反射机制
Python的 getattr(), setattr(), __dict__, __class__, __bases__, __subclasses__() 等机制,为对象遍历和类继承链的探索提供了便利。沙盒逃逸的经典起手式就是从某个沙盒内允许访问的对象出发,沿着这些属性“爬”到包含危险函数的目标对象。
# 从一个简单的对象开始,寻找可用的子类,最终找到os模块
for subclass in object.__subclasses__():
if subclass.__name__ == 'catch_warnings':
for subsub in subclass.__init__.__globals__.values():
if isinstance(subsub, type(__builtins__)):
# 可能找到了一个包含危险函数的命名空间
break
3.2 模式二:污染框架或库的内部状态
正如Sanic题目所示,现代Web框架为了灵活性和性能,会在内存中维护复杂的内部状态(路由表、配置、中间件链、单例对象等)。如果存在类似 pydash.set_() 这种支持深度赋值的功能,且输入可控,就可能篡改这些内部状态,导致权限绕过、路径遍历、甚至远程代码执行。
需要警惕的库和模式包括:
- 深度合并/赋值库:如
pydash,lodash(JavaScript中常见,Python也有类似实现),其set,merge,defaultsDeep等方法。 - 反序列化漏洞:
pickle,PyYAML的yaml.load,都可能被用来构造恶意对象污染应用状态。 - 模板注入:Jinja2, Mako等模板引擎,如果允许用户控制模板内容,可能绕过沙盒执行代码。
3.3 模式三:利用Python执行环境的特性
生成器回溯只是其中一例。其他特性还包括:
- 装饰器与闭包:通过
__closure__属性访问闭包变量,可能找到有用的数据或函数引用。 - 异常对象:异常对象的
__traceback__属性同样包含了丰富的堆栈信息,可以用于回溯。 - Code对象:通过
__code__属性可以访问函数的字节码和常量表,有时常量表中会硬编码敏感信息。
4. 防御之道:如何构建更安全的Python执行环境?
了解了攻击手段,防御思路也就清晰了。对于需要执行不可信Python代码的场景,没有银弹,必须采取纵深防御策略。
1. 使用经过严格审计的专用沙盒
- PyPy的沙盒:PyPy解释器提供了一个可选的沙盒功能,在操作系统层面进行隔离,理论上更安全,但配置复杂。
- gVisor / nsjail:考虑不在语言层面,而是在容器或系统调用层面进行隔离。将不可信代码放在一个高度限制的容器中运行。
2. 如果必须自己实现,遵循最小权限原则
- 彻底清空并重建
__builtins__:不要只是删除open、eval,应该创建一个全新的、仅包含白名单函数的__builtins__字典。safe_builtins = { 'len': len, 'range': range, 'print': print, # 谨慎考虑是否允许print # ... 仅添加明确需要的函数 } exec(user_code, {'__builtins__': safe_builtins}, {}) - 使用
ast进行静态分析:在执行前,解析代码的抽象语法树,禁止导入(Import,ImportFrom)、危险函数调用、属性访问模式(如访问__开头的属性)等。import ast class SecurityVisitor(ast.NodeVisitor): def visit_Import(self, node): raise SecurityError('Import statements are not allowed') def visit_Attribute(self, node): if node.attr.startswith('_'): raise SecurityError(f'Access to private attribute {node.attr} is forbidden') self.generic_visit(node)
3. 对用户输入进行严格限制和过滤
- 对于像
pydash.set_()这样的功能,必须严格校验key路径。不仅过滤_.,最好采用白名单机制,只允许设置预定义的、安全的属性路径。 - 永远不要相信用户输入的代码或配置能直接用于修改程序核心状态。
4. 运行在隔离的进程中并设置资源限制
- 使用
subprocess或multiprocessing在独立进程中运行不可信代码。 - 使用
resource模块或操作系统工具(如ulimit,cgroups)限制其CPU时间、内存用量和运行时间。 - 使用
chroot或文件系统命名空间限制其文件访问范围。
安全是一个持续的过程,而非一劳永逸的状态。这道CISCN题目再次提醒我们,在追求开发效率和功能强大的同时,必须对底层机制可能带来的安全风险保持敬畏。每一次成功的沙盒逃逸,都是对开发者安全意识的一次深刻拷问。
更多推荐



所有评论(0)