Zend VMの奈落:`unserialize()`が引き起こすオブジェクトインジェクションの物理構造と防衛の極意
PHPの標準関数である `serialize()` と `unserialize()` は、永続化やプロセス間通信において極めて一般的なメカニズムである。しかし、この直列化・復元という一見素朴な操作の裏側で、Zend VMはメモリ上のバイナリ構造をパースし、型定義、プロパティ、そしてオブジェクトのライフサイクルを司るマジックメソッドを狂気的なまでの精密さで再構築している。
このプロセスの深淵を覗くとき、私たちは単なる「データの復元」ではなく、PHPエンジンが持つ動的ディスパッチの柔軟性がそのままセキュリティ上の致命的なアキレス腱へと変貌する瞬間を目撃することになる。本稿では、Zend VMの内部挙動、ガジェットチェーンが成立する物理的メカニズム、そしてモダンなPHPにおける根本的な防衛策について、低レイヤの視点から徹底的に解剖する。
—
1. Zend VMにおける `unserialize()` の内部挙動とマジックメソッドのディスパッチ
PHPのソースコード(C言語層)において、`unserialize()` は `ext/standard/var.c` 内のルーチンによって処理される。入力されたバイトストリームは、Zend VMのパーサーによって走査され、構造体がメモリ上に復元される。
特筆すべきは、オブジェクト(`zend_object`)の復元プロセスだ。ストリーム中に `O`(Object)または `C`(Custom Serialization)のシグネチャが検出された瞬間、Zendエンジンは以下のステップを踏む。
1. クラスエントリ(`zend_class_entry`)のルックアップ:
グローバルなシンボルテーブルから、指定されたクラス名に対応するポインタをO(1)のハッシュマップ検索で取得する。クラスがロードされていない場合、ここで自動ロード機構(`__autoload` / `spl_autoload_call`)が強制発動する。
2. メモリの割り当てとプロパティの初期化:
`emalloc()` を用いてオブジェクト領域を確保し、プロパティのデフォルト値を流し込む。この時点では、コンストラクタ(`__construct`)は一切実行されない。
3. マジックメソッドの遅延呼び出し:
復元が完了した直後、クラス定義に特定のマジックメソッドが存在するかどうかのフラグメント(`ZCE_}>\` 等の内部ビットマスク)がチェックされ、該当すればZend VMの関数呼び出しスタックに積まれる。
`__wakeup` と `__unserialize` の根本的な差異
PHP 7.4以前の主流であった `__wakeup()` と、現代のPHPにおいて推奨される `__unserialize()` では、Zend VM上でのハンドリングが決定的に異なる。
- `__wakeup()` の挙動:
オブジェクト全体のプロパティがすべて復元された後に呼び出される。つまり、不正なプロパティ値が注入された状態のオブジェクトが一度メモリ上に完成し、その後にバリデーションや修復を行おうとするアプローチである。これは、プロパティが書き込まれた瞬間からマジックメソッド発動までの間に、他の脆弱性が介入する余地を残す。
- `__unserialize(array $data)` の挙動:
PHP 7.4で導入されたこのメソッドは、Zend VMがプロパティを自動割り当てするフェーズをバイパスする。配列として生データが渡され、開発者が明示的にプロパティを代入する。これにより、未初期化状態や不正な型を持つオブジェクトの生成を水際で阻止できる。
—
2. ガジェットチェーン:なぜインジェクションはコード実行に至るのか
「オブジェクトインジェクション」とは、単に意図しないオブジェクトが生成されるバグではない。アプリケーション内に存在する既存のクラス群(ガジェット)の断片を、攻撃者が意図した順序で連鎖させ、最終的に任意コード実行(RCE)やファイル削除を引き起こす高度な攻撃手法である。
このチェーンの起動トリガーとなるのが、オブジェクト破棄時に発動する `__destruct()` や、前述の `__wakeup()` / `__unserialize()` である。
脆弱なコードの典型例
以下のコードは、一見すると無害に見えるが、Zend VMのコンテキストスイッチとマジックメソッドの連鎖によって崩壊する。
filePath)) {
file_put_contents($this->filePath, $this->content);
}
}
}
class ArbitraryCommandExecutor {
protected string $command;
// 内部でコマンドを実行する危険なマジックメソッド
public function __wakeup() {
// 開発者がデバッグ用や内部処理のつもりで書いたコード
system($this->command);
}
}
攻撃者が `unserialize()` に渡すペイロードを巧みに構築すると、メモリ上には以下のような依存関係(チェーン)が形成される。
1. `unserialize()` が実行され、`ArbitraryCommandExecutor` インスタンスが復元される。
2. 復元完了と同時に `__wakeup()` がトリガーされる。
3. `$this->command` に格納された任意のシェルコマンド(例: `rm -rf /` やリバースシェル)が、PHPプロセスを実行している権限でそのままOSの `system()` コールに渡される。
これが「ガジェットチェーンによる制圧」の物理的メカニズムである。攻撃者は新しいコードを書いているのではなく、既にアプリケーション内に存在する正当なコードの断片を、誤ったコンテキスト(悪意あるデータ)で強制実行させているに過ぎない。
—
3. OPcacheとプリローディング環境におけるリスク増幅
現代のプロダクション環境では、OPcacheおよびPHP 7.4で導入された「プリローディング(Preloading)」が常時稼働している。これにより、スクリプトのパースコストが排除され、すべてのクラス定義はあらかじめ共有メモリ(SHM)上にキャッシュ・常駐化される。
オブジェクトインジェクションの文脈において、OPcacheとプリローディングは以下の影響を持つ。
- オートロードのバイパス:
通常、存在しないクラス名がデシリアライズされた場合、オートローダーが発動してファイルシステムにアクセスする。しかし、プリロードされた環境下では、すべてのクラスエントリ(`zend_class_entry`)がメモリ上にすでに登録されているため、攻撃者はアプリケーションが想定していない任意のクラス(フレームワークの内部クラスやサードパーティ製ライブラリのクラス)を一切の遅延なく即座にインスタンス化できる。
- アタックサーフェスの拡大:
Composerを通じてベンダーディレクトリに含まれる数千のクラスがすべてメモリ上に常駐しているため、ガジェットの「部品」を探す攻撃者にとって、プリロード環境は宝の山と化す。
—
4. 根本的防衛策:`allowed_classes` と型の厳格なカプセル化
オブジェクトインジェクションに対する最大の防衛策は、「信頼できない入力に対して決して `unserialize()` をそのまま使わないこと」に尽きる。しかし、やむを得ずデータを復元する必要がある場合、Zendエンジンが提供するネイティブの安全弁を使用しなければならない。
`allowed_classes` オプションの厳格な適用
PHP 7.0以降、`unserialize()` の第2引数にはオプションの配列を指定できる。ここに許可するクラス名をホワイトリスト方式で明示的に渡すことが、現代のPHPセキュリティにおける最低限の防衛ラインである。
$allowed]);
if ($object === false && $data !== serialize(false)) {
throw new \RuntimeException(‘デシリアライズに失敗しました。’);
}
} catch (\Throwable $e) {
// ログ記録とインシデント検知
error_log(“Serialization Attack Detected: ” . $e->getMessage());
http_response_code(400);
exit(‘Invalid payload.’);
}
パラダイムシフト:JSONやMessagePackへの移行
そもそも、Zend VMのマジックメソッドやオブジェクト構造に依存する `serialize()` / `unserialize()` を、Web APIやセッションストレージのデータ交換に使用すること自体が、アーキテクチャ上の設計ミスと言わざるを得ない。
データの永続化や外部との通信には、以下の代替手段を強く推奨する。
1. `json_encode()` / `json_encode(…, JSON_THROW_ON_ERROR)`:
JSONは純粋なスカラー値と配列の構造しか持たず、デシリアライズ時に任意のクラスのインスタンス化やマジックメソッドの自動実行を引き起こすことは構造的に不可能である。オブジェクトの状態を保存したい場合は、DTO(Data Transfer Object)のプロパティ値を連想配列としてシリアライズし、受け取り側で明示的にコンストラクタ経由で再構築すべきである。
2. Sodiumなどの暗号学的署名:
どうしてもシリアライズデータを使用せざるを得ない場合(例:Laravel等のフレームワーク内部の署名付きCookie)、HMAC(Hash-based Message Authentication Code)を用いてペイロードが改ざんされていないことを暗号学的に保証し、鍵の漏洩を防ぐ厳格な環境変数管理を行わなければならない。
—
結びにかえて
Zend VMは、動的言語であるPHPを高速に動作させるために洗練された極限の仮想マシンである。しかし、その「動的であること」「マジックメソッドによる高度な抽象化」は、一歩間違えれば攻撃者にシステム全体の支配権を明け渡す諸刃の剣となる。
システムアーキテクトとして私たちがなすべきことは、フレームワークの背後で何が起きているのかをZend VMの低レイヤの視点から常に脳内トレースし、脆弱性の芽をアーキテクチャの設計段階で完全に刈り取ることである。「動くからよい」という妥協を捨て、メモリと実行コンテキストの安全性を支配した者だけが、真に堅牢なPHPWebシステムを構築することができる。