进阶 📋 6 个步骤 第 446 / 446 篇

一次 getattr 绕过怎么打到 RCE:给自家 MCP Python 沙箱做一次逃逸自测

受影响组件把原始 getattr 留在内置函数白名单里,攻击者能在运行时拼出双下划线名字、沿类继承链爬到进程执行入口,让代码执行工具变成任意命令执行。本教程按范围自查、隔离复现、为何字面黑名单挡不住、版本修复与三层加固的顺序,做一次可复现的沙箱逃逸自测。

2026.09.18· 18 分钟阅读· 约 2999 字· 🧯 沙箱安全 / 🔌 MCP

「代码执行型 MCP 工具」是最好用也最危险的一类工具:它让智能体能跑一段 Python 帮你算数、洗数据、试算法。安全边界全靠那个沙箱撑着。2026 年披露的 CVE-2026-53710 就是一个教科书式的反面教材——受影响的组件把原始的 getattr 留在了内置函数白名单里,攻击者于是能在运行时拼出双下划线名字,沿类继承链往上爬,一直摸到 subprocess.Popen,把「执行一段代码」变成「在本机执行任意命令」。这篇教程带你按攻击者的路径自查一遍,再把它堵上。

🧯 本教程适合:自建或自托管 MCP 服务器、且暴露了代码执行类工具的后端与安全同学。需要 Python 基础,能在隔离环境里动手验证。请只在你自己拥有、且与生产隔离的环境里跟着做。

先把这条链走一遍

漏洞的成因不复杂,但每一步都值得看清,因为它决定了你在哪一层拦得住:

环节发生了什么本可以在哪里拦住
白名单放行安全内置函数白名单里保留了原始 getattr从白名单里移除或换成受限包装
校验只查字面用字面字符串黑名单校验代码,禁的是写死的双下划线串改成语法树级别的属性访问检查
运行时拼名字代码在运行时把双下划线名字拼出来,绕过字面检查禁掉动态属性名拼接,或禁掉对双下划线的访问
爬类继承链从任意对象沿继承关系向上找,最终定位到可执行入口让沙箱里不存在可达的可执行入口
命令执行借 Popen 以服务进程的权限执行系统命令进程级隔离 + 最小权限运行账号
🔎 这一串里最容易被忽略的是第二个环节。很多沙箱的校验逻辑长得像「如果代码里出现这些危险字符串就拒绝」,看起来覆盖了所有已知写法,实际上一条运行时拼接就能绕开。黑名单永远追不上拼接,这是这类漏洞反复出现的根本原因。

Step 1:先自查你在不在受影响范围

1 四个条件一起看,别只看版本号
条件说明
组件是否受影响问题出在 python 沙箱服务器这个子项目,不影响网关与代理核心组件
版本是否低于修复版官方公告给出的修复版本是 1.0.2
传输方式HTTP / SSE 传输可能把这个工具暴露出来且不带鉴权;仅 stdio 暴露时网络可达性低很多
是否真的暴露了执行工具没挂执行类工具的服务,攻击面完全不同
# 先在受影响的仓库里定位这个子项目
#   路径形如:mcp-servers/python/python_sandbox_server/
#              src/python_sandbox_server/server_fastmcp.py

# 再看版本
grep -rn "version" pyproject.toml 2>/dev/null

# 最后确认暴露方式:HTTP / SSE 还是 stdio

版本号之外的三个条件同样重要。「低于 1.0.2」只是必要不充分条件:如果你的部署只用 stdio、且执行类工具没有对外注册,实际风险明显低于把同一套服务挂在带公网入口的 HTTP 端点上。评估优先级时要按暴露面排序,而不是按版本号一刀切。

Step 2:在隔离环境里做一次最小验证

2 验证的是「可达性」而不是「能不能搞破坏」

验证的目标只有一个:确认沙箱内是否存在一条通往进程执行的路径。确认到「能取到可执行入口的引用」就足够了,不需要真的跑命令。

# 思路(在一次性容器 / 虚拟机里执行,不要在开发机上做)
# 1) 通过执行类工具提交一段代码
# 2) 代码用运行时拼接的方式构造双下划线属性名
#    注意:这里是拼接而成的名字,不是字面量
# 3) 从任意对象出发,沿继承关系向上遍历
# 4) 检查是否能够定位到可执行入口的引用
#
# 只要第 4 步返回了引用,就说明边界是破的。
# 不需要继续执行任何命令。

做这类验证有三个纪律:环境一次性、与生产网络隔离、验证到「能证明可达」就停。把它当成给自家系统做压力测试,而不是复刻攻击。同时记得,任何复现用的代码片段都不要留在共享仓库里。

Step 3:搞清楚为什么字面黑名单挡不住

3 检查的对象错了层级

用字符串匹配去判断一段代码危不危险,检查的是文本;而真正决定行为的是这段文本被解释之后形成的结构。两者之间隔着一整层翻译过程,拼接、编码、别名、间接引用都可以在这层里完成转换。于是形成一种错觉:检查规则看起来越来越长、越来越像一个黑名单大全,但每次都能被绕过。

# 字符串检查的做法(脆弱)
if "__subclasses__" in code: 拒绝
#   只要把名字拼出来,字符串检查就失效

