パッチ公開の前に攻撃が始まる状態を「ゼロデイ脆弱性」と言いますが、それでは、この脆弱性のメカニズムはどんなものだろうかと思い、調べてみました。
結論から言えば、この問題はJoomla!側に重大な脆弱性はなく、PHPの既知の脆弱性(CVE-2015-6835)が原因でしたので報告します。
exploitを調べてみる
既にこの問題のexploitは公開されていますが、悪い子が真似するといけないのでURL等は割愛します。以下のページでは攻撃の原理が説明されています。ざっくり言うと、以下の様な攻撃です。
- Joomla! がUser-Agentをセッション変数に保存するので、セッション形式のデータ(文字列)をUser-Agent経由でセットする
- その際、MySQLのデフォルトでは、UTF-8の4バイト形式があると、それ以降をトランケートする仕様を悪用して、攻撃に不要なデータを切り詰める
- 何ということでしょう! 切り詰めをすると、文字列がオブジェクトに化けるではありませんか
- 上記によりJoomlaのJDatabaseDriverMysqli、SimplePie等のクラスのオブジェクトをインジェクションして、デストラクタにより任意コードを起動
問題は、上記の 3 です。なぜ、文字列を切り詰めると、文字列型のデータがオブジェクトに化けるのでしょうか。前記の記事にはその辺の説明はなかったので、自分で調べてみました。
シンプルな再現コード
以下は、セッション変数にログインユーザ名とUser-Agentのみをセッション変数にセットして、その際のセッション形式の文字列を表示するだけの簡単なプログラムです。出力は以下となります。<?php // インジェクションするクラス(攻撃対象の既存クラス) class Class1 { private $private = 'prv'; function __destruct() { echo "Class1::__destruct()\n"; } } session_start(); $id = 'ockeghem'; // ログインユーザ名 // $browser はUser-Agentであり外部からコントロールできる $browser = 'Mozilla/5.0'; $_SESSION['_default']['id'] = $id; $_SESSION['_default']['browser'] = $browser; echo str_replace("\0", '%00', session_encode()) . "\n";
_default|a:2:{s:2:"id";s:8:"ockeghem";s:7:"browser";s:11:"Mozilla/5.0";}セッション変数のシリアライズ形式は、以下のような形です。
- _default セッション変数のキー
- a:2:{...} 要素数2の配列
- s:2:"id" 文字列長の2の文字列「id」
_default|a:2:{s:2:"id";s:8:"ockeghem";s:7:"browser";O:6:"Class1":1:{s:15:"%00Class1%00private";s:3:"prv";}}これを念頭におきつつ、外部からUser-Agentとして以下を設定してみます(パーセントエンコード形式)。■は、実際には、UTF-8の4バイト形式の文字をセットします。
_aa|O:6:"Class1":1:{s:15:"%00Class1%00private";s:3:"prv";}■セッション変数のシリアライズ結果は下記となります。
_default|a:2:{s:2:"id";s:8:"ockeghem";s:7:"browser";s:57:"_aa|O:6:"Class1":1:{s:15:"%00Class1%00private";s:3:"prv";}■";}しかし、これはあくまで文字列です。それは、s:57という型指定がある以上、外部からは変更できないはずです。
しかし、Joomla!のデフォルトのセッションストレージはMySQLであり、文字エンコーディングはutf8_general_ciとなっています。この場合、MySQLは、UTF-8の4バイト形式の文字があると、その文字を含めてそれ以降が切り詰められる、というすごい仕様があります。
このため、セッションをいったんMySQLに保存して、次のページでそのセッションデータを読みだすと、以下のように■以降がカットされた状態となります。
_default|a:2:{s:2:"id";s:8:"ockeghem";s:7:"browser";s:57:"_aa|O:6:"Class1":1:{s:15:"%00Class1%00private";s:3:"prv";}このセッションデータをダンプすると、何ということでしょう! 文字列だった部分がオブジェクトになっているではありませんか。そして、オブジェクトが生成されているので、このデストラクタも実行されます。
array(2) { ← ダンプ
["_default"]=>
NULL
["57:"_aa"]=>
object(Class1)#1 (1) {
["private":"Class1":private]=>
string(3) "prv"
}
}
Class1::__destruct() ← デストラクタの実行
上記から、以下のことが言えます。- これは Joomla! の脆弱性ではない
この問題の原因は何か
phpallを使ってこの問題を確認すると、PHPの以下のバージョンで起こることが分かります- PHP-5.0.3~PHP-5.0.5
- PHP-5.1.x ~ PHP-5.3.x 全て
- PHP-5.4.0~PHP-5.4.44
- PHP-5.5.0~PHP-5.5.28
- PHP-5.6.0~PHP-5.6.12
- PHP-5.4.45
- PHP-5.5.29
- PHP-5.6.13
- PHP-7.0.0
- Fixed bug #70219 (Use after free vulnerability in session deserializer). (CVE-2015-6835)
Linuxディストリビューションではどうか?
PHP本家の対応は上記のとおりですが、RHEL/CentOS等のLinuxディストリビューションから、パッケージとして提供されているPHPのパッチの対応状況はどうでしょうか?- CentOS 5,6,7ともパッチ提供なし(参考)
- Ubuntu 12.04、14.04ともパッチ提供済み(参考)
- Debian/GNU Linux Debian6はパッチ未提供、7以降は提供済み
- Fedora Fedora21以降でパッチ提供済み(20はサポート終了でパッチ提供なし)
影響を受けるソフトウェア
PHPのこの脆弱性の影響は、Joomla!に限られるものではありません。セッションストレージとしてMySQLを使うことは、一般的によく行われているからです。このため、影響の有無判定はともかく、PHPの最新版を使うことを推奨します。
RHEL/CentOSについてはパッチが提供されていないので、個別の対策をとるしかないでしょう。例えば、以下の様な対応が考えられます。
- アプリケーション側で対応したバージョンを導入する(例:Joomla! 3.4.6)
- MySQL 5.5.3 以降で使える utf8mb4 エンコーディングを使用する
- セッションストレージとしてMySQLを避ける
- バリデーションによりUTF-8の4バイト形式をエラーにする
まとめ
Joomla!のリモートコード実行の問題を分析しました。これまで説明したように、この問題の直接の原因は、Joomla!側にはなく、PHPの既知の脆弱性(CVE-2015-6835)が原因です。私も「Joomla!にゼロデイ脆弱性」とツイートしてしまいましたが、Joomla!の問題ではないと訂正いたします。この問題は、
- PHPの通常は顕在化しない脆弱性CVE-2015-6835が
- MySQLのUTF-8 4バイト形式以降を切り詰めるという仕様のため顕在化した
【HASHコンサルティング広告】
HASHコンサルティングが販売するWAF(SiteGuard、SiteGuard Lite)は、このJoomla!のコード実行の問題に対応したシグネチャを提供済みです。お問い合わせはこちら。
HASHコンサルティング株式会社は、セキュリティエンジニアを募集しています。
興味のある方は、twitterやfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。
ワークアラウンドをもうひとつ。
返信削除MySQLのsql_modeにSTRICT_TRANS_TABLESまたはSTRICT_ALL_TABLESを指定すると切り詰めは発生しなくなります(エラーになるようになります)
https://gist.github.com/yoku0825/19f6fe20ecadb1928346
Twitter などでも指摘されておりますが、文字列型のカラムにバイナリを入れるという joolma! 側の設計にそもそもの問題があり、LONGBLOB などにデータを格納すべきだったと思います。
返信削除文字列型のカラムにバイナリを入れるという設計に問題があるのでこの脆弱性は、やはり joolma! の側の脆弱性といって差し支えないように思います。