Grav CMS 组合拳漏洞| CVE-2026-42613CVE-2026-42607复现研究
0x0 背景介绍Grav是一个基于文件的Web平台。在2.0.0-beta.2之前版本中存在两个高危漏洞可导致组合利用权限提升漏洞-CVE-2026-42613Grav的Login插件在处理用户注册请求时未对请求数据中的groups/access字段进行服务端校验。当管理员在插件配置中启用注册功能并将groups/access添加为注册表单的可用字段时未认证的攻击者可通过构造带有恶意groups/access参数的注册请求攻击者无需任何凭据直接创建出拥有超级管理员权限的账户实现未授权权限提升远程代码执行漏洞-CVE-2026-42607Grav管理后台的“直接安装”功能在解压用户上传的ZIP插件包时未对包内的文件类型和内容做任何安全检查,直接调用Installer::install后者仅使用ZipArchive::extractTo进行解压。拥有管理员权限的攻击者可以上传一个包含恶意PHP代码的合法结构插件ZIP包其中的PHP文件会随Grav的插件初始化事件自动执行进而在服务器上写入Webshell或执行任意系统命令最终获得服务器控制权。0x1 漏洞复现简单概述下# 1.docker-compose.yml文件 mkdir ~/grav-cve413 cd ~/grav-cve413 cat docker-compose.yml EOF services: grav: image: php:8.0-apache container_name: grav-cms ports: - 8080:80 volumes: - ./html:/var/www/html - ./php.ini:/usr/local/etc/php/conf.d/custom.ini command: bash -c # 安装必要工具和库 apt-get update apt-get install -y unzip git libfreetype6-dev libjpeg62-turbo-dev libpng-dev libzip-dev zlib1g-dev libicu-dev libyaml-dev # 启用 Apache 重写 a2enmod rewrite # 安装 PHP 扩展 docker-php-ext-configure gd --with-freetype --with-jpeg docker-php-ext-install -j$(nproc) gd zip intl pecl install yaml docker-php-ext-enable yaml # 下载 Grav 1.7.52 并部署 if [ ! -f /var/www/html/index.php ]; then curl -L -o /tmp/grav.zip https://getgrav.org/download/core/grav-admin/1.7.52 unzip -q /tmp/grav.zip -d /tmp/grav-extract mv /tmp/grav-extract/grav-admin/* /var/www/html/ mv /tmp/grav-extract/grav-admin/.* /var/www/html/ 2/dev/null || true rm -rf /tmp/grav.zip /tmp/grav-extract chown -R www-data:www-data /var/www/html/ fi apache2-foreground EOF # 2. php.ini cat php.ini EOF memory_limit 256M upload_max_filesize 20M post_max_size 25M max_execution_time 120 display_errors Off EOF # 3.修复阿里云镜像源 docker exec grav-cms bash -c sed -i s/deb.debian.org/mirrors.aliyun.com/g /etc/apt/sources.list apt-get update # 4.安装 Login 插件v3.8.0并创建配置文件目录 docker exec grav-cms bash -c rm -rf /var/www/html/user/plugins/login git clone -b 3.8.0 https://github.com/getgrav/grav-plugin-login.git /var/www/html/user/plugins/login mkdir -p /var/www/html/user/config/plugins cp /var/www/html/user/plugins/login/login.yaml /var/www/html/user/config/plugins/login.yaml # 5. 开启必要的参数示例 #/var/www/html/user/config/plugins/login.yaml enabled: true built_in_css: true built_in_js: true redirect_after_login: / redirect_after_logout: / user_registration: enabled: true fields: - username - password - email - fullname - groups # is - access # is0x2 漏洞复现前置条件管理员启用了用户注册且将groups、access加入注册表单字段安装并启用Admin插件攻击者持有管理员凭据或等效会话2.1-脚本验证两个单独的脚本可以先创建用户再执行RCEhttps://github.com/Kai-One001/cve-/blob/main/Grav_CMS_login_CVE-2026-42613.py https://github.com/Kai-One001/cve-/blob/main/Grav_CMS_RCE_CVE-2026-42607.py2.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钩子加载了解这些思路就清晰了。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.phppublicstaticfunctionunZip($zip_file,$destination){$zipnewZipArchive();$archive$zip-open($zip_file);if($archivetrue){Folder::create($destination);$unzip$zip-extractTo($destination);if(!$unzip){self::$errorself::ZIP_EXTRACT_ERROR;Folder::delete($destination);$zip-close();returnfalse;}$package_folder_name$zip-getNameIndex(0);// ...$zip-close();return$destination./.$package_folder_name;}// ...returnfalse;}这里的设计假设等价于ZIP与其中所有相对路径都是可信的extractTo在PHP层会按条目名写盘外层禁止.php上传无法约束压缩包内的evil.php、../../html/shell.php的检查3.4-[逻辑缺陷]loadInstallerrequire_once包内install.php接着往下看**Installer::install在文件复制/移动之前会调用loadInstaller。若根存在install.php核心会require_once相当于把是否执行这段PHP的裁决权交给了**ZIP作者。// system/src/Grav/Common/GPM/Installer.phpprivatestaticfunctionloadInstaller($installer_file_folder,$is_install){$installer_file_folderrtrim($installer_file_folder,DS);$install_file$installer_file_folder.DS.install.php;if(!file_exists($install_file)){returnnull;}require_once$install_file;很多CMS把install.php当作可信物里的安装钩子但在直接安装 接受任意ZIP的语义下这一钩子变成了即时代码执行点即便主插件PHP尚未被路由访问require_once已在服务端完成一次解释执行攻击面比仅写盘、待访问更靠前。3.5-[攻击子链] Grav 整包升级路径中的includeCLI DirectInstallCommand::upgradeGrav对类型为**grav的包会include解压目录下的system/install.php**并执行返回的callable。// system/src/Grav/Console/Gpm/DirectInstallCommand.phpprivatefunctionupgradeGrav(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)$installinclude$script)is_callable($install)){$install($zip);}else{thrownewRuntimeException(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:truefields: - username - password - email - fullname -groups# ← 启用攻击- access# ← 启用攻击3.8-攻击链路未认证攻击者 │ ├─[1]发送恶意注册请求(CVE-2026-42613)│ POST /user_register │ 注入 data[groups][]adminsdata[access][admin][super]true │ └─ Login 插件 register()方法未校验 groups/access → 写入用户 YAML │ ├─[2]获得 admin.super 超级管理员账户 │ 如 attacker / Str0ngPss! │ ├─[3]登录管理后台 /admin │ 获取 login-nonce → 提交凭证 → 建立会话 │ ├─[4]上传恶意插件 ZIP(CVE-2026-42607)│ POST /admin/tools/direct-install?taskdirectInstall │ └─ 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?cmdwhoami → 系统命令执行服务器完全控制0x4 修复建议1、升级最新版本将Grav升级至2.0.0-beta.2及以上版本https://github.com/getgrav/grav2、临时防护措施限制访问对 /admin/tools/direct-install 做IP白名单或额外HTTP认证仅维护窗口开放审计监控部署文件完整性监控监控user/plugins/**与Web根新建.php或者全站运维策略对更新包只做 签名校验后的安装。防火墙 / WAF拦截或告警POSTURI含direct-install taskdirectInstall multipart的组合对uploaded_file带.zip的上传加强审计与速率限制免责声明本文仅用于安全研究目的未经授权不得用于非法渗透测试活动。