# 结构检查的做法(有效)
#   解析成语法树,逐个节点检查:
#     - 是否访问了名字以双下划线开头 / 结尾的属性
#     - 是否调用了不在白名单里的内置函数
#     - 是否使用了属性名字来自变量的动态访问
#   一旦命中任一条件就拒绝,而不是去匹配字符串
🧱 一句话总结这条路线的区别:白名单管「允许什么」,黑名单管「禁止什么」。在表达能力足够强的语言里,允许的集合是可枚举的,禁止的集合不是。所以沙箱校验应该长成白名单的样子。

Step 4:先把版本升上去,再复验

4 官方修复版是 1.0.2
# 1) 升级前先记录当前版本,便于回滚
# 2) 升级到官方修复版本(1.0.2)或更高
# 3) 重启服务,确认版本号已变化
# 4) 用 Step 2 的方式复验:
#     同样的验证代码这次应该取不到可执行入口的引用
# 5) 复验通过后,再去处理 Step 5、Step 6 的加固
✅ 复验这一步别省。只升级不复验,你无法排除两件事:镜像缓存导致跑的还是旧版本;以及你自己的服务里还有其他同类路径没被这次修复覆盖。

Step 5:自己加固沙箱的三层

5 白名单、语法树、最小权限

如果这个组件是你自研自维护的,或者你在做类似形态的执行型工具,下面三层建议一起上:

【层一】收紧可用的内置函数
· 从白名单里移除原始 getattr
· 确实需要按名取属性时,换成自实现包装:
  - 只接受固定的属性名白名单
  - 显式拒绝以双下划线开头或结尾的名字
  - 拒绝名字来自变量的动态访问

【层二】把校验从字符串搬到语法树
· 解析成 AST 后逐节点检查属性访问与函数调用
· 检查对象是节点的语义,不是代码里出现了哪些字符
· 命中禁用形态直接拒绝,不做字符串匹配兜底

【层三】让沙箱真的隔离
· 用低权限、与业务无关的账号运行执行进程
· 限制文件系统可见范围与出网能力
· 加执行时长与资源上限,避免变成长任务的宿主

第三层最容易被当成「以后再说」。但前面两层防的是「通过这个漏洞跑命令」,第三层防的是「万一还是跑成了」。只做前两层,等于把全部希望押在校验逻辑没有下一个绕过上;对一个专门执行第三方代码的组件来说,这个假设太强了。

Step 6:把网络面收口

6 执行类工具不该是公网可匿名调用的
· HTTP / SSE 传输必须加鉴权,不能匿名可用
· 执行类工具只对受信任的调用方开放,按调用方做限额
· 不要把执行型工具与面向公网的入口放在同一条链路上
· 日志记清「谁在什么时候提交了什么」,且不落完整代码到共享日志
· 仅内部使用、只需要本地集成时,优先用 stdio 而不是 HTTP
📌 公告里有一句判断很重要:仅 stdio 的部署网络可达性明显更低。这不是说 stdio 更安全,而是说暴露面小。选择传输方式时把它当成一个显式的安全决策,而不是顺手选一个。

这件事的通用教训

把整个事件抽象一下,得到的是三条适用于所有执行型 Agent 工具的规则。其一,沙箱的白名单必须是可枚举的——决定允许哪些内置函数时用清单,别用「除了这些之外都行」的写法。其二,校验必须检查结构而不是文本——只要还在用字符串匹配判断代码安全,就只是把绕过成本从零提到了一点点。其三,隔离层不能省——权限、文件系统、出网、时长,这四项是兜底,不是优化项。

对智能体生态来说,这件事的另一层提示在于工具本身的可信度:一个能执行任意代码的 MCP 工具,它对宿主机的信任级别接近「你在这台机器上直接跑别人的脚本」。装上之前先问清楚三件事——它怎么校验、跑在什么权限下、有没有出网能力。这三个答案比它提供多少功能重要得多。

常见问题速查

现象常见原因处理
升级后复验仍能取到可执行入口镜像缓存,实际仍是旧版本确认运行中的版本号,清缓存后重启
自研沙箱仍能被绕过校验还在做字符串匹配改为语法树节点级检查
执行类工具在公网可匿名调用HTTP / SSE 传输没加鉴权立即补鉴权并按调用方限额
不知道自己的暴露面有多大没有做过工具清单盘点列出所有执行型工具及其调用方
担心还有同类问题只修了已知点,没查同类路径按「动态属性名 + 白名单内置函数」全库搜一遍
复现脚本被误提交到仓库验证代码没做清理从历史中移除并轮换可能泄露的凭证

结语

这个漏洞值得被当成一个教学案例留下来,因为它把沙箱设计里最容易做错的那一步暴露得很清楚:把「检查危险的字符串」误当成「检查危险的行为」。这两件事看着只差一层,实际上差一整个解释过程——而攻击者要做的,正是穿过这一层。

落到行动上,顺序很清楚:先确认版本与暴露面,把已知的修复版本升上去;再复验一次证明修复真的生效;然后回头加固自己的校验与隔离层。真正长期的收益在最后一步——把「白名单可枚举、校验看结构、隔离做兜底」这三条写进你团队做执行型工具的设计规范里。下一个同类漏洞出现时,你至少不用再从零判断。

← 返回教程中心