php2shell の仕組み
php2shell
今更だけど、php2shell の仕組みをある程度理解した。
php2shellというのは、2026年10月21日に話題になった、PHP のメモリ管理の脆弱性とcleanup時のバグを利用した攻撃手法。
自分は、今はPHPのサービスを管理してないし、PHPのメンテの仕事も抱えてなかったので一旦スルーしたけど、仕組み自体は気になっていて、時間があったので詳しく調べてみた。
この攻撃は、実際やろうとするとかなり難しいとは聞いていたが、仕組みを追ってみると自分の中で難しさの解像度が上がった。
post_max_size > memory_limit という変な設定をしていることが必須条件なんだけど、そんなサーバーは滅多に無いと思うので、あんな騒ぎになるほどのことか?とは思った。
しかし、PHPのWEBサイトを使っているだけで、”影響を受ける可能性” があると言われればまあそうだし、設定を確認して、うちは絶対大丈夫ですと言うのも嫌だし、偉い人に説明するのも難しいので、建前的には絶対に対応を求められる。
一番影響を受けたのは、どちらかというとアップデートしたり侵害調査をしたりの対応をしなければならない人たちだったと思う。
攻撃成立の条件
攻撃の難しさは、必須の条件が厳しいからだ。
- phpの設定にも依存し、設定によっては攻撃は全く成立しない。
- PHP のバージョンや設定に依存するため、攻撃対象に合わせて値を調整しないと成功しない。
なので、実際は影響を受ける(この手法で攻撃可能な)サーバーはかなり限られる。
具体的な条件は以下だと思う。
- まず、PHP が fork ベースのプロセスモデルで動いていること(かなり該当)
- 該当する構成
- Apache + mod_php
- PHP-FPM (nginx + PHP-FPM など)
- php-cgi
- つまり、Linuxならほぼ該当する。
- Windowsはforkじゃないので IIS + ZTS(Zend Thread Safety) なら該当しない。
- 該当する構成
- 攻撃対象の情報が知られていること(設定によっては漏れる)
- libphp.so 内の関数オフセット
- C言語の関数である、system()、zval_ptr_dtor 等がメモリにロードされたときの相対位置
- これは最終的な攻撃のポイントで必要になる
- 調べるには同一バイナリを手元に用意して解析する必要がある。
- ヒープスプレーパラメータの調整
- リクエストするときのフィールド数、サイズ、配置オフセット値が必要。
- PHPのバグを利用するために、ちょうど良い値が必要になる。
- 同一環境を手元に再現して実際に動かしチューニングする必要がある。
- 同一バイナリが必要なのと、PHP の設定値も把握しておく必要がある。
- libphp.so 内の関数オフセット
- 対象の php.ini 設定が条件を満たしていること
- session.upload_progress.cleanup = On(デフォルト)
- session.upload_progress.enabled = On(デフォルト)
-
post_max_size > memory_limit- リクエストを処理するメモリより、POSTで受信できるサイズが大きいという設定。
- 唯一デフォルトと異なる条件
- 普通はありえないので意図的には設定されない。ていうか設定ミス。
最後の 条件、post_max_size > memory_limit は、意図的に設定されることはないと思う。
この変な設定をしてないと、攻撃は成立しないので、ほとんどのサーバーでは問題にならないはずだ。
万が一こんな変な設定をやってしまっていたとしても、この設定ミスを修正するだけでとりあえずphp2shellの攻撃は成立しなくなる。
(php2shell以外の脆弱性のためにも)バージョンアップはしたほうが良いが、暫定対策としては設定を変更するだけでよい。
確かに、設定変更で暫定対策できるという情報はあった気がする。
同一バイナリについて
攻撃対象と同じ mod_php なり、PHP-FPM なりのバイナリを手元に用意して解析する必要がある。
- PHP バージョン
- HTTPレスポンスヘッダ(X-Powered-By, Server)から取得可能
- (ていうか、こういうヘッダをデフォルトで出すようになってるの本当にヤバいわ。)
- 普通は出さないようにしている。
- OS/ディストリビューション
- HTTPレスポンスヘッダ(Server)から取得可能
- 普通は出さないようにしている。
- CPU アーキテクチャ
- 直接知る方法はないけど、だいたいamd64 が多いし、パターンも少ないので推測できる。
- コンテナならベースイメージなど
- 直接知る方法は無い
- コンテナで動作しているとわかっていれば、PHPバージョンがわかれば、公式ベースイメージから候補を絞れる。
PHPの設定値について
- phpinfo() の結果を表示するような脇の甘いことをしてない限り取得できないが、ある程度推測可能。
- memory_limit, post_max_sizeはHTTP経由で探れる
- max_input_vars はデフォルト値を狙うことができる。
成功すると何ができるか
- PHP が動作しているプロセスの実行ユーザーの権限で、
system関数によりシェルコマンドを実行できる。- ちなみに、libphp.so 内の他の関数も同様にアドレスを計算して実行させることが可能。
systemが最も直接的で影響が大きい。
- ちなみに、libphp.so 内の他の関数も同様にアドレスを計算して実行させることが可能。
- 権限設定が適切であれば、攻撃者が system 関数を実行できても、サーバー全体を乗っ取れわけではないというか、ほぼ何もできない可能性もある。
- 自分がローカルのdocker コンテナで試した感じだと、あまり長いコマンドを実行させるのが難しいみたいだった。(かなり工夫がいるかも)
- 何度も同じ操作を繰り返して、ちょっとずつコマンドを組み立てる必要がある。しかも、実行結果を見ることはできない。(権限が適切なら。。。)
権限について
- ルート権限で実行できるわけではなく、あくまでPHP が動作しているプロセスの実行ユーザーの権限で実行される。
- 普通のサーバーでは、PHP の実行ユーザーは通常、root ではなく、専用の低権限ユーザー(例えば www-data や apache などの名前をつけたりする)であることが多い。
- これらのユーザーは、WEBページを書き換えられるような権限すら持っていないのが普通
- つまり、攻撃者が system 関数を実行できたとしても、権限をちゃんと設定していれば、大したことはできないが、
- ファイルアップロード機能で、アップロード先フォルダへの書き込みを権限を持っていることがある。
- そうなると、攻撃者がそこにファイルを作ったり消したり実行したりできるかも。(フォルダ名がわかればね)
- そうなるとコマンドの実行結果が確認可能になり、サーバーの内容をブラウザ経由で取得できるようになる。
攻撃の仕組み
前提: ASLR(Address Space Layout Randomization) と forkについて
- ASLR はプロセス起動時にメモリ配置をランダム化する OS の保護機能
- 通常はASLRによりメモリ配置がランダムなので、特定のアドレスを狙って攻撃するのは難しい。
- PHP は fork(親プロセスのメモリをコピーして子プロセスを作るシステムコール)でワーカーを作る
- fork で作られたワーカーは全て親と同じメモリ配置を持つ
- そのため、一度アドレスを特定すればforkされた別のワーカーでも同じアドレスが使える
前提: OOM(Out of Memory)によるヒープ破壊
- post_max_size > memory_limit の設定を利用する
- 大量のフィールド + memory_limit を超える巨大フィールドを POST で送る
- PHP が Fatal Error を出すが、プロセスは死なない(setjmp/longjmp による内部エラー処理)
- メモリの解放が行われずヒープにゴミが残り、同じワーカーが次のリクエストを処理する
- フィールドの数や値サイズを変えることで、ヒープに残るゴミのパターンを制御できる
前提: cleanup バグ
- session.upload_progress.cleanup のバグにより、 セッション ID 用に再利用されたメモリブロックの内容が Set-Cookie に漏れる
攻撃の流れ
とりあえず、公開されてるexploit を参考にしてローカルのdockerに対して再現した手法をまとめると以下のようになる。
- worker の入れ替え
- OOM でヒープを壊し、次のリクエストで segfault を起こしてワーカーを殺す
- 親プロセスが fork でクリーンなワーカーを作る
- ヒープの先頭アドレスを取得する
- OOM でヒープにゴミを残した後、大量のフィールドで空き領域を埋める(スプレー)
- cleanup バグで漏れたデータに含まれるアドレスを集計し、ヒープの先頭を特定する
-
worker の入れ替え(手順 1 と同じ)
- libphp.so 内の関数アドレスを取得する
- OOM のフィールドサイズを変えてヒープのゴミパターンを変える
- スプレーには配列形式のフィールド(name[0], name[1], …)を使う
- PHP が配列を処理すると内部で HashTable を作り、
- その中に libphp.so 内のクリーンアップ関数のアドレスが書き込まれる
- cleanup バグで漏れたデータからこのアドレスを取得する
- system() のアドレスを計算する
- 取得した関数アドレスから libphp.so の先頭アドレスを逆算し、
- 先頭アドレス + オフセットで system() の実アドレスを計算する
- (オフセットは同一バイナリを事前に解析して把握しておく。リクエスト不要)
-
worker の入れ替え(手順 1 と同じ)
- 偽データで system を実行させる
- OOM でヒープにゴミを残した後、偽の HashTable データを POST フィールドとして送り込む
- 本来のクリーンアップ関数のアドレスが入る位置に system() のアドレスを書いておく
- cleanup が偽データを本物の HashTable と誤認し、system(“コマンド”) が実行される
まとめ
- php2shell は、PHP の内部バグを利用して system() を実行させる攻撃手法。
- post_max_size > memory_limit という、変な設定ミスをしてないと、攻撃は成立しない。
- 攻撃手法が泥臭いというか、めちゃくちゃ複雑で手間がかかるし、前提条件が厳しい。
- phpで変な設定をしたり、権限設定がガバガバだと、攻撃者ができることも増える。
- 攻撃が成立したとき、ファイルをアップロードするようなフォルダがあり、そのパスがバレていると、ブラウザ経由でいろいろ情報をとられる可能性がある。
- こんな前提の厳しい脆弱性より、php ってもっと危ない部分ありそうやけどな
- バージョンや使用技術を知られることは避けるべき。WEBのアクセス先に拡張子が出るなんて絶対ダメ。