2020年12月19日土曜日

PHPビルドの楽しみ、あるいはポケモンとしてのPHPについて

この記事はPHP Advent Calendar 2020の19日目です。18日目は@You-sakuさんのPHPでYoutubeのAPIを利用したいでした。


私は以前、PHPのすべてのバージョンを使う環境として、phpallphpcgiallmodphpallを紹介してきました。これらは、PHPのすべてのバージョンをそれぞれコマンドライン、CGI、mod_phpの形式で動作させるものです。
現在では、PHPのバージョンを切り替えて動作させる仕組みはphpenvphpbrew等何種類もありますし、php-buildという、PHPをビルドするというそのものずばりの仕組みも広く使われています。
しかし、短いPHPスクリプトをPHPの全バージョンで試すという私の使用法からは、phpall(およびその派生としてphpcgiall、modphpall)はメリットがあり、私は今もPHPのビルドを続けています。

そう、私は今も、PHPのすべてのバージョンのバイナリをCLI、CGI、Apacheモジュールの形式で持っているのです。そして、元々は目的があって始めたことなのですが、いまや、「PHPのすべてのバージョンをコンプする」こと自体が目的化してしまっています。この感覚、何かに似ているなと思い返したら…そうです。ポケモンを集めるような感覚なのです。いまや、

徳丸にとってPHPはポケモン


と言っても過言ではありません。
PHPがポケモンだという主張は認めていただくとして、PHPにもポケモン同様にレアキャラがあるでしょうか…

あります。ビルドが困難であればあるほど、レアなポケモン(に相当するPHP)と言ってよいでしょう。
経験上、以下の条件を満たすPHPがレアな(ビルドが困難な)ものと言えます。
  • 古いバージョンのPHP(PHP4、PHP5.0、5.1、PHP5.2等)
  • 新しい環境で動く(Ubuntu 20.04等)
  • できるだけ多くの拡張モジュールを備える
  • 32ビット版よりも64ビット版
以下、その理由を簡単に説明しましょう。

古いバージョンのPHP

次の項と関連しますが、古いPHPはビルドすること自体が困難です。その一端は@hnwさんの以下の記事で読むことができます。


古いPHPがビルドが困難な理由は以下の通りです。
  • Cコンパイラの新バージョンでは文法チェックが厳しくなった結果、コンパイルエラーになりコンパイルできない
  • PHPが利用するライブラリの非互換なバージョンアップ
これらの回避策としては以下が考えられます。
  • 古いコンパイラ(gcc)を使う
  • コンパイルエラーを緩和するオプションを設定する
  • 古いライブラリを使う
  • PHPにパッチをあてる
最終的にはPHPにパッチをあてざるを得ない場合もありますが、これはできれば避けたいところです。新しいgccでコンパイルエラーになる箇所を見ていると「これ、単にバグじゃないか」と思うものが多い(それ以外ではライブラリの非互換)のですが、他の方法で回避できるのであれば回避したい…ということで、古いgccを試したりしましたが、今は以下のgccのオプションで回避して、最低限PHPソースにパッチをあてています。
-std=gnu89
-fgnu89-inline
ライブラリも結構ハマるところでして、以下は複数のバージョンを使っています。
  • zlib(1.1.4、1.2.11)
  • openssl(0.9.8、1.1.1)
  • curl(7.15.0、7.16.0、7.68.0、7.72.0)
  • libxml2(2.7.8、2.9.8、2.9.10)
意外にハマるのがcurl(libcurl)でして、非互換なバージョンアップのため、PHPのバージョンによってcurlのバージョンも変える必要がありました。

新しい環境で動く

前項の裏返しになりますが、新しい環境(OS)であるほど、古いPHPをビルドすることが困難になります。特にライブラリの非互換は避けようのないところで、OS(Linuxディストリビューション)にバンドルされているライブラリでうまくビルドできない場合は、古いライブラリを探してきてビルドすることから始めなければなりません。ライブラリ毎にビルド方法に癖があったりして苦労する場合もありますが、そこがまた楽しいのです。

できるだけ多くの拡張モジュールを備える

前項と関連しますが、できるだけ多くの拡張モジュールを指定した方がビルドの難易度が上がります。OSにバンドルされていないライブラリや、バンドルはされているが非互換のため古いライブラリを別途導入しないケースはなおさらです。

32ビット版よりも64ビット版

意外なことを書くと思われるかもしれませんが、古いPHP(4.1、4.2あたり)はAMD64でのビルドが困難です。ビルド自体はできるのですが、正常に動作しません。この時代はまだAMD64の現物がなかった(AMD64の出荷は2003年4月)からと思われます。PHP 4.0はビルドでき動作もするので、調整すればPHP 4.1や4.2も動作すると思うのですが、私はまだ成功していません。

