2020年5月30日土曜日

PHPの脆弱性CVE-2018-17082はApacheの脆弱性CVE-2015-3183を修正したら発現するようになったというお話

最近自宅引きこもりで時間ができたので、YouTube動画を投稿するようになりました。みんな見てねー。

徳丸浩のウェブセキュリティ講座

そんなことで、次の動画は、お気に入りのPHPの脆弱性 CVE-2018-17082 を取り上げようと思ったんですよ。表向きXSSで出ているけど、金床さんのツッコミにもあるように、実はHTTP Request Smuggling(HRS)だというやつです。でね、下準備であらためて調べていると、なんかよく分からない挙動がワラワラと出てくる。なんじゃ、こりゃ。CVE-2018-17082 全然分からない。僕は気分で CVE-2018-17082 を扱っている…

で、雑に整理すると、以下のような感じなんです。
  • 古い環境だとCVE-2018-17082は発現しない(2015年以前)
  • 少し古い環境だとCVE-2018-17082は発現する
  • 新しい環境だとCVE-2018-17082は発現しない(2019年以降 …これは当たり前)
これ自体はよくあることです。古いバージョンには脆弱性がなく、途中のバージョンで作り込まれたってことね。でもね、CVE-2018-17082はPHP 5.0.0にもあるんですよ。PHP 5.0.0が出たのは2004年ですからね。これだと辻褄が合わない。

試しに、自宅の環境のmodphpallを使ってPHP 5.0.0でCVE-2018-17082を試すとちゃんと発現するわけですよ。PHPのバージョンは関係ない。

で、次に、自宅の環境のCentOS/Debian/Ubuntuの各バージョン(パッチがまったくあたってないものとすべてのパッチがあたっているもの…LinuxAllという大げさな名前にしているのはくれぐれも内緒だ)で試すと、先の現象になるわけです。あまり古いと脆弱性のあるPHPバージョンなのに発現しない。CentOS6と7なんかひどくて、パッチのあたってない環境だと発現しないのに、すべてのパッチがあたっていると逆に発現する。
でも、ここにヒントがあると思ったわけですね。なぜって、CentOSはそもそもCVE-2018-17082のパッチは出してないわけですよ。ならば、PHP以外に影響があるに違いない。ひょっとすると、Apacheのバージョンが影響しているのかも。考えてみれば、そうだよね。だってCVE-2018-17082は、PHPのApache2handlerの脆弱性だから、相方のApacheの実装に影響を受ける可能性は十分にある。

なので、今度はApache 2.2と2.4のすべてのバージョンを導入した試験環境(1.3や2.0がないのにApacheAllという大げさな名前なのは内緒)で試したわけですよ。さいわい、リファレンス用としてPHP 5.3.3が入れてあるので、脆弱性が動くはずです。
でためしたら、やはり古いApacheだとCVE-2018-17082は発現しなくて、以下のバージョンだと発現する。
  • Apache 2.2.31以降
  • Apache 2.4.16以降
で、Apache 2.2.31で修正された脆弱性はなんだろう…と思って調べると、以下ですよ(これだけ)。

low: HTTP request smuggling attack against chunked request parser (CVE-2015-3183)

思わず息を呑みました(誇張じゃないよ)。出た、HRS。これだ。これに違いない。
ちなみに、CVE-2015-3183に対する修正はこちらでして、コミットメッセージに「Limit accepted chunk-size to 2^63-1 and be strict about chunk-ext」とあるように、巨大なチャンクサイズへの対応でしょうね。
でもね、皮肉な話じゃないですか。Apacheのあまり攻撃現実性がない脆弱性に対する修正の影響で、潜在的なPHPの脆弱性が顕在化して、こっちの方がはるかに攻撃の現実性があるわけですよ。別にApacheが悪い訳ではないですけどね。皮肉なもんだなーと思ったわけです。

ということで、全てが綺麗に説明がついたので、安心して動画制作に戻ろうと思います。
このように、私の動画は見えないところに手間をかけて制作していますんで、みんな見てくださいねー、よろしく!

徳丸浩のウェブセキュリティ講座

2020年3月27日金曜日

PHP 7.2以降におけるPDO::PARAM_INTの仕様変更

サマリ

PHP 7.2以降、PDOの内部実装が変更された。動的プレースホルダ(エミュレーションOFF)にてバインド時にPDO::PARAM_INTを指定した場合、PHP 7.1までは文字列型としてバインドされていたが、PHP 7.2以降では整数型としてバインドされる。
この変更により、従来PDOが内包していた「暗黙の型変換」は解消される一方、integerへの暗黙のキャストにより、整数の最大値を超えた場合に不具合が発生する可能性がある。

この記事を読むのに必要な前提知識

この記事は、以前の記事(下記)の続編のような形になっています。

PDOのサンプルで数値をバインドする際にintにキャストしている理由

