こんにちは、インフラエンジニアのryuです。
2026年10月21日、PHPに2件の重大な脆弱性が公開されました。いわゆる「PHP2Shell」と呼ばれる問題です。
そのうちの一つが、今回解説する CVE-2026-109618 です。
この脆弱性について調べていると、次のような説明を見かけます。
PHPでメモリ不足を発生させると、認証なしでリモートコード実行につながる可能性がある。
しかし、これだけでは疑問が残ります。
メモリが足りなくなれば、普通はエラーが出て処理が止まるだけではないのでしょうか。なぜ、そこから攻撃者がサーバー上でコードを実行できるのでしょうか。
私自身も、この部分を理解するために、PHPの内部構造やメモリ管理、修正前後の挙動を調べました。
この記事では、PHPやメモリの知識がほとんどない方でも理解できるように、基礎から順番に説明します。
一方で、後半ではPHPのZend EngineにあるHashTable、Bucket、zval、pDestructor、longjmp()といった内部構造も掘り下げます。
「メモリ不足が、どのような条件を経てRCEにつながるのか」を、仕組みと証拠の両面から理解することが目標です。
この記事は2026年10月24日時点の公開情報に基づきます。具体的な悪用手順ではなく、仕組みの理解と防御を目的としています。
1. まず結論:メモリ不足そのものがRCEになるわけではない¶
最初に、この記事で最も重要なことを説明します。
CVE-2026-109618は、単にPHPをメモリ不足にすればコマンドが実行される、という脆弱性ではありません。
問題は、メモリ不足が発生した際に、PHPの内部データが「作成途中」のまま残り、その後の終了処理で「正常に作成されたデータ」として扱われることです。
通常、PHPは不要になった配列や文字列などのメモリを後片付けします。
しかし、作成途中のデータを正常なデータだと誤認すると、後片付けの処理が、本来参照すべきではないメモリを読み取る可能性があります。
さらに、特定の条件が重なると、そのメモリに含まれる値が「呼び出す関数のアドレス」として解釈され、意図しない関数呼び出しにつながります。
この関数呼び出しを攻撃者が十分に制御できた場合、RCE(Remote Code Execution:リモートコード実行)に至る可能性があります。
つまり、問題の本質は次の3点です。
- メモリ不足によって内部処理が途中で中断される
- 中断されたデータが不整合な状態で残る
- 終了処理がその不整合なデータを信頼して使用する
PHP公式は、Apacheのmod_php構成で、認証なしのHTTPリクエストによるRCEを確認しています。
ただし、すべてのサーバーで同じ入力を送れば必ず成功するという意味ではありません。メモリ配置、PHPのビルド、拡張機能、Webサーバーの構成などが影響します。
出典:PHP公式アドバイザリ:CVE-2026-109618
2. そもそもPHPとは何か¶
PHPはサーバー側で動くプログラム¶
PHPは、Webサイトの裏側で動くプログラミング言語です。
例えば、WordPressで記事を表示するとき、PHPはデータベースから記事を読み込み、ブラウザへ返すHTMLを生成します。
ここで大切なのは、PHPの処理はブラウザではなく、サーバー側で実行されるということです。
PHPに重大な脆弱性がある場合、影響を受けるのはページの表示だけではありません。サーバー内のファイルやデータベースなどにも危険が及ぶ可能性があります。
WordPressのログイン画面より前にPHPが動く¶
WordPressの認証は、WordPressのプログラムが実行されてから行われます。
一方、HTTPリクエストの解析は、WordPressのプログラムが動き始める前にPHPが実施することがあります。
例えば、フォームから送信されたデータを$_POSTや$_FILESに格納する処理です。
そのため、PHP自体の入力解析に脆弱性がある場合、WordPressのログイン認証に到達する前に問題が発生する可能性があります。
WordPressのパスワードを強くしたり、二要素認証を設定したりしても、PHP本体の認証前脆弱性を修正することにはなりません。
HTTPリクエストとは¶
HTTPリクエストとは、ブラウザなどがWebサーバーに送る要求です。
例えば、記事を表示するときはGET、フォームを送信するときはPOSTが使われます。
ファイルアップロードを含むフォームでは、multipart/form-dataという形式がよく使われます。
この形式では、送信内容を複数のパートに分け、それぞれに項目名やファイル名などの情報を付けます。
PHPは受信したデータを解析し、内部の配列に格納します。
この入力データを内部配列へ変換する処理が、今回の脆弱性を理解するうえで重要です。
3. CVE-2026-109618の基本情報¶
| 項目 | 内容 |
|---|---|
| CVE番号 | CVE-2026-109618 |
| 通称 | PHP2Shellを構成する脆弱性の一つ |
| 公開日 | 2026年10月21日 |
| 深刻度 | Critical |
| CVSS v4.0 | 9.2 |
| 主な原因 | OOM時の不適切な中断処理による内部状態の破損 |
| 確認された影響 | 認証なしのリモートコード実行 |
| RCEが実証された主な構成 | Apache mod_php |
| 根本的な対策 | 修正版PHPへの更新 |
最初の修正版は次のとおりです。
| PHP系列 | 最初の修正版 |
|---|---|
| PHP 8.2 | 8.2.35 |
| PHP 8.3 | 8.3.36 |
| PHP 8.4 | 8.4.27 |
| PHP 8.5 | 8.5.12 |
PHP 8.1以前は、通常のアップストリームサポートが終了しています。古い系列を利用している場合は、サポート対象のPHPへの移行、またはOS・ホスティング事業者が提供する修正済みパッケージの確認が必要です。
4. OOMとは何か? メモリリークとの違い¶
メモリはプログラムの作業スペース¶
メモリは、プログラムが実行中のデータを一時的に保存する場所です。
例えば、PHPが文字列を作成したり、配列に要素を追加したりするとき、メモリ上に保存場所を確保します。
このうち、実行中に必要に応じて確保・解放する領域を「ヒープ」と呼びます。
ヒープは、作業机にたとえると理解しやすいでしょう。
作業中は机の上に書類を置き、不要になった書類は片付けます。新しい書類を置く場所が足りなければ、作業を続けられません。
プログラムのメモリ管理も、これに近い考え方です。
OOMは「必要なメモリを確保できないこと」¶
OOMは、Out Of Memoryの略です。
PHPでは、memory_limitという設定で、PHPのメモリ使用量に上限を設けられます。
例えば、次の設定です。
memory_limit = 128M
これは、PHPが管理するメモリ使用量に128MiBの制限を設ける設定です。
PHPがその制限を超えてメモリを確保しようとすると、メモリ不足のエラーが発生します。
ここで重要なのは、サーバー全体の物理メモリがなくなっているとは限らないことです。
サーバーに十分な空きメモリがあっても、PHP自身のmemory_limitに到達すればエラーが発生します。
メモリリークとは別の問題¶
メモリリークとは、不要になったメモリを適切に解放できず、使えるメモリが減っていく問題です。
OOMとメモリリークは、次のように区別できます。
| 用語 | 意味 | 今回との関係 |
|---|---|---|
| OOM | メモリ確保に失敗すること | 脆弱な処理を中断させるきっかけ |
| メモリリーク | 不要なメモリを解放し忘れること | 必須条件ではない |
| メモリ破壊 | メモリ内のデータや管理状態が不正になること | 脆弱性の核心 |
| メモリ情報漏えい | 本来見えないメモリ内容が外部へ漏れること | 別の問題。攻撃の補助になる場合がある |
したがって、CVE-2026-109618を「メモリリークによるRCE」と説明するのは正確ではありません。
より正確には、OOM時の中断処理に起因するメモリ状態の破損です。
5. PHPの配列は内部でどう管理されているのか¶
ここからは、PHPの内部構造を見ていきます。
最初は少し難しく感じるかもしれませんが、「値」「保管場所」「管理台帳」の3つに分けると理解しやすくなります。
zval:PHPの値と型を管理する構造¶
PHPでは、次のような変数を扱えます。
$number = 123;
$name = "Taro";
$items = ["apple", "orange"];
123は整数、"Taro"は文字列、$itemsは配列です。
PHPの内部では、値だけでなく、その値がどの種類なのかも管理する必要があります。
この役割を持つ構造がzvalです。
zvalには、値に関する情報と、整数・文字列・配列などを区別する型情報が含まれます。
例えば、型情報が「配列」を示している場合、PHPは値の領域を配列構造への参照として扱います。
ここで重要なのは、型情報が間違っていると、同じメモリ内容でもPHPが異なる意味で解釈してしまうことです。
HashTable:配列全体を管理する台帳¶
PHPの配列は、内部では主にHashTableという構造で管理されています。
PHPのソースコードでは、zend_arrayという構造名も登場します。
HashTableは、配列の要素数や格納場所などを管理する役割を持ちます。
例えば、次の配列を考えます。
$user = [
"name" => "Taro",
"age" => 30
];
PHPの内部では、"name"と"age"というキー、それぞれに対応する値、配列全体の管理情報などが保持されます。
HashTableを台帳にたとえると、どの要素がどこに保存されているかを記録しているものです。
Bucket:配列要素を格納する場所¶
Bucketは、配列の各要素を保持する内部構造です。
大まかには、値を表すzval、キーなどの情報を持ちます。
先ほどの配列であれば、"name"に対応する値と"age"に対応する値が、それぞれ要素として管理されます。
ここで、次の3つを区別してください。
| 構造 | 役割 | たとえ |
|---|---|---|
zval |
値と型を表す | 中身と種類を書いたラベル |
Bucket |
配列の要素を保持する | 一つひとつの保管箱 |
HashTable |
配列全体を管理する | 保管箱の管理台帳 |
この関係を理解すると、今回の脆弱性の流れが見えてきます。
nNumUsedは何を表すのか¶
HashTableには、nNumUsedという管理情報があります。
これは、内部のBucket領域のうち、使用済みとして扱う範囲に関係するカウンタです。
厳密には、PHP配列の論理的な要素数と常に同じとは限りません。削除済みのスロットなども関係するためです。
重要なのは、PHPの内部処理が、このような管理情報を使って、どこまでのBucketを処理するか判断することです。
もし管理情報だけが先に更新され、対応するBucketの初期化が終わっていなかったらどうなるでしょうか。
台帳では「使用済み」となっているのに、実際の保管箱は未完成という状態が生まれます。
これが、今回の具体的な攻撃経路を理解する鍵です。
6. OOMによって「作りかけのBucket」が残る¶
Datadog Security Labsが公開した解析では、PHPのHashTableへ要素を追加する処理が取り上げられています。
その経路では、使用済み領域の管理情報が更新された後、キーなどに必要なメモリ確保が行われます。
通常なら問題ありません。
メモリを確保し、必要な情報を書き込み、Bucketを完成させればよいからです。
しかし、途中でOOMが発生すると状況が変わります。
正常時とOOM発生時の違い¶
| 処理段階 | 正常な場合 | OOMが発生した場合 |
|---|---|---|
| ① 管理情報の更新 | 使用済み領域を更新する | 使用済み領域を更新する |
| ② Bucketの選択 | 新しい要素の保存先を選ぶ | 新しい要素の保存先を選ぶ |
| ③ メモリの確保 | 必要なメモリを確保できる | メモリ確保に失敗する |
| ④ 初期化 | キーや値を正しく初期化する | 初期化が完了しない |
| ⑤ 処理結果 | 完成したBucketが残る | 未完成のBucketが使用済み範囲に残る可能性がある |
重要なポイント
正常時は、管理情報と実際のBucketの状態が一致しています。
しかし、OOMが発生した場合、管理情報だけが更新され、対応するBucketの初期化が終わらないことがあります。
つまり、PHPから見ると「使用済みの要素」のはずなのに、実際には中身が完成していない状態です。
OOMが起きたタイミングが重要です。
管理情報が更新される前に安全に失敗するなら、未完成の要素を使用済みとして扱う問題は起きません。
一方、管理情報を更新した後に処理が中断されると、初期化が終わっていない領域が使用済みの範囲に含まれる可能性があります。
この段階では、まだRCEではありません。
問題が表面化するのは、その後の後片付けです。
なぜ処理が途中で中断されるのか¶
修正前のPHPでは、OOM時の処理にzend_bailout()という仕組みが使われていました。
その内部では、longjmp()によって、実行中の処理から別の実行位置へ移動します。
通常の関数呼び出しは、呼び出された関数が終了すると、呼び出し元へ戻ります。
一方、longjmp()は、途中の関数を通常の戻り方で終了させず、あらかじめ設定された場所へ制御を移します。
例外処理に似た働きですが、C言語の低レベルな制御移動です。
この仕組み自体が必ず危険というわけではありません。
問題は、途中で処理を飛ばされた関数が、自分の管理していた内部状態を元に戻す機会を失うことです。
上級者向け:何が危険な順序なのか¶
概念を理解するため、実際のPHPソースを簡略化した擬似コードを示します。
/* 概念説明用。実際のphp-srcのコードを
そのまま抜き出したものではありません。 */
table->nNumUsed++;
Bucket *bucket = select_next_bucket(table);
bucket->key = allocate_and_copy_key(key);
/* このメモリ確保でOOMが起きる可能性 */
initialize_value(&bucket->val, value);
仮にallocate_and_copy_key()の途中でlongjmp()が発生すると、後続の初期化が実行されません。
しかし、先に更新したnNumUsedは、そのまま残る可能性があります。
正常な処理なら、初期化が終わってから管理情報を確定する、または失敗時に元へ戻す必要があります。
今回の根本問題は、こうした中断時の整合性を保証できていなかったことです。
なお、PHP公式は、失敗したメモリ確保が、破損した内部データそのものに直接関係している必要はないとも説明しています。
重要なのは、内部状態が一時的に不完全なタイミングでOOMが発生し、そのまま中断されることです。
出典:PHP公式アドバイザリ、Datadog Security Labs
7. なぜ未完成のデータが後片付けで問題になるのか¶
PHPのリクエストが失敗したとしても、作成済みのデータを解放する必要があります。
この処理をリクエストの終了処理、あるいはシャットダウン処理と呼びます。
例えば、リクエスト中に作成した配列が不要になれば、PHPはその配列の要素を確認し、必要なメモリを解放します。
正常な配列であれば、問題なく後片付けできます。
しかし、HashTableの管理情報が「このBucketまで使用済み」と示しているのに、そのBucketの初期化が完了していない場合、終了処理が未完成の領域を読み取る可能性があります。
解放したメモリは必ずゼロになるわけではない¶
ここで、もう一つ重要なメモリの性質があります。
プログラムがメモリを解放しても、その領域に保存されていたバイト列が直ちにすべてゼロになるとは限りません。
メモリ管理システムは、解放された領域を後で再利用することがあります。
そのため、再利用された領域に、以前のデータが一部残っている場合があります。
これは、机の引き出しを別の人に貸したとき、以前入っていたメモが残っているようなものです。
本来、新しい利用者は自分のデータを正しく初期化する必要があります。
しかし、初期化途中で処理が止まり、残っていた古い内容を新しいデータとして読み取ってしまうと、問題が起きます。
ヒープグルーミングとは¶
攻撃者がメモリの割り当てや解放の順番に影響を与え、特定の領域が再利用されるように調整する手法を、ヒープグルーミングと呼びます。
ただし、これは攻撃者がサーバー内の好きなメモリアドレスに自由に書き込めるという意味ではありません。
メモリの再利用は、アロケータの実装、割り当てサイズ、処理順序、拡張機能、他のリクエストなどに影響されます。
Datadogが示した経路では、このようなメモリ再利用の性質が重要な役割を果たしています。
8. 未完成Bucketから偽のHashTableへ¶
ここが今回の脆弱性で最も重要な部分です。
未完成のBucketを読み取っただけでは、必ずRCEになるわけではありません。
単にクラッシュする場合もありますし、意味のない値として扱われる場合もあります。
RCEにつながるには、読み取られたバイト列が、PHPの内部構造として都合よく解釈される必要があります。
第1段階:古いバイト列がzvalとして読まれる¶
Bucketには値を管理するzvalが含まれます。
未初期化領域に残った古いバイト列がzvalとして読まれ、その型情報が配列を示しているように見えると、PHPはその値を配列への参照として扱う可能性があります。
これは、実際には別用途のデータなのに、「配列」というラベルが付いたデータとして解釈される状態です。
第2段階:参照先がHashTableとして解釈される¶
zvalが配列を示す場合、PHPは対応する配列構造を参照します。
その参照先が、本物のHashTableではないにもかかわらず、HashTableのような内部構造として扱われることがあります。
このような領域を、説明上「偽のHashTable」と呼びます。
ここでの「偽」とは、PHPが安全な方法で新しい配列を作ったという意味ではありません。
別用途のメモリを、配列の管理構造として誤って読み取っているという意味です。
第3段階:後片付けの関数ポインタが使われる¶
PHPのHashTableには、配列要素を破棄するときに使用する関数ポインタpDestructorがあります。
もし偽のHashTableとして解釈された領域に、意図しない関数アドレスが置かれていた場合、後片付けの処理がそのアドレスを使用する可能性があります。
未完成BucketからRCEまでの各段階¶
| 段階 | PHP内部で起きること | 追加で必要な条件 |
|---|---|---|
| ① 未完成Bucket | 初期化されていない領域に古いバイト列が残る | その領域が後片付けで参照される |
② zvalとして解釈 |
古いデータがPHPの値として読まれる | 型情報などが配列として解釈できる |
| ③ 偽のHashTable | 別用途の領域を配列の内部構造として扱う | 参照先が有効で、必要な構造が成立する |
④ pDestructorを参照 |
後片付けで関数ポインタを使用する | 関数アドレスが有効である |
| ⑤ 意図しない関数呼び出し | 本来とは異なる関数が呼び出される | 呼び出し規約や引数が成立する |
| ⑥ RCE | サーバー上で意図しないコードが実行される | コード実行につながる関数呼び出しが成立する |
ここが最も重要です。
未完成Bucketが存在するだけでは、RCEにはなりません。
PHPが古いデータを配列として誤認し、参照先をたどり、関数ポインタを使用し、さらに関数アドレスと引数が成立して、初めてRCEにつながる可能性があります。
どこか一つでも条件が成立しなければ、エラーやクラッシュなどで終わる場合があります。
この一連の動作は、PHPが本来とは異なる意味でメモリを解釈してしまうことによって発生します。
つまり、メモリに保存されているバイト列そのものよりも、それをPHPが何として扱うかが重要なのです。
9. pDestructorとは何か? 関数ポインタを理解する¶
通常のポインタは「データの場所」を指す¶
ポインタは、メモリ上の場所を表す値です。
例えば、C言語では次のように書けます。
int number = 100;
int *ptr = &number;
ptrは、numberが保存されている場所を指します。
ポインタを使うと、その場所に保存された値を読み書きできます。
関数ポインタは「関数の場所」を指す¶
一方、関数ポインタは、実行する関数の場所を指します。
次のコードは、考え方を説明するための安全な例です。
#include <stdio.h>
void cleanup_message(void) {
puts("Cleanup completed");
}
int main(void) {
void (*callback)(void) = cleanup_message;
callback();
return 0;
}
callbackには、cleanup_messageという関数のアドレスが保存されています。
callback()を実行すると、そのアドレスにある関数が呼び出されます。
つまり、関数ポインタは、処理内容そのものではなく、どの関数を呼ぶかを示す情報です。
pDestructorは配列の後片付け役¶
PHPのHashTableには、要素を破棄するときに呼び出す関数を指すpDestructorがあります。
名前のDestructorは、後片付けや破棄処理を意味します。
通常は、PHPが設定した正しい関数が呼び出されます。
しかし、偽のHashTableを参照してしまうと、本来のものではない関数アドレスを使う可能性があります。
ここで誤解してはいけないのは、pDestructorの中にOSコマンドの文字列が保存されているわけではないということです。
pDestructorが保持するのは、関数のアドレスです。
そして、関数を呼び出すには、アドレスだけでなく、呼び出し規約や引数も成立する必要があります。
関数ポインタの誤用がRCEにつながる条件¶
例えば、何らかの理由で、本来の後片付け関数とは異なるネイティブ関数が呼び出されたとします。
その関数が外部コマンドの実行などに関係する機能を持ち、渡される引数も適切であれば、OS上で予期しない処理が実行される可能性があります。
Datadogの検証では、最終的にglibcのsystem()呼び出しに到達したことが、システムコールの記録などによって確認されています。
ただし、これは特定の検証環境で成立した攻撃経路です。
関数ポインタの値が壊れたからといって、自動的にRCEが成立するわけではありません。
10. ASLRがあるのに、なぜRCEが成立するのか¶
ここからは上級者向けの内容です。
ASLRはアドレスを予測しにくくする仕組み¶
ASLRは、Address Space Layout Randomizationの略です。
プログラムや共有ライブラリなどをメモリへ配置する際、その配置場所をランダム化するセキュリティ機能です。
例えば、ある関数がライブラリの中に存在していても、そのライブラリがプロセス内のどこに配置されるかは、実行ごとに変わる場合があります。
これにより、攻撃者が固定のアドレスを使って特定の関数を呼び出すことを難しくします。
関数のオフセットと実際のアドレスは違う¶
共有ライブラリ内の関数の位置は、概念的には次の関係で表せます。
関数の実行時アドレス = ライブラリの配置先アドレス + ライブラリ内の相対位置
それぞれの意味は次のとおりです。
| 項目 | 意味 |
|---|---|
| ライブラリの配置先アドレス | 実行中のプロセスで、そのライブラリが読み込まれた位置 |
| ライブラリ内の相対位置 | ライブラリの先頭から対象関数までの位置 |
| 関数の実行時アドレス | 実際に関数が存在するメモリ上の位置 |
ライブラリ内の相対位置が分かっていても、ASLRによって配置先が変われば、実際のアドレスは変わります。
そのため、同じPHPのバージョンであっても、すべてのサーバーで同じアドレスを使えるとは限りません。
攻撃者はサーバーの全メモリを知る必要があるのか¶
必ずしもそうではありません。
重要なのは、攻撃に必要な構造や関数について、利用可能なアドレスを成立させられるかどうかです。
ただし、その方法は環境や攻撃経路によって異なります。
メモリ情報漏えいが利用できる場合もありますが、すべての経路で同じ情報漏えいが必須だと断定することはできません。
また、ASLRが有効だからといって、脆弱性が存在しなくなるわけでもありません。
ASLRは攻撃の難易度を上げる防御であり、PHPの不正なメモリ状態そのものを修正するものではないからです。
同じPHPバージョンなら必ず再現するのか¶
再現性には、少なくとも次のような要素が影響します。
- PHPのバージョンとビルドオプション
- CPUアーキテクチャとOS
- Apache
mod_php、PHP-FPMなどの実行方式 - 読み込まれたPHP拡張機能
- メモリアロケータの状態
- Webサーバーのリクエスト制限
- ワーカープロセスの数や再起動
- 同時に処理される他のリクエスト
したがって、脆弱なPHPを使用していることと、特定の攻撃が毎回成功することは別の問題です。
一方、再現が難しいことを理由に安全と判断するのも誤りです。
PHP公式は、通常の複数ワーカー構成のmod_phpで、認証なしのHTTP経由RCEを実証しています。
11. Apache mod_phpとPHP-FPMでは何が違うのか¶
PHPには、Webサーバーと連携する複数の実行方式があります。
代表的なのが、Apacheのmod_phpとPHP-FPMです。
mod_phpの場合¶
mod_phpでは、Apacheのプロセス内でPHPが実行されます。
PHP公式によると、mod_phpではリクエスト開始処理が失敗した場合でも、リクエスト終了処理が呼び出される経路があります。
そのため、開始処理中のOOMによって不整合な状態が残り、終了処理がその状態を使用する可能性があります。
この構成では、アプリケーション固有のPHPコードが動く前に成立する、完全なHTTP経由RCEが確認されています。
PHP-FPMの場合¶
PHP-FPMは、PHP専用のワーカープロセスがリクエストを処理する方式です。
PHP公式は、リクエスト開始時の失敗について、FPMでは子プロセスが終了するため、mod_phpと同じ終了処理経路にならないと説明しています。
ただし、正常なリクエスト開始後にOOMが発生する経路は別です。
PHP公式はソースコード解析上のリスクを指摘していますが、CVE-2026-109618について、FPMでの完全な攻撃実証は公式アドバイザリの時点では示されていません。
したがって、次の2つは区別する必要があります。
「mod_phpでRCEが実証された」という事実と、「PHP-FPMでは対策が不要」という判断は同じではありません。
PHP-FPMも修正版へ更新すべきです。
12. PHPはどのように修正されたのか¶
CVE-2026-109618の修正は、単にHashTableの特定の一箇所を修正するものではありません。
問題の根本には、OOM時にlongjmp()で処理を即座に中断し、内部状態が未完成のまま残ることがありました。
修正版では、OOM時の制御方法が見直されています。
修正前:OOM時に途中から抜ける¶
修正前は、メモリ確保に失敗すると、処理の途中からzend_bailout()によって抜ける経路がありました。
このとき、呼び出し途中の関数が内部状態を復元する前に制御が移る可能性がありました。
修正後:安全な地点までエラーを遅らせる¶
修正版では、OOMを即座にlongjmp()で処理する代わりに、遅延させる仕組みが導入されています。
PHPの実行エンジンが安全に中断できる地点で、エラーを処理する考え方です。
また、メモリ消費がさらに高い閾値に達した場合には、プロセスを直ちに終了させる仕組みも用意されています。
これにより、内部状態が不完全なまま終了処理に渡されることを防ぎます。
重要なのは、OOMが起きなくなったわけではなく、OOMが起きたときの壊れ方を防ぐ修正だということです。
【参考】修正コミット:php-src「Avoid state corruption via bailout」
13. Dockerで修正前後の挙動を確認する¶
仕組みを理解するには、実際に修正前後のPHPを比較すると分かりやすくなります。
今回の調査では、次の公開リポジトリを参考にしました。
php2shell-cve-2026-109618-109619-lab
このリポジトリは、脆弱性の条件や修正前後の違いを確認するための検証環境です。
実際のRCEを再現する攻撃コードではありません。
比較するPHPのバージョン¶
検証では、修正前のPHP 8.4.4と、修正後のPHP 8.4.27を比較します。
検証スクリプトは、メモリ制限を設定した状態でリクエストを送信し、処理の途中でOOMが起きたときの状態を観察します。
主な観察結果は次のとおりです。
| PHP | memory_limit |
観察結果 |
|---|---|---|
| 8.4.4 | 8M |
リクエスト開始途中でOOM。未完了のセッション進捗情報が残る |
| 8.4.4 | -1 |
同じ入力が最後まで処理される |
| 8.4.27 | 8M |
OOMは起きるが、修正後の処理経路でHTTP 500となり、同じ未完了状態は残らない |
| 8.4.27 | -1 |
同じ入力が最後まで処理される |
ここで重要なのは、修正後のPHPでもメモリ不足のエラーは起きるということです。
脆弱性の修正とは、メモリ不足を完全になくすことではありません。
メモリ不足が発生したとき、内部状態を不正なまま扱わないようにすることです。
HTTP 500やクラッシュだけではRCEを証明できない¶
検証中にHTTP 500が返ったり、接続が閉じられたりしても、それだけでRCEを実証したことにはなりません。
同様に、HTTP 200が返ったから安全というわけでもありません。
RCEを実証するには、実際に意図しないコードが実行されたことを示す証拠が必要です。
Datadogの研究では、システムコールの記録やプロセスの親子関係などが、その証拠として使われています。
一方、今回のDocker検証ラボで確認できるのは、OOM発生時の状態や修正前後の挙動の違いです。
この違いを明確にしておくことが、脆弱性研究では非常に重要です。
ASanを使えば何でも検出できるのか¶
ASan(AddressSanitizer)は、メモリへの不正アクセスを検出するための仕組みです。
しかし、PHPにはZend独自のメモリアロケータがあります。
このアロケータが大きな領域をまとめて管理している場合、ASanからは内部の細かい割り当てや解放が見えないことがあります。
検証では、USE_ZEND_ALLOC=0を使ってZendのアロケータを無効にする方法があります。
ただし、この設定ではPHPのmemory_limitの挙動も変わります。
そのため、ASanで検出しやすい条件と、memory_limitによるOOMを再現する条件が必ずしも一致しません。
ASanでエラーが出なかったことは、メモリ安全性が完全に保証されたことを意味しません。
14. CVE-2026-109619とは何が違うのか¶
PHP2Shellとして公開された脆弱性は、CVE-2026-109618だけではありません。
もう一つがCVE-2026-109619です。
この2つは、どちらもPHPの内部状態に関係しますが、問題の発生方法が異なります。
| 項目 | CVE-2026-109618 | CVE-2026-109619 |
|---|---|---|
| 主な問題 | OOMによる中断後の不完全な内部状態 | 解放済みメモリへの古い参照が残ること |
| 重要なタイミング | メモリ確保失敗と終了処理 | リクエスト間の状態管理 |
| 理解のための例え | 作りかけの書類を完成品として扱う | 捨てた書類の保管場所を次の作業でも参照する |
| 影響 | 条件次第でRCE | 条件次第でRCE |
CVE-2026-109619では、リクエストに属するメモリが解放された後も、グローバルな状態に古い参照が残る問題が扱われています。
そのため、後続のリクエストが同じワーカーで処理されることが重要になる経路があります。
両者は似ていますが、CVE-2026-109618を単純な「リクエスト間のUse After Free」と説明するのは不十分です。
15. WordPressやPHPサーバーの管理者はどう対策すべきか¶
最優先はPHP本体の更新¶
最も重要なのは、実際にWebリクエストを処理しているPHPを修正版へ更新することです。
WordPress本体やプラグインが最新版であっても、PHP本体が脆弱なままなら、今回の問題を解消したことにはなりません。
また、コマンドラインで表示されるPHPのバージョンと、Webサーバーで使用しているPHPのバージョンが異なる場合があります。
php -vだけで判断せず、Webサーバー側の実行環境を確認してください。
302リダイレクトだけではPHPを無効化できない¶
例えば、古いWordPressサイトへのアクセスを新しいECサイトへ転送している場合を考えます。
ブラウザからの通常のアクセスはリダイレクトされるかもしれません。
しかし、Webサーバーがリダイレクトを返す前にPHPを実行する経路や、リダイレクト対象外のURLが残っていれば、脆弱なPHPが引き続き動作する可能性があります。
「サイトを転送している」ことと、「古いPHPが外部から実行されない」ことは別です。
不要なPHPの公開を停止できるか、Webサーバーやホスティング事業者の設定を確認する必要があります。
すぐ更新できない場合の暫定対策¶
PHP公式は、修正をすぐ適用できない環境向けに、管理者レベルでmemory_limit=-1を強制する方法を暫定策として説明しています。
これは、PHPの設定上限に到達することで発生する予測可能なOOMを避ける目的です。
ただし、OS側で実際のメモリ確保に失敗する可能性は残ります。
さらに、無制限のメモリ使用はサービス停止や過剰なリソース消費につながる可能性があります。
そのため、memory_limit=-1は完全な対策ではなく、運用上のリスク評価が必要な暫定措置です。
PHP公式は、RLIMIT_ASやRLIMIT_DATAを代替策として使用しないよう警告しています。これらはメモリ確保失敗を発生させ、脆弱な経路を通る可能性があるためです。
また、Webサーバーやリバースプロキシで、リクエストサイズ、multipartのパート数、ヘッダー数、入力変数数などを制限することも有効な防御層です。
ただし、WAFやレート制限だけで完全に防げるとは限りません。
侵害調査で見るべきポイント¶
過去の攻撃を調査する場合は、PHPのメモリ不足エラーだけを見るのでは不十分です。
Webサーバーのアクセスログ、PHPのエラーログ、ワーカーの異常終了、Webワーカーからの予期しないプロセス起動などを時系列で確認する必要があります。
Datadogの実証では、コード実行後もApacheワーカーが生存し、HTTP 200を返す例が示されています。
したがって、クラッシュやHTTP 500が記録されていないことを理由に、侵害がなかったと判断することはできません。
まとめ:本当に危険なのは、メモリ不足後の「不整合な状態」¶
CVE-2026-109618を理解するうえで最も重要なのは、メモリ不足そのものではありません。
PHPが内部データを作り終える前に処理を中断し、その不完全なデータを後片付けで正常なものとして扱ってしまうことです。
今回取り上げたDatadogの経路では、未完成のBucketがzvalとして解釈され、さらに偽のHashTableを経由して、後片付け用の関数ポインタが利用されることでRCEにつながりました。
この過程には多くの条件が必要であり、単純なOOMがそのままRCEになるわけではありません。
しかし、PHP公式が実際に認証なしのRCEを確認している以上、再現の難しさを理由に対策を先送りするべきではありません。
PHPを修正版に更新することが、最も確実な対策です。
そして、この脆弱性から学べるのは、PHPに限らない重要な設計上の教訓です。
エラーが起きたときこそ、プログラムの内部状態を正しく保たなければならない。
正常に動作しているときだけでなく、メモリ確保の失敗や異常終了まで考慮することが、安全なシステムを作るうえで欠かせません。
参考資料・出典¶
本記事では、公式アドバイザリ、PHPのソースコード、セキュリティ研究機関の解析を参考にしています。
- PHP Project:CVE-2026-109618公式アドバイザリ — 原因、影響、修正内容、緩和策、CVSS。
- PHP Project:CVE-2026-109619公式アドバイザリ — 関連脆弱性との違い。
- PHP Project:2026年10月21日のセキュリティ発表 — 修正版の公開情報。
- Datadog Security Labs:PHP2Shellの解析 — HashTable、Bucket、関数ポインタを利用したRCEの実証。
- php-src:Zendの型定義
zend_types.h—zval、HashTableなどの構造。 - php-src:HashTable実装
zend_hash.c— 配列要素の管理と破棄処理。 - php-src:修正コミット
506e87fa7dcd— OOM時の処理の変更。 - JPCERT/CC:PHPにおける複数の脆弱性に関する注意喚起 — 国内向け注意喚起と修正版情報。
- Wiz Research:PHP2Shell — 脆弱性の発見と技術的背景。
- GitHub:PHP2Shellローカル検証ラボ — 修正前後の挙動を確認する非武器化の検証環境。



