なぜPHPアプリにセキュリティホールが多いのか?

第39回MOPS:静的PHPソースコード脆弱性スキャナ RIPS

第32回 PHPセキュリティ月間(Month of PHP Sercurity)「PHPセキュリティ月間」MOPS - Month of PHP Securityについて簡単に紹介しました。

今回は静的にPHPソースコードを分析しセキュリティ脆弱性を検査するツールの紹介です。

MOPS Submission 09: RIPS - A static source code analyser for vulnerabilities in PHP scripts
http://www.php-security.org/2010/05/24/mops-submission-09-rips-a-static-source-code-analyser-for-vulnerabilities-in-php-scripts/index.html

昨年発見された脆弱性のおよそ3割がPHPアプリケーション関連の脆弱性とされています。PHPは簡単にWebアプリケーションが作れるため、セキュリティに十分留意したWeb開発フレームワークを利用しなくてもアプリケーションが作れてしまいます。Webアプリケーションが簡単に作れることはよいことですが、これが多くの脆弱性を生む原因になっています。

Johannes Dahse氏はこのような状況を改善するためにRIPSと呼ばれる、オープンソースのPHPソースコード分析ツールを開発しました。

この記事を執筆するにあたり、筆者は執筆時点の最新版rips-0.32.zipを利用しました。MacOS 10.6上のPHP 5.3で実行してみたところ一見動作しているように見えますが正常に動作しませんでした。LinuxのPHP 5.2で動作させると期待通りの正常に動作しました。PHP 5.3では動作しない可能性があるので注意してください。

RIPSの基本設計

RIPSは危険性がある関数をPVF(Potentialy Vulnerable Function 潜在的に危険な関数 ─ system(), pg_query()など)と定義し、$_GET, $_POST, $_COOKIE, $_FILES, $_SERVER, $_ENVのユーザ入力をソースとするデータがPVFに渡されると脆弱性があるとレポートします。

RIPSは構文解析を行いPVFを見つけ、PVFへ渡されたパラメータが危険なパラメータでないか、変数の遷移を確認します。構文解析にはPHPスクリプトを解析しトークンに分解するモジュールであるTokenizerを利用しています。TokenizerはPHPのパーサを利用しているので正確にPVFや変数を識別できます。論文執筆時点で、RIPSはpreg_replace_callback関数やhilight_file関数など危険性がよく知られていない関数を含め139のPVFを識別するそうです。

図1 RIPSのスクリーンショット
図1 RIPSのスクリーンショット

静的なソースコード脆弱性スキャナと聞くとコマンドラインアプリケーションを想像するかも知れませんが、RIPSはWebアプリケーションとして実装されています。恐らくUIが作りやすいからでしょう。スキャンするファイルをWebサーバ上に置いてスキャンします。

脆弱性スキャン

RIPSが脆弱性を検出する過程を簡単なスクリプトで解説します。

サンプルコード
<?php
    $a = $_GET['a'];
    $b = $a;
    system($b, $ret);
?>

Tokenizerでトークンに分解されたスクリプト情報から、system関数が利用されていることが検出されます。次にsystem関数は危険な関数なのでPVFとして識別されます。同時にパラメータ$bが追跡すべき変数として認識され、その変数ソースが危険なソースでないか追跡されます。変数のソースが危険なソースであれば脆弱性として識別します。

サンプルコードのスキャン結果
4 : system($b ,$ret);
    3 : $b = $a;
        2 : $a = $_GET['a'];
図2 スキャン結果のスクリーンショット
図2 スキャン結果のスクリーンショット

危険なデータソースである$_GET['a']と危険な関数であるsystem関数が識別され、脆弱性として正しくレポートされています。

今度は危険でないサンプルコードをスキャンしてみます。

サンプルコード2
<?php
    $a = $_GET['a'];
    $b = 'date';
    system($b, $ret);
?>
図3 スキャン結果のスクリーンショット
図3 スキャン結果のスクリーンショット

期待通りに脆弱性はレポートされませんでした。

次にもう少し複雑なサンプルコードをスキャンしてみます。

サンプルコード3
<?php
    $a = $_GET['a'];
    $b = escapeshellarg($a);
    $c = 'cal ' . $b;
    system($c, $ret);
?>

脆弱性はないので、対策済みのコードも表示されるレベルに変えると次のような結果となります。