この記事では、PDOを用いたサンプルスクリプトでbindValue時にinteger型へのキャストを明示している理由を説明しています。パラメータを文字列として渡した場合、PDO::PARAM_INTにより整数型を指定しても、SQL文生成時に文字列リテラルとしてバインドされるため、「暗黙の型変換」により意図しない動作がおこり得ます。それを避けるために、値をintegerにキャストすることにより、SQL文には数値リテラルとして生成され、暗黙の型変換による不具合を避けることができます。一方、integer型へのキャストすると、整数の範囲を超えた場合にオーバーフローが起こる危険性があり、一長一短ではあります。

PHP 7.2での変更内容

PHP 7.2以降では、bindParamあるいはbindValue時にPDO::PARAM_INTを指定した場合、値はPHPのint型にキャストされます。ここで注意すべきことは、MySQLのINTではなく、PHPのinteger型にキャストされるため、32ビット環境では32ビット符号付き整数、64ビット環境では64ビット符号付き整数にキャストされ、その範囲を超えた場合はintegerの最小値あるいは最大値に変更されることです。

検証

検証のために、先のブログ記事と同じデータを用意します(再掲)。
CREATE TABLE xdecimal (id DECIMAL(20));   -- DECIMAL(20)は10進20桁の数値型
INSERT INTO xdecimal VALUES (18015376320243459);
INSERT INTO xdecimal VALUES (18015376320243460);
INSERT INTO xdecimal VALUES (18015376320243461);
PHPサンプルは下記のとおりですが、暗黙の型変換を起こすために、WHERE句で + 0を追加しています。
<?php
  $db = new PDO("mysql:host=127.0.0.1;dbname=test;charset=utf8", DBUSER, DBPASSWD);
  $ps = $db->prepare("SELECT id FROM xdecimal WHERE id = :id + 0");
  $id = '18015376320243461';
  $ps->bindValue(':id', $id, PDO::PARAM_INT); // intへのキャストはしない
  $ps->execute();
  $row = $ps->fetch();
  echo "$id->${row[0]}\n";
PHP 7.1.33で実行結果(32ビット環境、64ビット環境とも)
【実行結果】18015376320243461 -> 18015376320243459
【実行されるSQL文】SELECT id FROM xdecimal WHERE id = '18015376320243461' + 0

PHP 7.2.0での実行結果(64ビット環境)
【実行結果】18015376320243461 -> 18015376320243461
【実行されるSQL文】SELECT id FROM xdecimal WHERE id = 18015376320243461 + 0

PHP 7.2.0での実行結果(32ビット環境)
【実行結果】18015376320243461 ->
【実行されるSQL文】SELECT id FROM xdecimal WHERE id = 2147483647 + 0

端的な違いは、リテラルがシングルクォートで囲まれているか否か、すなわち、文字列リテラルか数値リテラルかです。PHP 7.2以降ではの変更、バインド時にPDO::PARAM_INTを指定した場合にバインド値が数値リテラルとして生成され、これはSQLの文法として本来の姿になったと考えます。これによる現実的なメリットは、文字列リテラルを数値に変換する際の「暗黙の型変換」が避けられ、正確な数値比較や演算が行えるようになったことです。

この変更による悪影響

PDO::PARAM_INT指定時に、値がいったんPHPのintegerにキャストされることから、integerの上下限を超えた値を指定した場合、強制的にPHP_INT_MAXまたはPHP_INT_MINに値が変更されます。具体例は、前述の「PHP 7.2.0での実行結果(32ビット環境)」を参照ください。integerの取りうる値の範囲は環境依存ですが、現在一般的な環境では以下のようになる場合が多いと思います。

32ビット環境: -2147483648 ~ 2147483647
64ビット環境: -9223372036854775808 ~ 9223372036854775807

PDOはどのように使えばよいか

今どき32ビット版のPHPを本番環境で使っている(かつPHP 7.2以降が動いている)環境はあまりないかもしれませんが、64ビット版であっても、数値の範囲について注意が必要です。PHP_INT_MAXを超える可能性がある値について、PDOはDecimal型のような型を指定する方法がないため、以下のように明示的なキャストを指定する方法などが考えられます。
$ps = $db->prepare("SELECT id FROM xdecimal WHERE id = CAST(:id AS DECIMAL(20))"); // 明示的な型変換
$ps->bindValue(':id', $id, PDO::PARAM_STR); // いったん文字列型でバインドして、CASTで明示的に型変換

まとめ

PHP 7.2におけるPDOの実装変更について紹介しました。この変更はドキュメント化されていないようで、かつ、PHP Internalsメーリングリストでも話題になっていないようです。もしドキュメント等を知っている方がおられたら教えてください。
この変更は非互換かつドキュメント化されていないものであるので、ひょっとすると手違いなどで変更された可能性もあると思っています。

フォロワー

ブログ アーカイブ