eval() 远程代码执行(实验室)一个自包含的实验室,复现 CVE-2026-26030:通过 Microsoft Semantic Kernel(Python,< 1.39.4)中的内存向量存储搜索过滤器实现提示注入远程代码执行。
仅供伦理实验室使用。在专用 virtualenv 中隔离;payload 无害(在无头 PoC 中
touch标记文件;在 UI 演示中open -a Calculator),并以您自己的非特权用户身份运行。
./setup.sh # 构建两个隔离的 venv(1.39.3 易受攻击,1.39.4 已修补)
需要 python3.13(SK 的 numpy/scipy 依赖的 wheel)。可通过 PYTHON= 覆盖。
./run.sh # 对两个 venv 运行相同的 payload
易受攻击的输出以 RCE CONFIRMED 结尾;已修补的输出拒绝相同的 payload,显示 '__subclasses__' ... is not allowed。
OpsBot,一个内部工程知识库代理(通过 Semantic Kernel 使用 Groq 托管的 Llama),暴露了一个 search_runbooks(team) 工具。攻击者提示注入一个恶意的 team 值;代理调用该工具,易受攻击的过滤器运行,并在 TextEdit 中打开一张“勒索信”——而代理则不知不觉地报告 runbook 结果。Payload(open -e <note>)非阻塞且无害;该信预先放置在 /tmp/PWNED_by_CVE-2026-26030.txt。
该工具在一个干净的子进程(run_filter.py)中运行真正易受攻击的过滤器 eval。这是 macOS 的必需措施,而非作弊:服务器自身的 fork() 被异步 LLM/httpx 线程 + numpy/scipy 干扰,因此从该进程启动 GUI 会静默无操作。子进程正是 CVE 代码路径(_parse_and_validate_filter → 在记录上运行 lambda)。
注意:干净子进程拆分是此 macOS 演示工具的怪癖,而非漏洞本身。在 Linux 部署的代理上,进程内
os.system直接触发——无需子进程。
echo 'GROQ_API_KEY=gsk_...' > .env # 从 console.groq.com/keys 获取免费密钥
./demo-ui/run.sh # http://127.0.0.1:8000
攻击者消息已预加载到输入框中;按发送。
一个代理暴露了一个由 InMemoryCollection 支持的搜索工具。LLM 从对话中发出一个过滤器表达式字符串(lambda x: x.team == 'platform')。该字符串可受攻击者影响——通过用户提示,或通过检索/工具内容中的注入文本——并进入 _parse_and_validate_filter,该函数对其执行 compile() 和 eval()(connectors/in_memory.py:383):
code = compile(tree, filename="<filter>", mode="eval")
func = eval(code, {"__builtins__": {}}, {}) # nosec
__builtins__ 被清空,并且首先应用了 AST 允许列表——所以这是一个沙箱绕过,而非缺少防护。两个漏洞使其可被绕过:
ast.Attribute 访问不受限制——没有双下划线禁止列表,因此允许 ().__class__.__base__.__subclasses__ 双下划线遍历。ast.Call 的名称检查仅在 func 是 Name 或 Attribute 时检查 func。当 func 是 Subscript(而它是被允许的)时,func_name 保持 None,并且完全跳过允许函数检查。因此,将任何可调用对象包装为 [obj.method][0](https://github.com/inertfluid/sk-cve-2026-26030-lab/blob/main/args) 即可调用任何内容。链式调用:
object.__subclasses__()[i] -> BuiltinImporter.load_module('os') -> os.system(cmd)
lambda x: [[[().__class__.__base__.__subclasses__][0]()[107].load_module][0](https://github.com/inertfluid/sk-cve-2026-26030-lab/blob/main/%27os%27).system][0]('touch /tmp/pwned_by_filter')
每次调用的 func 都是一个 Subscript;每一步遍历都是简单的属性访问。索引(107)是 BuiltinImporter 在 object.__subclasses__() 中的位置——它因 Python 构建而异,因此 exploit.py 和演示在运行时计算它。
该补丁在允许列表之上添加了一个危险属性禁止列表,因此双下划线遍历在 eval 之前被拒绝:
Access to attribute '__subclasses__' is not allowed in filter expressions.
This attribute could be used to escape the filter sandbox.
| file | purpose |
|---|---|
setup.sh | 构建两个隔离的 venv(易受攻击 + 已修补) |
run.sh | 在两种 venv 上运行无头 PoC |
exploit.py | 端到端:真实集合 + 搜索过滤器 -> RCE |
find_sink.py | 在已安装的包中定位 eval/compile 接收点 |
probe.py | 仅验证器的绕过最小确认 |
demo-ui/app.py | FastAPI + SK + Groq 代理;易受攻击的 search_runbooks 工具 |
demo-ui/run_filter.py | 在干净的子进程中运行真正的过滤器 eval(服务器 fork() 被干扰) |
demo-ui/index.html | 聊天 UI;在 RCE 时在 TextEdit 中打开勒索信 |
关于代理安全性的最清晰论述:“数据”与“指令”之间没有界限。模型根据用户的 runbook 查找编写的过滤器变成了 os.system。沙箱存在——一个允许列表和清空的 __builtins__——但仍然被属性遍历 + 下标调用绕过。在文章中值得提出的缓解层次:根本不要 eval 模型输出;如果必须,在封闭语法上限制,而不是在具有开放属性访问的节点允许列表上限制;并隔离工作进程(seccomp / 无网络 / 非特权),这样代码执行就不会导致游戏结束。