phpMyAdminにおける任意スクリプト実行可能な脆弱性の検証

 phpMyAdmin(3.3.10.2未満、3.4.3.1未満)には、リモートから任意のスクリプトが実行可能な脆弱性があります。このエントリでは、この脆弱性が発生するメカニズムと対策について報告します。

概要

 phpMyAdminには下記の2種類の脆弱性の組み合わせにより、リモートから任意のスクリプトを実行させられる脆弱性があります。

 該当するバージョンは以下の通りです。

 影響を受ける条件は、上記バージョンのphpMyAdminを使用していることに加えて、以下をすべて満たす場合です。

影響

 外部から、PHPの権限で任意のスクリプトを実行できます。検証イメージについては、NTTデータ先端技術株式会社から公表されている検証レポートを参照下さい。
 公開されているPoC(概念実証コード)は、そのままの形で悪用でき、脆弱の有無確認から任意スクリプト実行の裏口作成までを行うもので、影響が大きいと考えられます。上記に該当するサイトは、後述する対策を直ちに実施して下さい。

CVE-2011-2505の概要

 脆弱性の詳細は、このエントリに説明されていますが、以下にもう少し掘り下げて説明します。
 CVE-2011-2505は、任意のセッション変数を外部から任意の値に変更できるというものです。この説明を見て、「register_globalsでも有効になっていたのか?」と疑問に思った方は惜しいところまでいっていますが、register_globalsが原因ではありません。
 該当のソース(libraries/auth/swekey/swekey.auth.lib.php 266行目以降)は以下の通りです。

if (strstr($_SERVER['QUERY_STRING'],'session_to_unset') != false)
{
    parse_str($_SERVER['QUERY_STRING']);
    session_write_close();
    session_id($session_to_unset);
    session_start();
    $_SESSION = array();
    session_write_close();
    session_destroy();
    exit;
}

 ここで、parse_strという関数が使われています。この関数は、クエリストリング形式の文字列を入力として、それを解釈し、PHPの変数として代入するものです(省略可能な第2引数を指定した場合は、そこに配列として値が返ります)。具体例で説明すると、入力が'a=xyz&b[x]=p23&_SESSION[user]=yamada'の場合、以下の結果となります。

<?php
  session_start();
  parse_str('a=xyz&b[x]=p23&_SESSION[user]=yamada');
  var_dump($a);
  var_dump($b);
  var_dump($_SESSION);

【実行結果】
string(3) "xyz"
array(1) {
  ["x"]=>
  string(3) "p23"
}
array(1) {
  ["user"]=>
  string(6) "yamada"
}

 先に引用した箇所には、parse_str($_SERVER['QUERY_STRING']); という行があるので、URL上のクエリ文字列により、任意の変数を設定できることになります。このスクリプトの最後はexit;でアプリケーションを終了させるので、通常の変数の設定には攻撃としての意味はありませんが、セッション変数が変更できることが問題です。セッション変数は、後続のページに影響を与えるからです。
 これが、CVE-2011-2505の「変数汚染」脆弱性の原因です。
 なお、$_SESSION = array(); という行があるので、セッション変数をクリアしているように見え、セッション変数の汚染はできないように思えますが、実際には汚染が可能です。その理由は、以下の通りです。

  • session_write_close(); の呼び出しにより、セッションの内容を書き出している
  • その後、session_id($session_to_unset); によりセッションIDを変更しているので、セッションの内容は上書きされない
  • 攻撃者は元のセッションIDに戻すことにより、汚染した方のセッションを悪用できる

 ここで、$session_to_unset という変数はセットされていないように見えますが、クエリ文字列session_to_unsetをセットすれば、parse_str関数によりこの変数がセットされるという流れです。分かりにくいプログラムですね。
 ここで、この脆弱性を修正したバージョン(3.4.3.1)の該当箇所を引用します。

