2010年9月27日月曜日

文字コードに起因する脆弱性を防ぐ「やや安全な」php.ini設定

補足

この記事は旧徳丸浩の日記からの転載です(元URLアーカイブはてなブックマーク1はてなブックマーク2)。
備忘のため転載いたしますが、この記事は2010年9月27日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
補足終わり

PHPカンファレンス2010にて「文字コードに起因する脆弱性とその対策」というタイトルで喋らせていただきました。プレゼンテーション資料をPDF形式slideshare.netで公開しています。

文字コードのセキュリティというと、ややこしいイメージが強くて、スピーカーの前夜祭でも「聴衆の半分は置いてきぼりになるかもね」みたいな話をしていたのですが、意外にも「分かりやすかった」等の好意的な反応をtwitter等でいただき、驚くと共に喜んでいます。土曜にPHPカンファレンスに来られるような方は意識が高いというのもあるのでしょうね。

さて、その場で少し触れた「文字コードをやや安全に扱う方法」を紹介したいと思います。
実はこの方法を紹介するかどうかは悩みどころでして、完全に安全になるならともかく、「これで全部おk」みたいに開発者が文字コードのセキュリティに無関心になる方法に働くといやだなと思うわけです。しかし、ネットには、既に根拠の不明確なバッドノウハウであふれているわけで、少しマシなバッドノウハウを流通させることで、改善になるのではないかと思い、公開に踏み切りました。しかし、専門家として、どこまで安全にできるかとか、安全になる根拠は示したいと思います。

文字コードをやや安全に扱うphp.ini設定

まずは結論を示します。php.iniに(あるいは.htaccessで)以下を設定します。
;; 出力バッファリングを無効にする (追記:文字エンコーディングの変換をしなければ、On でもいいです)
output_buffering      = Off

;; HTTPレスポンスの文字エンコーディングを設定
default_charset       = UTF-8

;; デフォルトの言語を日本語にする
mbstring.language = Japanese

;; HTTP 入力変換を有効にする
mbstring.encoding_translation = On

;; HTTP 入力エンコーディング変換を UTF-8 に設定(UTF-8→UTF-8の変換)
mbstring.http_input   = UTF-8

;; HTTPレスポンスは変換しない
mbstring.http_output  = pass

;; 内部エンコーディングを UTF-8 に設定
mbstring.internal_encoding = UTF-8    

;; 無効な文字は「?」に
mbstring.substitute_character = "?"
php.iniで指定できない条件として以下も必須です。
htmlspecialcharsの第3引数に必ず'UTF-8'を指定する

PHPのソースはUTF-8でセーブする

データベースの接続・テーブル・データベースの文字エンコーディングは全てUTF-8で統一する
この設定の要点を箇条書きで説明します。
  • Webアプリケーションの入口から出口まですべてUTF-8で統一する
  • 入力時にUTF-8→UTF-8の変換をすることで、不正な文字エンコーディングを除去する
  • HTTPレスポンスの文字エンコーディングをUTF-8と明示する
この設定でなぜ、どこまで安全になるか、PHPカンファレンスで実演した脆弱性デモとの対応で説明します。

文字コードをやや安全に扱うphp.ini設定はなぜ安全か

以下の説明は、先に紹介したプレゼン資料と対比させながらご覧ください。
「デモ1:半端な先行バイトによるXSS」への対応
半端な先行バイトはUTF-8でもあり得ますが、UTF-8→UTF-8変換の過程で半端な先行バイトは除去されます。
「デモ2:UTF-8非最短形式によるパストラバーサル」への対応
非最短形式のUTF-8もUTF-8→UTF-8変換の過程で除去されます
「デモ3:5C問題によるSQLインジェクション」への対応
UTF-8では原理的に5C問題は発生しません
「デモ4:UTF-7によるXSS」への対応
default_charsetをUTF-8に設定することで、HTTPレスポンスヘッダの文字エンコーディングが設定され、UTF-7によるXSSを防げます。
「デモ5:U+00A5によるSQLインジェクション」への対応
実はこれについてはphp.iniだけでは防げませんが、データベース側の設定もUTF-8に統一すれば防げます。
「デモ6:U+00A5によるXSS」への対応
アプリケーションの内部でUTF-8を一貫して使うことにより、文字集合の変更がなくなるので、U+00A5によるXSSは発生しません。

