
Root-cause分析、パッシブバージョンチェッカー、およびCVE-2026-18322(SupsysticのSmart Popup WordPressプラグインにおける認証不要の権限昇格)のラボPoC。
WordPress プラグイン Smart Popup by Supsystic(popup-by-supsystic)における未認証の権限昇格脆弱性 CVE-2026-18322 に関するセキュリティリサーチ。本リポジトリには、上流のソース差分から導出した根本原因分析、抽出したパッチ、影響を受けるインストールを特定するための検出ツール(パッシブバージョンフィンガープリンタ)、およびエクスプロイト PoC 用のラボが含まれています。
本脆弱性は公開済みで修正済みです。本成果物は防御目的で公開されています。運用者が影響を受けるホストを発見し、修復するための支援を目的としています。
| バージョン範囲 | 状態 |
|---|---|
< 1.13.0 | 脆弱 |
>= 1.13.0 | 修正済み |
1.13.0(2026年7月31日リリース)が最初の修正リリースであり、以下に述べるチェーンの3つのリンクすべてを修復します。詳細は分析の§1を参照してください。
3つの独立した弱点が組み合わさり、未認証での管理者作成が成立します。
1. パーミッションマップの衝突 — havePermissions() は2つのパーミッションマップを array_merge() で結合していました。両方とも 文字列 キー PPS_USERLEVELS('userlevels')を使用しており、array_merge() は文字列キーを 上書き するため、ベースコントローラの短いデフォルトリストがポップアップモジュールのリストを丸ごと置き換え、save および他の9つのメソッドから管理者制限を黙って剥ぎ取っていました。
$permissions = $mod->getController()->getPermissions(); // [... 'save' ...]
$permissionsBase = $mod->getController()->getBasePermissions(); // ['getListForTbl','removeGroup','clear']
$permissions = array_merge($permissions, $permissionsBase); // base wins — 'save' is gone
このチェックは フェイルオープン します。マップに存在しないアクションは決して拒否されません。
2. 見知らぬ相手にメール送信される再利用可能な nonce — save は依然として pps_nonce を要求していましたが、購読確認メールにまさにその nonce アクションが埋め込まれていました。ログアウトユーザーの場合、WordPress の nonce は uid=0 をキーとするため、匿名の購読者向けに発行されたトークンは 任意の 匿名攻撃者に対して検証を通ってしまいます。
3. サーバー側のロール許可リストが存在しない — createWpSubscriber() は設定されたロールをそのまま WP_User::set_role() に渡していました。プラグインの安全なロールリスト(administrator を除外)は、管理画面のドロップダウンをレンダリングする際にのみ適用されていました。これは UI のみの制御でした。
連鎖:購読して nonce を取得 → それを popup::save に対してリプレイして sub_wp_create_user_role=administrator を設定 → 購読フローをトリガー → 永続的な管理者アカウント。
ファイル/行参照を含む完全なウォークスルー:docs/ANALYSIS.md。
poc/cve_2026_18322_check.py は、サイトが影響を受けるバージョンを実行しているかどうかを判定します。エクスプロイトは一切試みません。平文の HTTP GET を発行し、インストールが自身について公開しているバージョンを読み取るだけです。
git clone <this-repo> && cd CVE-2026-18322
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
requests は推奨ですが任意です。ツールは標準ライブラリにフォールバックします。
# Single target
python3 poc/cve_2026_18322_check.py https://example.com
# With supporting evidence
python3 poc/cve_2026_18322_check.py https://example.com -v
# Many targets, concurrently, exporting results
python3 poc/cve_2026_18322_check.py -f targets.txt -t 20 --json results.json
python3 poc/cve_2026_18322_check.py -f targets.txt --csv results.csv --only-vulnerable
# Through a proxy, ignoring TLS errors (lab use)
python3 poc/cve_2026_18322_check.py https://staging.internal -k --proxy http://127.0.0.1:8080
ターゲットは引数として、または -f を介して指定できます(- は標準入力から読み取ります)。ホスト名のみの場合は https:// とみなされます。
$ python3 poc/cve_2026_18322_check.py https://example.com -v
[VULNERABLE] https://example.com/ (Smart Popup by Supsystic 1.11.2) < 1.13.0 affected by CVE-2026-18322; upgrade to 1.13.0+
- asset-version: plugin asset enqueued with ?ver=1.11.2 (https://example.com/)
- homepage-reference: references /plugins/popup-by-supsystic/ (https://example.com/)
- readme: plugin readme.txt is publicly readable (https://example.com/wp-content/plugins/popup-by-supsystic/readme.txt)
- readme-stable-tag: Stable tag: 1.11.2 (.../readme.txt)
終了コード:0 = 脆弱なターゲットなし、1 = 少なくとも1つが脆弱、2 = 使用法エラー。
2つのパッシブシグナルを使用し、いずれも公開リソースへの平文 HTTP GET です。
?ver=PPS_VERSION 付きでエンキューするため(classes/frame.php:410,473)、ホームページが正確なバージョンを漏らすことがよくあります。readme.txt — Stable tag: は信頼性が高く、古い可能性のあるキャッシュされたアセットよりも優先されます。チェンジログはフォールバックとして機能します。ホームページスキャンは、名前が変更された wp-content ディレクトリ(例:Bedrock の /app/)も検出するため、readme の検索はサイトの実際のレイアウトに従います。
検出ツールはエクスプロイトを試みません。 本 CVE の武器化されたエクスプロイトはここには公開されていません。完全なチェーンを実行する唯一のコードは、ローカルラボにハードゲートされています(以下を参照)。
パッシブチェッカーはバージョンフィンガープリンティングのみを実行します。その HttpClient は GET 以外の HTTP メソッドを一切公開していません — これは構造上強制されており、テストスイートでアサートされています。したがって、誤って状態変更リクエストを発行することはできません。popup::save を呼び出したり、sub_wp_create_user_role パラメータを送信したり、購読フォームを送信したり、確認メールをトリガーしたり、ユーザー、ポップアップ、設定を作成または変更したりすることは一切ありません。読み取るのは2つの公開リソース — ホームページと readme.txt — のみで、それ以外は何も行いません。
この安全性には代償があります。率直に言えば、判定はホストが公開しているバージョンメタデータの精度に依存します。トレードオフと失敗モード:§9。
lab/exploit_full_chain.py は、ここにあるコードの中で実際の攻撃を実行する唯一の部分であり、WordPress 管理者を作成します。これは、分析を信頼に基づくのではなく再現可能にするために公開されています。実証できない認可バグに関する主張は、発見ではなく単なる断言です。これは lab/ の使い捨てスタック専用に書かれており、それ以外の何のためでもありません。
--lab-confirm は必須です。 両方のチェックは最初の HTTP リクエストの前に assert_lab_target() で実行されるため、拒否された呼び出しはターゲットに一切触れません。http://localhost:8025/api/v1/…)を通じて購読確認メールから再利用可能な pps_nonce を収集します。そのメールを他のどこかから取得するコードパスは存在しないため、ラボ外のホストに対してチェーンには最初のステップがありません。lab/docker-compose.yml のすべてのサービスは 127.0.0.1 にバインドされています。したがって、出荷状態では実際のサイトに向けることはできません。リクエストを発行する前に REFUSING TO RUN で停止します。
$ python3 exploit_full_chain.py https://example.com --lab-confirm
REFUSING TO RUN: example.com resolves to non-local address(es) 104.20.23.154, ...
This script creates a WordPress administrator and is for the local lab only.
To test a real site, use the non-destructive checker in ../poc/.
これらのガードは装飾ではなく構造を支えるものです。スクリプトは 実際の チェーンであり、それをデモンストレーションに留めているのは、使い捨てのローカルスタック外では実行されないという点です。これらを削除しないでください。また SECURITY.md を参照してください — これらを弱める変更や、ラボ外で実行することを意図した単体のエクスプロイトは、本リポジトリの対象外です。実際のサイトが影響を受けているかどうかを知るには poc/ を使用してください — チェッカーは何にも触れることなく同じ問いに答えます。
管理下のホストについて確実な答えを得るには:
grep -n "array_merge(\$permissions, \$permissionsBase)" \
wp-content/plugins/popup-by-supsystic/classes/frame.php
何らかの出力があれば、そのホストは脆弱です — この行こそが 1.13.0 で置き換えられたものです。
自身が所有するシステム、または明示的にテストを許可されたシステムに対してのみ使用してください。SECURITY.md を参照してください。
lab/ には、脆弱性をエンドツーエンドで実行する使い捨ての Docker スタックが含まれており、分析を信頼に基づくのではなく検証可能にします。
cd lab
./setup.sh # WordPress + plugin 1.12.0
../.venv/bin/python exploit_full_chain.py http://localhost:8080 --lab-confirm
./verify.sh # confirm from the database
./teardown.sh
最も有用な出力はバージョン比較です。3つのリリースに対する同一のリクエスト:
./setup.sh --plugin-version 1.11.2 # exploit succeeds
./setup.sh --plugin-version 1.12.0 # exploit succeeds
./setup.sh --plugin-version 1.13.0 # exploit fails at phase 2
ラボは意図的に脆弱であり、そのエクスプロイトスクリプトは WordPress 管理者を作成します。 すべてが
127.0.0.1にバインドされており、スクリプトはローカルラボ以外に対しては実行されません — ラボのエクスプロイトはラボ内でのみ実行されるを参照してください。これはスキャナーではありません。実際のサイトを確認するにはpoc/を使用してください。
sub_wp_create_user_role が特権ロールに設定されたポップアップ、および pl=pps&mod=popup&action=save を含む admin-ajax.php への POST リクエスト。クエリとログの grep:§10。
.
├── README.md
├── SECURITY.md Scope, authorized-use policy, disclosure
├── LICENSE MIT
├── requirements.txt
├── docs/
│ └── ANALYSIS.md Full root-cause analysis and fix rationale
├── patches/
│ ├── 01-frame-permission-merge.diff array_merge() → _mergePermissions()
│ ├── 02-subscribe-role-allowlist-and-nonce.diff Role allowlist + dedicated nonce
│ └── 03-v1.12.0-confirmation-page-hardening.diff Unrelated hardening added in 1.12.0
├── poc/
│ └── cve_2026_18322_check.py Passive version fingerprint (GET only)
├── lab/ Disposable Docker lab — INTENTIONALLY VULNERABLE
│ ├── docker-compose.yml MariaDB + WordPress 1.12.0 + Mailpit (loopback only)
│ ├── setup.sh / teardown.sh Bring the lab up / destroy it
│ ├── exploit_full_chain.py Full attack chain — CREATES AN ADMIN; lab-gated
│ └── verify.sh Independent confirmation from the database
├── scripts/
│ └── fetch_versions.sh Export plugin versions from SVN and regenerate diffs
└── tests/
└── test_check.py Offline unit tests for the passive checker
pip install -r requirements.txt pytest
python3 -m pytest tests/ -v
スイートは完全にオフラインです — 記録済みのフィクスチャを使用し、ライブターゲットには決してアクセスしません。1.13.0 の境界をまたぐバージョントリアージ、ターゲットの正規化、および各ステータスを決定する証拠ルールをカバーしています。
安全性の特性は文書化されるだけでなくアサートされています。完全なスキャンが状態変更リクエストを発行しないこと、および HttpClient が GET 以外の HTTP メソッドを公開しないことです。
開発中に見つかった2つのリグレッションが固定されています。CRLF の readme.txt が行アンカー解析を妨げること、および HTTP エラーページがプラグインの証拠として誤ってカウントされることです。
上流のソースから分析を再現するには:
./scripts/fetch_versions.sh # exports 1.11.2 / 1.12.0 / 1.13.0 and regenerates patches/
MIT — LICENSE を参照してください。分析対象のプラグインは GPLv2-or-later であり、ここでは 再配布されていません。scripts/fetch_versions.sh が公式 WordPress SVN リポジトリから取得します。
| ステータス | 意味 |
|---|
VULNERABLE | プラグインが < 1.13.0 で検出された |
NOT_VULNERABLE | プラグインが >= 1.13.0 で検出された |
PLUGIN_DETECTED_VERSION_UNKNOWN | プラグインは存在するが、バージョンを特定できない — 手動で確認すること |
PLUGIN_NOT_DETECTED | プラグインの証拠なし(不在の証明ではない) |
ERROR | ターゲットに到達不能、または不正な形式 |