文章总结: 该文档详细分析了GravCMS在2.0.0-beta.2之前版本存在的两个高危漏洞组合利用链。CVE-2026-42613为权限提升漏洞,攻击者可通过注册功能直接创建管理员账户;CVE-2026-42607为远程代码执行漏洞,管理员上传恶意ZIP插件包可导致服务器被控制。文档提供了完整的漏洞复现步骤、流量特征分析,并深入剖析了Installer模块解压逻辑缺陷和install.php自动加载机制等核心漏洞原理。建议用户及时升级到安全版本并加强文件上传验证。 综合评分: 85 文章分类: 漏洞分析,WEB安全,应用安全,安全工具,渗透测试
Grav CMS 组合拳漏洞| CVE-2026-42613&CVE-2026-42607复现&研究
原创
404号浪漫 404号浪漫
404号浪漫
2026年5月12日 22:14 北京
在小说阅读器读本章
去阅读
点击蓝字,关注我们
0x0 背景介绍
Grav是一个基于文件的Web平台。
在2.0.0-beta.2之前版本中,存在两个高危漏洞可导致组合利用
1. 权限提升漏洞->CVE-2026-42613
Grav的Login插件在处理用户注册请求时,未对请求数据中的groups/access字段进行服务端校验。当管理员在插件配置中启用注册功能,并将groups/access添加为注册表单的可用字段时,未认证的攻击者可通过构造带有恶意groups/access参数的注册请求,攻击者无需任何凭据直接创建出拥有超级管理员权限的账户,实现未授权权限提升
2. 远程代码执行漏洞->CVE-2026-42607
Grav管理后台的“直接安装”功能在解压用户上传的ZIP插件包时,未对包内的文件类型和内容做任何安全检查,直接调用Installer::install,后者仅使用ZipArchive::extractTo进行解压)。拥有管理员权限的攻击者可以上传一个包含恶意PHP代码的合法结构插件ZIP包,其中的PHP文件会随Grav的插件初始化事件自动执行,进而在服务器上写入Webshell或执行任意系统命令,最终获得服务器控制权。
漏洞详情
| 漏洞类型 | 影响版本 | 利用复杂度 | CVE编号 | | — | — | — | — | | 未授权操作 代码执行 | < 2.0.0-beta.2 | 低 | CVE-2026-42613 CVE-2026-42607 |
攻击效果:
- 未授权创建管理用户配合上传恶意ZIP可导致RCE。
0x2 漏洞复现
前置条件
- 管理员启用了用户注册,且将groups、access加入注册表单字段
- 安装并启用Admin插件;攻击者持有 管理员 凭据(或等效会话)
2.1-脚本验证
- 两个单独的脚本,可以先创建用户再执行RCE
https://github.com/Kai-One001/cve-/blob/main/Grav_CMS_login_CVE-2026-42613.pyhttps://github.com/Kai-One001/cve-/blob/main/Grav_CMS_RCE_CVE-2026-42607.py
2.2-复现流量特征 (PCAP)
https://github.com/Kai-One001/PCAP-For-Cybersecurity.rule/blob/main/2026/Grav_CMS_RCE_CVE-2026-42613%26CVE-2026-42607.pcap
- 挑取部分的截图
- 明文可见项目结构
- 后续Shell(没忍住shell了)
0x3 漏洞原理分析
3.0-架构与模块定位
本次分析是Grav v1.7.52核心发行包,没有看user/plugins/admin,因此下文代码级实证以当前工作区可核对的为主。
Grav的包管理分为三层:管理后台暴露HTTP入口;Grav Package Manager在system核心中完成下载、解压与安装编排;Installer作为底层驱动,封装ZipArchive 解压、目标目录校验以及install.php 钩子加载,了解这些思路就清晰了。
| 层级 | 核心文件(职责) | 在漏洞链中的角色 |
| — | — | — |
| 入口层 | Admin 插件:AdminController.php(directInstall 任务,Gpm.php;路由 /admin/tools/direct-install | 接收multipart 上传的 ZIP、校验会话与 admin-nonce,将文件交给GPM/Installer |
| 逻辑层 | system/src/Grav/Common/GPM/GPM.php (copyPackage、getPackageType、getPackageName 等) | 识别包类型(plugin/theme/grav)、解析安装路径 |
| 逻辑层 / 驱动层 | system/src/Grav/Common/GPM/Installer.php(unZip、install、loadInstaller) | 解压 ZIP → 移动/复制 user/plugins 或 user/themes → require_once 包内 install.php |
| 入口层(CLI,同源逻辑) | system/src/Grav/Console/Gpm/DirectInstallCommand.php | 与 Web 共用 Installer::unZip / Installer::install;Grav 整包升级路径下 include 解压出的 system/install.php |
#
3.1-锁定关键路径
从管理员选择的 ZIP 文件出发,数据流在核心层必然经过Installer::unZip:这里使用ZipArchive::open 与extractTo整包解压,解压结果路径由ZIP内第一条目录名与临时目录拼接得到,随后Installer::install 在未使用sophisticated模式时对插件默认走moveInstall,将整个解压出的目录树迁入user/plugins/<slug>/(或主题目录)。
再往后,loadInstaller如果发现包根下存在install.php,会require_once 该文件并解析约定类名的安装器类——这意味着 ZIP 内的任意 PHP 在解压后、安装流程中即可被解释器加载执行,与禁止直接上传 .php的假设形成断裂。
3.2 [核心入口] 先看「流量落点」再看「Installer」
废话不多说,我们需要找到 谁把不可信数据交给了解压器。漏洞描述可知将Web入口指到/admin/tools/direct-install 的directInstall任务。顺着这个接口往下追,Admin插件最终会调用核心里的Installer::install()。因此,即使当前工作区没有Admin源码,只要Grav核心的Installer对ZIP采取「先解压、再信任」的策略,任何复用该API的入口(Admin、未来其他插件、甚至误暴露的封装)都会继承同一风险。
3.3-[逻辑缺陷] unZip:解压即信任
全局找定位到system/src/Grav/Common/GPM/Installer.php,是因为所有 GPM安装路径都要把包落到临时目录再迁入user/;DirectInstallCommand里也能看到显式调用Installer::unZip($zip, $tmp_source)。
进unZip后,可以看到打开归档后直接extractTo,没有在循环里对$zip->getNameIndex($i)做安全判定——Zip Slip与包内任意文件两类问题在解压这都没有被拦。
// system/src/Grav/Common/GPM/Installer.php public static function unZip($zip_file, $destination) { $zip = new ZipArchive(); $archive = $zip->open($zip_file); if ($archive === true) { Folder::create($destination); $unzip = $zip->extractTo($destination); if (!$unzip) { self::$error = self::ZIP_EXTRACT_ERROR; Folder::delete($destination); $zip->close(); return false; } $package_folder_name = $zip->getNameIndex(0); // ... $zip->close(); return $destination . '/' . $package_folder_name; } // ... return false; }
- 这里的设计假设等价于ZIP与其中所有相对路径都是可信的
- extractTo在PHP层会按条目名写盘,外层禁止
.php上传无法约束压缩包内的evil.php、../../html/shell.php``的检查
3.4-[逻辑缺陷] loadInstaller:require_once 包内 install.php
接着往下看Installer::install,在文件复制/移动之前会调用loadInstaller。若根存在install.php,核心会require_once相当于把是否执行这段PHP的裁决权交给了ZIP作者。
// system/src/Grav/Common/GPM/Installer.php private static function loadInstaller($installer_file_folder, $is_install) { $installer_file_folder = rtrim($installer_file_folder, DS);
$install_file = $installer_file_folder . DS . 'install.php';
if (!file_exists($install_file)) { return null; }
require_once $install_file;
- 很多CMS把
install.php当作可信物里的安装钩子 - 但在直接安装 = 接受任意ZIP的语义下,这一钩子变成了 即时代码执行点
- 即便主插件PHP尚未被路由访问,
require_once已在服务端完成一次解释执行,攻击面比仅写盘、待访问更靠前。
3.5-[攻击子链] Grav 整包升级路径中的 include
CLI DirectInstallCommand::upgradeGrav对类型为grav的包会include解压目录下的system/install.php并执行返回的callable。
// system/src/Grav/Console/Gpm/DirectInstallCommand.php private function upgradeGrav(string $zip, string $folder): void { if (!is_dir($folder)) { Installer::setError('Invalid source folder'); }
try { $script = $folder . '/system/install.php'; /** Install $installer */ if ((file_exists($script) && $install = include $script) && is_callable($install)) { $install($zip); } else { throw new RuntimeException('Uploaded archive file is not a valid Grav update package'); }
这条链路常需要 能执行CLI的权限,与HTTP Admin的威胁模型不太一样,但说明执行解压产物中的PHP是GPM的结构性选择;审计时不可忽视 运维侧滥用或被入侵账户利用bin/gpm 的场景。
#
3.6-推导最大危害
在 已具备管理员账号 的前提下,可以构造 结构符合Grav插件约定的恶意包(含blueprints.yaml、主类PHP等),在onPluginsInitialized等周期钩子里写WebShell、读配置中的数据库口令、或横向改写站点文件。
这与后台本来就能改模板不同:任意PHP在服务器进程身份下运行,可调用system、proc_open、读/etc/passwd(在权限允许时)等,属于全站失陷。若攻击者通过CSRF、会话固定或stolen cookie 借用管理员浏览器,也能让用户无感知下完成上传,放大社工与浏览器侧风险。(也同下面的漏洞)
3.7-Login插件问题
根本原因
在register()方法中,groups和access字段仅在输入数据中不存在时,才会被设置为配置默认值:
// Line 254-260if (!isset($data['groups'])) { $groups = (array) $this->config->get('plugins.login.user_registration.groups', []); if (count($groups) > 0) { $data['groups'] = $groups; }}// Line 262-267if (!isset($data['access'])) { $access = (array) $this->config->get('plugins.login.user_registration.access.site', []); if (count($access) > 0) { $data['access']['site'] = $access; }}
如果攻击者在POST请求体中包含了groups或access,!isset()检查将返回false,从而跳过配置默认值的设置,攻击者提供的值将原封不动地传递下去。随后,这些值会被直接赋给用户对象:
if (isset($data['groups'])) { $user->groups = $data['groups']; // attacker-controlled}if (isset($data['access'])) { $user->access = $data['access']; // attacker-controlled}$user->save();
validateField()方法包含一个switch语句,仅对以下字段进行验证:username、password、password2、permissions、state和language。groups和access字段会落入default分支,完全不受任何验证。
# user/config/plugins/login.yamluser_registration: enabled: true fields: - username - password - email - fullname - groups # ← 启用攻击 - access # ← 启用攻击
3.8-攻击链路
未认证攻击者 │ ├─[1] 发送恶意注册请求 (CVE-2026-42613) │ POST /user_register │ 注入 data[groups][]=admins & data[access][admin][super]=true │ └─ Login 插件 register() 方法未校验 groups/access → 写入用户 YAML │ ├─[2] 获得 admin.super 超级管理员账户 │ (如 attacker / Str0ngP@ss!) │ ├─[3] 登录管理后台 /admin │ 获取 login-nonce → 提交凭证 → 建立会话 │ ├─[4] 上传恶意插件 ZIP (CVE-2026-42607) │ POST /admin/tools/direct-install?task=directInstall │ └─ AdminController::taskDirectInstall() 接收文件 → 调用 Installer::install() │ └─ Installer::unZip() 使用 ZipArchive::extractTo() 直接解压(无文件类型/路径检查) │ └─ 插件文件落地至 user/plugins/shellplugin/ │ ├─[5] 插件初始化触发 │ Grav 加载插件 → 执行 shellplugin.php 中的 onPluginsInitialized() │ └─ 恶意代码在插件目录写入 Webshell(如 shell.php) │ └─[6] 远程命令执行 访问 /user/plugins/shellplugin/shell.php?cmd=whoami → 系统命令执行,服务器完全控制
#
0x4 修复建议
1、升级最新版本:将Grav升级至2.0.0-beta.2及以上版本
https://github.com/getgrav/grav
2、临时防护措施:
- 限制访问:对 /admin/tools/direct-install 做IP白名单或额外HTTP认证,仅维护窗口开放
- 审计监控:部署文件完整性监控,监控user/plugins/**与Web根新建.php(或者全站)
- 运维策略:对更新包只做 签名校验后的安装。
- 防火墙 / WAF:拦截或告警POST+URI含direct-install + task=directInstall + multipart的组合;对uploaded_file带.zip的上传加强审计与速率限制
#
免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。
/** 别说,吃着雪糕敲文章还挺舒服**/
#
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:404号浪漫 404号浪漫 404号浪漫《Grav CMS 组合拳漏洞| CVE-2026-42613&CVE-2026-42607复现&研究》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论