2011年11月29日火曜日

KDDIの新GWで「かんたんログイン」なりすましの危険性あり直ちに対策された

au/KDDIの2011年秋冬モデル(現時点ではF001のみ)にてEZwebとPCサイトビューア(以下PCSV)のゲートウェイが統合されたことに伴い、かんたんログインを実装しているサイトに対して、F001のPCSVからJavaScriptを用いた「なりすまし」攻撃ができる状態でした。この問題をKDDIに通報したところ、直ちに対策が取られ、現在は安全な状態です。以下、詳しく報告します。

目次


概要

以前、「EZwebの2011年秋冬モデル以降の変更内容とセキュリティ上の注意点」にて、auの2011秋冬モデルにてEZwebとPCサイトビューア(以下PCSV)のゲートウェイが統合される(以下「新EZサーバー」と表記)ことによるセキュリティ上の懸念と、Cookieの仕様変更について予測しました。両者は結果として「アタリ」でした。本稿では、セキュリティ上の問題について報告します。なお、セキュリティ上の問題はKDDIに報告し、既に解消しています。

経緯

経緯を時系列で示します。全て2011年です。
  • 9月28日(水):秋冬モデルよりEZwebとPCSVのIPアドレスが統一されると発表(魚拓)
  • 11月9日(水):F001が関西など一部地域で発売開始。関東は11月10日発売開始。
  • 11月10日(木):徳丸がF001を購入。検証を開始するも、この日のうちには問題を発見できず。
  • 11月11日(金):早朝、OpenPNEの海老原昂輔(@co3k)さんから、PCSVにてEZ番号を偽装できる旨の連絡と、OpenPNEの対応と情報公開の相談を受ける。KDDIに連絡して対処してもらうことを優先し、公表をしばらく見合わせることを合意
  • 11月11日(金):ツテを通じてKDDIに連絡
  • 11月12日(土):F001のPCSVのIPアドレスが変更されていることを確認。成りすましの危機は回避される。この状態のまま現在に至る
  • 11月28日(月):EZfactoryにて「※ セキュリティ確保のため、EZブラウザとPCサイトビューアーは、従来通り異なるIPアドレス帯域を使用しております」と発表(直リンク)(魚拓)

ということで、連絡の翌日には回避策がとられました。迅速な対応だと思います。

何が問題か

ここで扱っている問題は、PCSVのJavaScriptを悪用して、ケータイWebサイトの「かんたんログイン」なりすましの可能性です。基本的には、以下のエントリで説明した内容です。

携帯JavaScriptとXSSの組み合わせによる「かんたんログイン」なりすましの可能性

このレポートではXSSによる攻撃のみに言及していますが、その後の研究により、DNSリバインディング攻撃を使っても攻撃が可能であることが分かっています。もっとも、ケータイブラウザのJavaScriptでは、XMLHttpRequestオブジェクト(以下XHR)によるHTTPリクエストヘッダを変更に大幅な制限がかけられたため、この攻撃は成立しません。一方、XHRに大きな制限をかけたことで、ケータイJavaScriptはインターネット一般のJavaScriptとの互換性が大きく損なわれました。
今回の問題は、ケータイブラウザ(iモード、EZweb、Yahoo!ケータイのブラウザ)ではなく、PCSVを使った攻撃であることが、従来とは異なります。PCSVからの攻撃が可能になった理由は、F001以降のEZweb端末で、EZブラウザとPCSVの使うゲートウェイが共通になったためです。つまり、かんたんログインの前提として、ケータイブラウザからのリクエストであることを確実にチェックする方法として、リモートIPアドレスを用いていたのに、その同じIPアドレスにPCSVからのリクエストが混じるようになったためです。

経緯説明(1)基本的なチェックは対処済みだった


KDDIは、これによる危険性を承知していたようで、F001のPCSVのXHRには以下の処置が施されていました。
  • リクエストヘッダX-UP-SUBNO(EZ番号)が追加できない
  • リクエストヘッダUser-Agentが変更できない

これらにより、PCSVを使ってもEZ番号の詐称ができないよう配慮されていました。User-Agentの変更もできないようにしている理由は、EZwebかPCSVの区別をつけるためと、au以外の事業者の端末に成りすましができないようにとの配慮でしょう。

