Plone 未认证 RCE,6.2.3 已修复(Hotfix 20261110)
五个缺陷组成的利用链让匿名访问者可在 Plone 6.0 至 6.2 及更早版本上执行代码;11 月 10 日的更新修复了该问题。
TL;DR
- 五个缺陷组成的利用链让匿名访问者可在 Plone 站点上执行代码。版本管理修饰器类在未声明权限的情况下公开了 edit 方法,四个软件包又把整个 Python 模块开放给受限代码。
- 所有受支持的版本均受影响:Plone 6.2、6.1 和 6.0,以及通过修复包覆盖的可追溯至 4.3 的旧版本。
- 主要缺陷评为 CVSS v3.1 9.8 严重、CWE-862。截至 2026 年 11 月 11 日,尚未分配 CVE,未发现实际利用,也未列入 CISA 的 Known Exploited Vulnerabilities 目录。
- 将五个软件包更新到修复版本,或安装 Products.PloneHotfix20261110 并重启每个 Zope 实例。参见“如何修复?”。
我受影响吗?
必须同时满足以下条件:
- 运行 Plone 6.2、6.1 或 6.0,或可追溯至 4.3 的旧版本。公告列出了所有受支持的版本,并说明旧版本同样受影响。
- 站点安装了
Products.CMFEditions,即提供portal_modifier工具的版本管理包。Plone 默认包含它。 - 攻击者可通过 HTTP 访问站点。入口是
portal_modifier对象,Zope 的获取机制使它在任意文件夹下都可访问。
该利用链无需账号即可工作。其中一个缺陷还会在编辑者保存受版本管理的内容时执行,因此未直接暴露的站点仍保留内部路径。供应商表示完整的一组软件包更新是推荐的修复方式;修复包覆盖不再接收软件包更新的旧版本。
官方 plone/plone-backend 容器镜像在 Docker Hub 上有 486,034 次拉取,这反映了运行该技术栈的部署规模。
| 分支 | 受影响版本 | 修复后的软件包组合 |
|---|---|---|
| Plone 6.2 | 6.2.3 之前的全部版本 | Products.CMFPlone 6.2.3、Products.CMFEditions 5.0.2、Products.MimetypesRegistry 4.0.2、plone.batching 3.0.3、plone.restapi 10.2.1 |
| Plone 6.1 | 6.1.6 之前的全部版本 | Products.CMFPlone 6.1.6、Products.CMFEditions 4.0.7、Products.MimetypesRegistry 3.0.2、plone.batching 2.0.8、plone.restapi 9.15.8 |
| Plone 6.0 | 6.0.16 之前的全部版本 | Products.CMFPlone 6.0.16、Products.CMFEditions 4.0.7、Products.MimetypesRegistry 3.0.2、plone.batching 2.0.8、plone.restapi 9.15.8 |
| Plone 4.3 至 5.2 | 全部版本 | 无软件包更新;安装 Products.PloneHotfix20261110 |
在运行 Zope 的虚拟环境或 buildout 中检查已安装的软件包:
pip show Products.CMFPlone Products.CMFEditions Products.MimetypesRegistry plone.batching plone.restapi
# Buildout: print the pinned versions
bin/buildout annotate | grep -E 'Products.CMFPlone|Products.CMFEditions|Products.MimetypesRegistry|plone.batching|plone.restapi'
# Constraints or requirements files used to build the instance
grep -R -E 'Products.CMFPlone|Products.CMFEditions|Products.MimetypesRegistry|plone.batching|plone.restapi' constraints.txt requirements.txt buildout.cfg
Plone 站点根目录下新增的 @@check-modifier-conditions 页面是查看修饰器条件是否被修改的最快方式。以 Manager 身份登录并打开 https://your-site/@@check-modifier-conditions。它会把当前条件与默认值比较,并提供恢复选项。
如何修复?
更新对应 Plone 分支那一行中的每个软件包,然后重启所有 Zope 实例。如果运行旧版本,则改为安装修复包。
- 在 Plone 6.2 上安装修复组合:
pip install -U "Products.CMFPlone==6.2.3" "Products.CMFEditions==5.0.2" "Products.MimetypesRegistry==4.0.2" "plone.batching==3.0.3" "plone.restapi==10.2.1" - 在 Plone 6.1 上安装修复组合:
pip install -U "Products.CMFPlone==6.1.6" "Products.CMFEditions==4.0.7" "Products.MimetypesRegistry==3.0.2" "plone.batching==2.0.8" "plone.restapi==9.15.8" - 在 Plone 6.0 上安装修复组合:
pip install -U "Products.CMFPlone==6.0.16" "Products.CMFEditions==4.0.7" "Products.MimetypesRegistry==3.0.2" "plone.batching==2.0.8" "plone.restapi==9.15.8" - 在 Buildout 中,把相同版本固定到
[versions]段并重新运行bin/buildout。在约束文件中使用双等号形式,例如Products.CMFPlone==6.2.3。 - 对于 4.3 至 6.2 中无法接收软件包更新的任何版本,安装修复包并重启每个 Zope 实例:
pip install Products.PloneHotfix20261110修复包支持 Python 2.7 和 Python 3。如果软件包索引找不到该拼写,请使用规范化名称products-plonehotfix20261110。 - 重启所有 Zope 实例,而不只是第一个。修复在进程启动时加载。
如果暂时无法更新,请在反向代理处阻止所有包含 portal_modifier 的 URL。Zope 的获取机制使该对象在任意文件夹下都可访问,因此要在路径的任意位置阻止,而不仅在站点根目录。
- 在 nginx 中,在兜底 location 之前添加正则 location:
location ~ portal_modifier { return 403; } - 在使用
mod_rewrite的 Apache 中,在代理规则之前添加规则:RewriteEngine On RewriteRule portal_modifier - [F,L] - 更新后保留该规则。预公告建议保留,而且没有额外成本。
代理规则只是临时措施。Apache、nginx 和 Traefik 在匹配前会解码百分号编码,因此 /%70ortal_modifier 也会被拦截。Varnish 不解码路径,因此其规则还需覆盖编码形式。如果另一条路径能到达受限代码,该规则并不能修复沙箱逃逸。
根本原因是什么?
利用链的起点是 Plone 的版本管理修饰器从未声明谁可以调用其方法。修饰器是包装版本管理操作的 SimpleItem。只要没有安全声明保护,Zope 就会公开发布带 docstring 的方法。ConditionalModifier.edit 和 ConditionalTalesModifier.edit 没有任何声明,因此匿名访问者可以调用它们。
5.0.1 中 Products/CMFEditions/Modifiers.py 的易受攻击类如下:
class ConditionalModifier(SimpleItem):
"""This is a wrapper for a modifier."""
modifierEditForm = PageTemplateFile(
"www/modifierEditForm.pt", globals(), __name__="modifierEditForm"
)
def edit(self, enabled=None, title="", REQUEST=None):
"""See IConditionalModifier."""
self.title = title
if enabled is not None and (
enabled == "True" or (isinstance(enabled, bool) and enabled)
):
self._enabled = enabled
else:
self._enabled = False
ConditionalTalesModifier 继承它并添加条件:
class ConditionalTalesModifier(ConditionalModifier):
"""This is a wrapper with a tales condition for a modifier."""
def edit(self, enabled=None, condition=None, title="", REQUEST=None):
"""See IConditionalTalesModifier."""
ConditionalModifier.edit(self, enabled, title)
if condition is not None and condition != self.getTalesCondition():
self._condition = Expression(condition)
Expression(condition) 把攻击者的字符串编译为 TALES 表达式。该表达式随后在 isApplicable 中求值,版本管理机制在决定是否应用修饰器时会调用它:
def isApplicable(self, obj, portal=None):
"""See IConditionalTalesModifier."""
if not self._enabled or not self.getTalesCondition() or self.isBroken():
return False
context = createExpressionContext(obj, portal)
return self._condition(context)
仅缺少权限检查这一项,就足以让匿名访问者修改条件并触发它。利用链的后半部分是沙箱逃逸。受限 Python 本应只暴露软件包明确声明为公开的名称。四个软件包却用 allow_module 打开了整个模块,从而公开模块中的每个名称,包括它导入的模块和函数。
6.2.2 中的 Products.CMFPlone 打开了三个模块:
allow_module("Products.CMFPlone.utils")
# ...
allow_module("Products.CMFPlone.browser.navtree")
# ...
allow_module("OFS.ObjectManager")
Products.MimetypesRegistry 打开了它的 common 模块:
allow_module("Products.MimetypesRegistry.common")
plone.batching 打开了自己的软件包:
allow_module("plone.batching")
plone.restapi 打开了标准库的 json 模块:
allow_module("json")
json 模块对序列化本身无害,但它的一些函数可以调用对象上的受保护方法,因此也扩大了受限代码的能力范围。关键在于导入图:以这种方式打开的模块会公开它导入的辅助函数,而这些辅助函数可以到达 os。供应商公告称,暴露的名称让受限代码能够到达 os 等模块并逃出沙箱。
如何被利用?
- 攻击者向
portal_modifier对象发送请求并调用其edit方法。由于该类没有安全声明,该方法可被公开调用。 - 请求设置
enabled和condition值。ConditionalTalesModifier.edit把该值保存为已编译的Expression。 - 攻击者触发修饰器,或等待编辑者保存受版本管理的内容,使版本管理机制调用
isApplicable。 isApplicable以进程或保存内容的编辑者的权限对已保存的条件求值。- 受限 Python 通常会在沙箱边界阻止该表达式。全局
allow_module声明暴露了通向os的模块和导入,因此表达式逃出沙箱并执行命令。 - 命令以 Zope 进程用户身份运行。在默认部署中,该用户拥有 Plone 实例、数据库连接和上传的文件。
目前没有公开的利用代码。公告称存在可用的概念验证并与供应商共享,但没有公开请求序列。上述步骤依据公告描述和修复后软件包中的代码路径。
攻击如何展开?
该攻击把一次未认证请求变成存储的 TALES 表达式,然后滥用过宽的模块声明,在受限沙箱之外对该表达式求值。
没有权限声明"] B --> C["TALES 条件保存为
Expression"] C --> D["isApplicable 对
条件求值"] D --> E["allow_module 暴露
导入的辅助函数"] E --> F["受限 Python 到达 os"] F --> G["以 Zope 进程用户身份执行代码"]
攻击者能做什么?
到达该利用链的攻击者可以以提供 Plone 站点的 Zope 进程权限执行代码。公告称匿名路径可以在服务器上执行代码,同一条件也可以在编辑者保存受版本管理内容时以该编辑者的权限执行。
影响包括:
- 读取和修改站点内容以及 Zope 进程可访问的文件。
- 读取进程可用的配置和机密,包括数据库连接设置。
- 以 Plone 服务账号在主机上执行命令。
- 把站点作为进入其数据库和内部服务所在网络的立足点。
条件与“我受影响吗?”相同:受影响的版本、可通过 HTTP 访问的 portal_modifier 工具,以及没有更新或代理阻断。CMFEditions 公告将主要缺陷评为 CVSS v3.1 9.8 严重、CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H、CWE-862。公告并未称该缺陷可获得主机 root 权限。它称代码在服务器上执行,而进程权限限制了该代码能做什么。
为什么修复有效?
修复在同一次发布中关闭了匿名入口并收窄了沙箱。第一项更改在 Products.CMFEditions 5.0.2 中为修饰器类添加安全声明:
class ConditionalModifier(SimpleItem):
"""This is a wrapper for a modifier."""
+ security = ClassSecurityInfo()
+
modifierEditForm = PageTemplateFile(
"www/modifierEditForm.pt", globals(), __name__="modifierEditForm"
)
+ @security.protected(ManagePortal)
def edit(self, enabled=None, title="", REQUEST=None):
"""See IConditionalModifier."""
@security.protected(ManagePortal) 表示 Zope 在调用 edit 之前检查 Manage portal 权限。匿名访问者不再通过。访问器获得 ViewManagementScreens,大小保护钩子变为私有。
第二项更改把每个全局 allow_module 替换为显式列表。在 plone.batching 3.0.3 中:
-from AccessControl import allow_module
+from AccessControl import ModuleSecurityInfo
from plone.batching.batch import Batch
-allow_module("plone.batching")
+# Only declare Batch public. Do not use allow_module here: that would make
+# every name in this module available to restricted code, including
+# imported names like allow_class.
+ModuleSecurityInfo("plone.batching").declarePublic("Batch")
allow_class(Batch)
Products.CMFPlone 6.2.3 用 ModuleSecurityInfo 对象和公开名称列表替换 allow_module("Products.CMFPlone.utils")。Products.MimetypesRegistry 4.0.2 只公开 MimeTypeException。plone.restapi 10.2.1 完全删除 allow_module("json")。沙箱仍暴露模板和脚本需要的名称,但不暴露这些名称导入的模块。
该修复对供应商列出的五个软件包是完整的。第三方附加组件如果对自己的模块调用 allow_module,仍可能扩大沙箱;公告不覆盖这些附加组件。如果运行不受信任的代码,请检查附加组件是否存在相同模式。
如何检查是否已遭入侵?
先使用新的条件检查器。以 Manager 身份打开站点根目录下的 https://your-site/@@check-modifier-conditions。该页面列出条件与默认值不同的每个修饰器,显示当前值并提供恢复选项。差异是线索而非证据:默认值会随时间变化,管理员或附加组件也可能有意设置条件。
在 Web 服务器日志中搜索对修饰器工具的请求。解码百分号编码并忽略大小写,因为 Zope 的获取机制使对象可在任意深度访问,攻击者也可以对路径编码:
grep -iE 'portal[_%]?modifier' /var/log/nginx/access.log
grep -iE 'portal(%5F|_)modifier' /var/log/apache2/access.log
包含该标记的请求值得打开查看:它显示源地址、方法和响应码。对修饰器对象的 edit 请求返回 200,比扫描器的 404 更有价值。公告没有公开入侵指标,失败的尝试可能除请求行外不留痕迹。
如果检查器显示了你未设置的条件变更,请恢复它,轮换 Zope 进程可读取的机密,并检查站点是否有被修改的内容。如果确认代码执行,请将服务器视为已遭入侵,并从已知良好的备份重建,因为代码执行可能在检查器所检查的文件之外留下后门。
时间线
- 2026-11-02:Plone 发布预公告和
portal_modifier代理规避措施。 - 2026-11-10 13:26 UTC:修复后的软件包和 Products.PloneHotfix20261110 上传到 PyPI。
- 2026-11-10 13:30 UTC:公开公告描述五个缺陷和修复版本。
常见问题
有 CVE 吗?
截至 2026 年 11 月 11 日没有分配 CVE。五个公告以 GHSA 标识符发布,公告称已申请 CVE 编号。请跟踪 GHSA 条目,不要等到 CVE 分配后才更新。
这会影响 Plone 5 或 Plone 4 吗?
会。公告称可追溯至至少 Plone 4.3 的旧版本受影响。Plone 5.2 及更早版本已不再获得安全支持,因此这些版本的支持路径是修复包,而不是软件包更新。
需要全部五个软件包更新吗?
更新四个关键软件包,以及已安装的 plone.restapi。公告称如果不使用 plone.restapi 可以跳过它;它是扩大权限绕过的中危伴随项,不是入口。其余四个是公告所述利用链所必需的。
是否已在野利用?
截至 2026 年 11 月 11 日未发现实际利用。供应商在发布当天公开了技术细节,预公告警告当前的 AI 工具可能很快把描述变成可用的利用程序。请把预公告到你完成更新之间的窗口视为暴露期。
我可以保留代理阻断而不更新吗?
代理阻断会阻止路径包含 portal_modifier 的请求,供应商建议更新后保留。如果另一条路径到达受限代码,它不能修复沙箱逃逸,也不能覆盖触发已保存条件的内部编辑者路径。请把它当作临时措施,而不是修复。
修复包支持 Python 2.7 吗?
支持。修复包支持 Python 2.7 和 Python 3,可在 Plone 4.3、5.0、5.1、5.2、6.0、6.1 和 6.2 上运行。
使用 Ostorlab 查找暴露的 Plone 站点
Ostorlab 的 Web 智能体深度扫描可绘制面向互联网的 Plone 站点、其可达端点和暴露组件,让你在修补前看清哪些实例仍在响应。对清单中的站点运行扫描,并用结果优先处理可从互联网访问的实例。
参考文献
- Plone 安全修复 20261110 公告:https://community.plone.org/t/plone-security-hotfix-20261110/23313(2026 年 11 月 11 日核查)
- 预公告和代理规避措施:https://community.plone.org/t/pre-announcement-plone-security-fix-20261110/23303(2026 年 11 月 11 日核查)
- Products.CMFEditions 公告 GHSA-67qm-q795-jh7h:https://github.com/plone/Products.CMFEditions/security/advisories/GHSA-67qm-q795-jh7h(2026 年 11 月 11 日核查)
- Products.CMFPlone 公告 GHSA-92qw-564p-pmg6:https://github.com/plone/Products.CMFPlone/security/advisories/GHSA-92qw-564p-pmg6(2026 年 11 月 11 日核查)
- Products.MimetypesRegistry 公告 GHSA-xcj2-cmp9-fj62:https://github.com/plone/Products.MimetypesRegistry/security/advisories/GHSA-xcj2-cmp9-fj62(2026 年 11 月 11 日核查)
- plone.batching 公告 GHSA-6hcx-c235-cxh3:https://github.com/plone/plone.batching/security/advisories/GHSA-6hcx-c235-cxh3(2026 年 11 月 11 日核查)
- plone.restapi 公告 GHSA-vgxm-pwch-89q6:https://github.com/plone/plone.restapi/security/advisories/GHSA-vgxm-pwch-89q6(2026 年 11 月 11 日核查)
- PyPI 上的 Products.PloneHotfix20261110:https://pypi.org/project/Products.PloneHotfix20261110/(2026 年 11 月 11 日核查)
- Products.CMFEditions 5.0.2 源码:https://github.com/plone/Products.CMFEditions/blob/5.0.2/src/Products/CMFEditions/Modifiers.py(2026 年 11 月 11 日核查)
- plone.batching 3.0.3 源码:https://github.com/plone/plone.batching/blob/3.0.3/src/plone/batching/init.py(2026 年 11 月 11 日核查)
- Products.CMFPlone 6.2.3 源码:https://github.com/plone/Products.CMFPlone/blob/6.2.3/src/Products/CMFPlone/init.py(2026 年 11 月 11 日核查)
- plone.restapi 10.2.1 源码:https://github.com/plone/plone.restapi/blob/10.2.1/src/plone/restapi/init.py(2026 年 11 月 11 日核查)
- Plone 后端容器拉取次数:https://hub.docker.com/r/plone/plone-backend(2026 年 11 月 11 日核查)