file_get_contents はファイルの中身全体を文字列として読み込む関数です。よく平の PHP でパパっと何かを作る時に使います。
この記事ではこの file_get_contents 関数を使った任意コード実行とそれがプログラムの脆弱性になる状態を紹介します。
PHP: file_get_contents – Manual
file_get_contents 関数はファイル名を指定しファイルの中身を読み込む関数です。PHP の公式ドキュメントには次の様にあります。
file_get_contents(
string$filename,
bool$use_include_path=false,
?resource$context=null,
int$offset= 0,
?int$length=null
): string|falsefilenameデータを読み込みたいファイルの名前。
この filename には主にファイルパスが入りますが、その実ここで使える文字列の形式は幅広いです。例えば次の様に URL が使えます。
<?php
// PHP 公式ドキュメントから引用
// 例1 とあるウェブサイトのホームページのソースの取得と出力
// @see https://www.php.net/manual/ja/function.file-get-contents.php
$homepage = file_get_contents('http://www.example.com/');
echo $homepage;
この使える文字列の中に任意コード実行につながるものがあります。それはphar://で始まる文字列です。phar://は Phar ファイルという PHP のソースコードをまとめたファイルを呼び出すための記述です。
PHP: phar:// – Manual
PHP: Phar – Manual
Phar ファイルはライブラリとしてよく使われていましたが、昨今では Composer に取って代わられています。この Phar ファイルのファイル形式中にあるメタデータが任意実行の原因になりえます。
?? シリアライズ化された Phar メタデータ。serialize() 形式で格納される。
このメタデータは Phar が読み込まれる際にデシリアライズされます。これにより、ソースコード内の任意のクラスのオブジェクトを Phar ファイル経由で生成できます。ここで任意のクラスのオブジェクトをデシリアライズでインスタンス化できる都合上、マジックメソッドの __destruct や__unserialize も実行できます。この時点で不正な処理を実行できます。このあたりは次の記事が詳しいです。ざっくばらんにいうと call_user_func_array の様なデータを元に任意に処理を実行する処理が PHP にあり、デシリアライズ起点で起動できるマジックメソッドにそれがあると任意実行につながるという話です。
安全でないデシリアライゼーション(Insecure Deserialization)入門 | 徳丸浩の日記
file_get_contents 関数で Phar ファイルの読み込みができる、Phar ファイルの読み込みが自動でデシリアライズを行う、デシリアライズの際に任意コード実行できるパターンがある、この流れで file_get_contents 関数から任意コード実行の流れが生まれます。大変限定的な流れですが、ノーガードでプログラムを書いた際にこの脆弱性が埋め込まれる可能性は案外あります。というのも file_get_contents 関数とデシリアライズによる任意実行が全く別の場所やライブラリ間にあっても、問題なくこの流れを構築できるためです。
この攻撃を防ぐのは用意です。そもそも file_get_contents 関数に”phar://”で始まる様な文字列が渡らない様にすればよいです。ここで紹介した脆弱性があるプログラムは十中八九ディレクトリトラバーサルの脆弱性も兼ね備えています。
<?php
/** ダメなパターン例。これぐらいノーガードでないと攻撃できません */
$serverFile = file_get_contents($_GET['file']);
/** 対策例。特定のディレクトリ以下にしかアクセスできない様にするのも手です */
// 読み込み可能なファイルの置き場
$dir = "/var/www/storage/user/";
// ファイル名のみにリクエストを加工。これはディレクトリトラバーサル対策も兼ねています
// basename 関数はロケール依存で挙動が変わることに注意
$file = basename($_GET['file']);
// ディレクトリとファイル名の結合によって、
// 変なプロトコルで始まったり、..の様な文字列が入ったりする余地をなくします。
$serverFile = file_get_contents($dir . $file);
/** 対策例。Laravel の様な大手フレームワークには脆弱性対策が備わっています。そちらを使います */
// これだけでこの記事であげた脆弱性は対策済みです。
// 実際の運用ではリクエストのバリエーションもしたいところです。
$serverFile = \Storage::get(request()->input('file'));