図4 スキャン結果のスクリーンショット
図4 スキャン結果のスクリーンショット

3行目で対策が取られていることがわかります。これはsystem関数の最初のパラメータが危険なパラメータで、安全にするための関数が escapeshellarg関数, escapeshellcmd関数だと登録されているため、このような結果になります。

RIPSに保存されている検出用データのイメージ
"system" => array (
    array(1), array("escapeshellarg", "escapeshellcmd")
);

実際にはconfig/PVF.phpファイルでPVFが、config/securing.phpで対策用の関数が定義されています。config/securing.phpを見たところ対応しているデータベースはMySQLのみで、まだPostgreSQLやSQLiteには対応していませんでした。PostgreSQLやSQLiteサポートを追加することは簡単にできそうでした。RIPSはまだまだ開発中のツールだと言えるでしょう(注:RIPS 0.34以降はPostgreSQL、SQLiteもサポートしています⁠⁠。

include/require文にも対応しており、複数のファイルにまたがるアプリケーションでもスキャンは可能です。しかし、アプリケーションを実際に実行していないので、正確にインクルードするファイルが特定できる訳ではありません。このため、スキャン結果が不正確になる場合もあります。

Tokenizer

RIPSのコアとなるTokenizerモジュールを使ったことがある方は少ないのではないでしょうか? 筆者も何年も前にコードカバレッジツールを作成する際に利用したきりです。

Tokenizerの利用は簡単でtoken_get_all関数でファイルの中のPHPトークン(文、変数、関数など)を取得します。トークンは名前でなくID(整数)で保存されているのでtoken_name関数で名前を取得します。トークンのへの分割はPHPに組み込まれている本物のPHPパーサを利用しているのでPHPスクリプトを確実にトークンに分解できます。

<?php
    $a = $ GET['a'];
    system($a, $ret);
?>

をトークンに分解すると以下のようになります。

name: T_OPEN_TAG            value: <?php    line: 1
name: T_VARIABLE            value: $a   line: 2
name: T_WHITESPACE          value:      line: 2
                        =
name: T_WHITESPACE          value:      line: 2
name: T_VARIABLE            value: $_GET    line: 2
                        [
name: T_CONSTANT_ENCAPSED_STRING    value: 'a'  line: 2
                        ]
                        ;
name: T_WHITESPACE          value:      line: 2
name: T_STRING              value: system   line: 3
                        (
name: T_VARIABLE            value: $a   line: 3
                        ,
name: T_WHITESPACE          value:      line: 3
name: T_VARIABLE            value: $ret     line: 3
                        )
                        ;
name: T_WHITESPACE          value:      line: 3
name: T_CLOSE_TAG           value: ?>   line: 4

RIPSはこのようにして取得したトークンから分析に必要なトークンを抽出して処理します。

RIPSが処理するトークン
T_INCLUDEinclude/require文
T_FUNCTION関数(ユーザ定義、システム/モジュール関数、両方)
T_RETURNreturn文
T_VARIABLE変数
T_STRING文字列
T_EXIT, T_THROWプログラムの終了
Curly braces {}制御構造の判定に必要

これらに加えて、extract(), list() や define() など特別なトークンを生成する関数も処理対象となります。論文に記載されていませんでしたが、E_ECHO(echo文)も抽出対象です。

RIPSが各トークンをどのように処理するか、詳しい解説はDahse氏の論文を参照してください。

Web UI

RIPSのWeb UIはシンブルで分かりやすいです。スキャンしたいPHPのスクリプトファイルをWebサーバがアクセスできるディレクトリ(場所はドキュメントルート以外でよい)に置き、⁠path/file」にディレクトリ名をセットして「scan」ボダンを押すだけです。サブディレクトリもスキャンしたい場合は「subdir」チェックボックスをチェックします。

デフォルトではJavaScriptインジェクション(クロスサイトスクリプティング)以外の脆弱性をスキャンします。JavaScriptインジェクション脆弱性のスキャンはfalse-positive(誤って脆弱と検出される)場合が多いからだと考えられます。すべてをレポートさせることも可能です。

図5 Web UI
図5 Web UI

左側のアイコンをクリックするとソースコードや攻撃コードの生成ウィンドウが開きます。攻撃コードの生成ウィンドウはCURLモジュールを使った攻撃用のPHPスクリプトが生成できるようになっていました。筆者が使ったバージョンまたは環境では問題があるようで、完全な攻撃コードは生成できませんでした。しかし、サンプルコードのような明白な脆弱性であれば、ほんの少し手直しすれば実際に攻撃できるコードを生成しました。

RIPSはスキャンする脆弱性のタイプ、verbosity level(脆弱性レポートのレベル⁠⁠、コードのスタイルを選択できるようになっています。Verbosity Levelは以下のようになっています。

RIPSレポートのVerbosity Level
Level1安全性を確保するための関数を呼ばずに、危険な状態でPVF関数を呼び出すケースのみ
Level2include文とデータベースクエリのスキャンも含める
Level3安全性を確保するための関数を呼び出してPVF関数を呼び出すケースも含める
Level4RIPSがスキャンした際の脆弱性以外の情報も含める
Level5すべてのPVF関数呼び出しを含める

Level1の場合はfalse-positive(誤って脆弱性と判断してしまう状態)にならないように調整されいるとしています。Level1の場合、false-nagetive(脆弱性があるのに検出できない状態)は存在して当然と考えるべきでしょう。

一時的なJavaScriptインジェクションを十分に検出するにはLevel2、永続的なJavaScriptインジェクションを検出するにはLevel3に設定する必要があります。当然、false-positiveとなるレポートはレベルが上がるにつれて増えてきます。Level4以上では情報レベル、つまり脆弱性情報ではないレポートも含まれています。

実際のアプリケーションのスキャン

論文にはセキュリティ研修用の脆弱性があるプログラムのスキャン結果が載っていました。実際のアプリケーションではどのような結果になるのか、いくつかのPHPアプリケーションをsourceforge.netからダウンロードしてきてスキャンし、脆弱性が検出できたアプリケーションを紹介します。明示していない限り筆者はソースコード中味を確認していません。

修正されるかどう分かりませんが、作者へ脆弱性情報は連絡済みです。

WebFileBrowser

図6 WebFileBrowser
図6 WebFileBrowser

Verbosity level 1で的確に脆弱なコードを検出しました。このアプリケーションは非常に古いアプリケーションらしく、move_uploaded_file関数を使用せず、copy関数でアップロードされたファイルをコピー、unlink関数で削除しています。このようなコードはファイルを不正にアップロードされたり、削除される場合があります。このようなコードは書いてはいけません。

必ずしも危険な訳ではありませんが、extract関数によって$HTTP_POST_VARSがスコープに取り入れられていることも警告されています。先程の脆弱なコードをこの脆弱性を生む可能性が高いコードによって、実際にファイルアップロード攻撃が行える可能性が高い事が分かりました。

図7 WebFileBrowser - JavaScriptインジェクション
図7 WebFileBrowser - JavaScriptインジェクション

RIPSが検出可能なJavaScriptインジェクションをすべてレポートさせるにはVerbosity level 3以上が必要と書かれていましたが、Verbosity level 1でもかなり検出(30)しました。Level1で多数レポートされたことも原因ですが、Level3に変えても脆弱性の追加はわずか(+5)でした。最近のブラウザはHTTPのLocationヘッダによるJavaScriptインジェクションは行えなくなっているのですが、これも脆弱性としてレポートされます(JavaScriptインジェクションが実行できなくても、リダイレクトを制御されるのはセキュリティ上好ましくないのどちらにしても修正すべき⁠⁠。

SimpleQuiz

図8 SimpleQuiz
図8 SimpleQuiz

このアプリケーションもVerbosity level 1で的確に脆弱なコードを検出しました。今回は典型的なSQLインジェクション脆弱性です。mysql_real_escape_string関数でエスケープすべきパラメータがエスケープされていません。sqlmapなどのツールによって簡単にアクセス可能なデータベースのデータすべてを盗めます(直接インターネットに接続されている場合、可能性ではなくほぼ確実に盗れることがこのレポートだけで判断できます⁠⁠。

図9 SimpleQuiz - JavaScriptインジェクション
図9 SimpleQuiz - JavaScriptインジェクション

このアプリケーションでもLocationヘッダの不具合を2つ検出しました。Verbosity Level 1から3までは検出されるJavaScriptインジェクションの数には変化がありませんでした。

phpMyInventory

図10 phpMyInventory
図10 phpMyInventory

このアプリケーションは2003年からメンテナンスされておらず、register_globals=onが前提となっているレガシーアプリケーションです。このコードは古かったので中味を見てみました。SQLインジェクションの塊と言えるコードでした。Verbosity level 1では何らかの不具合でまったく問題を検出できませんでした。スクリーンショットはLevel 2のスナップショットです。

流石にregister_globals=onのコードに対応していないと思われるレポート結果でした。RIPSは最近のアプリケーションなのでregister_globals=onが前提のアプリケーションの脆弱性が検出できなくてもバグとは言えないでしょう。古いアプリケーションのスキャンにはRIPSは使用できないと考えたほうがよいでしょう。

JavaScriptインジェクションの検出もVerbosity level 1では何らかのエラーで脆弱性が検出できないようでした。Level 2、Level 3では多くのJavaScriptインジェクション脆弱性がレポートされました。これらの多くは実際に攻撃可能なJavaScriptインジェクション脆弱性です。

まとめ

RIPSはアプリケーションのセキュリティ状態の概要を見るには非常に有用なアプリケーションです。セキュアなPHPアプリケーション開発や受け入れ検査の最低ラインとして利用するのもよいでしょう。ただし、ソースコードを分析するタイプのスキャナには必ず、false-positive、false-negativeの問題があります。間違って脆弱性として検出されたり、されなかったりします。

また、最近のWebアプリケーションはWebアプリケーション開発フレームワークを利用したMVCモデルで構築されることが多いと思います。今回はMVCモデルのアプリケーションのスキャンは試しませんでしたが、恐らくfalse-negativeとなるケースが増えると予想しています。MVCモデルの場合、多くのオブジェクトとスクリプトファイルが使用されているため、変数のトラッキングが不正確になってしまうのではないかと考えています。

いずれにせよPHPアプリケーションのセキュリティ状態の概要を把握するには便利なツールであることには変わりありません。インストールはPHPをインストールしたWebサーバに展開するだけです。一度自分が開発したアプリケーションや使用中のアプリケーションをスキャンしてみてはいかがでしょうか?

Dahse氏が参考文献としてあげたURL

1.Fabien Coelho, PHP-related vulnerabilities on the National Vulnerability Database
http://www.coelho.net/php_cve.html
2.The PHP Group, Predefined Variables
http://www.php.net/manual/en/reserved.variables.php
3.The PHP Group, system - Execute an external program and display the output
http://www.php.net/system
4.The PHP Group, escapeshellarg - Escape a string to be used as a shell argument
http://www.php.net/escapeshellarg
5.The PHP Group, token get all - Split given source into PHP tokens
http://www.php.net/token-get-all
6.The PHP Group, token name - Get the symbolic name of a given PHP token
http://www.php.net/token-name
7.The PHP Group, Execution Operators
http://php.net/manual/en/language.operators.execution.php
8.The PHP Group, Control Structures
http://php.net/manual/en/language.control-structures.php
9.The PHP Group, List of Parser Tokens
http://php.net/manual/en/tokens.php
10.Nenad Jovanovic, Christopher Kruegel, Engin Kirda, Pixy: A Static Analysis Tool
for Detecting Web Application Vulnerabilities (Short Paper)
http://www.seclab.tuwien.ac.at/papers/pixy.pdf
11.Nenad Jovanovic, Christopher Kruegel, Engin Kirda, Pixy: A Static Analysis Tool
for Detecting Web Application Vulnerabilities (Technical Report)
http://www.seclab.tuwien.ac.at/papers/pixy_techreport.pdf
12.Nenad Jovanovic, Christopher Kruegel, Engin Kirda, Precise Alias Analysis for
Static Detection of Web Application Vulnerabilities
http://www.iseclab.org/papers/pixy2.pdf
13.Davide Balzarotti, Marco Cova, Vika Felmetsger, Nenad Jovanovic, Engin Kirda,
Christopher Kruegel, Giovanni Vigna, Saner: Composing Static and Dynamic Analysis to Validate Sanitization inWeb Applications
http://www.iseclab.org/papers/oakland-saner.pdf
14.OWASP, OWASP SWAAT Project
http://www.owasp.org/index.php/Category:OWASP_SWAAT_Project

おすすめ記事

記事・ニュース一覧