確認方法

PHPカンファレンスでも紹介した「尾骶骨テスト」や「つちよしテスト」が有効です。以下の文字を入力・登録して、どのように表示されるかを調べてください。



  • ¥(U+00A5) バックスラッシュに変換されないか
  • 骶(U+9AB6) JIS X 0208にない文字
  • 𠮷(U+20BB7) BMP外の文字 UTF-8では4バイトになる

  • このテストにより、文字集合の変更がどこかで起こってないかを確認することができます。データベースによってはBMP外の文字(U+10000以降の文字)に対応していないものもあるので、「つちよしテスト」を全てのアプリケーションがパスできるとは限りません。逆に、U+00A5がバックスラッシュ(0x5C)に化けるアプリケーションには潜在的な危険性があります。
     

    注意事項

    今回紹介したphp.ini設定は、UTF-8→UTF-8の変換以外はバッドノウハウでもなんでもなくて、ごくまともな設定です。問題は、UTF-8→UTF-8の変換に文字エンコーディングの妥当性チェックを頼っているので、サーバーの移設などでphp.iniの設定が変更され、文字エンコーディングの自動変換がされなくなった場合に危険になることです。php.iniが変更された可能性がある場合は、phpinfoなどにより、mbstring.encoding_translationがOnになっていることを確認してください。同様に、default_charsetがUTF-8になっていることを確認してください。

    こういう問題もあるので、本当はスクリプトで、レスポンスヘッダの文字エンコーディング指定や、文字エンコーディングのチェックをした方が頑丈なアプリケーションになります。新規開発の場合は、フレームワークの設定やカスタマイズなどで、これらの処理が行われることを確実にするとよいでしょう。

    また、講演中にhtmlspecialcharsの第3引数(文字エンコーディング指定)を必ず指定するように強調しましたが、それは今回紹介する方法でも変わりません。技術力のある方の中には、「でも入口で不正な文字エンコーディングが除去されているから、htmlspecialcharsの第3引数は指定しなくても一緒でしょ」と言う人もいると思いますが、以下のような実効性もあるのです。

    それは、mbstringではチェックしないが、htmlspecialcharsではチェックするという項目があるのです。UTF-8の5バイト、6バイトの形式です。これを確認するスクリプトを作りました。
    <?php
      $u8_6 = "\xFC\x84\x80\x80\x80\x80"; // 6バイト形式のUTF-8
      var_dump(mb_check_encoding($u8_6, 'UTF-8'));
      echo bin2hex(mb_convert_encoding($u8_6, 'UTF-8', 'UTF-8')) . "\n";
      var_dump(htmlspecialchars($u8_6, ENT_QUOTES, 'UTF-8'));
      echo bin2hex(htmlspecialchars($u8_6, ENT_QUOTES));
    
    【実行結果】
    bool(true)         # checkは通っている
    fc8480808080       # UTF-8→UTF-8の変換後もそのまま
    string(0) ""       # htmlspecialcharsにUTF-8を指定すると削除される
    fc8480808080       # htmlspecialcharsに文字エンコーディングを指定しないと、そのまま
    
    このように、mbstringではUTF-8の5、6バイトの表現を認めていますが、htmlspecialcharsを通すと、5バイト以上の表現は除去されます*1。IEやFirefoxなど、現在広く使用されているブラウザはUTF-8の5、6バイトの表現を認識しないと思いますが、万一、UTF-8の5、6バイトの表現を用いた攻撃手法が見つかっても、htmlspecialcharsが削除してくれると安心です。こういうこともあり得るので、htmlspecialcharsの第3引数は指定するようにしましょう。これも、フレームワークに組み込むか、ラッパー関数を用意しておけば簡便です。

    本当は、htmlspecialcharsのデフォルト文字エンコーディングがmbstring.internal_encodingになってくれると楽ちんなのですが、現在はISO-8859-1がデフォルトなので、こういう面倒なことになります。

    まとめ

    「文字コードをやや安全に扱うphp.ini設定」を紹介し、安全になる根拠および限界を説明しました。「これさえやっておけば、おk」となるのではなく、むしろ文字コードの問題に興味を持っていただくためのきっかけになれば幸いです。なお、文字コードの話は、SQLインジェクションとかXSSなど脆弱性対策をした上での話ですので、php.iniだけで脆弱性対策が不要になるわけではないので、念のため。

    *1 この問題については、id:t_komuraさんの素晴らしいブログエントリ「最新の PHP スナップショットでの htmlspecialchars()/htmlentities() の修正内容について」に詳しい分析があります

    2010年3月29日月曜日

    DNSリバインディングによる無線LANパスフレーズの読み出しに成功

    補足

    この記事は旧徳丸浩の日記からの転載です。元URLアーカイブはてなブックマーク1はてなブックマーク2
    備忘のため転載いたしますが、この記事は2010年03月29日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
    補足終わり

    DNSリバインディング攻撃により、無線LANアクセスポイント(AP)に設定したWPAのパスフレーズ(PSK)を読み出す実験に成功したので報告する。

    先のエントリではDNSリバインディング攻撃によるルータ設定変更の実験を報告したが、エントリでも触れたように設定変更についてはCSRF脆弱性を悪用することによっても可能だ*1。両手法の違いとして、CSRFは攻撃先の情報を読み出すことはできないのに対して、DNSリバインディングでは読み出しも可能だ。DNSリバインディングによるセッションCookieの読み出し例については、「ケータイtwitter(twtr.jp)においてDNS Rebinding攻撃に対する脆弱性を発見・通報し、即座に修正された」でも言及した。

    無線LANアクセスポイントに対するDNSリバインディングのリスクを考える上で、もちろん設定変更もリスクではあるが、設定変更された場合、元々の所有者が無線LANにアクセスできなくなるなどの副作用が想定される。一方、パスフレーズの読み出しが行えれば、被害者のユーザに気づかれないままに攻撃される可能性が高く、それだけリスクも大きくなる。

    また、Wi-Fiの普及に伴い、ゲーム機や携帯電話などでも広く無線LANが利用できるようになったことを受けて、無線LAN機器メーカー各社は無線LANの簡易設定方式を提唱・実現している。これはよいことだが、一方で、誰も無線LANの設定画面を見たこともなく、APの管理用パスワード(WEPやWPAのパスフレーズではない)がデフォルトのままになっているケースが大半であると予想される。これは危険な状態ではないか。

    AOSSによる簡単設定

    筆者が実験用に所有しているAPは、株式会社バッファローのWLA2-G54Cである。auの携帯電話biblioのWi-Fi機能を評価するために購入したものだ。バッファロー製APの特徴としてAOSSという簡単設定機能により、ボタン一つで無線LANの設定が可能である。その一端を以下に示そう。 APの設定でまず行うことは、APのIPアドレスの設定だ。ブラウザでもできなくはないが、APのユーティリティを使用すれば簡単だ。



    上図でパスワードを入力する欄があるが、これは現在の管理パスワードを入力する欄でデフォルトは空である。ここで管理パスワードを設定できるわけではない。その後、バッファローの接続ユーティリティからAOSSによるプロファイルの追加を指定すると以下のような画面となる。



    この状態でAPのAOSSボタンを長押しすると、APとクライアントのネゴシエーションが始まり、SSIDや暗号化の方法、パスフレーズなどが自動的に設定される。



    安全な設定が簡単に行えるのだが、この手順に従うと、APの管理画面を一度も起動することなく、またAPの管理パスワードを設定する機会もないのだ。AOSSはPC以外に、ゲーム機(ニンテンドーDSなど)や携帯電話でも利用できるもので、おそらくバッファロー製無線LAN製品のユーザの大半が、管理パスワードを設定しないままに使用していると予想される。

    APの管理画面からパスフレーズが見える

    この状態で、APの管理画面をブラウザから表示させる。無線LANのメニューを表示すると以下のような画面だ。



    これを見ていやな予感のしたあなたの勘は正しい。HTMLソースを表示させると以下のような部分がある(一部マスク表示にした)。
    <input name="wl_wpa_psk" type="password" maxlength="64" size="56"
    value="7c188cXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX67">
    すなわち、管理画面が起動できれば、パスフレーズを見ることができるのだ。実は、これはこれで便利な面もあって、AOSSで設定したパスフレーズを確認することにより、AOSS対応でないクライアントを手動設定できるのだが、そういう利用方法を意図したものとも思えない。ともかく、ここまで紹介した内容から以下のことが分かる。
    • AOSSによる設定では管理画面を見る必要がない
    • 管理画面のデフォルトパスワードは空である
    • 管理画面にログインすると無線LAN暗号化のパスフレーズが参照できる
    これらの事実から、バッファロー製AP WLA2-G54Cは、DNSリバインディング攻撃により無線LAN暗号化パスフレーズが読み出せると判断し、実験してみることにした。BCNの調査によると、バッファロー社は無線LAN分野の2009年のシェアが53.0%ということで、影響も大きいと思った。

    侵入実験の実際

    以下に、侵入実験に用いたスクリプトの一部を示す。ドメインは仮のものに置き換えた
    // リクエスタのセット
    var requester = new XMLHttpRequest();
    requester.open('GET', 'http://root:@wlan.example.com/advance/ad-lan-wireless_sec_g.htm');
    ...
    // パスフレーズの切り出し
    var response = requester.responseText;
    var regexp = /wl_wpa_psk.*value="([^"]+)"/m;
    if (response.search(regexp) != -1) {
        pass = RegExp.$1;
    }
    このスクリプトはインターネット上のワナサイトに置かれる。このコンテンツが読み出された直後に上記FQDNに対するIPアドレスをAPのプライベートアドレスに書き換える。コンテンツ読み出しから4分後に上記リクエストが送出されるようにスクリプトを作成した。実行例を以下に示す。



    図示したように、パスフレーズが読み出されていることが分かる。この後、ワナサイトにこの値をPOSTして、攻撃は完成である。

    対策

    バッファロー製AP WLA2-G54Cをマニュアルの指示通りに設定した状態において、DNSリバインディングにより暗号化パスフレーズ(PSK)を読み出すことに成功した。ユーザ側の対策としては、以下に尽きるだろう。
    • 管理パスワードを適切に設定する
    しかし、コンシューマ向け製品ということを考慮すると、現在の製品仕様のままでは全てのユーザに管理パスワードを徹底させることも難しいだろう。したがって、製品提供者側の対策が今後は重要となる。以下のような対策案が考えられる。
    • APの管理パスワードの初期値を機器毎に違うものにする(根本的対策)
    • 製品設定の流れの中で管理パスワードの変更を強制する(根本的対策)
    • 管理画面URLのホスト部は数値IPアドレスのみを許容する(保険的対策)

    まとめ

    バッファロー製AP WLA2-G54Cを対象として、DNSリバインディング攻撃により暗号化パスフレーズ(PSK)を外部から読み出せることを確認した。他社製品の場合は、パスフレーズが読み出せるかどうかは不明であるが、マニュアルを読む限りは管理パスワードの初期値が空で、変更も要求されないものが多いよう*2なので、仮に暗号化パスフレーズが読み出せないとしても、設定変更は可能である可能性が高い。この場合、APの設定を「暗号化なし」に設定変更することにより、内部ネットワークへの侵入や盗聴などが行える。

    無線LANの簡単設定機能は、AOSS以外にも「らくらく無線スタート」やWPSなどがある。これらは、安全な無線LAN設定を簡略化するという意味で社会的な意義のあるものだと思うが、基本的な想定として、「管理画面にはインターネットからアクセスできないので管理パスワードは設定しなくても大丈夫」と考えられているように見受ける。しかし、DNSリバインディングやクロスサイト・リクエスト・フォージェリ(CSRF)などの受動的攻撃により、インターネット越しに管理画面にアクセスすることは可能だ。ルータにCSRF脆弱性が多数指摘されてきた歴史は、「site:jvn.jp クロスサイト・リクエスト・フォージェリ ルータ」でGoogle検索してみると直ぐ分かる。DNSリバインディングについては、パスワードの設定さえすれば対策できるため、製品の脆弱性としては指摘しにくいが、今後十分発生し得る脅威として、メーカー側には対策を要望する。

    *1 ルータにCSRF脆弱性があればという前提だが
    *2 NECアクセステクニカ株式会社製のAtermシリーズは例外的にパスワードの変更が設定手順上で要求される

    2010年3月25日木曜日

    DNSリバインディングによるルータへの侵入実験

    補足

    この記事は旧徳丸浩の日記からの転載です。元URLアーカイブはてなブックマーク1はてなブックマーク2
    備忘のため転載いたしますが、この記事は2010年03月25日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
    補足終わり


    このエントリでは、DNSリバインディング攻撃によりブロードバンドルーターの設定を外部からの侵入を許すように変更してみたので報告する。

    DNSリバインディングに対する関心が高まってきている。本年1月12日には、読売新聞夕刊一面トップで、iモードブラウザ2.0のDNSリバインディングによる不正アクセスが報じられた。3月17日のcomputerworld.jpでは、『2010年に最も警戒すべきセキュリティ脅威は「DNSリバインディング」』という翻訳記事が紹介された。実は「最も警戒すべきセキュリティ脅威」というタイトルは誤報だったわけだが*1、DNSリバインディングというマニアックな攻撃手法が注目を浴びるきっかけにはなったと思う。

    そこで、専門家の方にはいまさらかもしれないが、DNSリバインディング攻撃により、自宅のブロードバンドルーターの設定を変更してみた。思いの外簡単にできたが、対策も容易であるので、その両方を説明しよう。

    侵入実験の前提

    まず一般的なDNSリバインディングの概要説明は、「DNS Rebinding」を参照頂きたい。この説明では、イントラネット内のサーバーへの侵入の例を挙げているが、今回の実験では、ブロードバンドルータへの侵入となる。実験に用いた自宅のルータは、プライベートアドレス(内部ネットワーク)からは設定が変更できるが、インターネット側からは設定できないという仕様となっている。そのため、DNSリバインディング攻撃が有効だ。この種のルータに対する攻撃手法としてはクロスサイト・リクエスト・フォージェリ(CSRF)という選択肢も考えられるが、このエントリでは言及しない。

    実験では、まず、ルータの設定変更に必要なHTTPリクエストを調査した。その結果、POSTリクエストを3回送信すれば、外部からの侵入を許すように変更できることがわかった。

    次に、インターネット上にワナサイトを用意して、ワナサイトをリクエストした後にDNSの内容を書き換え、一定時間置いた後に上記3つのPOSTリクエストを送出するようにした。実験に用いたブラウザはFirefox 3.6.2であり、最初のリクエストから4分間の間隔をとれば、DNSリバインディングが成立することがわかった*2。このため、3回のリクエストはそれぞれ、4分後、4分10秒後、4分20後に送出されるように、JavaScriptによるスクリプトを作成した。

    ここで問題になるのが、ルータの認証をどうやってすり抜けるかだ。他の攻撃手法、たとえばCSRFの場合は、ユーザが認証している状態を悪用するわけだが、DNSリバインディングではこの方法は使えない。なぜなら、DNSリバインディングによるリクエストのHost:フィールドはワナサイトのドメインになっているからだ。このため、実験では、攻撃者がルータのパスワードを推測できるという想定にした。以下の説明では、ユーザIDとパスワードがともに「admin」になっていると想定する。これはそれほど突飛な想定ではない。この種のルータの初期設定はパスワードなしか、固定の初期パスワードになっていることが多いからだ。

    侵入実験の実際

    下図に侵入前のルータの設定を示す。赤い四角で囲った部分に、新たな設定を追加する。



    次に、以下は侵入実験に用いたスクリプトのごく一部だ。このルータはBASIC認証が使われているので、以下のようにURLの中にBASIC認証のIDとパスワードを記述した。スクリプトキディが真似するとまずいので、スクリプトの全容は非公開とする。また、router.example.comはワナサイトのドメインだが、仮にものに置き換えた。
    var requester = new XMLHttpRequest();
    requester.open('POST', 'http://admin:admin@router.example.com/entry_ipnat_main_bottom.html');
    このスクリプトを含むワナが閲覧されたら直ぐに、router.example.comのIPアドレスを192.168.0.1(ルータのアドレス)に書き換える。その後3回のリクエストが正常にルータに送信されると、設定を反映するためにルータがリセットされた。攻撃成功である。
    攻撃成功後のルータの設定を下図に示す。



    ご覧のように、192.168.0.10の端末に対して、インターネットからTCP3389ポートを受け付けるようになった。3389はWindowsのリモートデスクトップが使用するポートなので、この設定変更により外部からの侵入が可能になった。

    対策

    ルータやファイアウォールの設定変更は、DNSリバインディングのデモとしては定番と言えるものだが、実際の結果を見るとギョッとした人も多いのではないか。しかし、幸いなことに、対策は容易である。前述のように、DNSリバインディング攻撃は、ユーザの認証状態を引き継ぐことができないので、認証がかかっているサイトの場合、パスワードが分からないと侵入できない。従って、良質のパスワードを設定するだけで、侵入を食い止めることができる。

    しかし、問題は、これだけブロードバンドのインターネットが普及した状況下で、ブロードバンドルータの多くがデフォルトパスワードのまま(パスワードなしも含めて)の設定と予想されることだ。従って、DNSリバインディングによる攻撃はいつ起こっても不思議ではないし、既に起こっているかもしれない。

    従来、ブロードバンドルータのパスワード初期設定がいい加減だった理由は、「外部からは設定変更できないから安全だ」という意識が働いていたからだと推測する。しかし、ルータに対する外部からの侵入手法は、DNSリバインディングの他、ルータのCSRF脆弱性を悪用するなど複数存在する。一方で、ブラウザ側でDNSリバインディングを完璧に対策するのは困難だ。その理由は、IPアドレスの変更は、DNSの正式な仕様として認められた挙動であるからだ。このため、ルータやファイアウォールの初期パスワードやパスワードの管理について、さらなる工夫が求められる。

    さらに網羅的なDNSリバインディング対策については、稿を改めて説明する。

    追記(2010/03/31)

    本エントリと関連して、「DNSリバインディングによる無線LANパスフレーズの読み出しに成功」を書きましたのであわせてお読みください。

    脚注
    *1 原題は「Top Ten Web Hacking Techniques」なので脅威という訳もおかしいが
    *2 4分間が長すぎると感じる人もいると思うが、この時間を短くするAnti-DNS Pinningという手法が存在するし、ワナサイト上でYoutubeの映像でも流しておけば4分くらいはワナサイト上にとどまるユーザもいるだろう

    フォロワー