PHPのPharを使ったコード実行

(Last Updated On: 2018年8月23日)

phar(PHPプログラムを1つのアーカイブファイルにまとめて利用するファイル形式)のメタデータにserializeが使われている点は議論になったことがあるように思います。そもそも危険なpharファイルだったら意味なし、という結論だったはずです。結構前の事なので詳しくは記憶していません。

pharモジュールはデフォルトで有効化されており、影響範囲は大きく危険度は高いです。

BlackHat 2018でpharアーカイブを利用した攻撃方法が紹介されています。簡単に紹介します。詳しい内容は発表者のPDFを参照ください。WordPress, Type3, TCPDFへの攻撃方法が記載されています。

TL;DR;

ファイル関数で攻撃用ファイルを処理するだけで任意コードを実行される可能性があります。攻撃用ファイルをpharファイル(phar://)としてアクセスする必要があります。入力パラメーターがpharファイルとして”ファイル関数”で処理されないよう、入力データバリデーションを行い対策します。

※ “ファイル関数”とはfopen(), file_exists(), file_get_contents()など全てのファイル処理関数です。

※ 特急で対策したい場合、pharモジュール無しのPHPを作って使います。

攻撃に利用される仕様

PHPのファイル関数は全てfopen wrapperと呼ばれる仕組みが組み込まれています。このため、httpsやftpといったリモートファイルもローカルファイルと同じ感覚で利用できるようになっています。

phar形式のファイルもfopen wrapperで処理されます。このため、pharファイルはtarまたはzip形式でアーカイブされたファイルであるにも関わらず、ローカルファイルと同様に操作することが可能です。

例えば、PHPソースからビルドした場合、ext/phar/phar.pharが作成されます。

$ php -r 'var_dump(file_exists("phar://ext/phar/phar.phar/phar.inc"));'

bool(true)

とpharアーカイブの中身を見て、phar.incがあることを確認できます。

PHPはphar形式ファイルであることを”拡張子で判断しません”。”phar://” fopen wrapperが指定された場合、ファイルの中身を見て実行/確認しようとします。

  • include/require/include_once/require_once – 実行
  • 他のファイル系関数 – 確認

この仕様は攻撃に使われる可能性もあるため、拡張子を使うよう変更を提案したことがあります。しかし、あまり反応が良くなかったので仕様変更はありません。

前述の通り、全てのPHPのファイル系関数はfopen wrapperをサポートしています。file_exists(), is_readable(), 何を使ってもpharアーカイブの中身は確認されます。この際に、pharアーカイブのメタデータはunserilize()されます。

unserialize()自体にメモリエラーがあることも多く、現在では脆弱性として修正されていません。更に__wake()や__destruct()といったオブジェクト管理用のメソッドがunserialize()に自動実行されます。これらにより任意コード実行が可能になります。

※ unserialize()のメモリ問題を脆弱性として修正しない、としたときにpharのメンテナはメタデータをJSON形式に変える、といった対応をすべきだったと思います。

最近ではcomposerを使ったアプリケーションが大半を占めると思います。autoloaderでインストールされたクラス全てにアクセスできます。Composerを使ったアプリの場合、攻撃可能なオブジェクトとその位置は割と容易に推測できます。この状況もリスクを増加させています。

攻撃方法

  1. 攻撃用pharファイルを攻撃対象の何処かに置く(.jpgなどの拡張子で構わない。例えば、attack.jpg)
  2. どれでも構わないのでファイル関数に”phar://path/to/attack.jpg”などとしてアクセスさせる

この2つのステップが成功すると任意コード実行が可能になる場合があります。

※ include系の関数でこれが出来ると、普通のローカルスクリプトインクルードによる任意コード実行問題になります。

つまり、

  • 攻撃者がpharファイルをアップロードできる
  • ファイル関数のパラメータのどれかに攻撃者が設定した”パス”が未検証のまま使われる

この条件を満す場合、攻撃に成功します。実際にはautoloaderを使ったりするのでもう少し条件があります。基本的には

  • unserialize時の__wakeup()
  • オブジェクト削除時の__destruct()
  • unserializeのメモリ管理バグ

を使って任意コード実行が可能になります。詳しくは発表者の資料を参照ください。

攻撃用pharファイルにPHPファイル関数をアクセスさせるだけでよいので、XXEを使った間接攻撃も攻撃に利用可能です。

対策方法

攻撃方法から対策方法も明らかです。

  • pharモジュールを使わない
  • 攻撃用pharファイルをアップロードさせない
  • 攻撃用パラメータを設定させない

この2番目、3番目の両方※を行います。

※ 多層防御で防御する

※ URLをファイル関数で利用できなくするallow_url_fopen=Offはphar://には適用されません。このため、allow_url_fopen=Offは対策になりません。

pharモジュールを使わない

phar fopen wrapperはpharモジュールの機能です。pharモジュールがロードされていなければphar:// ラッパーは機能しません。

しかし、pharはデフォルトで組み込まれるモジュールです。ソースから–disable-phar configureオプションを付けてPHPをビルドする必要があります。

アップロード機能を使わせない

使わせざるを得ない場合もありますが、ユーザーによるアップロードができなければ問題を回避できます。

プラグインなどのファイルに紛れ込まされると厄介です。信頼可能なコード/ファイルだけを使うようにします。(難しいですが、そもそも攻撃用PHPコードが入っているようなモノだと、この攻撃以前の問題です)

拡張子でファイル形式を判断しないため、どんなファイルでも攻撃に利用できます。MIMEタイプをチェックするだけでも一定の効果はあります。

しかし、JPEG/PDFファイルとしてもpharファイルとして妥当と検証されるファイルも作れるのでファイルタイプチェックだけでは十分ではありません

入力をバリデーションする

これが最も汎用的な対策です。攻撃者は攻撃用の入力パラメーターを設定する必要があります。攻撃には

phar://path/to/attack.jpg

といった入力が許可されている必要があります。

ファイル名となるユーザーからの入力はには普通は : / といった文字列も必要なく、長さも短いはずです。例えば、ホワイトリスト型で英数字、 .(ドット)1つだけ、などに限定していれば攻撃できません。普通にファイルとなるパラメーターを全てバリデーションしていれば問題ないはずです。あまり無いと思いますが、ライブラリなどが勝手にファイル名として処理してしまうパターンに注意してください。

XXEの場合、外部エンティティ参照のURLをバリデーションします。そもそもXXEを防ぐ為に外部エンティティ参照のURLはバリデーションしていると思います。普通にバリデーションしていれば問題ないはずです。

参考:

バリデーションには3種類のバリデーションがある 〜 セキュアなアプリケーションの構造 〜
ほぼ全てのインジェクション攻撃を無効化/防止する入力バリデーション 〜 ただし出力対策も必須です 〜

PHPの修正

取り敢えずはファイル関数でserializeされたメタデータをunserializeしないようにするしかないと思います。

互換性問題がありますが、メタデータのシリアライズ形式をJSONに変えるのが良いと思います。

以前にpharは危険なのでINI設定を追加し特定の拡張子、例えば.pharだけpharファイルとして処理されるようにしては?と提案していました。これが在れば、単純に拡張子チェックだけで問題を回避可能でした。

投稿者: yohgaki