if (!empty($_GET['session_to_unset']))
{
	session_write_close();
	session_id($_GET['session_to_unset']);
	session_start();
// 以下同じ

 これなら分かりやすいですね。そして修正前のスクリプトでは、parse_str関数の呼び出しが、変数session_to_unsetの設定だけに使われていたことが分かります……。なんと言いますか、鶏を割くにいずくんぞ牛刀を用いん、の類で、単にクエリストリングsession_to_unsetを使いたいだけなにのにparse_str関数を呼び出して、プログラムは読みにくくなるし、脆弱性は混入するし、でいいことはまったくありません。最初からこう書けばいいのに、そうしなかった理由は、昔からのregister_globals前提のスクリプトを、register_globalsを設定しなくても動くように変更したためでしょうか(憶測)。
 これで、CVE-2011-2505の説明は終わりです。

CVE-2011-2506の概要

 続いて、CVE-2011-2506の説明です。こちらは、ConfigGenerator.class.phpというファイルの中でセッション変数の内容をPHPのコメントに書き出している箇所があり、そこに脆弱性があります。該当箇所のソース(setup/lib/ConfigGenerator.class.php 38行目)を引用します。

    // servers
    if ($cf->getServerCount() > 0) {
        $ret .= "/* Servers configuration */$crlf\$i = 0;" . $crlf . $crlf;
        foreach ($c['Servers'] as $id => $server) {
            $ret .= '/* Server: ' . strtr($cf->getServerName($id), '*/', '-') . " [$id] */" . $crlf

 引用の最下行で、「/* Server:」で始まるコメントを書き出しています。生成されるコメントの例を以下に示します。

config/config.inc.php

/* Server: the test server [1] */

 $cf->getServerName($id)が、「the test server」、$idが「1」です。
 ここで、前者の方で、strtr関数で「*」を「-」に変換しているのは、スクリプトインジェクション攻撃対策のサニタイジングです。ここに以下の文字列を挿入した場合、

*/ system('echo /etc/passwd'); /*

 サニタイズしない場合、生成されるコメントは以下となります。コメントが閉じられ、system関数によりOSコマンドが実行されます。

/* Server: */ system('echo /etc/passwd'); /* [1] */

 一方、サニタイズした場合は、コメントは以下となり、スクリプトの注入はできません。

/* Server: -/ system('echo /etc/passwd'); /* [1] */

 なお、該当箇所の開発者はstrtr関数の仕様を勘違いしているようです。strtr関数は文字単位の置換であり、第2引数に指定された文字を第3引数に指定された文字に変換するものですが、文字列中の位置に意味があり、第2引数のn番目に指定された文字を第3引数のn番目の文字に変換します。先の例では、第2引数が「*/」、第3引数が「-」ですが、第2引数の方が長い文字列の場合はそれは無視されます*1。すなわち、「*」を「-」に変換するという意味になります。開発者の意図が、仮に「*」と「/」の両方を「-」に変換することである場合は、第3引数は、「--」でなければなりません。このあたり、プログラムがちょっとずつ甘いなぁと思いますが、幸いこれは脆弱性ではありません。
 問題は、$idをサニタイジングしていないことにあります。このため、セッション変数の汚染により$idの方にスクリプトインジェクション攻撃を仕掛けることができるというのが、CVE-2011-2506の脆弱性です。
 ここで、この脆弱性を修正したバージョン(3.4.3.1)の該当箇所を引用します。該当する最後の行だけです。

            $ret .= '/* Server: ' . strtr($cf->getServerName($id) . " [$id] ", '*/', '-') . "*/" . $crlf

 ご覧のように、「$cf->getServerName($id) . " [$id] "」をまとめてサニタイズするようにしています。これで問題はないと思いますが、strtr関数の使い方は相変わらず間違っています。
 CVE-2011-2506の説明は以上です。これまで説明したように、CVE-2011-2505脆弱性でセッション変数を汚染して、CVE-2011-2506脆弱性を悪用してPHPスクリプトを書き出すというのが攻撃の流れになります。
 ここで、読者の参考のため、PoCの攻撃手順のHTTPリクエストを以下に示します。

GET /phpmyadmin/setup/index.php                                  セッションID、トークンの取得など
GET /phpmyadmin/?_SESSION[ConfigFile][Servers][*/攻撃スクリプト  セッション変数汚染
POST /phpmyadmin/setup/config.php                                攻撃コードの埋め込み
GET /phpmyadmin/config/config.inc.php?eval=攻撃コード            攻撃コードの実行

対策

 上に示したように、スクリプト注入の攻撃が成立するためには、攻撃者がphpMyAdminのsetupディレクトリとconfigディレクトリの配下のスクリプトにアクセスする必要があります。しかし、これらのディレクトリは、phpMyAdminの設定用のスクリプトがおかれている訳ですから、外部から認証なしにアクセスできること自体が、そもそも脆弱であると言えます*2このことから、phpMyAdminバージョンに関わらず、以下を強く推奨します。

  • phpMyAdminのインストールされたディレクトリには外部からアクセスできないよう適切なアクセス制限を設ける

 具体的には以下が考えられます。

 上記のHTTP認証を書ける場合の注意を補足します。phpMyAdmin自体にもHTTP認証の機能がありますが、それは使用せずに、Apache等の設定でphpMyAdminディレクトリ以下全体にHTTP認証をかけるようにします。その理由は、phpMyAdminのHTTP認証はあくまでアプリケーションの認証であり、setupスクリプトやconfigディレクトリに関してはアクセス制限は掛からないからです。このため、Apache等でディレクトリ単位のアクセス制御を行い、phpMyAdmincookie認証を設定するのがよいと思います。
 また、configディレクトリは、普段は必要のないものなので、設定が終わったら削除するべきです。これだけでも、このエントリで紹介したスクリプトインジェクションは防げます。

 このような対策の一方で、phpMyAdminを最新版にバージョンアップして下さい。

まとめ

 phpMyAdminスクリプト実行の脆弱性(CVE-2011-2505、CVE-2011-2506)について報告しました。
 私がこの脆弱性に関心をもったきっかけは、スクリプトが実行される脆弱性とはどのようなものなのだろうという興味からでした。もちろん、ファイルインクルードとか、evalインジェクション、あるいはアップローダ脆弱性など、任意スクリプトが実行できる脆弱性はいくつかあるわけですが、phpMyAdmin脆弱性はどれでもないように思えたからです。
 調べた結果はご覧の通りですが、私は、設定ファイルをPHPスクリプトとして生成するというphpMyAdminの設計がそもそも危なっかしいと思います。設定ファイルはテキスト形式にしておけば、スクリプトインジェクションの可能性もありません。おそらく、導入を簡単にするための工夫なのだと思いますが、よくない設計だと思います。
 また、parse_strの使用も問題です。第2引数をとらないparse_strは、常に変数汚染の危険性が伴いますが、phpMyAdmin3.4.3.1のソースを調べたところ、parse_strを使っている箇所がまだ1つ残っていました(libraries/auth/swekey/swekey.auth.lib.php 146行目)。脆弱性となるかどうかは調べていませんが、潜在的に危険であると言えます。

 しかし、私が一番問題だと思うのは、phpMyAdminを危険な状態で設置しているサイトが少なからずあることだと思います。phpMyAdmin自体に認証機能があるので油断している利用者がいるのかもしれませんが、それは危険です。phpMyAdminはインターネットに公開して使用されることを想定した設計になっていないように思います。phpMyAdminはインターネット越しに使わないのが一番よいと思いますが、レンタルサーバーなどでやむを得ずインターネット越しにphpMyAdminを利用する場合は、IPアドレス制限やHTTP認証などを併用するべきです。
 加えて、configディレクトリは設定終了後削除しておくべきです。configディレクトリが残っている状態でphpMyAdminを起動すると、以下のようなエラーメッセージが表示されます。

 このような警告メッセージを放置することは非常に危険だということです*3

 まとめると、phpMyAdminスクリプトインジェクション攻撃を受けるのは、以下の三重苦が揃うことが原因だと考えます。

[PR]

*1:PHPオンラインマニュアルによると、「from と to の長さが異なる場合、長い方の余分な文字は無視されます。」とあります

*2:これら設定用のスクリプト自体には認証機能はありません。

*3:phpMyAdminのどのバージョンからこのメッセージが表示されるかは調べていません