PHP 5.6.25/7.010以降で修正されたセッションデータインジェクション
- Fixed bug #72681 (PHP Session Data Injection Vulnerability). (CVE-2016-7125)
の解説です。
この脆弱性を利用するとオブジェクトインジェクションが簡単に行えます。結構深刻な問題ですが、あまり話題にはなっていないように思います。
最初にこのエントリを書いた時にはレガシーコードの残骸を読み間違えて、幅広い影響があると勘違いしていました。現在のPHPでは値が設定されていないセッション変数は定義できないので、影響するケースは”セッション変数を制御できる状態”に限られます。このレガシーコードの残骸は必要ないので以下の修正をPHPのmasterブランチにコミットする予定です。
このコード実行脆弱性を利用するには、そもそもセッション変数を改ざんできる脆弱性が必要です。セッション変数を改ざんできる脆弱性自体がそもそも致命的です。
脆弱性の原理
この脆弱性はPHPの古いセッションデータシリアライザー1がregister_globalsに対応した仕様であることが原因です。
修正前の古いシリアライザー(phpとphp_binary)はセッション変数を一つずつシリアル化します。つまり、$_SESSION[‘ユーザーが送信したデータが含まれるキー’] = ‘ユーザーが送信したデータ’があると、’ユーザーが送信したデータが含まれるキー’+’ユーザーが送信したデータ’をシリアル化します。2
このため、セッション配列に
foo|O:8:"stdClass":0:{}
のようなデータが設定されると、この文字列がシリアル化されます。
アンシリアライズする際には、変数一つ一つがアンシリアライズされる形になります。この結果、アンシリアライズ化(次にセッションを開始)される際にオブジェクトが生成されてしまいます。例えば、
<?php // PHP 5.6に攻撃する場合 ini_set('session.serialize_handler', 'php'); session_start(); $_SESSION['_SESSION'] = 'foo|O:8:"stdClass":0:{}'; session_write_close(); session_start(); var_dump($_SESSION);
これを実行すると、修正前のPHP 5.6.25未満では
array(1) { ["s:23:"foo"]=> object(stdClass)#1 (0) { } }
となり、もともと一つの入力文字列だった物から、不正にオブジェクトが生成されています。
これが脆弱性の原理です。PHP7では多少仕様が異る為、少し攻撃方法が異なりますが同様に攻撃できます。
攻撃パターン
この攻撃を行うには$_SESSIONにユーザーが設定した任意文字列を挿入する必要があります。例えば、以下のようなコードは脆弱です。
<?php session_start(); // そもそもセッション変数を自由に設定できる時点で大問題ですが。。 $_SESSION = $_SESSION + $_GET; ?>
前の項の解説で”オブジェクトインジェクションが可能”を説明しました。簡単にオブジェクトインジェクションによる攻撃を解説します。PHPのオブジェクトはシリアライズ/アンシリアライズされる際に、マジックメソッド(__wakeupや__destructなど)を呼び出します。マジックメソッドを利用するとPHPコードを不正に呼び出すことが可能になります。詳しくは
- https://www.owasp.org/index.php/PHP_Object_Injection
- http://blog.a-way-out.net/blog/2014/07/22/php-object-injection/
実際、複数のPHPアプリケーションでオブジェクトインジェクション脆弱性が報告されています。
この問題を利用すると脆弱なコードはセッション変数の改ざんに加え、不正なPHPコードを実行される可能性がある、ことになります。
脆弱性の修正
修正後のPHPでは、シリアル化されたキー文字列が$_SESSIONに保存されている場合、無視するようになりました。
この動作が気になる場合、php_serializeシリアライザーを利用しましょう。セッションデータの保存形式が異るので、別のソフトウェアからセッションデータを読み込んで管理タスクを実行している、といった場合には互換性問題が発生します。この点には注意してください。
脆弱性の回避
PHP5.6/7.0ユーザーならphp_serializeシリアライザーはそもそもこの影響を受けないので、php_serializeを使う方が良いでしょう。PHP5.6/7.0では利用できますが、デフォルトでは有効化されていません。php_serializeが影響を受けない理由は$_SESSION配列を丸ごとシリアル化しているからです。
この他にもバリデーションや変換が有効です。$_SESSION配列にユーザーが送信した任意文字列を保存することはあまりないと思います。
ファイルアップロードでセッションを使ったアップロードプログレスが有効な場合、ファイルアップロードで攻撃できます。 プレフィックス(session.upload_progress.prefix)の値によって影響範囲が変わります。デフォルト設定ではアップロードプログレスで攻撃できません。
まとめ
インジェクション可能なデータ型はオブジェクトに限りません。スカラーや配列型もインジェクションできます。
セッションデータを改変できるような脆弱なコードやPHP設定でなければ攻撃されることはないので大丈夫です。
その他の脆弱性
最近、PHPのリリースで多くのバッファーオーバーフロー脆弱性が報告されているので心配になっている方もいるかも知れません。最近のPHPリリースで修正されているメモリ関係の脆弱性の多くは、memory_limitを2GB未満に設定していると、メモリ制限の方が先に機能して攻撃できないです。仮にmemory_limitが大きくても、普通はpost_max_sizeなどの値が十分に小さいので攻撃できないことが多いでしょう。3これらはユーザーが入力バリデーションであまりに大きな値の入力を拒否していなくても、最低限の保護機能として機能し、通常設定では攻撃できない物が多いです。実際、これらのバグは「普通の設定だと攻撃できない」ので昔はバグとして取り扱っていませんでした。
入力バリデーションの最も良いセキュリティ機能の1つは、未知の脆弱性にも対応できることが多いことです。今回紹介したオブジェクトインジェクション脆弱性も入力バリデーションで防止できるケースは少くないでしょう。
- phpおよびphp_binary。私が追加した新しいphp_serializeは影響なし。しかし、PHP7でもデフォルトはphpシリアライザーです。 ↩
- register_globalsが当たり前だった頃に開発されたため、どのグローバル変数がセッション変数なのか内部的に保存し、一つ一つの変数をシリアライズ/アンシリアライズする、という仕様になっていました。数字で始まるキー名が使えない、など現在では全く不必要な制限があるシリアライザーです。新しいphp_serializeを利用しない理由は古いセッションデータとの互換性維持しかありません。 ↩
- 例えば、整数オーバーフローの場合、メモリをほとんど使わなくても攻撃できる場合もあります。バッファ操作ミスで余計なメモリまで読み込んでしまう場合もあります。memory_limitやpost_max_sizeで完全に全ての種類の攻撃を防げる訳ではありません。 ↩