Pharファイルは複数のPHPスクリプトをアーカイブ&パッケージ化して一つのファイルでアプリケーションとして実行する仕組みです。
PharファイルはPHPプログラムその物なのでこれを実行してしまうとPHPで実行できることは何でもできてしまいます。そもそもPharファイルはプログラムなので信頼できないPharファイルを実行したらやりたい放題なのに、なぜPharのデシリアライズがセキュリティ問題になるのか?解説します。
Pharファイルの構造
Pharファイルの構造はざっくりと
- スタブ
- マニフェスト
- コンテント(コード)
- シグニチャ
といった構造になっています。
スタブは__halt_compiler()を呼んで、Pharファイルが直接PHPファイルとして実行されることを防止します。マニフェストには大きさやファイル数、APIバージョンといったPharファイルのメタ情報が保存されています。そのメタ情報の保存にPHPのserialize()形式のデータが利用されています。
PHPのserialize()とunserialize()
オブジェクトを含むシリライズされたデータを信頼してはならない、はPHPに限った話ではなくRuby、Pythonでも同じです。RubyやPythonのマニュアルには、何が起きても知りません、と明記されています。
信頼できないPharファイルはそもそも実行してはならないのですが、読み込んでもならない仕組みになっています。Pharファイルを読み込むだけでもserialize()されたメタデータがunserialize()されるからです。
PHPのシリアライザーはオブジェクトをサポートしており、unserialize()された時に__wakeup()、オブジェクトが破棄される時に__destruct()メソッドが自動的に呼ばれます。この動作がPOP攻撃と呼ばれる種類の攻撃を可能にします。
※ 特殊メソッドが呼ばれる問題以外にもunserialize()のメモリ破壊問題を攻撃可能です。
Pharファイルを利用したPOP攻撃
Pharファイルを利用したPOP攻撃はMoPB(Month of PHP Bugs)※でよく知られているStefan Esser氏が詳しく解説しています。※ MoPBはこのブログでも紹介しています。POP攻撃自体は10年以上前から知られている攻撃手法ですが、あまり直感的ではないので今でも時々問題になります。WordPress 5.7.1がPOP攻撃に脆弱だったことがこのブログを書く理由です。
PharファイルのPOP攻撃は次のように実行されます。
- 攻撃に利用できるコールバック関数を持つ__destruct()を持つクラスを探す。(任意コード実行をさせる為)
- file_get_contents()などで攻撃者が指定したファイル読み込みを行うコードを探す。(phar:// ストリームラッパーで読ませるとPharのserialize()データが読まれオブジェクトが生成され廃棄される時に__destruct()が実行される)
- 1.の__destruct()を攻撃するPharファイルを作成しアップロードする。(攻撃コードを含むserialize()されたメタ情報を含むPharファイルをどこかに置く)
- 3.で配置した攻撃用Pharファイルを2.で見つけたファイル読み込みを行うコードに読み込ませる。
- 攻撃用のPharファイルに含まれるserialize()データがunserialize()され 1. で見つけた__destruct()で攻撃者のコードが実行される。
といった感じでコード実行攻撃を行います。これは攻撃例で他にも色々なパターンでPOP攻撃が可能です。
POP攻撃対策
POP攻撃は直感的ではないですし、ライブラリの中で攻撃可能な特殊メソッドを持つクラスが無いようにするのは非現実的です。しかし、単純な必須のセキュリティ対策で防止できます。それは入力データバリデーションを徹底して行うことです。
紹介した攻撃例では “phar://” で始まるファイルパスをfile_get_contents()などのファイル関数で読み込ませる必要があります。普通のアプリケーションで”phar://” で始まるファイルパスのユーザーから与えられることはあり得えず、”phar://”で始まるパスは不正な入力データです。CWE-20(不適切な入力データ検証脆弱性)で対策で要求される入力データバリデーションをしていれば攻撃されることはありません。
セキュアなWebアプリケーションで実装されている入力データバリデーションとはCWE/SANS TOP 25:2011 Monster Mitigation #1で解説されている入力データバリデーションです。
徹底した入力データバリデーションはISO標準、CERT(2017年からIPAも)が求めるソフトウェアセキュリティ対策の第一原則です。
おまけ
“phar://” で始まらなければ良いなら、”phar://”で始まるパスを無害化すれば良い!と”phar://”で始まるパスから”phar://”を取り除いてもダメです。ブラックリスト型の対策は本質的に脆弱なので簡単に回避されます。例えば、”phar://phar://” といったパスを与えれば、先頭から”phar://”を取り除いても無意味です。
WordPress 5.7.1ではファイルパスがURI形式である場合に拒否することでこの問題を回避しています。5.7.1と5.7.2のコード差分でPOP攻撃回避はここで行われています。
@@ -1824,12 +1826,15 @@ class PHPMailer */ protected static function fileIsAccessible($path) { + if (!static::isPermittedPath($path)) { + return false; + } $readable = file_exists($path); //If not a UNC path (expected to start with \\), check read permission, see #2069 if (strpos($path, '\\\\') !== 0) {
※ static::isPermittedPath() はURI形式パスの確認だけします。
フェイルセーフ対策としてアプリケーション・ライブラリの奥深くでバリデーションするのも良いのですが、本質的なセキュリティ対策ではないです。そもそも”phar://”で始まるファイルをパスを”妥当なデータ”として処理していることが間違いです。
phar://でなくてもhttps://で意図しないリモートファイルを読み込んでしまう場合も問題の原因になることがあります。そもそもリモートや特殊ファイルでなくてもローカルファイルでもおかしなパスへのアクセスは厳禁です。
不正なデータはできる限り早くバリデーションして拒否する、これがセキュアプログラミングの基本中の基本です。これを普通に実施するだけでPharファイルによるコード実行も不正ファイルアクセスも防ぐことが可能です。