言語の機能としてシリアライズされた”データ”からオブジェクトを生成(PHPの場合、unserialize関数)したり、呼び出すメソッド/関数を指定できる機能(PHPの場合、call_user_func関数/call_method関数など)を使ったりすると意図しないメソッド/関数が呼ばれるケースを想定しなければなりません。
unserialize関数の問題点
unserialize関数は”シリアライズされたデータ”をアンシリアライズ処理する際に、オブジェクトが規程の状態に初期化されるよう特殊メソッドの__wakeupを自動的に呼び出します。
http://php.net/manual/ja/function.unserialize.php
多くのオブジェクトはそもそも__wakeupメソッドを定義していなかったり、定義していても害を与えないことが多いです。
しかし、”シリアライズされたデータ”がユーザー入力である場合、ロード済み(定義済み)のクラスならどのクラスの__wakeupメソッドも呼び出せます。__autoloadが有効な場合、__autoloadのためのコード、自動的に読み込めるクラスのどの__wakeupメソッドも呼び出せます。
任意コードが実行できる訳ではありませんが、__autoload、__wakeupメソッドの実装によっては好ましくない動作になる場合があります。
例えば、サーバー内で保存しているユーザーのデータを初期化するようなコード
<?php class foo { function __wakeup() { clean_up_user_data(); } } ?>
があった場合、攻撃者が$_GETなどに攻撃用のシリアライズされたデータを埋め込んでfoo::__wakeup()を読み込ませてサービス妨害をする、といった攻撃の可能性があります。
攻撃の影響は__wakeupメソッドの中身次第です。
unserialize問題の対策
unserializeマニュアルに記載されているように、ユーザーへ渡すデータはメソッド呼び出しが発生しないjson_encode/decodeを利用します。
警告
ユーザーからの入力をそのまま unserialize() に渡してはいけません。 アンシリアライズの時には、オブジェクトのインスタンス生成やオートローディングなどで コードが実行されることがあり、悪意のあるユーザーがこれを悪用するかもしれないからです。 シリアル化したデータをユーザーに渡す必要がある場合は、安全で標準的なデータ交換フォーマットである JSON などを使うようにしましょう。json_decode() および json_encode() を利用します。
※ うかつにHTMLコンテクストにJSONを入れると問題の原因です。詳しくはJSONのエスケープをどうぞ。
call_user_func*関数/call_method*関数の問題
これらの関数は任意の関数やメソッドが呼び出せます。あまり使用したことがない方の為に、PHPマニュアルのcall_user_func_array()ページのサンプルコードを掲載します。
<?php function foobar($arg, $arg2) { echo __FUNCTION__, " got $arg and $arg2\n"; } class foo { function bar($arg, $arg2) { echo __METHOD__, " got $arg and $arg2\n"; } } // foobar() 関数に引数を 2 つ渡してコールします call_user_func_array("foobar", array("one", "two")); // $foo->bar() メソッドに引数を 2 つ渡してコールします $foo = new foo; call_user_func_array(array($foo, "bar"), array("three", "four")); ?>
関数やオブジェクトのメソッドが呼び出せることが分かります。パラメータがユーザー入力の場合、ユーザーが任意の関数/メソッドを呼び出せることになります。
例えば、以下のようなコードとパラメータは危険性が高いことが分かります。
<?php $user = new User; // $_GETなどの値を使って、メソッド/パラメータを設定 call_user_func_array([$user, 'setAdminPrivilege'], [1]); // 管理者に設定?!
call_user_func*関数/call_method*関数問題の対策
call_user_method*関数はPHP 4.1.0以降、非推奨なので使用しないようにしましょう。
call_user_func*関数で任意のメソッド/関数を呼び出されると意図しない動作が可能になります。これらの関数にユーザー入力を利用する場合、許可されたメソッド/関数のみ利用できるようにする(ホワイトリスト)と安全性が担保できます。
オブジェクトを指定できるcall_user_func_array関数のオブジェクト変数パラメーターがユーザー入力によって操作できる場合もあります。この場合、メソッドのみが呼び出せるよりも自由度が高く、有効な攻撃が行えるリスクが高くなります。オブジェクト変数のクラスをバリデーションして許可したクラスのオブジェクト変数のみ実行(ホワイトリスト)するようにします。
プラグインなどの場合、以下のようなコードを書いて不正なオブジェクト、不正なメソッドが呼び出されないようにします。
<?php class UserDefinedPlugin interface MyAppPluginInterface { // プラグイン定義 } function call_plugin_method($object, $method, $params) { if (!($object instanceof MyAppPluginInterface)) { die('攻撃を検出'); } $iface_methods = array_flip(get_class_methods('MyAppPluginInterface')); if (empty($iface_methods[$method])) { die('攻撃を検出'); } // 有用なコードをこれ以降に記述 }
根本的な対策
根本的な対策はシリアル化したデータが改ざんされていないことを保証することです。改ざんされていないことを保証するには
の2つ方法があります。これで自分のPHPプログラムが生成したシリアル化したデータであることが保証できます。
再生攻撃を防ぐため、鍵情報はセッション($_SESSION)に保存し、セッション開始時にrandom_bytes()を使って十分な強さ(長さ)の鍵を作ります。
アプリケーション単位で鍵を共有すると再生攻撃が可能になり、脆弱性の原因になります。暗号化/ハッシュでバリデーションするなら、ついでに再生攻撃もできないようにする方が良いです。
まとめ
リスクは通常のインジェクション攻撃ほど高い訳ではありませんが、開発者が意図しないコード(命令、データ)が部外者によって実行されること自体が問題です。
一般に攻撃できるリスクが高くないとはいえ、ここで紹介した問題を利用して攻撃可能になるコードは十分ありえます。そもそも意図しないコードは一切実行できないようにコーディングしておきましょう。
メソッド/関数の不正呼び出しは通常のインジェクション攻撃とは多少異るように見えますが、
- オブジェクトパラメーター : 識別子
- メソッド/関数名パラメーター : 命令
- 引数パラメーター: データ
と考えることができます。
参考