経緯説明(2)ハイフンをアンダースコアに変えるトリックは対策済み

CGIやPHP等一部の言語では、foo-barという形のリクエストヘッダは、HTTP_FOO_BARという形式で受け取ることになります。これは、CGIの仕様として、リクエストヘッダを環境変数にして受け渡す際の約束です(下表)。PHPは、CGIの形式を踏襲しています。

HTTPリクエストヘッダ環境変数として受け取る形式
foo-barHTTP_FOO_BAR
User-AgentHTTP_USER_AGENT
X-UP-SUBNOHTTP_X_UP_SUBNO

一方、Java EE(Servlet)(HttpServletRequest#getHeaderメソッド)や.NET(Request.Headersプロパティ)では、リクエストヘッダを元のまま受け取ります。以下、CGIの形式の場合について議論します。
CGI等では、X-UP-SUBNO(EZ番号)はHTTP_X_UP_SUBNOというキーで受け取るので、最初からX_UP_SUBNOというリクエストヘッダにすれば、CGIプログラムやPHPスクリプト側ではEZ番号として受け取る可能性があります。

HTTPリクエストヘッダ環境変数として受け取る形式
X_UP_SUBNOHTTP_X_UP_SUBNO

この方法については以前テストしたことがあり、WAS Forum 2010で発表しました。このスライドの54ページ以降を参照下さい。ただし、このトリックは、ソフトバンクの一部端末でかつEnd-to-EndのSSLの場合のみ有効で、平文通信の場合はゲートウェイ側でチェックされているようです。これらの事実から、SSLではかんたんログインを受け付けるなという結論になります。
F001でもこのトリックが使えないかと思い、購入直後からチェックしましたが、ハイフンをアンダースコアに変更しても、EZ番号やUser-Agentに相当するヘッダは送信できませんでした。
アンダースコアがダメ(ちゃんとチェックされていた)だったので、ヌルバイトや改行など、過去問題になったことのある手法を試してもチェックをできなかったので、その日(11月10日)はあきらめて寝てしまいました。

経緯説明(3)海老原氏が発見したトリックとは

ところが、その日の深夜に海老原さんからメールが入っていました。メールによると、X.Up.Subnoというヘッダを送る(ハイフンやアンダースコアではなくドットを使う)ことで、PHPスクリプトからはHTTP_X_UP_SUBNOとして受け取れるというのです。確認してみると、CGIプログラムおよびPHPスクリプトでは確かにそうなります。

海老原さんからの相談は、既にOpenPNEはF001のIPアドレスを有効にしてかんたんログインが可能にする対応を配布済みだが、上記トリックにより成りすましができる(可能性がある…後述)。リストを取り下げると、その理由も説明しなければならず、他のサイトに対して攻撃されるリスクがあるので悩ましい、というものでした。
これに対して、徳丸からは、まずはKDDIに通報して、速やかに対処されればそれでよし、遅くなるようであればあらためて公開方法を考えようとアドバイスしました。海老原さんも合意され、徳丸からKDDIに連絡することになりました。

経緯説明(4)KDDIに連絡→翌日に対処

徳丸からは、信頼できる方を通じてKDDIの該当部署にピンポイントに連絡を取りました。至急動いて下さるという返事を頂いたので、しばらく待つことにしまして、その旨を海老原さんにも伝えました。
すると、翌日になって変化がありました。PCSVからのリクエストのリモートIPアドレスが変化しました。

11月12日朝までのIPアドレス:111.107.116.*
11月12日夜以降のIPアドレス:111.87.241.*

すなわち、11月12日の朝までは、EZfactoryにIPアドレス帯域として記載されたIPアドレスからリクエストが来ていましたが、同日の夜(21:55頃最初観測)以降は、別のIPアドレスにPCSVが移されたことになります。
PCSVからのリクエストが別IPになれば、PCSVを用いた「かんたんログイン」の成りすましはできなくなります。

実証例

検証用の比較的シンプルなスクリプトを以下に示します。User-Agentは実機そのまま、EZ番号は、0509999…で始まる架空のものにしてあります。

検証用HTML
---------------------
<html>
<head>
<script>
function test()
{
    var requester = new XMLHttpRequest();
    requester.open('GET', 'dump.php', true);
    requester.onreadystatechange = function() {
       if (requester.readyState == 4) {
           onloaded(requester);
       }
    };
    requester.setRequestHeader("User.Agent", "KDDI-FJ31 UP.Browser/6.2_7.2.7.1.K.8.160 (GUI) MMP/2.0");
    requester.setRequestHeader("X.UP.SUBNO", "05099999999999_vi.ezweb.ne.jp");
    requester.send(null);
}

function onloaded(requester)
{
    res = requester.responseText;
    document.getElementById('result').innerHTML = res;
}
</script>
</head>
<body onload="test()">
<div id="result"></div>
</body>
</html>

dump.php
---------------------
<?php
  echo "UA:" . htmlspecialchars($_SERVER['HTTP_USER_AGENT']) . "<br>";
  echo "EZNO:" . htmlspecialchars($_SERVER['HTTP_X_UP_SUBNO']);
?>

F001のPCSVで実行した結果の画面を下図に示します。User-AgentとEZ番号が偽装されている様子が分かります。



外部からJavaScriptを実行できる条件

この節は技術的に誤りでした。海老原さんの日記によると、F001のPCSVはアドレスバーからのJavaScript実行が可能なので、全てのサイトで、任意のJavaScriptを利用者が実行できるそうです。
F001のPCSVからJavaScriptで攻撃を行う場合、JavaScriptの同一生成元ポリシーを回避しなければ、攻撃対象サイトでJavaScriptを実行することができません。
この同一生成元ポリシー回避として、よく使われるテクニックは、クロスサイト・スクリプティング(XSS)です。攻撃対象サイトに1箇所でもXSS脆弱性があれば、それを悪用してJavaScriptを実行することができます。
もう一つの方法は、DNSリバインディング攻撃です。ケータイのDNSリバインディング攻撃については、「iモードIDを用いた「かんたんログイン」のDNS Rebinding脆弱性」を参照下さい。この問題に対して、対策を施しているサイトはDNSリバインディング攻撃によるJavaScript実行はされません。F001のPCSVでは、Hostヘッダの書き換えはできません。

影響を受けるサイトの条件

この問題の影響を受けるサイトは以下の全ての条件を満たすサイトです。
  • かんたんログインを実装している
  • 新EZサーバーのIPアドレスを許可している
  • ヘッダ名の記号をアンダースコアに変更する言語を使っている(CGIやPHPなど)
  • JavaScriptを外部から実行する手段がある(前項参照)

これらの条件を全て満たすサイトは、かなり存在する(存在した)と予想しています。


影響

前項の条件を満たすサイトでは、以下の影響があります。

  • 利用者の介在なしに、任意の利用者へのなりすましが可能

ただし、利用者のケータイIDは既知であるとします。
ケータイIDの収集は容易です。攻撃に先立ち、ケータイ向けWebサイトを開設して利用者を集めるだけで、閲覧者のケータイIDを収集することができます。また、ランダムに生成したケータイIDを攻撃に用いることも可能です。

サイトの実装によっては、auの利用者だけでなく、NTTドコモやソフトバンクの利用者に成りすましできる場合があります。その条件は、リモートIPアドレスによってキャリア判定していない場合です。言い換えれば、User-AgentやケータイIDによってキャリア判定している場合は、これらヘッダがJavaScriptにより改変可能なので、NTTドコモやソフトバンクの端末に成りすまし可能です。

ところで、この攻撃の前提として、WebサイトにXSS脆弱性かDNSリバインディング攻撃の対策をしていないという条件がつきます。このため、攻撃を受けるサイトは、元々脆弱なので、新たな脅威は生まれないのではないか、と思う読者もいると思います。
しかし、XSSやDNSリバインディング攻撃は受動的攻撃であり、罠を閲覧した利用者のみが影響を受けるのに対して、ケータイIDの変更による成りすまし攻撃は、利用者の介在なしに任意の利用者に成りすましできる点が異なります。すなわち、この問題により非常に多数の利用者が影響を受ける点が問題です。
ひょっとすると、従来XSSやDNSリバインディング攻撃の脅威を知りつつも、「影響が小さいので対策しない」という判断を(好ましくはないですが)しているサイトもあるかもしれません。そのようなサイトに対しても大きな脅威が生まれます。


対策

PCSVによるかんたんログイン成りすましは、既にKDDIにより対策されていますが、もし対策されていないとすると、以下が対策になります(なりました)。

  • XSS脆弱性をすべてなくす かつ
  • DNSリバインディング攻撃対策をする(ホスト名のチェックなど)

これらは、勝手にJavaScriptを実行されないようにする対策です。今回の問題がなくても、上記は必須です。

※追記:今回の問題に対して効果がないので取り消しましたが、上記が元々必須であることには変わりません。

加えて、以下を実施すると良いでしょう。
  • HTTPリクエストヘッダを生のまま取得できる関数を使用する

Java Servlet(Java EE)や.NETでは、これは元々実現されています。
PHPを使う場合は、getallheaders()関数を使うと、生のHTTPリクエストヘッダを取得できます。
これらにより、X.Up.Subnoと指定したヘッダは、X.Up.Subnoのまま取得されることになります。HTTP_X_UP_SUBNOにはなりません。

さらに、かんたんログインの安全性を保つための必要条件として以下があります。これは、「間違いだらけの「かんたんログイン」実装法」を@ITに寄稿した際にまとめたものですが、読者の便宜のために再掲します。DNSリバインディング攻撃対策については重複しています。
  • 携帯電話事業者のゲートウェイからのアクセスのみを受け付ける(IPアドレスチェック)
  • リモートIPアドレスを基にキャリア判定を行う(User-AgentなどHTTPリクエストヘッダで判定してはいけない)
  • 契約者固有IDとしてはiモードID、EZ番号、ユーザーID(ソフトバンク)のみを用いる
  • HTTPSではかんたんログインを受け付けない
  • Hostヘッダのチェックまたは名前ベースのバーチャルホストを設定する
  • ソフトバンクの非公式JavaScript対応端末へ何らかの対処を行う

非常に多くの条件ですが、これらで本当に安全になるかは誰にも分かりません。そのため、この機に、かんたんログインをやめ、パスワード認証等に移行することを真剣に検討しましょう。

今回の問題は、端末あるいはau設備の脆弱性なのか

さて、これまで説明したF001のPCSVの問題は、端末ないしau設備の脆弱性なのでしょうか。
私は、KDDIが責任を持つべき脆弱性ではないと考えています。KDDIは、かんたんログイン手法に対して、なんら保証をしていないからです。
とはいえ、完全に「シロ」と言い難い面もあります。
KDDI au: そのほかの技術情報 > HTTP Requestヘッダ」には、以下の記述があります。
※2011年秋冬モデル以降の一部機種ではPCSVとEZブラウザのIPアドレス帯域は統合されますが、例えば、ユーザーエージェントを組み合わせることでブラウザを判別することができます。

また、同じページから参照されているユーザエージェントの「判定CGI(サンプル)」は、PCSVからの「成りすまし」に対して、誤判定します。
しかしながら、これらはあくまでもミクロの問題です。同じページには以下の記載があります。
EZ番号とは、EZweb契約ごとにユニークに付与されるIDです。例えば、ユーザーの判別などに利用することができます。

※IPアドレス/ユーザーエージェントでのフィルタ、ID/パスワード認証を併用するなど、サイト内容に応じた適切な方法でご利用ください。

かつては、同じページ内に「ユーザ認証に用いる場合には…」という記載があり、EZ番号による認証をキャリアとして認めているように読める文面でした(参考:「EZ番号に関する注意書きが変更された」)。それが「ユーザの判別」という曖昧な表現に変更され、かつ「ID/パスワード認証を併用する」という注記が追加されたことから、KDDIはEZ番号によるかんたんログインに対して保証を与えていないと私は解釈します。

しかし、大手著名サイトを含む多くのサイトがかんたんログインを実装していることもまた事実です。この影響を考慮し、KDDIはPCSVのIPアドレスを分けるという対処をしたのだと思います。この対応は妥当だったと私は考えます。

まとめ

au/KDDIの2011年秋冬モデルF001のPCSVを用いたかんたんログインなりすまし問題について説明しました。一時的に、多数のユーザに対する成りすまし可能な状態がありましたが、KDDIにより早期に対処され、現在では安全と思われます。しかし、この機に、かんたんログインが極めて危うい状況にあることをご認識いただき、かんたんログインを廃止することを強く推奨します。

[PR]
安全なWebアプリケーションの作り方DRMフリーのPDFによる電子版もあります。

2011年11月7日月曜日

PHP5.4のhtmlspecialcharsに非互換問題

PHP5.4.0から、htmlspecialchars関数のデフォルト文字エンコーディングがISO-8859-1(Latin-1)からUTF-8に変更されます。これに伴い、従来動いていたアプリケーションが動かなくなるケースが出てきます。典型的には、以下の両方の条件に該当するアプリケーションは、マルチバイト文字が表示されなくなります。

  • 内部文字エンコーディングとしてEUC-JPまたはShift_JISを用いている
  • htmlspecialcharsの第3引数を指定していない

htmlspecialchars関数の第3引数の変更内容
htmlspecialchars関数の第3引数には、処理対象文字列の文字エンコーディングを指定します。この指定をしない場合、従来(PHP5.3まで)はISO-8859-1とみなされていたのに対して、PHP5.4ではUTF-8とみなされるようになります。
また、第3引数として空文字列('')を指定した場合mbstring.internal_encodingで指定した文字エンコーディングが指定されることになっています。この仕様はPHP5.4でも変わりませんが、細かい挙動が変わります。これらを表としてまとめました。


PHP5.3まで
第3引数意味
何も指定しないISO-8859-1
空文字列''を指定mbstring.internal_encodingに従う
文字エンコーディングを明示明示された文字エンコーディング

PHP5.4以降
第3引数意味
何も指定しないUTF-8
空文字列''を指定mbstring.internal_encodingに従う (注意点を後述)
文字エンコーディングを明示明示された文字エンコーディング


第3引数を指定していない場合の影響

前述のように、htmlspecialchars関数の第3引数を指定していない場合、PHP5.3までは、文字エンコーディングがISO-8859-1が指定されたとみなされます。この場合、入力内容にかかわらず不正な文字エンコーディングと判定されることはありません。したがって、文字エンコーディングのチェックが働かない代わりに、エラーになることもありませんでした。
これに対して、PHP5.4の仕様により文字エンコーディングがUTF-8とみなされた場合に、Shift_JISやEUC-JPの2バイト文字が入力されると、高い確率で「UTF-8として不正」というエラーになり、htmlspecialchars関数の出力は空になります。つまり、プログラムが正常に動作しません。

  • htmlspecialchars関数の第3引数を指定しておらず、内部文字エンコーディングがShift_JISあるいはEUC-JPの場合、プログラムが正常に動かなくなる

これの簡単な対処法がないが探しましたが、簡単な方法はないようです。以下の3つを考えました。

  • プログラムを修正してhtmlspecialchars関数の第3引数を明示する
  • 内部文字エンコーディングをUTF-8に変更する
  • PHPのソースにパッチをあてて、アプリケーションの文字エンコーディングがデフォルトになるようにする

大規模なアプリケーションの場合、上の2つは一筋縄ではいかない感じですね(htmlspecialcharsの呼び出し箇所が多いため)。PHPにパッチをあてることを推奨するものではありませんが、現実的な選択肢として検討したくなるプロジェクトもありそうです。それ以前に、PHP5.4に移行しないという選択になりそうですが、PHP5.3のメンテナンスがいつまで続くかは不透明なので、PHP5.4への以降は計画しておかなければならないと思います。

まとめてhtmlspecialchars関数を修正する方法

htmlspecialchars関数を一括して修正する方法として、以下の方法があります。アプリケーション中のhtmlspecialcharsを全てhxなど別名に変換します。hxという名前がアプリケーション中で使われていれば、別の名前にします。
次に、関数hxを以下のように定義します。アプリケーションの文字エンコーディングはEUC-JPと仮定します。Shift_JIS等の場合は適宜変更して下さい。

function hx($str, $flags = ENT_COMPAT, $charset = 'EUC-JP') {
  return htmlspecialchars($str, $flags, $charset);
}

この方法により、比較的楽に、安全に文字エンコーディングを指定できることになります。

PHPそのものにパッチをあてる

あまりお勧めしませんが、PHPにパッチをあてて、htmlspecialcharsの第3引数を省略した際の挙動を、mbstring.internal_encodingに従うよう変更するというアイデアもあります。
パッチの案を以下に示します。

--- html.c.org  2011-11-06 16:29:18.438601385 +0900
+++ html.c      2011-11-06 16:29:52.608727050 +0900
@@ -368,11 +368,7 @@
        int len = 0;
        const zend_encoding *zenc;

-       /* Default is now UTF-8 */
-       if (charset_hint == NULL)
-               return cs_utf_8;
-
-       if ((len = strlen(charset_hint)) != 0) {
+       if (charset_hint != NULL && (len = strlen(charset_hint)) != 0) {
                goto det_charset;
        }

ご覧のように、元のソースは文字エンコーディングが指定されていない場合、UTF-8を返すよう力強くハードコーディングされていますが、パッチではこの処理を削除し、文字エンコーディングが空文字列の場合と同じ処理になるようにしています。あまりテストはしていないので、もし採用される場合は、十分なテストをしてから使って下さい。


PHPの教科書はどうか

従来、PHPの教科書には、「htmlspecialcharsの第3引数は指定しなくてもよい」という説明が多かったようです。試みに、手元のPHPの教科書を調べたところ、以下の5つは、htmlspecialcharsの第3引数を指定せずに説明しています。

これらのうち、逆引きレシピについては、htmlspecialchars関数の第3引数を指定するように修正記事が出ています。立派な態度ですね。
一方、以下の書籍は、htmlspecialchars関数の第3引数を指定するように説明しています。

やはり、最近の書籍はhtmlspecialchars関数の第3引数を指定するように説明する傾向が伺えます。古い書籍でPHPを勉強された方や、古くからhtmlspecialchars関数の第3引数を指定しない方法で通している方は注意が必要です。

文字エンコーディングとして空文字列が指定されている場合の挙動の変化

次に、htmlspecialchars関数の第3引数に空文字列を指定している場合の挙動の変化について報告します。htmlspecialcharsはmbstring関数に比べて、指定できる文字エンコーディングが限られます。たとえば、Shift_JIS系の文字エンコーディングはShift_JISという指定のみが許され、SJIS-winやcp932は受け付けられません(参考:htmlspecialcharsのリファレンス)。しかし、PHP5.3までのhtmlspecialchars関数は、第3引数に空文字列が指定されていた場合、mbstring.internal_encodingにSJIS-winと指定していても、htmlspecialchars関数側ではShift_JISという指定に読み替えられていました。
一方、PHP5.4では、この読み替えが行われず、第3引数に空文字列を指定、かつmbstring.internal_encodingにSJIS-winと指定した場合、以下のエラーとなります。
PHP Warning:  htmlspecialchars(): charset `SJIS-win' not supported, assuming utf-8 in /home/wasbook/test/test2.php on line 12
htmlspecialcharsの第3引数に空文字列をしている例は見たことがないので、現実には問題にならないかもしれませんが、これはPHP5.4.0beta2のバグではないでしょうか。PHP5.3に比べて厳格にする理由が見あたらないからです。PHP5.4.0beta2のバグとして報告してもよいかもしれません。

まとめ

PHP5.4のhtmlspecialchars関数の挙動の変化について報告しました。既存のアプリケーションが動かなくなる可能性があり、PHP5.4移行にあたり注意が必要です。


追記
廣川さんの日記にて、htmlspecialcharsの第3引数に空文字列を指定した場合の挙動について報告がありました。PSとして、『SJIS-win、CP932、eucJP-winについてPHP 5.3と動作が同じとなるようにパッチをコミットしておきました。』ということですので、PHP5.4.0の正式版までには修正されると思われます。やはり、これはバグという認識で良かったようですね。廣川さんありがとうございました。


[PR]
安全なWebアプリケーションの作り方」電子書籍版販売しています。電子版はこちら

フォロワー

ブログ アーカイブ