phpallをUbuntu16.04(32ビット)からUbuntu20.04(64ビット)に移行

さて、著者はphpallの環境を当初Ubuntu12.04(32ビット)に構築し、その後Ubuntu16.04(32ビット)に移行していましたが、PHP 8.0α版が出た際にUbuntu20.04(64ビット)に移行しました。
そのきっかけとなったのは、PHP 8.0から導入されたJITを試そうとして時のことです。どうもx86(32ビット)では、JITは使えないようなのです。下図はphpinfoからの該当部分です。


JITのRFCではx86もサポートされていると書かれていましたが、本番投入は見送られたのでしょうか。私は、phpallを64ビット環境に移す決断をしました。以下はUbuntu20.04(64ビット)でのphpinfoで、JITが有効化できていることがわかります。


残る課題

楽しみとしてのPHPビルドについて紹介しましたが、現状課題が残っています。前述のように、64ビット環境でのPHP4.1と4.2のビルドには成功していないからです。ここは私の技術の至らなさではあるのですが、将来の楽しみにとっておこうと思います。やり方をご存知の方がいれば教えていただけるとありがたいです。

2020年11月14日土曜日

auじぶん銀行のフィッシングSMSが届いた

3日前に、auじぶん銀行の巧妙な不正出金についてYouTube動画を公開しました。みんな見てねー。

auじぶん銀行アプリに対する不正出金の驚くべき手口

そうしたところ、先程私のiPhoneに以下のようなSMSが届きました。

ふーん、au自分銀行のときは宅配事業者を装ったSMSということでしたが、これはどうなんでしょうね。開いてみると…


詐欺サイトの警告が出ていますが、構わずに開いてみると…


きたきたきたー。これですよ。auじぶん銀行のフィッシング(SMSの場合はスミッシングと言いますが)サイトのようですよー。「閉じる」をタップすると…


うーむ、これがauじぶん銀行の本物のフィッシングサイトですよ。これかー。僕が作った偽サイトよりもきちんと作ってありますねー(笑い)。
ちなみに、本物のauじぶん銀行サイトはこちら。


よく似ていますね。お客様番号とログインパスワードの欄は共通ですが、偽物の方は暗証番号と誕生日を要求しています。私がデモ用に作った偽サイトは、さらにカタカナの氏名を要求していますが、氏名はサイトにログインすると容易にわかるので省略しているのでしょうね。

せっかくの「本物の偽サイト」ですので、少し実験してみましょう。お客様番号等をでたらめな文字列にして、ログインボタンをタップしてみます。


「サーバーに接続中です。このまましばらくお待ち下さい」というダイアログが表示され、しばらくたった後、「入力に誤りがあります。」というダイアログが表示されます。
これは、攻撃者が、入力内容(お客様番号とログインパスワード)を用いて本物サイトにログインを試み失敗したためと考えられます。このように、裏で本物のサイトと中継する形式のフィッシングを中継型フィッシングと呼びます。

ログインパスワードを得るためだけであれば、中継型にする必要はないのですが、オンラインバンキングの多くはパスワードだけでは送金や出金はできず、追加の認証を求めるわけで、その追加の認証を突破する目的で行われます。
ちなみに、au自分銀行の場合は、スマホアプリかカード番号裏の乱数表が用いられますが、こちらのリリースで公表されている手口の場合は、以下を突破していると考えられます。
  • キャッシュカード暗証番号
  • キャッシュカードの裏面にある確認番号表の数字
  • お客さま携帯電話宛てに発行されたワンタイムパスワード
実際にどうなっているかは、本物の会員番号とパスワードを入力すればわかるのですが、さすがにそれはできないので、ここまででやめました。

おそらくは、冒頭に紹介した動画の手口かと思いますので、興味のある方は動画の方を御覧ください。

auじぶん銀行アプリに対する不正出金の驚くべき手口

フィッシングは恐ろしい手口であり、こちらの報道(アーカイブ)では、「警察庁によると、6、7月に同様の被害が全国で32件(約1060万円)確認されている」とあります。くれぐれもご注意下さい。また、中継型フィッシングを想定すると、二段階認証も突破される可能性が高いので、サイト運営者側もそれを想定した対策が求められます。


追記(2020年11月15日11:00)

今朝当該サイトにアクセスしてみたところ、DNSの設定が削除され、当該IPアドレスにもアクセスできない状態になっていました。関係者の皆様ありがとうございました。

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メーリングリストでも話題になっていないようです。もしドキュメント等を知っている方がおられたら教えてください。
この変更は非互換かつドキュメント化されていないものであるので、ひょっとすると手違いなどで変更された可能性もあると思っています。

フォロワー

ブログ アーカイブ