Quantcast
Channel: 徳丸浩の日記
Viewing all 194 articles
Browse latest View live

東京の図書館で技術書を借りよう

$
0
0
こんにちは。東京都品川区在住の徳丸です。東京で消耗しながら生活しています。
都会での生活には、家賃が高いとか、通勤が地獄のようだ、などのデメリット(消耗)がありますが、一方メリットもたくさんあります。書籍が手に入りやすいこともその一つです。若い頃地方の工場(鹿児島県霧島市)でエンジニアとして生活していて痛感したことの一つに、
  • 田舎では技術書との出会いが不自由だ
ということがありました。なので東京出張の旅に大きな書店に出向いて技術書を買いあさっていました。書泉グランデにドラゴンブックの原書が平積みにされていたのを見たのは今から20年以上前のことですが、私はその衝撃を今でも生々しく覚えています。

実は東京は大きな書店があるというだけでなく、公共図書館に技術書が多く所蔵されていることをご存知でしょうか? かつて、図書館に技術書があると図書館に行きたくなくなるとおっしゃられた市長がおられましたが…
大体ね、公立の図書館は駄目なんですよ。
なんか技術とかあったり、哲学とかあったり、もうあれ見た瞬間に行きたくなくなるもんね。
【テキスト起こし】武雄新図書館構想発表記者会見 - Google ドキュメントから引用
個人の感想としてはそういう方もおられるでしょうが、エンジニアとしては、やはり技術書も所蔵してくれると助かります。

そこで、東京23区の図書館にどの程度技術書が蔵書されているかを数値的に示すために、「新春座談会 このコンピュータ書がすごい! 2016年版 -2015年に出たコンピュータ書ならこれを読め!-」のトップテンが、実際のどの程度蔵書されているかをカーリルで調べてみました。トップテンは下記の通りです。リンクは、それぞれの東京都の図書館での蔵書(カーリルによる)を示します。

結果は以下の通りです。蔵書の種類が多い順に並んでいます。

このコンピュータ書がすごい! 2016年版の蔵書冊数
書籍番号(1)(2)(3)(4)(5)(6)(7)(8)(9)(10)蔵書種類蔵書数
杉並区45113114111022
大田区1263352263942
文京区332121222918
葛飾区321221211915
中野区131212111913
練馬区78212211824
世田谷区59211111821
江東区43151312820
新宿区4312232717
品川区4311322716
港区3221311713
荒川区211111178
目黒区541123616
足立区441122614
豊島区222121610
千代田区12111167
北区33232513
江戸川区33231512
板橋区3121259
台東区2211157
渋谷区1212157
墨田区2111156
中央区3124

ご覧のように、杉並区の図書館は、トップテンすべてを所蔵しています。すごい! ワーストは中央区の2種類ですが、中央区以外の区立図書館は、5種類以上を蔵書していることがわかります。

東京在住・在勤でなくても利用できる図書館は多い

東京23区の図書館に技術書の蔵書が多いことを紹介しましたが、これら図書館の多くは、その区に在住・在勤でなくても利用できます。
たとえば、文京区、世田谷区、目黒区、品川区等の図書館は、在住・在勤地が「どこであっても」図書館カードを作成できます。気前がいいですね。
また、港区の図書館は、東京23区に在住あるいは在勤を証明するものがあれば、図書館カードを作成できます。
僕自身は、品川区、港区、文京区の図書館カードを持っていて、カーリルで検索して受け取りに一番便利な図書館でネット予約して借りています。書籍の受け取りは最寄りの図書館や地域センターを指定できるのでとても便利です。
読者のみなさまがどの図書館カードを作成できるかについては、こちらのサイトから簡単に調べることができます。

さて、技術者であれば技術書は図書館で借りずに購入すべきだという意見があると思いますが、私は必ずしもそうではないと思います。大きな書店に行かなくても、ネット予約した書籍を地域の図書館で受け取れるのはとても便利ですし、「書籍の内容を購入前に確認したい」、「書籍の悪評が本当か自分の目で確かめたい」、「書籍のサンプルコードに脆弱性がないか確かめたい」などは、エンジニアであれば当然の欲求ではないでしょうか。私自身、図書館で借りた後に「これは持っておくべき書籍だ」と思い購入した本は何冊もあります。

ということで、技術書に触れるための機会の一つとして、図書館も選択肢に加えてみてはいかがでしょうか?


【HASHコンサルティング広告】
HASHコンサルティング株式会社は、新しい技術に関心の高いセキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。


以下の書籍は東京の図書館の多くで借りることができます。

OWASPのSQLインジェクション対策方針を読んで「おまえは俺か」と思った

$
0
0
つい最近まで、グローバル・スタンダードのセキュリティ施策ではバリデーションが極めて重視されている、いささか過剰ではないかと思っていたのですが、OWASPの文書を読みなおしたところ、これは僕の思い過ごしだったかと思い始めました。あくまでOWASPに限った話ではありますが…
OWASP Top 10 2004については、以下のようなプレゼンをしたことがあります(2012年3月27日)。
OWASP Top 10 2004をはじめとして、バリデーションが過剰に重視されているのではないかという指摘でした。
しかし、最近OWASPの文書を読みなおしてみると、OWASP Top 10 2004当時にあった「バリデーション至上主義」のようなものはすっかり影を潜め、私が(そして日本の専門家の多くが)言っていることとほとんど変わらないことに気が付きました。以下、SQLインジェクション対策に焦点を絞り、以下の3種類の文書を元に説明します。
  • OWASP Top 10
  • SQL Injection Prevention Cheat Sheet
  • OWASP Top 10 Proactive Controls

OWASP Top 10

OWASPのドキュメントでももっとも有名なものがOWASP Top 10ですが、副題としてThe Ten Most Critical Web Application Security Risksとなっているように、ウェブアプリケーションにおいても重大なセキュリティリスクのトップ10を集めたものです。その最初の版である2004年版では、筆頭に「Unvalidated Input(検証されていない入力)」がありました。その後2007、2010、2013と改版されており、2007以降ではUnvalidated Inputはトップテンからは消えています。2010以降の第1位は、Injection(SQLインジェクション、OSコマンドインジェクション等)となっています。

そこまでは認識していたのですが、2013年版のInjectionの解説では防御方法が以下のように説明されています。実は2010年版もほぼ同じです。
防止方法
インジェクション攻撃を防止するには、コマンドとクエリから信頼出来ないデータを常に区別することが必要です。

1. 推奨されるオプションは、インタープリタを全く用いない安全なAPIを利用するか、パラメータ化されたインターフェースを用いる事です。ただ、ストアードプロシージャなどの、パラメータ化していてもインジェクション攻撃が可能なAPIには注意して下さい。

2. パラメータ化されたAPIが利用出来ない場合、インタプリタにて定められたエスケープ構文を用いて特殊文字のエスケープ処理を慎重に実施すべきです。OWASP's ESAPIはこれらの定番のエスケープを多く提供します。

3. 多くのアプリケーションが特殊文字の入力を必要とするため、“ホワイトリスト”による入力検証も推奨しますが、完全な防御ではありません。特殊文字が必要な場合、上述の1.と2.を用いることで、安全に使用出来ます。OWASP's ESAPIは、ホワイトリストの入力検証ルーチンの拡張可能なライブラリがあります。

https://www.owasp.org/images/7/79/OWASP_Top_10_2013_JPN.pdfより引用
1はプレースホルダを使えということですね。「インタプリタを全く用いない」とは、静的プレースホルダを指すのでしょう。動的プレースホルダ(俗にいうクライアントサイド・プリペアードステートメント)は、呼び出し側でSQLを構文解析するので、インタプリタを用いることになります。
また、「ホワイトリスト」の定義が曖昧ですが、「特殊文字が必要な場合」とはアプリケーション仕様として特殊文字を許容する場合という意味でしょうから、ホワイストリストも「アプリケーションの仕様に従って」という意味だと考えます。
つまり、以下の様になりますね。
  • 根本的対策としてはプレースホルダの使用か記号文字のエスケープである
  • 可能な限りプレースホルダの使用を優先すること
  • アプリケーションの仕様にしたがって入力値検証することを推奨するが、完全な防御にはならない
これなら概要としてはまったく同意ですので、目くじらを立てる必要もないのでした。

SQL Injection Prevention Cheat Sheet

SQLインジェクション防御のチートシート(カンニングペーパー)というタイトルのドキュメントです(英語版)。こちらについては、OWASP名義で下記の記事を投稿しましたのでご笑覧ください。
肝心の防御方法については以下のように書かれています。
  • Defense Option 1: Prepared Statements (with Parameterized Queries)
  • Defense Option 2: Stored Procedures
  • Defense Option 3: Escaping All User Supplied Input
ストアド・プロシージャについては前述の私の記事でマイルドにツッコミを入れていますので参考にしてください。また、前述のOWASP Top 10でもストアド・プロシージャに対するツッコミが入っていますね。このツッコミに私は同意します。

また、本稿と関連するトピックスについては下記のように書かれています。
まずエスケープに関して。

Defense Option 3: Escaping All User Supplied Input

This second technique is to escape user input before putting it in a query. However, this methodology is frail compared to using parameterized queries and we cannot guarantee it will prevent all SQL Injection in all situations. This technique should only be used, with caution, to retrofit legacy code in a cost effective way. Applications built from scratch, or applications requiring low risk tolerance should be built or re-written using parameterized queries.

【参考訳】この第2の手法は、ユーザ入力をクエリ内に入れる前にエスケープすることです。しかし、この方法論はパラメータ化クエリ(訳注:プレースホルダを用いたSQL呼び出しのこと)を用いる場合と比較して弱く、我々はそれがすべての状況ですべてのSQLインジェクションを防げると保証することができません。この手法は、既存のコードを費用対効果がよい方法で改修する場合に限り、用心深く用いられるべきです。スクラッチから構築するアプリケーションや、リスク許容度の低いアプリケーションは、パラメータ化クエリを用いて構築あるいは書き換えされるべきです。
バリデーションについて。

3 Additional Defenses

3.2 White List Input Validation
Input validation can be used to detect unauthorized input before it is passed to the SQL query. For more information please see the Input Validation Cheat Sheet. Proceed with caution here. Validated data is not necessarily safe to insert into SQL queries via string building.

【参考訳】無許可の入力がSQLクエリに渡される前に検知するために入力バリデーションを用いることができます。詳細は、入力値検証チートシートを参照ください。以下に注意して下さい。バリデーションされたデータは、文字列組み立てを通じてSQLクエリに挿入する上では必ずしも安全ではありません
方向性としては前項のOWASP Top 10と同じですが、エスケープについては、より強く使わないことを推奨しています。引用した内容について、私は完全に同意します。

OWASP Top 10 Proactive Controls

こちらは、「Proactive Controls」ですから、事前の対策についてまとめられたものですね(英語版日本語版)。先ほどのSQL Injection Prevention Cheat Sheetよりも全体的な話題となります。トップテンは下記の通りです。
  1. 早期に、繰り返しセキュリティを検証する
  2. クエリーのパラメータ化
  3. データのエンコーディング
  4. すべての入力値を検証する
  5. アイデンティティと認証管理の実装
  6. 適切なアクセス制御の実装
  7. データの保護
  8. ロギングと侵入検知の実装
  9. セキュリティフレームワークやライブラリの活用
  10. エラー処理と例外処理
1の「セキュリティを検証」とは、脆弱性の検査を開発プロセスの中で繰り返し行えということですね。
問題は2から4です。SQLインジェクションについては、2のクエリーのパラメータ化、すなわちプレースホルダを使えと書かれています。
SQLインジェクションを防ぐには、信頼できない入力値がSQLコマンドの一部として解釈されるのを避ける必要があります。最も良い方法は「クエリーのパラメータ化」と呼ばれる実装方法です。この方法では、SQLの問い合わせ構文とパラメータは、それぞれ別々にデータベースサーバーに送信され、データベース上で解析されます
「SQLの問い合わせ構文とパラメータは、それぞれ別々にデータベースサーバーに送信され」の部分はわかりにくいですが、これは静的プレースホルダの性質を説明しています。詳しくは「安全なSQLの呼び出し方」の解説を参照下さい。つまり、OWASP Top 10とは別の表現で、動的プレースホルダではなく静的プレースホルダを使えと推奨しているのです。

3の「テータのエンコーディング」とはエスケープ処理のことですが、この節では主にXSSを例示に用いています。SQLインジェクションも同じ方法で対策することは可能ですが、本文中ではエスケープによるSQLインジェクション対策については触れられていません。

4の「すべての入力値を検証する」については面白いことが書いてありました。

入力チェックとセキュリティに関する補足

入力チェックの段階では、信頼できない入力値を「無害な状態に」変換してしまう必要はありません。危険と思われるデータも「正しいデータ」として受け入れなければならない場合があります。潜在的に危険な文字も「正しいデータ」として受け入れなければならない場合があるため、入力検証によって信頼できない入力が無害になるとは限りません。アプリケーションのセキュリティは、入力値が実際に使われる箇所で担保されるべきです。たとえば、入力値をHTMLの一部として出力するのであれば、クロスサイトスクリプティング対策としてHTMLエンコーディングを実装します。同様に、入力値をSQL文の一部として使うのであれば、クエリーのパラメータ化を使います。どのような場合であれ、セキュリティ対策を入力チェックに依存してはいけません
『「無害な状態に」変換してしまう必要は』ないというのは、サニタイズは無用だということでしょうね

(2016/3/7 14:00追記)
奥さん(@kazuho)から指摘をいただきました。Input validation does not necessarily make untrusted input “safe” since it may be necessary to accept potentially dangerous characters as valid input. の訳として、『信頼できない入力値を「無害な状態に」変換』は誤訳であり、「入力値検証によって、信頼できない入力値が無害なものになるとは限りません」くらいの意味であるということです。確かにそうですね。引用した訳はOWASPジャパン有志による翻訳ですが、ここは重要な箇所なので私訳に差し替えました。
(追記終わり)


そして、「アプリケーションのセキュリティは、入力値が実際に使われる箇所で担保されるべきです」という文句がいいですねぇ。「おまえは俺か」と思いましたよ。例えば、次の記事で書いたようなことですね。
この記事で私は以下のように書きました。
これは、入口でのチェックだと漏れやすいから、脆弱性が発生するその箇所で対策するという考え方にシフトしているのだと私は考えます。
ここで念押ししておかなければなりませんが、「入力チェックをしなくていい」、なんて誰も言ってないですからね。入力チェックはするべきですが、SQLインジェクション対策という文脈では、入力チェックをあてにしてはいけない、ということです。バリデーションしていれば防げたのに、という脆弱性はたくさんあります。その辺については以下の資料にまとめています。

まとめ

OWASPの最近の主要ドキュメントを読む限り、SQLインジェクション対策の方法論について日米(日本とグローバル)の差はほとんどないことが分かりました。まとめると以下のようになります。
  • とにかくプレースホルダを使う
  • どーーしてもプレースホルダを使えないとか、既存アプリの脆弱性対処を素早く行う必要がある場合に限りエスケープ手法を用いる
  • バリデーションはすべきだが完全ではないので保険的に用いること
私のブログの読者にとっては目新しいことは何もないと思いますが、私は私なりに、グローバル・スタンダードとしての脆弱性対策と日本のそれに差異があることを気にしていたのです。「日本独自の脆弱性対策手法」なんていうと、何かガラパゴス的なものを連想するではありませんか。しかし、こうして米国側から「歩み寄って」下さったおかげで、もはやガラパゴスなんて心配は無用であり、私はいい気持ちになりました。


【HASHコンサルティング広告】
HASHコンサルティング株式会社は、SQLインジェクションの正しい対策方法に関心のあるセキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。


決済代行を使っていてもクレジットカード情報が漏洩するフォーム改ざんに注意

$
0
0
先日以下の記事が公開されました。決済代行会社を使っていたのにカード情報が漏洩したというものです。
同社は、薬局への医薬品の卸売りのほか、運営するショッピングサイト「eキレイネット」でコラーゲンやヒアルロン酸などの美容関連製品を販売している。流出した疑いがあるのは、平成26年10月8日~27年11月5日、サイトでカードを使って商品を購入した顧客の氏名や住所、クレジットカードなどの情報だった。この間、1955人が利用していた。
 名の売れた大企業ではない。従業員わずか10人の小さな会社がサイバー攻撃の標的になったのだ。
 問題が発覚したのは昨年11月。決済代行会社からカード情報が流出した疑いがあると指摘があった。
従業員10人なのに「標的」に サイバー攻撃、中小企業が狙われる理由より引用
これに対して、以下のブックマークコメントがつきました。
そもそも、決済代行会社を使っているのになぜカード情報が漏れる?普通は自分たちの側には一切カード情報を残さないものだが。getで送るとうっかりアクセスログに残る、とかの例はあるけど。
はてなブックマークより引用
実は2013年以降、決済代行会社を使っているのにカード情報が漏洩する事件が続いています。その場合の漏洩経路には以下の二種類が知られています。
  • 決済代行を使っているのに、カード情報を自社サイトでも保存していた
  • カード情報の入力フォームを改ざんされ、別サイトにカード情報を転送された
前者の例としては、以下の記事で紹介しました。
しかし、実際の事件を調べてみると、多いのは後者の経路です。このパターンとして最初の、そしてもっとも有名な事例は、JINSオンラインショップからのカード情報漏洩でしょう。
ジェイアイエヌでは、クレジット情報漏えいの専門調査機関であるPayment Card Forensics株式会社(PCN)に調査を依頼。4月8日に受領した報告書によれば、3月6日にサーバーにバックドアプログラムが設置され、第三者のデータベースにカード情報が転送されるようにアプリケーションのプログラムが改ざんされていたことが判明した。
JINS、不正アクセスによるカード情報流出は最大2059人、当初発表下回るより引用
セキュリティコードをはじめクレジットカード情報は、弊社では保管しておりません。
保管していない情報が流出した理由につきましては、オンラインショップの支払方法入力画面に改ざんが加えられ、入力したクレジットカード情報が不正に外部のサーバに送信されるよう改ざんされたためであります。
よくあるご質問 | JINS - 眼鏡(メガネ・めがね)より引用
入力フォームが改ざんされて、入力内容が外部に送信されるような仕掛けが組み込まれたということですね。恐らく、JavaScriptが追加されて、フォームの入力内容を外部に送信するように、Webビーコンのように動的にIMGタグを生成するとか、XMLHttpRequestオブジェクトによりフォームの内容を送信する仕組みが組み込まれたのでしょうね。

では、JINSオンラインショップの事件以降、同種の事件がどの程度発生しているかを調べてみたところ、以下の様に継続的に、フォーム改ざんが攻撃経路と思われるカード情報漏洩事件が発生しています。

サイト名漏洩期間漏洩件数セキュリティコード決済代行
JINS オンラインショップ2013/3/6~2013/3/142,059漏洩使用
光文社が運営する3サイト2013/12/29~2014/1/211,160漏洩
ホビーショップタム・タム2014/5/3~2014/6/20923漏洩使用
eキレイネット2014/10/8~2015/11/5不明漏洩使用
ONYONEベースボールギア2014/11/1~2015/2/2772漏洩使用
中村屋2014/11/8~2014/12/241,422漏洩
エアコンの森Plus2015/1/1~2015/7/1712 使用
プリマージュオンラインショップ2015/4/1~2015/7/22563漏洩使用
DiXiM Store2015/8/1~2015/9/11480漏洩使用
ブルーラグオンラインストア2015/8/1~2015/8/1645 使用
シネマイクスピアリ2015/10/17~2015/10/301,414漏洩使用

特徴としては、下記の三点です。
  • 漏洩の対象が特定期間に取引した利用者に限られ漏洩件数は比較的少ない
  • セキュリティコードが漏洩するケースが多い
  • 決済代行を利用していても漏洩に至っている(決済代行業者に落ち度はない)
ということで、決済代行を使っているからカード情報は漏れないと油断していると危険です。
対策としては、JINSオンラインショップの下記が参考になります。
画面遷移型のクレジットカード情報非保持サービスの採用
このサービスを採用することにより、オンラインショップの購入画面で決済方法としてクレジットカード決済を選択した場合に、決済代行会社が管理するウェブサイトへ画面が遷移することで、購入者のクレジットカード情報が当社サーバ等のシステムを一切通過しないこととなり、当社システムからの情報漏えいの可能性が排除されることとなります。
なお、上記再発防止策に対しては、本調査委員会より、非常に高い情報セキュリティレベルが実現可能であり、セキュアなクレジット決済を行うことが可能になる施策であると評価されております。
不正アクセス(JINSオンラインショップ)に関する調査結果(最終報告)より引用
ちょっと上記だけだとわかりにくいですが、決済代行サービスには大別して、データ伝送(API)型と画面遷移型があり、JINSオンラインショップは元々データ伝送型を採用していたが、画面遷移型に切り替えるということですね。これですと、クレジットカード情報の入力フォームは決済代行業者が提供するものになり、ECサイト事業者は関与を切り離すことができます。リスクの移転というやつですね。

また、一般的な脆弱性管理やWAFの導入などに加えて、改ざん検知システムの導入も有効です。Webページの改ざんは、中々人手では検知が難しく、上記に紹介したサイトの事例でも、大半がカード事業者か決済代行業者からの連絡で事件が発覚しています。改ざん検知システムによりページ改ざんをすばやく検知できれば、被害を小さくできることが期待されます。


【HASHコンサルティング広告】
HASHコンサルティング株式会社は、ウェブサイトの保護に関心のあるセキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。

ウェブアプリケーションにおいて「ホワイトリスト」と"White List"は用法が異なる

$
0
0
海外(主に米国)のウェブアプリケーションセキュリティのドキュメントを読むと、"white list input validation"という言い方がたびたび出てきます。たとえば、OWASPのSQL Injection Prevention Cheat Sheetには、まさにWhite List Input Validationという節があります。
3.2 White List Input Validation
Input validation can be used to detect unauthorized input before it is passed to the SQL query. For more information please see the Input Validation Cheat Sheet.

【私訳】
3.2 ホワイトリスト入力値検証
SQLクエリに渡される前に無許可の入力値を検知するために入力バリデーションを用いることができます。詳細は、入力値検証チートシートを参照ください。
しかし、長年このwhite listの意味が私には謎でした。というのは、引用した文に続いて、以下の文があるからです。
Validated data is not necessarily safe to insert into SQL queries via string building.

【私訳】
バリデーションされたデータは、文字列組み立てを通じてSQLクエリに挿入する上では必ずしも安全ではありません
ホワイトリストで検証した値が安全ではない、ですと?
しかし、このような文章は珍しくありません。以下は、OWASP Top 10 2016からA1-Injectionの解説です。
3. Positive or “white list” input validation is also recommended, but is not a complete defense as many applications require special characters in their input. If special characters are required, only approaches 1. and 2. above will make their use safe.

【私訳】 3.ポジティブ(いわゆる「ホワイトリスト」)入力値検証も推奨されますが、 多くのアプリケーションが特殊文字の入力を必要とするため、完全な防御ではありません。特殊文字が要求される場合、上述の1.と2.(訳注: 安全なAPIの使用とエスケープ処理)のみにより、特殊文字の使用が安全になります。
このような用例から、私は英語圏のウェブアプリケーションセキュリティに関する文書では、white listの定義は「アプリケーションが許可した入力値」、もっと言えば「アプリケーションの入力値に対する仕様」であると解釈するしかないと考えるに至りました。このあたり、海外のドキュメントに対する知識が豊富というわけではないので、私の理解が間違いであればご指摘下さい。

しかし、この定義は私の「ホワイトリスト」の語感とは異なります。ホワイトリストというからには、

ホワイト = 安全
リスト = 列挙されたもの

であるはずであり、「安全な値の列挙」がホワイトリストの定義だと思うからです。そして、用語辞典でのホワイトリストの定義もそのようになっています。
ホワイトリストとは、警戒する必要のない対象の一覧表のこと。対義語はブラックリスト。
ホワイトリスト - Wikipediaより引用
ホワイトリストとは、対象を選別して受け入れたり拒絶したりする仕組みの一つで、受け入れる対象を列挙した目録を作り、そこに載っていないものは拒絶する方式。また、そのような目録のこと。対義語は「ブラックリスト」(black list)で、目録に載っているものだけを拒絶し、それ以外は受け入れる方式である。
ホワイトリストとは|ホワイトリスティング|white list|WL - 意味/定義 : IT用語辞典から引用
私の語感や用語集での意味は上記の通りだとしても、日本のウェブアプリケーション開発者の語感はどうだろう、それを知りたいと思うようになりました。そこで、簡易な調査ではありますが、Twitter上で以下の選択肢によるホワイトリストに対するアンケートを実施しました。
  • 許可された入力値をリストとして列挙したもの
  • 入力値が安全になるように文字種等を制限したもの
  • 入力値に対するアプリケーションの仕様
  • その他(よろしければメンションで教えて下さい)
実に580名もの方にご協力いただき、ありがとうございました。結果は下記の通りで、「許可された入力値をリストとして列挙したもの」が84%と大半を占める結果とりました。

ということで、私の語感は、日本の多くのウェブアプリケーション開発者と共通しているようだと考えました。

このエントリの結論は下記のとおりです。
  • ウェブアプリケーションの分野で、日本語の「ホワイトリスト」と英語の"White List"では、用法に違いが見られる
  • ホワイトリスト=アプリケーション仕様という意味だと、どんな場合にでも使えるが、必ずしも安全ではない
  • ホワイトリスト=許可リストという意味だと、使える局面は限定されるか、安全なものとして扱える
  • 両者の意味を混同することは危険である
  • 「ホワイトリスト」という用語が出てきたら、警戒心をもって、どのような意味かを深読みしよう

参考: 僕が「ホワイトリスト」を採用しなかった訳


【HASHコンサルティング広告】
HASHコンサルティング株式会社は、ウェブアプリケーションのセキュリティに関心のあるセキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。

StartSSLにドメイン認証不備の脆弱性

$
0
0
Osama almanna's blogにて、StartSSLにドメイン認証に脆弱性があったと報告されています。
In 9 March, 2016 During my research I was able to replicate the attack and issue valid certificates without verifying the ownership of the website which I will explain later in my post, the vulnerability was reported and fixed within hours. 
ウェブサイトの所有権を検証しないで、正当な証明書が交付されるというものですね。脆弱性は報告の後数時間で修正されたとのことです。

以下、彼のブログ記事を元に、脆弱性の内容と修正方法について説明します。

問題の説明

StartSSLのドメイン認証証明書は無料で交付されるため、私も実験用サイトなどで利用しており、アカウントを既にもっています。以下、tokumaru.orgというドメイン名の認証を試みます。
以下は、Validation WizardからDomain Validationを選択して、ドメイン名(tokumaru.org)を入力しているところです。

その後サイト側でwhois情報を参照(以前はこのステップはなかったと記憶していますが)し、whoisに登録されたメールアドレスその他、認証に用いるメールアドレスを返します。
POST /Validate/GetWhois HTTP/1.1   ← whois情報を得るためのリクエスト


domainName=tokumaru.org&isNewWhois=1

HTTP/1.1 200 OK ← whois情報その他からのメールアドレス一覧
Content-Type: application/json; charset=utf-8
Content-Length: 105
...

"0|proxy@whoisprotectservice.com|postmaster@tokumaru.org|hostmaster@tokumaru.org|webmaster@tokumaru.org|"
上記のレスポンスは、ドメイン名の所有者であることを確認するためのメールアドレスの一覧ですね。これを元に、画面は以下のように変わります。


ラジオボタンは動的に生成されていますが、DOMの内容を見ると、以下の内容になっています。
<input name="ValidateEmails" type="radio" checked="checked" value="proxy@whoisprotectservice.com">proxy@whoisprotectservice.com
<input name="ValidateEmails" type="radio" value="postmaster@tokumaru.org">postmaster@tokumaru.org
<input name="ValidateEmails" type="radio" value="hostmaster@tokumaru.org">hostmaster@tokumaru.org
<input name="ValidateEmails" type="radio" value="webmaster@tokumaru.org">webmaster@tokumaru.org
ここで「Send Verification Code」ボタンを押すと、以下のリクエストがAJAXで送信されます。
POST /Validate/SendDomainVerifyEmail HTTP/1.1               ← 認証コードを送信するリクエスト
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-Length: 69
Cookie: fid=DE64D0FE74B0…

domainName=tokumaru.org&sendToEmail=postmaster%40tokumaru.org&index=0

HTTP/1.1 200 OK ← レスポンス
Content-Type: application/json; charset=utf-8
Content-Length: 12

{"status":1}
赤字で示したpostmaster%40tokumaru.orgに検証コードを送り、それを受信できることでドメイン名の正当な所有者であることを検証するという、ドメイン認証証明書の検証方法としてよくある方法です。ここで指定したメールアドレスに以下のようなメールが届きます。

このメール中のverification codeを先の画面上で入力してValidationボタンを押せば、ドメイン認証は完了です。

ここで問題は、さきほど赤字で示したメールアドレスを別のものに差し替えてもそのまま動いてしまうことでした。例えば、cracker@gmail.comのようなメールアドレスに差し替えても、そこにverification codeが送信され、tokumaru.orgのドメイン名の認証が通ってしまうということです。これはまずい…

どう修正すべきか

Osama almanna's blogでは、わざわざ古いOWASP Top 10 2004を参照して、これはinvalidated input vulnerability(検証されていない入力値の脆弱性)と指摘しています。確かに、正しいメールアドレスのリスト(ホワイトリスト)はサイト側は分かっているので、これと比較検証することで、正しいメールアドレスであることは確認できます。
しかし、メールアドレスは4つの選択肢から選ぶわけなので、フルのメールアドレスは表示のためだけに用い、ラジオボタンの属性値は1から4の数字にしてしまえば、仮に検証で多少ミスをしたとしても、まったく関係のないメールアドレスを指定できてしまうことはなかったはずです。つまり、メールアドレスの選択肢はユーザ入力ではなく、システム側で生成した値なのに、それを文字列として受け取ることが問題と言えます。
まとめると、以下のようになります。
  • 利用者本人からも改変されると困る値はtype=hiddenのinput要素やラジオボタン、セレクト要素などで受け渡しせずに、セッション変数を用いるべし
  • 上記で複数の値から利用者に選択させる場合は、選択肢の中身はセッション変数におき、数字等で選択の指定をさせるとよい


【HASHコンサルティング広告】
HASHコンサルティング株式会社は、ウェブアプリケーションのセキュリティに関心のあるセキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。


hiddenなinput要素のXSSでJavaScript実行

$
0
0
脆弱性診断をやっていると、たまにtype=hiddenのinput要素にXSSがあるけど、現実的な攻撃には至らないものにぶちあたることがあります。サンプルコードを以下に示します。
<body>
入力確認をお願いします。
<?php echo htmlspecialchars($_GET['t']); ?><br>
<form action='submit.php'>
<input type='hidden' name='t' value='<?php
echo htmlspecialchars($_GET['t']); ?>'>
<input type='submit'>
</body>
正常系の呼び出しは下記のようになります。

http://example/hidden-xss.php?t=yamada


HTMLソースは下記の通りです。
<body>
入力確認をお願いします。
yamada<br>
<form action='submit.php'>
<input type='hidden' name='t' value='yamada'>
<input type='submit'>
</body>
このスクリプトの何が悪いかというと、属性値をシングルクォートで囲っているのに、htmlspecialcharsのENT_QUOTESオプションを指定していないために、シングルクォートがエスケープされないところにあります。しかし、現実的な攻撃は難しいとされていました。

'><script>alert(1)</script>を指定すると、以下のHTMLが生成されますが、JavaScriptは実行されません。
<input type='hidden' name='t' value=''&gt;&lt;script&gt;alert(1)&lt;/script&gt;'>
' onmouseover='alert(1) を指定する方法はどうか。以下のように、onmouseover属性は作れますが、type=hiddenの場合、マウスカーソルを合わせることができずイベントも発生しません。
<input type='hidden' name='t' value='' onmouseover='alert(1)'>
ところが、malaさんのツイートで知りましたが、PortSwigger Web Security Blogに以下のPoCが発表されていました
<input type="hidden" accesskey="X" onclick="alert(1)">
上記のタグをXSSで生成させると、下記の条件でJavaScriptが実行されます。

  • 被害者ユーザがFirefoxを使っている かつ
  • 被害者がSHIFT+ALT+X キーを押す

これを応用して、前記のサンプルコードを攻撃してみましょう。

http://example/hidden-xss.php?t='+accesskey%3d'X'+onclick%3d'alert(1)

HTMLソースは下記となります。
<body>
入力確認をお願いします。
' accesskey='X' onclick='alert(1)<br>
<form action='submit.php'>
<input type='hidden' name='t' value=''accesskey='X' onclick='alert(1)'>
<input type='submit'>
</body>
ブラウザ側で SHIFT+ALT+X を押すと、下記のようにJavaScriptが実行されます。


ということで、type=hiddenなinput要素に閉じたXSSであっても、被害者がFirefoxを使っている場合、JavaScriptを起動できる場合があることが分かりました。

問題は、被害者にどうやって SHIFT+ALT+X を押させるかですが、以下のようにiframeを使う手があります。攻撃対象サイトは半透明にしていますが、実際の攻撃では透明にするなど、工夫の余地があります。


被害者が罠の誘導にだまされて SHIFT+ALT+X を押してしまうと、下記のようにJavaScriptが動きます。


まとめ

type=hiddenなinput要素に閉じたXSSでは、従来現実的な攻撃は難しいと思われていた(要出典)と考えますが、accesskeyとユーザーへの誘導により、JavaScriptを実行できる場合があることが分かりました。
脆弱性診断の実務では、従来でもこのような「エスケープ漏れ」に対しては指摘は行っていたと思いますが、その危険度の判定が変わる可能性があります。具体的には、元々「Information(念のためお知らせ)」としていた場合は、「Low(低)」くらいが妥当ではないでしょうか。元々Lowでつけていた場合は、Lowのままでもよいかと思いますが、現実的なリスクは変わることになります。

アプリケーション開発の立場においては、現実的な攻撃の可能性にまどわされないで、エスケープすべきものは淡々と正しくエスケープするようにしておけば、この手の「新たな攻撃経路」に右往左往する必要はありません。


【HASHコンサルティング広告】
HASHコンサルティング株式会社は、ウェブアプリケーションのセキュリティに関心のあるセキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。

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

$
0
0
先日PHPカンファレンス北海道2016にて「『例えば、PHPを避ける』以降PHPはどれだけ安全になったか」と題して基調講演を担当致しました。その際のスライドはこちら

そうしたところ、以下のご指摘をいただきました。
39番目のスライドは下記ですね。intへのキャストは下から3行目の (int) $id を指します。



SQLデータベースは、int型よりも大きな桁数を扱える場合があるので、intへのキャストを避けた方がよいという指摘は一般論としてはもっともなものだと考えます。PHPの場合、9223372036854775807を越える数字文字列をint型にキャストすると、9223372036854775807が返ります(64ビット環境の場合)。これを考慮していない場合、悪用される可能性はあります。
$ php -r "var_dump((int)'999999999999999999999999999999');"
int(9223372036854775807)
それにも関わらず、MySQLとPDOの組み合わせの場合、int型へのキャストが望ましい状況があります。その理由を説明します。

PoC

PDOのプレースホルダの挙動について、以下のサンプル(PoC)で紹介します。

テーブル定義とデータ
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サンプル
<?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");
$id = '18015376320243461';
$ps->bindValue(':id', $id, PDO::PARAM_INT); // intへのキャストはしない
$ps->execute();
$row = $ps->fetch();
echo "$id -> ${row[0]}\n";
$db = null;
このスクリプトはテーブル xdecimal からid=18015376320243461を検索して表示します。

生成されるSQL文

上記スクリプトのプレースホルダにより生成されるSQL文は下記のとおりです。
SELECT id FROM xdecimal WHERE id='18015376320243461'
ポイントは、PDO::PARAM_INTと整数型を指定しているのに、文字列リテラル(赤字部分)として値が生成されているところです。

MySQLと暗黙の型変換の問題

ここで問題は、列idの型がDECIMAL(20)という数値型なのに、文字列型の値と比較しているところです。MySQLは、この場合、両者を浮動小数点型に変換してから比較します。以下は、MySQL 5.6のリファレンスマニュアルから該当部分の引用です。
次のルールでは、比較演算の際にどのように変換が発生するのかについて説明します。
  • NULL-safe <=> 等価比較演算子の場合を除いて、一方または両方の引数が NULL の場合は、比較の結果も NULL になります。NULL <=> NULL の場合は、結果が true になります。変換は必要ありません。
  • 比較演算の両方の引数が文字列の場合は、文字列として比較されます。
  • 両方の引数が整数の場合は、整数として比較されます。
  • 16 進値が数字と比較されない場合は、バイナリ文字列として処理されます。
  •  引数の一方が TIMESTAMP または DATETIME カラムで他方が定数の場合は、比較が実行される前に定数がタイムスタンプに変換されます。これは、ODBC により適合させるために実行されます。これは、IN() への引数には実行されません。念のため、比較を行う際は、常に完全な日付時間、日付、または時間文字列を使用してください。たとえば、日付または時間の値とともに BETWEEN を使用したときの結果を最適にするには、CAST() を使用して、明示的に値を目的のデータ型に変換します。
  • テーブル (複数可) からの単一行のサブクエリーは、定数とみなされません。たとえば、サブクエリーで DATETIME 値と比較される整数が返される場合は、比較が 2 つの整数として実行されます。整数は時間値には変換されません。オペランドを DATETIME 値として比較するには、CAST() を使用して、明示的にサブクエリーの値を DATETIME に変換します。
  • 引数のいずれかが 10 進値の場合、比較はその他の引数に依存します。その他の引数が 10 進値または整数値の場合、引数は 10 進値として比較され、その他の引数が浮動小数点値の場合、引数は浮動小数点値として比較されます。
  • ほかのすべてのケースでは、引数は浮動小数点 (実) 数として比較されます
12.2 式評価での型変換(MySQL 5.6 リファレンスマニュアル)より引用
すなわち、列 id と、文字列リテラル'18015376320243461'の双方を浮動小数点数に変換してから比較することになります。

PoCの実行結果

前記PHPスクリプトの実行結果は以下の通りです。

$ php xdecimal.php
18015376320243461 -> 18015376320243459

18015376320243461を検索したのに、18015376320243459が返るという不思議な結果となっています。SQL文単体の実行では下記となります。
mysql> SELECT id FROM xdecimal WHERE id='18015376320243461';
+-------------------+
| id |
+-------------------+
| 18015376320243459 |
| 18015376320243460 |
| 18015376320243461 |
+-------------------+
3 rows in set (0.00 sec)
このような奇妙な結果となる原因は、MySQLの「引数は浮動小数点 (実) 数として比較されます」という仕様に起因します。浮動小数点数(倍精度)の仮数部の桁数は52ビット(暗黙のビットを足して53ビット、10進16桁弱)しかなく、18015376320243461という10進17桁の整数を正確に表現できないことが原因です。

バインド値を整数にキャストした場合

一方、bindValueでバインドする値をint型にキャストすると、生成されるSQL文と実行結果は下記となります。
mysql> SELECT id FROM xdecimal WHERE id=18015376320243461;
+-------------------+
| id |
+-------------------+
| 18015376320243461 |
+-------------------+
1 row in set (0.00 sec)
今度は浮動小数点数を経由しないため、正確な結果が返ります。先のスライドでint型へのキャストをいれていた理由は、このためです。

もっとよい方法はないか?

64ビット版のPHPを使った場合でも、int型の最大値は9223372036854775807なので、これを超えると冒頭の指摘のように不具合がおきます。この場合はどうしたらよいでしょうか?
そもそも数値型を使わずに文字列型を使う方法もありますが、その場合は数値計算が出来ません。せっかくDECIMAL型は65桁までの十進数が使えるのにもったいないですね。

ちょっと面倒ですが、以下のように型変換を明示すれば、暗黙の型変換およびそれに伴う浮動小数点数への変換を防ぐことが出来ます。
SELECT id FROM xdecimal WHERE id=CAST(:id AS DECIMAL(20))
実行結果は以下の通りです。大丈夫ですね。
mysql> SELECT id FROM xdecimal WHERE id=CAST('18015376320243461' AS DECIMAL(20));
+-------------------+
| id |
+-------------------+
| 18015376320243461 |
+-------------------+
1 row in set (0.01 sec)

まとめ

PDOのサンプルスクリプトで、バインド時に整数型の値をintにキャストしていた理由を説明しました。intへのキャストは桁あふれの危険性はあるものの、浮動小数点数への暗黙の型変換よりはマシという意味で、一種のバッドノウハウだと思います。
MySQLの暗黙の型変換は本当にやっかいで、詳しくは下記の参考文献をお読み下さい。本当に望ましい書き方は、PHPスクリプトではなくSQL文側にキャストを書くことでしょうが、もっと良い方法があれば、ご教授下さい。

参考文献


蛇足

実は、先のid型にインデックスをつけた場合は、先ほどとは挙動が変わります。
mysql> ALTER TABLE xdecimal ADD INDEX(id);
Query OK, 0 rows affected (0.15 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> SELECT id FROM xdecimal WHERE id='18015376320243461';
+-------------------+
| id |
+-------------------+
| 18015376320243461 |
+-------------------+
1 row in set (0.00 sec)
インデックスの有無で挙動が変わるのは、MySQLのバグではないかと思いますが、この「バグ」を修正するのは中々やっかいだなと思います。


【HASHコンサルティング広告】
HASHコンサルティング株式会社は、ウェブアプリケーションのセキュリティに関心のあるセキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。

Ruby on Railsの潜在的なリモートスクリプトインジェクション脆弱性CVE-2016-2098

$
0
0
今年の2月末に、Ruby on Railsに潜在的なリモートスクリプトインジェクションの脆弱性CVE-2016-2098が報告されています。攻撃コード(PoC)も公開されていますが、現実の攻撃が行われているという発表はないようです。この脆弱性の内容と対策について報告いたします。

背景

Hello Worldのような以下のシンプルなアプリケーション(コントローラ)を考えます。
class HelloController < ApplicationController
def index
render 'hello/hello'
end
end
これに対するテンプレート hello/hello.html.erb は以下だとします。
<div>Hello world</div>
ご覧のように、上記テンプレートを指定した場合、Hello worldが表示されます。
次に、以下のテンプレート hello/bye.html.erb を用意します
<div>Good bye world</div>
これを呼び出すには、コントローラにてrender 'hello/bye'を実行すればよいわけですが、helloとbyeを切り分けられるように、コントローラを以下のように修正します。
class HelloController < ApplicationController
def index
render params[:template]
end
end
これを呼び出すには、以下のようにします。


しかし、renderメソッドの引数を外部から自由に指定できるようにするのは、危なっかしい感じです。

renderメソッドの inlineオプション

renderメソッドにはinlineオプションというものがあり、テンプレート文字列を指定することができます。
render :inline => "<%= Time.now %>"
実行結果を以下に示します。Time.nowメソッドの呼び出しにより、現在時刻が表示されています。


すなわち、外部からrenderメソッドを操作してinlineオプションを指定できれば、任意のRubyスクリプト(上記例ではTime.now)を実行できることになります。具体的には、renderメソッドに下記のハッシュを指定すればよいことになります。
hash = {:inline => "<%= Time.now %>"}
render hash
しかし、そんなことができるのでしょうか?

JSONによりハッシュを外部から指定できる

公開されているCVE-2016-2098のPoCでは、JSONによりrenderメソッドにハッシュを指定する方法が示されています。その様子を以下に示します(少し変えています)。以下の攻撃例では、FileUtils.touchメソッドにより、rootedというファイルを作成することで攻撃の証拠を示しています。


上記の攻撃で、ファイル rooted が作成された様子を下図に示します。
$ ls -l rooted
-rw-rw-r-- 1 ockeghem ockeghem 0  6月  1 22:29 rooted

対策

Ruby on Railsの以下のバージョンで修正されています(今年の2月29日公開)。
  • Rails 3.2.22.2
  • Rails 4.1.14.2
  • Rails 4.2.5.2
これらにバージョンアップすることで攻撃は防がれるようになりますが、そもそもrenderメソッドに任意の値を指定できることがとても危険な状態であると考えます。このため、以下を推奨します。
  • 可能な限りrenderメソッドの引数には外部由来の値を指定しない
  • やむを得ずrenderメソッドの引数を外部から指定する場合は、ホワイトリスト(たとえば指定可能なテンプレートファイル名の一覧)による検証を行う

まとめ

Ruby on Railsの潜在的なリモートスクリプトインジェクションの脆弱性CVE-2016-2098について説明しました。renderメソッドには強力なオプション指定機能があるため、外部から自由な値を指定できると危険です。そもそも、renderメソッドのinlineオプションでrubyスクリプトが実行できることは、脆弱性ではなく仕様です。
このため、この問題は本来Ruby on Railsの脆弱性というよりは、アプリケーション側の問題であと考えます。この問題が「潜在的な」脆弱性と表現されているのは、このような背景からだと思います。


【HASHコンサルティング広告】
HASHコンサルティング株式会社は、ウェブアプリケーションのセキュリティに関心のあるセキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。


アイ・オー・データのポケットルーターWFS-SR01の脆弱性について調べてみた

$
0
0
2016年11月2日に、アイ・オー・データ機器が販売する「ポケドラ」ことWFS-SR01に複数の脆弱性があることが報じられました。
株式会社アイ・オー・データ機器が提供する WFS-SR01 は、無線 LAN ルータ機能を搭載したポータブルストレージ機器です。WFS-SR01 のポケットルーター機能には、次の複数の脆弱性が存在します。
  • 任意のコマンド実行 - CVE-2016-7806
  • アクセス制限不備 - CVE-2016-7807
JVN#18228200: WFS-SR01 における複数の脆弱性より引用
任意のコマンド実行ということは、OSコマンドインジェクションでもあるのだろうかと思い、腕試しの目的で当該機種を入手して調べてみました。その結果を報告します。

ポケドラとは

ポケドラは、Wi-Fiモバイルストレージと呼ばれるカテゴリに属し、以下の機能を提供しています。
  • Wi-Fi経由で使用できるストレージ
  • モバイルバッテリー
  • SDカードリーダー
  • ポケットルーター
これらのうち、「ポケットルーター」機能が問題になりました。名称がややこしいのですが、よく普及している「モバイルーター」とは異なり、「ポケットルーター」には 3GやLTEのような自力のネット接続機能はありません。ホテル等が備える有線LANに接続して、それをスマホ等のWi-Fi機器に中継する機能を提供しています。
もう少し技術的にいえば、有線LAN側はDHCPクライアントとして有線LANに接続(固定IPも可能ですが)し、それをNATで複数のWi-Fi機器で共有します。そのためのDHCPサーバー機能もあります。以下に設定画面の一部を示します。



試してみる

私が入手した端末は、ファームウェアバージョン 2.000.034 というものです。これを仮想的なインターネット環境(IPアドレスはグローバルを想定して203.0.113.0/24を使用)につなげました。有線LAN側(インターネット側)は、実験環境で用意したDHCPにより 203.0.113.10 がポケドラに割り当てられています。
お約束にしたがい、まずはポートスキャンにかけてみましょう。以下は、有線LAN側、すなわち場合によってはインターネットに接続されている側からの結果です。
$ nmap -p 1-65535 203.0.113.10

PORT STATE SERVICE
23/tcp open telnet
80/tcp open http
81/tcp open hosts2-ns
139/tcp open netbios-ssn
445/tcp open microsoft-ds
5880/tcp open unknown
おやおや、いきなり興味深い結果が表示されました。80のhttpは管理画面でしょうが、一般的なルーターでは、インターネット側からアクセスは通常できないように制御されています。23のtelnet、139と445のファイル共有も興味深いですね。
それでは、まずtelnetでログインしてみましょう。管理画面のデフォルトアカウントはadmin/adminなので、それで試してみます…
$ telnet 203.0.113.10
Trying 203.0.113.10...
Connected to 203.0.113.10.
Escape character is '^]'.

WFS-SR01 login: admin
Password:

BusyBox v1.12.1 (2012-04-26 15:28:18 PHT) built-in shell (ash)
Enter 'help' for a list of built-in commands.

$ uname -a
Linux WFS-SR01 2.6.21 #5 Fri Nov 1 13:36:46 CST 2013 mips unknown
$ whoami
-sh: whoami: not found
$ who
USER TTY IDLE TIME HOST
admin pts/0 00:00 Feb 7 15:50:37
$ free
total used free shared buffers
Mem: 28000 26340 1660 0 1388
Swap: 0 0 0
Total: 28000 26340 1660
$ df
Filesystem 1k-blocks Used Available Use% Mounted on
rootfs 4800 4800 0 100% /
/dev/root 4800 4800 0 100% /
/dev/sda1 1951676 1580 1950096 0% /data/UsbDisk1/Volume1
$
なんと、いきなりtelnetでログインできてしまいました。
unameの表示から、Linux 2.6.21、CPUはMIPSであることがわかります。
それにしても…「任意のコマンド実行」とはこれのことですか…あまりのあっけなさに、腕試しをしようという目論見はなんとも不発な感じで微妙な気持ちになりましたが、これはこれで興味深いので、さらに試してみました。

インターネットに向けてファイルを公開してしまう

telnetはいったん中断して、今度はファイル共有を試します。
仮想インターネットに接続したWindows XP端末から、\\203.0.113.10にアクセスしてみます。すると、下記のようにログオンプロンプトが表示されます。


ここで、先程同様 admin/admin を試すと、以下のようにファイル共有ができました。


ファイルの読み込みだけでなく、書き込みも可能でした。
しかし、ここまでの問題であれば、デフォルトパスワードを変更しておけば悪用はできないはずで、「違法ではないが一部不適切」と強弁できなくもないなと思いました。


rootログインしてftpdを動かす

そこで再びtelnetに戻り、rootのパスワードを試してみました。いくつかの方法を試した後、rootのパスワードが判明しましたが、詳細は伏せます。判明したパスワードを見て、以下のような感想を持ちました。

パスワード-もっと強くキミを守りたい- : IPA情報処理推進機構より引用

それはともかく、rootでログインしてみましょう。
$ telnet 203.0.113.10
Trying 203.0.113.10...
Connected to 203.0.113.10.
Escape character is '^]'.

WFS-SR01 login: root
Password:
login: can't chdir to home directory '/root'

BusyBox v1.12.1 (2012-04-26 15:28:18 PHT) built-in shell (ash)
Enter 'help' for a list of built-in commands.

# who
USER TTY IDLE TIME HOST
root pts/0 00:00 Nov 30 20:50:40
# pwd
/
#
まぁ、パスワードがわかればログインできますよね…
rootがとれましたので、ちょっと遊んでみようということで、なにか外部からプログラムをアップロードして動かしてみることにします。といってもあらたにビルドするのは面倒なので、busybox本家サイトから、MIPS用のフル版busyboxバイナリをダウンロードして、ホケドラに挿しているSDカードに保存しました。
# ./busybox-mipsel
BusyBox v1.21.1 (2013-07-08 11:09:23 CDT) multi-call binary.
BusyBox is copyrighted by many authors between 1998-2012.
Licensed under GPLv2. See source distribution for detailed
copyright notices.
...
動いていますね。
ポケドラに元々インストールされているbusyboxではできないこととして、ftpdを起動してみます。
# ./busybox-mipsel  tcpsvd 0 21 ./busybox-mipsel  ftpd -w /
別の端末から接続してみましょう。
$ ftp 203.0.113.10
Connected to 203.0.113.10.
220 Operation successful
Name (203.0.113.10:ockeghem): anonymous← パスワードは要求されない
230 Operation successful
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> pwd
257 "/"
ftp> ls
200 Operation successful
150 Directory listing
total 0
drwxr-xr-x 2 root root 802 Apr 4 2014 bin
drwxr-xr-x 3 root root 20 Apr 4 2014 boot
drwxr-xr-x 3 root root 0 Nov 30 20:50 data
drwxr-xr-x 6 root root 0 Nov 30 20:50 dev
drwxr-xr-x 14 root root 0 Nov 30 20:50 etc
...
226 Operation successful
ftp> cd /data/UsbDisk1/Volume1/
250 Operation successful
ftp> pwd
257 "/data/UsbDisk1/Volume1"
ftp> ls
200 Operation successful
150 Directory listing
total 1596
drwxrwxrwx 2 root root 4096 Nov 9 22:30 System Volume Information
-rwxrwxrwx 1 root root 1576152 Nov 30 20:58 busybox-mipsel
-rwxrwxrwx 1 root root 10923 Dec 2 2016 ockeghem.png
drwxrwxrwx 2 root root 4096 Nov 30 20:57 test
226 Operation successful
ftp> put shop.zip ← shop.zipをアップロード
local: shop.zip remote: shop.zip
200 Operation successful
150 Ok to send data
226 Operation successful
2753 bytes sent in 0.00 secs (10380.2 kB/s)
ftp> ls
200 Operation successful
150 Directory listing
total 1596
drwxrwxrwx 2 root root 4096 Nov 9 22:30 System Volume Information
-rwxrwxrwx 1 root root 1576152 Nov 30 20:58 busybox-mipsel
-rwxrwxrwx 1 root root 10923 Dec 2 2016 ockeghem.png
-rwxrwxrwx 1 root root 2753 Nov 30 21:54 shop.zip
drwxrwxrwx 2 root root 4096 Nov 30 20:57 test
226 Operation successful
ftp> quit
221 Operation successful
$
認証なしで書き込みもできるので、攻撃者にとっては便利そうです。これはほんの一例ですが、ポケドラに挿したSDカードの内容が読み取れることはもちろんですが、DNSの設定を変更されることで、意図しないサイトに接続を誘導される等の攻撃も考えられます。

いったんまとめ

ここまで検証したことをまとめると、下記のようになります。

  • 任意のコマンド実行 - CVE-2016-7806  telnetポートが有効であり、外部からadmin/adminという容易に推測できるアカウントでログインでき、rootログインされてしまう可能性もある
  • アクセス制限不備 - CVE-2016-7807 telnetに加え、Windowsファイル共有、管理画面等がインターネット側からも使え、情報漏えいや不正な操作に使われてしまう

最新版での対策

2016年11月15日に最新のファームウェアが公開されました(画面上は2.000.040と表示)。このバージョンでは、以下の変更がなされたようです。
  • telnetポートが塞がれた(有線LAN、無線LANとも)
  • 有線LAN(インターネット)側のポートを塞いだ
これらにより、上記で紹介した問題は対策されていると考えます。

そもそもこれは脆弱性だったのか

見出しをご覧になって、「おかしなことを言う」と思われたでしょうが、そもそもポケドラはインターネットに直接接続することを想定していたのかという疑問があります。ネットの商品説明や取扱説明書を見ても、「ホテルの有線LANをWi-Fi化」などという表現であり、インターネットにつなぐことができるという表現ではありません。
しかしながら、「ポケットルーター」機能と銘打っており、実際にインターネットに接続すれば一応のWi-Fiルーターとして使用できることから、ユーザーの中にはインターネットに直接接続する方もいるでしょう。そのようなユースケースを重く見て、株式会社アイ・オー・データ機器は、当該問題を脆弱性して公表し、製品回収までしたのだと予想します。そのような株式会社アイ・オー・データ機器の姿勢に私は敬意を表します。

まとめ

本稿で紹介した「脆弱性」は、実装上のミスというよりは、商品コンセプトにやや曖昧なところがあり、意図しない使われ方をした場合に危険な状態になることを想定できなかったことによるものと考えます。そして、今まで発表された IoT機器のセキュリティ問題の多くは、「ユースケースを想定しきれていなかった」ことによるわけで、その意味で象徴的な事例とも言えます。

また、改修後のポケドラは、必要最低限の対策であり、一般的な家庭用Wi-FIルーターが備えるセキュリティ機能はないことから、あくまでホテル等で提供されるインターネット接続をWi-Fiに変換するような目的で使用されることを推奨します。つまり、インターネットに直接接続することは、直ちに危険ということではないにしても、避けた方がよいということです。

追記

複数の方から、ホテルのインターネットでも部屋間で通信できるのでインターネットと同等のリスクとなり得るという指摘をいただきました。たしかにその通りです。ご指摘いただきありがうございました。この件ついては更に追記するかもしれません。

PHPの全バージョンの挙動をCGIモードで試す

$
0
0
PHPの挙動を調べていると、マニュアルにも、ChangeLogにも載っていない変更にしばしば遭遇します。たとえば、PCRE系関数(preg_xxxx)の正規表現指定(第1引数)において、過去のPHPではNULLバイトを許容していましたが、最近のPHPでは、正規表現中のNULLバイトをエラーにしています。この変更は、マニュアルには載っておらず、ChangeLogには記載されているもののNULLバイトとは書いていないので、ちょっと気がつきにくいですね。
Fixed bug #55856 (preg_replace should fail on trailing garbage)
このような場合、ソースコードの該当箇所を調べるか、適当にあたりをつけたバージョンのPHPをビルドして試すなどの手法がとられているかと思いますが、@hnwさんが phpall を発表されたことで、この種の調査が一挙に楽になりました。
例えば、以下のようなサンプルスクリプトを用意して、
<?php
ini_set('display_errors', 'On');
$vul = 0;
function vul($x) {
echo "vulnerable\n";
exit;
}
echo 'version=' . phpversion() . "\n";
$x = preg_replace("/^test/e\0/", "vul('\\0');", "test");
echo "not vulnerable\n";
var_dump($x);
phpallを用いると、以下の実行結果を得ます。
$ phpall regexpinj.php
... 略
php-5.4.4: version=5.4.4 vulnerable
php-5.4.5: version=5.4.5 vulnerable
php-5.4.6: version=5.4.6 vulnerable
php-5.4.7: version=5.4.7 Warning: preg_replace(): Null byte in regex in ...
php-5.4.8: version=5.4.8 Warning: preg_replace(): Null byte in regex in ...
php-5.4.9: version=5.4.9 Warning: preg_replace(): Null byte in regex in ...
これにより、PCRE関数で正規表現のNULLバイトチェックが入ったのは PHP 5.4.7であることが一目瞭然分かるわけです。
しかし、phpallでも調査できないような課題があります。それはHTTP固有の問題、たとえばHTTPヘッダやCookie、セッション等の問題です。また、$_GETや$_POSTの挙動もCLI版のPHPでは把握ができません。

ウェブアプリケーションのセキュリティを専門とする立場からは、前述の課題は、下記の脆弱性に関連するものであり、決して軽視はできません。
  • HTTPヘッダインジェクション
  • セッション固定
  • セッションアダプション
  • セッション汚染
  • その他セッション系脆弱性
どうしたらこれを調べられるかと考えているうちに、全てのPHPをCGIモードで動かせばいいというアイデアに至りました。名付けて phpcgiall の誕生です。

phpcgiallの動作原理

動作原理といっても、単に全てのPHPをCGIモードで動かすだけですが、テストのしやすさなども考慮して、以下のような設定を用いています。

まず、各バージョンのPHPに関する設定ですが、以下のような Apache設定ファイルをバージョン毎にジェネレートしています。以下は PHP 5.6.29の例です。これら設定ファイルは、~/phpcgi.confディレクトリに置かれます。CGI版のPHPバイナリは、~/phpcgi/ ディレクトリに置かれます。また、PHPスクリプト(コンテンツ)は、私のDropboxフォルダ上の共通のディレクトリ上に配置しています。
Alias /php-5.6.29/ "/home/ockeghem/Dropbox/www/"
<Location /php-5.6.29>
AddHandler application/x-httpd-php-5.6.29 .php
Action application/x-httpd-php-5.6.29 /php-cgi/php-5.6.29
Options Indexes FollowSymLinks
Order allow,deny
allow from all
</Location>
これに対して、共通の設定として、下記を httpd.confの末尾に入れています。
Alias /d/ "/home/ockeghem/Dropbox/www/"
<Directory "/home/ockeghem/Dropbox/www">
Options Indexes FollowSymLinks
AllowOverride None
Order allow,deny
Allow from all
</Directory>

ScriptAlias /php-cgi/ /home/ockeghem/phpcgi/
<Directory "/home/ockeghem/phpcgi">
AllowOverride None
Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
Order allow,deny
Allow from all
</Directory>

Include /home/ockeghem/phpcgi.conf/
この設定により、/php-5.6.29/phpinfo.php にアクセスすると、~/Dropbox/www/phpinfo.phpをPHP 5.6.29が実行した結果を返すことになります。



実際には、各バージョンのPHPの呼び出しは、curlコマンドを呼び出す簡単なPythonスクリプトを用いています。

継続行とヘッダインジェクション

それでは、phpcgiallを用いて、PHPのHTTPヘッダインジェクション対策の変遷について調べてみましょう。
PHPのheader関数はPHP-5.1.2で改行のチェックが入り、HTTPヘッダインジェクションができなくなっているはずでしたが、実際には以下の抜けがありました。
  • キャリッジリターン(\x0D)のみで攻撃可能なブラウザがあった
  • IE限定だが、継続行(LWS)を用いた攻撃ができた
いずれも最新版のPHPでは解消されています。このうち、継続行の問題の方を phpcgiall で確かめてみましょう。
継続行を用いた HTTPヘッダインジェクションとは、以下のように改行の後に空白を用いる継続行(Linear White Space)を用いるものです。
header("Location: http://exapmle.jp/\r\n Set-Cookie: a=b;");
すると、古いPHPでは、以下のように「継続行」形式として改行チェックをスルーしてしまいます。
Location: http://example.jp/
Set-Cookie: a=b;
ブラウザ側で「継続行」として一つのヘッダとして認識してくれれば問題はないのですが、IEは上記を2つのヘッダとして認識するために、Set-Cookieヘッダが追加された形になります。詳しくは、ブログ記事PHPにおけるHTTPヘッダインジェクションはまだしぶとく生き残るを参照下さい。

それでは、phpcgiallにてこの問題を検証してみましょう。PoCは下記です。
<?php
header("Location: http://example.jp/\r\n Set-Cookie: a=b;");
実行結果は下記の通りです。ログファイルからWarningの表示を確認しています。
$ grep Warning *
php-5.4.38.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ...
php-5.4.39.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ...
...
php-5.5.22.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ...
php-5.5.23.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ...
...
php-5.6.6.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ...
php-5.6.7.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ...
...
php-7.0.0.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ...
php-7.0.1.log:<b>Warning</b>: Header may not contain more than a single header, new line detected ...
上記から、header関数で「継続行」が禁止されたのは、PHP 5.4.38、5.5.22、5.6.6、7.0.0であることがわかります。

キャリッジリターンのみを用いたHTTPヘッダインジェクションはどうか?

次に、キャリッジリターン(\x0D)のみを用いたHTTPヘッダインジェクションについて調べてみましょう。PoCは下記となります。
<?php
header("Location: http://example.jp/\rSet-Cookie: a=b;");
これに対して、問題があることが確実なバージョンとして、PHP 5.3.0での結果を見てみましょう。キャリッジリターンをわかりやすく表示するために、sedによりキャリッジリターンを [CR] と変換して表示しています。
$ curl --dump-header - http://localhost/php-5.3.0/headerinj-cr.php | sed 's/\r/[CR]/'
HTTP/1.1 302 Moved Temporarily[CR]
Date: Wed, 14 Dec 2016 12:55:39 GMT[CR]
Server: Apache/2.2.22 (Ubuntu)[CR]
X-Powered-By: PHP/5.3.0[CR]
Location: http://example.jp/Set-Cookie: a=b;[CR]
Vary: Accept-Encoding[CR]
Content-Length: 0[CR]
Content-Type: text/html[CR]
[CR]
あれあれ? Set-Cookieヘッダ(赤字)の前にキャリッジリターンがありません。これでは、Set-Cookieが独立したヘッダとして、ブラウザに認識されません。
試みに、CGI版のPHPをコマンドラインから起動して、PHPの出力を見てみましょう。
$ ~/phpcgi/php-5.3.0 headerinj-cr.php | sed 's/\r/[CR]/'
Status: 302 Moved Temporarily[CR]
X-Powered-By: PHP/5.3.0[CR]
Location: http://example.jp/[CR]Set-Cookie: a=b;
Content-type: text/html[CR]
[CR]
今度は、Set-Cookieヘッダの前にキャリッジリターンがありますね。
どうやら、ApacheがCGIインターフェースを処理する際に、ラインフィード(0x0A)を伴わない単独のキャリッジリターン(0x0D)を削除してしまうようです。
ということで、キャリッジリターンのみを用いたHTTPヘッダインジェクションのテストは、CGIプログラムの形ではうまくいかないことがわかりました。Warningから該当バージョンは追えますが、脆弱な挙動として現れないのはちょっと残念ですね。

まとめ

@hnwさんのphpallを拡張する形で、PHPの前バージョンをCGIモードで動かす環境 phpcgiall を作成しました。
概ね期待通りの結果を得られましたが、HTTPヘッダの微妙な挙動については、Apacheモジュール版とCGI版では微妙な差があり、検証に注意を要することが課題と言えます。
(続く)

PHPの全バージョンの挙動をApacheモジュールとして試す

$
0
0
この投稿は PHP Advent Calendar 2016の16日目の記事です。

エグゼクティブサマリ

PHPのバージョン間の挙動の違いを調査するツールとして、@hnwによるphpallや、それを改造したphpcgiallがあったが、現実のPHPの利用環境とは違いがあり、検証の妨げになる場合があった。このため、PHPのバージョン毎にApacheを異なるポートで動かすことにより、全てのバージョン(229種)のPHPをApacheモジュールとして動作させることに成功し、modphpallと命名した。modphpallはPHPの検証に有効であることを、キャリッジリターンのみで起こるPHPヘッダインジェクションを用いて確認した。

はじめに

昨日の日記では、PHPの全バージョンをCGIモードで試す phpcgiall について紹介しました。HTTPヘッダインジェクションやセッションの挙動について確認するためには、CLI版のPHPでは限界があり、ウェブスクリプトとして全バージョンのPHPを試すことができる phpcgiall は有効であるものの、ヘッダの微妙な挙動については、Apacheモジュール版と差異があることがわかりました。

それに、やはりPHPの本流はCGI版ではなく、Apacheモジュール版であろという思いから、なんとかphpallのApacheモジュール版はできないだろうかと考えるようになりました。
基本的にアイデアは3つありました。
  • Apacheに同時に複数バージョンのPHPを組み込めるようにする
  • Apacheに組み込むPHPを次々に切り替える
  • Apacheのインスタンスを複数起動して、それぞれ異なるバージョンのPHPを動かす
上の2つは、そもそも実現が難しかったり、性能が悪かったり等の予想がありました。そこで、PHPのバージョン毎に異なるポート番号でApacheを起動することで、Apacheモジュール版PHPの全バージョンを一つのサーバーで動かすことを考えました。modphpallと命名しました。

PHPのビルド

既にCLI版とCGI版のPHP版があるので、Apacheモジュール版のPHPをビルドすることには、さほど困難はありませんでした。ただ、時間はそれなりにかかりますね。
PHP 3.0.18(PHP 3の最終バージョン)から最新のPHP 7.1.0 まで、229バージョンのPHPバイナリができあがりました。

Apacheのバージョン

使用するApacheバージョンは、あまり良く考えずに、PHP 3とPHP 4はApache1.3.42(1.3系の最終バージョン)、PHP 5以降はApache2.2.31(2.2系の最新)を用いましたが、これで特に問題は出ていません。

ディレクトリ構成

ディレクトリ構成は下記のとおりです。

~/apache1.3PHP 3, 4向け。Apache 1.3.42のバイナリ、コンフィグレーション
~/apache2.2PHP 5, 7向け。Apache 2.2.31のバイナリ、コンフィグレーション
~/phplibPHP 3, 4, 5, 7のApacheモジュール版バイナリ
~/Dropbox/wwwドキュメントルート。PHPスクリプトを配置

設定ファイル

PHPのバージョンの数だけ、ポート番号を変えてApacheを起動することになるため、設定ファイル(httpd.conに相当するもの)は、簡単なスクリプトでジェネレートしています。
以下のような設定フアイルのテンプレートをPHPのバージョン毎に用意しています。
Include "conf/httpd_header.conf"

Listen %PORT%
LoadModule php5_module /home/ockeghem/phplib/%PHPVER%.so
ServerName phpcgiall:%PORT%
PidFile "logs/httpd_%PHPVER%.pid"

Include "conf/httpd_tail.conf"
そして、スクリプトで%PORT%と%PHPVER%を書き換えて、以下のような設定ファイルを生成します。
Include "conf/httpd_header.conf"

Listen 49364
LoadModule php5_module /home/ockeghem/phplib/php-5.6.29.so
ServerName phpcgiall:49364
PidFile "logs/httpd_php-5.6.29.pid"

Include "conf/httpd_tail.conf"
そして、同じスクリプトで、以下のような形で起動します。
apache2.2/bin/httpd -f conf/httpd_php-5.6.29.conf
起動後、phpinfo()の表示例を示します。


使用メモリ量

同一サーバー内で229ものApacheインスタンスを起動するので、メモリ使用量が心配になります。手元の環境は、Ubuntu 12.04LTS (32ビット)ですが、VMに割り当てるメモリを当初21Gバイトとしていました。現実には2Gバイト程度でも一応動くようです。現在、暫定的に8Gバイトのメモリを割り当てていますが、起動当初の状態は下記となります。
$ free
total used free shared buffers cached
Mem: 8266464 3794592 4471872 0 48444 1938060
-/+ buffers/cache: 1808088 6458376
Swap: 2093052 0 2093052
$

使ってみる

それでは、modphpallを試してみましょう。PoCは、昨日の日記で示したキャリッジリターンのみを用いたHTTPヘッダインジェクションです。
<?php
header("Location: http://example.jp/\rSet-Cookie: a=b;");
PHP 5.3.0による実行結果は下記のとおりです。昨日同様、キャリッジリターン(0x0D)をsedにより[CR]と変換して表示しています。
$ curl --dump-header - http://localhost:49220/headerinj-cr.php | sed 's/\r/[CR]/'
HTTP/1.1 302 Found[CR]
Date: Thu, 15 Dec 2016 10:20:07 GMT[CR]
Server: Apache/2.2.31 (Unix) PHP/5.3.0[CR]
X-Powered-By: PHP/5.3.0[CR]
Location: http://example.jp/[CR]Set-Cookie: a=b;
Content-Length: 0[CR]
Content-Type: text/html[CR]
[CR]
めでたく(?)Set-Cookiヘッダの前にキャリッジリターンが出力されており、一部のブラウザ(IE、Google Chrome、Safari等)ではHTTPヘッダインジェクションが起こる結果が出ました。
他のバージョンも含めると、問題が発生するバージョンは下記となります(見やすくなるように表示の順序を入れ替えています)。
$ grep Set-Cookie *.log | sed 's/\r/[CR]/'
php-3.0.18.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
php-4.0.0.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
php-4.0.1.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
...
php-4.4.9.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
php-5.0.0.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
php-5.0.1.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
...
php-5.3.9.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
php-5.3.10.log:Location: http://example.jp/[CR]Set-Cookie: a=b;
$
キャリッジリターンのみでHTTPヘッダインジェクションが起こるのは、PHP 5.3.10までであることがわかります。

まとめ

PHPの全バージョンの挙動をApacheモジュールとして試すことのできる modphpall を作成し、その成果として、キャリッジリターンのみでHTTPヘッダインジェクションを起こすPHPバージョンを確認しました。CGIモードとApacheモジュールの挙動の差は小さいものですが、その小さい差が問題に成る場合もあるので、今後のPHPの検証に役立てたいと思います。

PDOに複文実行を禁止するオプションが追加されていた

$
0
0

エグゼクティブサマリ

PHP 5.5.21、PHP 5.6.5 以降、PHPにPDO::MYSQL_ATTR_MULTI_STATEMENTSというオプションが追加され、PDO+MySQLの組み合わせで、SQLの複文を禁止できるようになった。この設定はSQLインジェクションの緩和策として有効である。

はじめに

2013年12月に公開した PHP+PDO+MySQLの組み合わせではSQLインジェクション攻撃で複文呼び出しが可能 にて、PDOとMySQLの組み合わせで、SQLインジェクションの文脈で複文呼び出しが可能であることを報告していましたが、その後のPHPのバージョンアップで、複文実行を禁止するオプションが追加されていましたので報告します。
対象のバージョンは以下の通りです。
  • PHP 5.5.21 以降
  • PHP 5.6.5 以降
  • 全ての PHP 7.0、7.1
前述の記事を書いた後、3大CMSの一角である Drupal に、Drupageddon (CVE-2014-3704) と呼ばれる恐ろしいSQLインジェクション脆弱性が発見されました。詳しくは、以下の記事を参照下さい。
この記事の中で、私は以下のように書きました。
実験に使用した環境はMySQLを使っていますが、SQLの複文が実行できていることになります。これは、DrupalがPDOを使っているためで、詳しくは以下のエントリを参照ください。
  • PHP+PDO+MySQLの組み合わせではSQLインジェクション攻撃で複文呼び出しが可能
すなわち、タラレバの話にはなりますが、PDOがMySQLでの複文実行を許していなければ、Drupageddonは高危険度の脆弱性には至らなかった可能性があります。少なくとも、攻撃経路はかなり限定されたはずです。
Drupalの開発陣にとっては、この事実はよほど悔しかったのかもしれません。以下のような時系列で、PDO+MySQLで複文を許さなくするオプションが提案されます。
  • 2014年10月15日 Drupageddonを修正した Druapl 7.32 がリリースされる
  • 2014年11月14日 PDOに複文を許さなくするオプションが提案される
  • 2015年 1月22日 上記を実装したPHP 5.5.21およぴ PHP 5.6.5 がリリースされる
すなわち、Drupageddonの公表からわずか3ヶ月ほどで、このオプションが実装・公開されたことになります。

どうしてこのオプションに気づいたか

私はこのオプションの存在に気がついていなかったのですが、昨日modphpallでDrupal8を動かしていたところ、下記の警告が表示されていることに気がつきました。

PHP (multiple statement disabling)5.5.9 (more information)
PHP versions higher than 5.6.5 or 5.5.21 provide built-in SQL injection protection for mysql databases. It is recommended to update.

multiple statement disabling …ですと?
この情報から、PDO::MYSQL_ATTR_MULTI_STATEMENTSというオプションが追加されたことに気がつきました。

オプションの使い方

マニュアルから、このオプション PDO::MYSQL_ATTR_MULTI_STATEMENTS の説明を引用します。
PDO::MYSQL_ATTR_MULTI_STATEMENTS (integer)
FALSE にすると、PDO::prepare() や PDO::query() でのマルチクエリの実行を無効にします。

この定数が使えるのは、データベースハンドルを新規作成する際の driver_options 配列内だけであることに注意しましょう。

これが使えるようになった PHP のバージョンは 5.5.21 および PHP 5.6.5。

PHP: MySQL (PDO) - Manualより引用
すなわち、new PDOする際に、driver_options配列に上記を設定すればよいことになります。下記は、Drupalのソースコードを参考に、PDO::MYSQL_ATTR_MULTI_STATEMENTS が存在する場合のみ、このオプションを指定しています。
$opt = array(/* 様々な接続時オプション */);
if (defined('PDO::MYSQL_ATTR_MULTI_STATEMENTS')) {
$opt[PDO::MYSQL_ATTR_MULTI_STATEMENTS] = false;
}
$db = new PDO("mysql:host=DBHOSTNAME;dbname=DBNAME;charset=utf8", DBUSER, DBPASSWORD, $opt);
上記オプションが有効な状態で複文を実行しようとすると、以下のようなエラーになります。
PDO Error:SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; ...

PDO::MYSQL_ATTR_MULTI_STATEMENTS を FALSE にする効果

この設定により、SQLインジェクション攻撃で複文を使用することができなくなります。Drupageddonに効果があることは前述のとおりですが、これは現実的には意味がありません。このオプションの真意は、Drupageddonのある古いDrupalを延命させることではないでしょう。むしろ、今後、SQLの複文を使った攻撃を緩和することにあります。
複文が使えなくなると、SQLインジェクションによる改ざんがほぼできなくなると考えられますので、情報漏えいによる実害があまりなく、改ざんが主な脅威であるようなサイトには特に有力な緩和策になります。

まとめ

PDOの新オプションPDO::MYSQL_ATTR_MULTI_STATEMENTSについて紹介しました。PDOは、従来複文を許しており、SQLインジェクション攻撃を受けた場合にデータの改変など脅威が増加するなど、MySQLi等に比べて影響が大きくなっていました。PDO::MYSQL_ATTR_MULTI_STATEMENTSをFALSEに設定することで、SQLインジェクション攻撃の影響を緩和することができます。わずかな追加で緩和策となるので、このオプションの利用を推奨します。

PHPMailerの脆弱性CVE-2016-10033について解析した

$
0
0

エグゼクティブサマリ

PHPMailerにリモートスクリプト実行の脆弱性CVE-2016-10033が公表された。攻撃が成功した場合、ウェブシェルが設置され、ウェブサーバーが乗っ取られる等非常に危険であるが、攻撃成功には下記の条件が必要であることがわかった
  • PHPMailer 5.2.17以前を使っている
  • Senderプロパティ(エンベロープFrom)を外部から設定できる
  • 現在出回っているPoCはMTAとしてsendmailを想定しており、postfixを使っている環境では問題ない
対策版として公開されている PHPMailer 5.2.19も不完全であるので、回避策の導入を推奨する。

はじめに

12月24日にPHPMalerの脆弱性CVE-2016-10033が公表され、とんだクリスマスプレゼントだと話題になっています。
 PHPからのメール送信に広く使われているライブラリの「PHPMailer」に重大な脆弱性が報告され、修正のためのパッチが12月24日付で公開された。悪用されれば任意のコードを実行される恐れも指摘され、米セキュリティ機関のSANS Internet Storm Centerは直ちにパッチを適用するよう呼び掛けている。

「PHPMailer」に重大な脆弱性、直ちにパッチ適用をより引用
「任意コードを実行される」なんて血沸き肉踊りますとても心配ですよね。ということで、詳しく調べてみることにしました。

PoC

PoCは既に公表されているということと、現実のところ攻撃は難しいと考えられるため、この脆弱性のPoCを下記に示します。
<?php
require("PHPMailer/class.phpmailer.php");
$to = 'tokumaru@example.jp'; // 自分のメールアドレスに変更して下さい

// Exploit
$from = '"a \" -OQueueDirectory=/tmp -X/tmp/exploit.php \"a"@example.jp';

$mail = new PHPMailer();
$mail->AddAddress($to);
$mail->Sender = $from;
$mail->Subject = 'CVE-2016-10033 PoC';
$mail->Body = '<?php phpinfo(); ?>'; // 実行コード
if(!$mail->send()) {
echo 'Mailer Error: ' . $mail->ErrorInfo . "\n";
} else {
echo "Message has been sent\n";
}
これを実行すると、/tmp/exploit.php として下記のファイルが生成されます。
05284 <<< To: hiroshi2009@tokumaru.org
05284 <<< Subject: CVE-2016-10033 PoC
... 中略
05284 >>> X-Mailer: PHPMailer 5.2.17 (https://github.com/PHPMailer/PHPMailer)
05284 >>> MIME-Version: 1.0
05284 >>> Content-Type: text/plain; charset=iso-8859-1
05284 >>>
05284 >>> <?php phpinfo(); ?>
05284 >>>
05284 >>> .
05284 <<< 250 2.0.0 uBS1bgVM005294 Message accepted for delivery
05284 >>> QUIT
05284 <<< 221 2.0.0 localhost closing connection
本文中のPHPスクリプトがログとして出力されていることがわかります。
実際の攻撃では、ドキュメントルート上にウェブシェルを設置する等の攻撃ができることになります。

なぜ攻撃が成立するか

PHPMailerのSenderプロパティは、メールの「エンベロープFrom」を設定するもので、受け取ったメールでは、Return-Pathヘッダとして設定されるものです。エンベロープFromについては、水無月ばけらさんの解説をお読み下さい。
PHPMaierでは、メール送信に先立ち、Senderとして設定されたメールアドレスがRFC準拠かどうかを確認した後、下記の流れでmail関数の第5パラメータにセットされます。
$params = sprintf('-f%s', $this->Sender);
... 色々な処理
$result = @mail($to, $subject, $body, $header, $params);
この第5パラメータは、sendmailコマンドに引き渡すオプションパラメータを指定するもので、小邨孝明さんの解説によると、sendmailコマンドの呼び出し前に、escapeshellcmd同等のエスケープ処理が施されます。
mb_send_mail関数(mail関数も同様)ですが、第5引数(additional_parameter)にユーザの入力を使用する場合は注意が必要です。mb_send_mail関数の第5引数は、内部でescapeshellcmd(内部関数名:php_escape_shell_cmd)によって引数の文字列全体がエスケープされます。

escapeshellcmd() は、以前に徳丸さんから OS コマンドインジェクションの防止には不適切であると指摘されています。ユーザの入力値を十分にチェックしておかないと、sendmailコマンドに任意のコマンドライン引数を渡されてしまうことになります。
mb_send_mail(),mail()で第5引数を設定する際の注意点より引用
そして、まさに『sendmailコマンドに任意のコマンドライン引数を渡されてしまう』ことが起こったわけです。

攻撃に用いるメールアドレスについて

先のPoCで用いた攻撃用のメールアドレスは下記のものですが、
"a \"  -OQueueDirectory=/tmp  -X/tmp/exploit.php \"a"@example.jp
これは、RFC5322等に適合しているため、PHPMailer内部のバリデーションを追加します。このあたりは@cakephperさんが詳しく解説されていますが、
悲しいことに、PCREが入っているほうがPHPの対象範囲が広くなってしまいます・・・
PCREが入っておらず、PHP5.2以上の場合はPHPのfilter_var()でメールアドレスチェックが行われるため救われます。PCREの正規表現よりもfiter_var()を優先すれば良いのに・・・

PHPMailerのリモートコード実行脆弱性(CVE-2016-10033)の影響範囲より引用
正規表現だとチェックをすりぬけ、filter_varだと弾かれると書いてありますが、これはおそらく、filter_varのメールアドレスチェックは、たとえクォートされていても空白を許さないからだと思います。このあたりは、こちらの記事で書いたことがあります。
さて、実は、先のメールアドレスをそのままsendmailに渡せば、メールアドレスのクォートがうまく働いて攻撃は成立しないのですが、mail関数内部でescapeshellcmd相当のエスケープ処理が入るため、実際のsendmail呼び出しは下記とになります。
sh -c /usr/sbin/sendmail -t -i  -f"a \\" -OQueueDirectory=/tmp -X/tmp/exploit.php \\"a"@example.jp
 -f"a \\"の部分に注目ください。escapeshellcmdがバックスラッシュのみをエスケープしたことが仇となって、実際にsendmailコマンドに渡される-fオプションは、「-fa \」までとなり、-Oパラメータや-Xパラメータがはみ出した状態になります。これが、OSコマンドパラメータインジェクションの原因です。

PHPMailer 5.2.19での対応

対策版とされる PHPMailer 5.2.19 では、以下のようにSenderプロパティをescapeshellarg関数でエスケープしてからmail関数に渡されるようになりました。
$params = sprintf('-f%s', escapeshellarg($this->Sender));
しかし、これはこれで不安ですね。その理由は、escapeshellarg関数の後、mail関数内部でescapeshllcmd相当のエスケープが行われるからです。二重にエスケープされるので予期しない動作になる可能性があります。あれ、これではまずい…
と思って、届出をしていたところ、先に発表されていました。CVE-2016-10045が割り当てられています。PoCも公開されていますね。


先のPoCに対して、Senderのメールアドレスを以下のように変更するだけで、5.2.19への攻撃ができます。ダブルクォートをシングルクォートに変えるだけですね。
$from = '"a \'  -OQueueDirectory=/tmp  -X/tmp/exploit.php \"a"@example.jp';
この場合、まず、escapeshellargの結果は下記となります。
-f'"a '\'' -OQueueDirectory=/tmp -X/tmp/exploit.php \"a"@example.jp'
そして、sendmailの呼び出しは下記となります。
/usr/sbin/sendmail -t -i  -f'\"a '\\'' -OQueueDirectory=/tmp -X/tmp/exploit.php \\"a"@example.jp\'
ややこしいですが、\がエスケープされて二重になっていますね。その結果、-fオプションが -f'\"a '\\''で切れてしまっています。この結果は、以下の3要素を連結したものとなっています。
-f'\"a '
\\
''
その結果、sendmailに実際に渡されるパラメータは、-f\"a \ です(''は空文字列ですので)。
結果として、-Oオプションや、-Xオプションがクォートの外にはみ出し、攻撃が成立しています。これは、escapeshellargとescapeshellcmdを両方していることの副作用ですので、安全なエスケープは難しそうです。

影響を受けるMTA

前述のように、現在公開されているPoCは、sendmailコマンドの-Xオプション(MTAのログを-Xで指定したファイルに出力する)を用いたものですが、postfixのsendmailラッパーでは-Xオプションは無効化されているため、攻撃の影響を受けません。私の気づいていない他の攻撃経路の可能性はありますが、さしあたり、sendmailをMTAとして使っている場合よりも、影響を受ける可能性は低いと考えられます。

ケーススタディ HASHコンサルティングのウェブサイト

CVE-2016-10033の影響を受けるソフトウェアの一つにWordPressがあげられています。既報のように、弊社 HASHコンサルティング株式会社のオフィシャルウェブサイトは、2016/9/30にWordPressを用いてリプレースしており、Contact Form 7による問い合わせフォームがあります。Contact Form 7は、WordPressのwp_mailという関数を通じてPHPMailerを呼び出しており、WordPress 4.7(最新版)にバンドルされるPHPMailer は5.2.14という脆弱なバージョンです。
つまり、ビンゴとなっているのですが、下記の2点から、弊社サイトは PHPMailerの影響を受けないと考えられます。
  • エンベロープFrom(Senderプロパティ)は固定とになっていて外部から操作できない
  • MTAとしてPostfixを使用している
特に、Senderプロパティを操作できない点が大きいですね。上記は、あくまで弊社サイトでの状況なので、個別のサイトについてはご確認ください。

対策

対策版として PHPMailer 5.2.19が公開されていますが、前述のようにこの対策はイマイチで、導入してもあまり安全性は変わりません。
このため、回避策として以下を推奨いたします。
  • Senderプロパティを設定しない、あるいは固定にする(強く推奨)
  • MTAとしてsendmailを避け、postfixを使用する
  • ドキュメントルート下のファイルやディレクトリをPHPスクリプトが動作するユーザから書き込み不可に設定する
  • 改ざん検知の仕組みを導入する(緩和策)
どーーーしても、Senderプロパティを動的に変更したい場合、PHPMailerの呼び出し側で、メールアドレスを厳し目にバリデーションするくらいでしょうか。この場合、空白、ダブルクォート、シングルクォート、バックスラッシュが弾かれることを確認ください。

まとめ

PHPMailerの脆弱性CVE-2016-10033とCVE-2016-10045について報告しました。現在公開されているPHPMailer 5.2.19の対策は不完全ですので、回避策の導入を推奨します。
メールアドレスのような複雑なルールの仕様の場合、バリデーションを厳密にしていても重大な脆弱性が混入する可能性があるという点で興味深いと感じしました。また、小邨孝明さんの予言が的中した形となり、氏の先見の明には感服するばかりです。

免責

このセキュリティ情報は予告なしに改訂される場合がある。このセキュリティ情報を適用した結果について徳丸浩およびHASHコンサルティング株式会社は一切の責任を負わず、利用者の利益のために、あるがままの状態で公開するものである。

PHPのmail関数、mb_send_mail関数のマニュアルに警告が追記されていた

$
0
0
昨日の記事PHPMailerの脆弱性CVE-2016-10033について解析したにて、PHPMailerの脆弱性CVE-2016-10033の原因はmail関数が内部で呼んでいるescapeshellcmd関数の仕様が原因であると指摘しました。そして、mail関数の危険性については、小邨孝明さんが2013年12月23日の記事にて指摘していました。
mb_send_mail関数(mail関数も同様)ですが、第5引数(additional_parameter)にユーザの入力を使用する場合は注意が必要です。mb_send_mail関数の第5引数は、内部でescapeshellcmd(内部関数名:php_escape_shell_cmd)によって引数の文字列全体がエスケープされます。

escapeshellcmd() は、以前に徳丸さんから OS コマンドインジェクションの防止には不適切であると指摘されています。ユーザの入力値を十分にチェックしておかないと、sendmailコマンドに任意のコマンドライン引数を渡されてしまうことになります。
mb_send_mail(),mail()で第5引数を設定する際の注意点より引用
私はこの記事が書かれて直ぐに読んでいましたが、残念ながら多くの方が知るとこではなかったように思います。しかし、今日になって、小邨さんの注意喚起がPHPのマニュアルに追記されていることに気が付きました。


additional_parameters(オプション)
パラメータ additional_parameters は、 追加のフラグをコマンドラインオプションとしてメール送信プログラムに渡す際に使用可能です。 メール送信プログラムは、設定オプション sendmail_path により設定されます。例えば、 sendmail を使用する際に -f オプションを使って エンベロープの sender アドレスを設定する際に使用できます。
このパラメータはコマンドの実行を防止するために内部的に escapeshellcmd() によってエスケープされます。 escapeshellcmd() はコマンドの実行を防止しますが、 別のパラメータを追加することは許してしまいます。セキュリティ上の理由から、 シェルコマンドへの望ましくないパラメータの追加を避けるために、 ユーザーはこのパラメータを適切に処理することが推奨されます。
escapeshellcmd() が自動的に適用されるため、 インターネット RFC でメールアドレスとして許可さているいくつかの文字を使用することができません。 mail() はそうした文字を許可しないため、プログラム中でそうした文字の使用が必須である場合、 メール送信の代替手段(フレームワークやライブラリの使用など)が推奨されます。
PHP: mail - Manualより引用
私は、CVE-2016-10033の主原因はPHPMailerというよりもPHPのmail関数側にあると考えていましたが、上記のようにマニュアル上で「免責」されていたことになります。
それでは、いつごろこの注意書きが追加されたのだろうとarchive.orgで確認したところ、下記のような結果でした。
したがって、2014年1月6日から同年2月9日のどこかのタイミングでこの注意書きが追記されたことになります。いずれにせよ、小邨さんの記事からほどなく、PHP本家のマニュアルに注意書きが追記されたことになります。

これはこれで立派なことですが、PHP利用者は常にマニュアルの変更を見張っているわけではないので、もう少し注意喚起を工夫できなかっただろうかと感じました。

PHPのescapeshellcmdを巡る冒険はmail関数を経てCVE-2016-10033に至った

$
0
0

エグゼクティブサマリ

2011年始めに徳丸がescapeshellcmdの危険性を指摘したが、この問題はmail関数のadditional_parameters経由で攻撃可能であることが2013年末に指摘された。その後2016年末に、PHPMailerの脆弱性CVE-2016-10033として現実のものとなった

経緯


escapeshellcmdはなぜ危険か

経緯のところで説明したように、escapeshellcmdはコマンド文字列全体をエスケープするための関数です。
サンプルとして以下のプログラムを考えます。
<?php
$mail = $_GET['mail'];
$cmd = "/usr/sbin/sendmail -i $mail";
system($cmd);
このプログラムは外部から指定されたメールアドレスに対して空のメールを送信するものです(実用上の意味はありませんが説明を簡単にするためですのでご容赦ください)。…が、一見して分かるようにOSコマンドインジェクション脆弱性があります。例えば、メールアドレスとして下記のメールアドレスを指定します。
sample@example.jp; cat /etc/passwd
実行されるコマンドは下記となり、メール送信に続いて、cat /etc/passwd が実行されます。
/usr/sbin/sendmail sample@example.jp; cat /etc/passwd
このような攻撃を防ぐために、escapeshellcmdは下記のように用います。
<?php
$mail = $_GET['mail'];
$cmd = "/usr/sbin/sendmail -i $mail";
$cmd = escapeshellcmd($cmd);
system($cmd);
この場合、実行されるコマンドは下記となり、OSコマンドインジェクションは避けられます。
/usr/sbin/sendmail -i sample@example.jp\; cat /etc/passwd
しかし、cat /etc/passwdという余計なオプションがついてしまいますので、$mailをダブルクォートで囲むことも一案です。
<?php
$mail = $_GET['mail'];
$cmd = "/usr/sbin/sendmail -i \"$mail\"";
$cmd = escapeshellcmd($cmd);
system($cmd);
実行結果は下記となります。
/usr/sbin/sendmail -i "sample@example.jp\; cat /etc/passwd"
今度は ; cat /etc/passwdも含めてメールアドレスの一部となり、メール送信はエラーになりますが、OSコマンドインジェクションもオプション追加も避けられています。
しかし、このような使い方を想定すると、ダブルクォート「"」はエスケープしてはいけないことになり、実際エスケープされていません。このため、escapeshellcmdのマニュアルには以下のように記載されています。
'および "は、対になっていない場合にのみエスケープされます
ということは、メールアドレスとして下記を指定した場合、
-OQueueDirectory=/tmp""-X/tmp/exploit.php""sample@example.jp
実行されるコマンドは下記となり、パラメータインジェクション攻撃が成立します。
/usr/sbin/sendmail -i "-OQueueDirectory=/tmp""-X/tmp/exploit.php""sample@example.jp"
この攻撃が成立する理由は、前述のとおり、escapeshellcmdの「'および "は、対になっていない場合にのみエスケープされます」という仕様によるものであり、これはescapeshellcmdのユースケースを考えると不可避であり、仕様の欠陥といえるものです。このため、OSコマンドインジェクション対策には、escapeshellcmdの利用を避け、escapeshellarg関数を用いる必要があります。
CVE-2016-10033の本質は、mail関数とmb_send_mail関数が、OSコマンドインジェクション対策として内部的にescapeshellcmd関数を呼び出しているために、潜在的にパラメータインジェクションに対して脆弱である点をついたものです。

CVE-2016-10033 PoCの巧妙さ

実際のCVE-2016-10033は上記の説明とは少し違います。mail関数は、宛先をメールヘッダから取得する -t オプションが指定されていますが、PHPMailerはSender(エンベロープFrom)の設定するため、sendmailコマンドの-fオプションをmail関数の第5パラメータadditional_parameters経由で指定しています。典型的なsendmail呼び出しは下記となります。
/usr/sbin/sendmail -t -i -fsample@example.jp
ご覧のように-fオプション全体やメールアドレスはダブルクォートで囲まれていません。しかし、これを突破するのは容易ではありません。なぜなら、
  • PHPMailerはSenderプロパティに対してRFC準拠のメールアドレスであることを確認している
  • sendmail呼び出しの前にescapeshellcmdによるエスケープ処理がなされている
  • sendmailコマンドのパラメータチェックを回避する必要がある
この3点を同時に満足するメールアドレスを見つける必要があるからです。私が先に示したPoCでは以下のメールアドレスを使いました。
"a \" -OQueueDirectory=/tmp  -X/tmp/exploit.php \"a"@example.jp
このメールアドレスは、RFC5321等を満たしています。escapeshellcmdを通ったあとのsendmailコマンド呼び出しは下記となります。
/usr/sbin/sendmail -t -i  -f"a \\" -OQueueDirectory=/tmp -X/tmp/exploit.php \\"a"@example.jp
"は偶数個あるためエスケープされませんが、バックスラッシュ「\」はエスケープされています。この不整合により、/bin/sh経由で呼び出されたsendmailコマンドが受け取るパラータは下記となります。
-t
-i
-fa \
-OQueueDirectory=/tmp
-X/tmp/exploit.php
\a@example.jp
「a \」と「\a@example.jp」はメールアドレスが必要なパラメータであり、RFCには違反していますが、sendmailは一旦これらをエラーとせずメール配送を開始するため、攻撃が成立してしまいます。

mail関数はどうすればいいか

mail関数の今のマニュアルには、以下のように書かれています。
escapeshellcmd() が自動的に適用されるため、 インターネット RFC でメールアドレスとして許可さているいくつかの文字を使用することができません。 mail() はそうした文字を許可しないため、プログラム中でそうした文字の使用が必須である場合、 メール送信の代替手段(フレームワークやライブラリの使用など)が推奨されます。
PHP: mail - Manual より引用
つまり、RFCよりも制限を厳しくしたバリデーションを施さないと危険だということですが、この状態はよろしくないと考えます。現に、PHPMailerだけでなく、Swift Mailerにも同種の指摘(CVE-2016-10074)がなされていますが、PoCが同じなので、mail関数の仕様による問題と考えられます。

これを改善するには、additional_parametersとして現在の文字列パラータに加えて、配列を許すようにする案が考えられます。配列としてパラメータを一つずつ指定できれば、escapeshellarg関数により安全にエスケープ処理が行えるからです。

まとめ

2011年はじめにescapeshellcmdの問題点を指摘した後、2016年末にPHPMaierの脆弱性CVE-2016-10033という重大な問題として顕在化した流れを報告しました。PHPは長い歴史の中で安全性を高めており、この記事や、このスライドにまとめたことがありますが、まだ危険な関数は残っています。今回の問題は、escapeshellcmd、mail、mb_send_mail関数の仕様が問題でした。
今回の問題を機に、さしあたりmail関数とmb_send_mail関数の仕様が見直されることを期待しています。


Joomla! 3.4まではUTF-8の4バイト文字を悪用して重複するログイン名が登録できた

$
0
0
以前の記事CMS四天王のバリデーション状況を調査したところ意外な結果になったで報告したように、Joomla!はログイン名の制限が非常にゆるやかになっています。であれば、🍣とか、💩などを含むログイン名が登録できるのだろうかという疑問が生じました。
とはいえ、以前、Joomla!の「ゼロデイコード実行脆弱性」はPHPの既知の脆弱性が原因で報告したように、少なくともJoomla! 3.4.5までは、MySQLの設定上 UTF-8 の4バイト文字は登録できず、それ以降の文字が全て切り詰められるという問題がありました。
このため、「admin🍣」というログイン名を登録しようとすると、🍣の切り詰めが起こって、adminユーザを二重に登録できなるのではないでしょうか?

試してみる

Joomla! 3.4.8の環境を用意して管理者ユーザーを「admin」としておきます。下記のように、default charsetはutf8となっています。
mysql> show create table  dnbd5_users;
CREATE TABLE `dnbd5_users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '',
`username` varchar(150) NOT NULL DEFAULT '',
... 省略
) ENGINE=InnoDB AUTO_INCREMENT=685 DEFAULT CHARSET=utf8
この状態で、「admin🍣」を登録します。下記のように、usernameとしてadminを持つユーザーが二重に登録されていることがわかります。
mysql> SELECT id, name, username, email FROM dnbd5_users where username='admin';
+-----+------------+----------+------------------+
| id | name | username | email |
+-----+------------+----------+------------------+
| 683 | Super User | admin | alice@example.jp |
| 684 | bob | admin | bob@example.jp |
+-----+------------+----------+------------------+
2 rows in set (0.00 sec)
これは一種のColumn SQL Truncationといえますが、セキュリティ上の問題はないのでしょうか?

セキュリティ上の影響はないのか

上記の現象がセキュリティ上の問題となるシナリオの典型例は下記のものです。
  • 攻撃者がセルフサービスでadmin🍣ユーザーを登録し、結果としてadminとして登録される
  • 攻撃者がユーザー=admin、パスワード=自分の登録したパスワードでログインする
  • 攻撃者の権限がSuper Userのものとなる
このシナリオの例を以前記事に書きましたので興味のある方は参照ください。
しかし、Joomla!の場合、ここまでの悪用はできないようです。その理由は以下の通りです。
Joomla!のログイン処理のSQL文は以下の通りですが、
SELECT id, password FROM dnbd5_users WHERE username='admin'
私がさまざまな条件でテストした範囲では、常に(先に登録された)Super Userの方がログイン処理に用いられ、後から追加したユーザー(bob)はログインチェックの対象にならないようです。
そして、仮にbobの方がマッチしたとしても、権限等はユニークな id で管理されているので、bobがSuer Userとしての権限を持つことはありません。
最悪の状態では、Super Userの方がログイン対象にならず、ログインできなくなるという事態は考えられますが、状況の実現には至っていません。

Joomla! 3.5以降の対応

上記は、Joomla! 3.4.8までの仕様ですが、Joomla! 3.5.0になって、データベースのデフォルトの文字エンコーディングが utf8mb4 に変更されました。これにより、Joomla! 3.5.0以降では、UTF-8の4バイト文字を悪用した攻撃は、基本的にできなくなると考えられます。

アップグレードではどうなるか?

また、旧バージョンからのバージョンアップの際にも、データベースのデフォルト文字エンコーディングが utf8mb4 に変更されるようです。
下記は、Joomla! 3.4.8インストール後にJoomla! 3.6.5にアップデートした状態で admin🍣 ユーザーを登録したものですが、たしかに admin🍣 というUsernameが作られています。


まとめ

Joomla! 3.4.8まででは、UTF-8の4バイト文字以降が切り詰められるという仕様を悪用して、admin🍣ユーザを登録することで、adminユーザを二重に登録できることを示しました。これによる重大な問題はなさそうですが、最悪adminユーザでログインできなくなる可能性があります。
Joomla! 3.5.0では、データベースのDEFAULT CHASETが utf8mb4 に変更されたため、この問題は解消されています。

GodaddyのSSL証明書にドメイン認証の脆弱性があり8850件の証明書が失効された

$
0
0

エグゼクティブサマリ

GoDaddy社の発行するドメイン認証SSL証明書に認証不備の脆弱性があり、予防的な処置として8850件の発行済証明書が失効された。これは同期間に発行された証明書の2%未満である。現在は脆弱性は解消されている。

概要

GoDaddy社は米国のホスティング(レンタルサーバー)やレジストラの大手で、認証局(CA)の事業も手がけています。
GoDaddyが発行するドメイン認証証明書の認証手続きに不備があったとして報告されています。
In a typical process, when a certificate authority, like GoDaddy, validates a domain name for an SSL certificate, they provide a random code to the customer and ask them to place it in a specific location on their website. When their system searches and finds the code, the validation is complete.

However, when the bug was introduced, certain web server configurations caused the system to provide a positive result to the search, even if the code was not found.

Information about SSL bug - The Garageより引用
すなわち、ドメイン認証の手段として、GoDaddyが発行するランダムなコードを含むファイルをウェブサイト上の特定の位置に設置し、そのファイルの内容を確認することでドメイン所有者であることを確認しているが、ファイルがなくてもドメイン所有者とされ、証明書が発行できていたというのです。

以下、この問題のGoogle groupsにおけるディスカッションも参考にしながら、悪い人がgoogle.comの証明書を入手しようとしたと想定して、悪用の手順を紹介します。
  • 悪人は、GoDaddyのコントロールパネルからwww.google.comのサーバー証明書を要求する
  • コントロールパネルがランダムなコード(以下、例としてtR7PasZyを用います)を発行し、利用者に http://www.google.com/tR7PasZy.html を作成して、その中にtR7PasZyというコードを含めるように要求する
  • 悪人は、実際には上記のページを設置できないが、設置したという報告をコントロールパネル上で行う
  • GoDaddyの認証システムは、http://www.google.com/tR7PasZy.html からコードを読み取ろうとする。この際の表示は下記となる(ステータス404)


HTMLソースは下記の通り。
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style> …省略… </style>
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
<p><b>404.</b> <ins>That’s an error.</ins>
<p>The requested URL <code>/tR7PasZy.html</code> was not found on this server. <ins>That’s all we know.</ins>

  • ステータス404なのに、レスポンス中にコード tR7PasZy が含まれているために、認証は成功してしまう
  • 悪人は、www.google.com の証明書を入手できる

※ 実際には、google.com等の著名ドメイン名はブロックされる可能性もありますが、上記は脅威の分かりやすい例として紹介しています。実際にGoogleの証明書が不正に発行されたわけではありません。

GoDaddy社の対応

GoDaddy社は今年1月3日にメールにて報告を受けた後に、1月6日にエスカレーションされ問題を認識しました。調査の結果、バグが混入したのは2016年7月29日であり、2017年1月10日までに解消されました。
この問題の影響を受けた可能性のある証明書は最大 8850件であり、同期間に発行された証明書の2%未満ということです。これらの証明書は、GoDaddy社により失効手続きがとられ、利用者による再発行手続きが必要となります。

まとめ

GoDaddy認証局のドメイン認証脆弱性について紹介しました。類似の過去事例としては、下記のようなものもあります。


バグの本質的な原因は、HTTPレスポンスのステータスをチェックしていなかったことにありますが、ファイル名とファイルの中身の両方に同一の認証コードを含めるという設計は、上記のように潜在的な問題を抱えていると考えます。したがって、ファイル名とファイル中に記述する認証コードは別々に発行することで、仮にステータスのチェックが抜けていても脆弱性にならないような設計となります。このように、予防的な設計を心がけることが重要です。

WordPress 4.7.1 の権限昇格脆弱性について検証した

$
0
0

エグゼクティブサマリ

WordPress 4.7と4.7.1のREST APIに、認証を回避してコンテンツを書き換えられる脆弱性が存在する。攻撃は極めて容易で、その影響は任意コンテンツの書き換えであるため、重大な結果を及ぼす。対策はWordPressの最新版にバージョンアップすることである。 本稿では、脆弱性混入の原因について報告する。

はじめに

WordPress本体に久しぶりに重大な脆弱性が見つかったと発表されました。
こんな風に書くと、WordPressの脆弱性なんてしょっちゅう見つかっているという意見もありそうですが、能動的かつ認証なしに、侵入できる脆弱性はここ数年出ていないように思います。そういうクラスのものが久しぶりに見つかったということですね。
以下は、ITMediaの記事からの引用です。
 WordPressではこの問題について、「セキュリティ問題は常に公開されるべきというのがわれわれのスタンスだが、今回のケースでは、何百万というWordPressサイトの安全を保証するため、意図的に公開を1週間先送りした」と説明している。
 この間にSucuriをはじめとするセキュリティ企業と連携し、攻撃が発生した場合でも各社のファイアウォールで防御できる態勢を確立。自動更新を通じてWordPressの更新版が行き渡り、できるだけ多くのユーザーが保護されるのを待ってから、情報を公開したという。

WordPress、更新版で深刻な脆弱性を修正 安全確保のため情報公開を先送りより引用

脆弱性のサマリは下記となります。

対象バージョンWordPress 4.7.0 および 4.7.1
影響コンテンツの改ざん
攻撃の認証要否不要
攻撃の難しさ極めて容易
対策WordPressのアップデート(4.7.2にて修正)

認証なしにリクエスト一発でコンテンツを改ざんできるため、影響は極めて深刻です。対象バージョンをお使いの方は、即刻アップデートすることをお勧めします。

攻撃の様子

PoCは複数公開されています。以下、一部を伏字にした形で攻撃の手順を説明します。
以下に攻撃前のコンテンツを示します。WordPressをインストールした直後であることが分かります。


このサイトに対して、以下のリクエストを送信します。一部伏字にしています。


攻撃後の画面は下記となります。攻撃を受けて、コンテンツが改ざんされていることが分かります。


上記から分かるように、認証不要で、攻撃に特別な情報も必要とせず、極めて容易にコンテンツが改ざんできるため、深刻な脆弱性であることがわかります。

脆弱性の原因

脆弱性の原因は、前述のSucuri Blogにて解説されていますが、この記事だと少し分かりにくいので解説を試みます。

WordPressでは以前からREST APIがプラグインとして用意されていましたが、WordPress 4.7以降で WordPress Coreにバンドルされました。今回の脆弱性は、このREST APIにあります。

以下の説明において、攻撃目標は、認証を回避して、id=1のコンテンツを改変することとします。WordPressのREST APIの内部では、以下の2つのメソッドが動きます。
  • update_item_permissions_check (権限の確認)
  • update_item (コンテンツの変更)
下記は、update_item_permissions_check メソッドのソースです。

wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php (Ver 4.7.1)

497: public function update_item_permissions_check( $request ) {
498: $post = get_post( $request['id'] );
499: $post_type = get_post_type_object( $this->post_type );
500: if ( $post && ! $this->check_update_permission( $post ) ) {
501: return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit this post...
502: }
503: if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] &&
! current_user_can( $post_type->cap->edit_others_posts ) ) {
504: return new WP_Error( 'rest_cannot_edit_others', __( 'Sorry, you are not allowed to update ...
505: }
506: if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
507: return new WP_Error( 'rest_cannot_assign_sticky', __( 'Sorry, you are not allowed to make ...
508: }
509: if ( ! $this->check_assign_terms_permission( $request ) ) {
510: return new WP_Error( 'rest_cannot_assign_term', __( 'Sorry, you are not allowed to assign ...
511: }
512: return true;
513: }

ここで、idとして以下のようなパラメータを指定した場合のそれぞれの返り値を示します。

コンテンツの性質返り値
存在しないコンテンツtrue
存在し権限のあるコンテンツtrue
存在し権限のないコンテンツfalse

存在しないコンテンツが指定された場合、update_item_permissions_checkメソッドの様々なチェックをすべてくぐり抜け、メソッド最後のreturn文にて true が返されるところが恐ろしいですね。
しかし、この「存在しないコンテンツ」については、以下の update_item メソッドの 526行目にてエラーが返され、結果としては何もしない *はず* でした。

523:  public function update_item( $request ) {
524: $id = (int) $request['id'];
525: $post = get_post( $id );
526: if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
527: return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
528: }
529: $post = $this->prepare_item_for_database( $request );
530: if ( is_wp_error( $post ) ) {
531: return $post;
532: }

ところが、id=1A が指定された場合に、update_item_permissions_checkメソッドとupdate_itemメソッドの両方で呼ばれている get_post関数が受け取るパラメータを確認してみましょう。

update_item_permissions_checkでは、get_post('1A')が呼ばれ、1AをIDとするコンテンツはないため、「コンテンツは存在しない」が返され、チェック結果は true となります(!)。
一方、update_itemメソッドは、$id を整数にキャストしているため、get_post(1)が呼ばれ、ID=1 のコンテンツが変更されることになります。これにより、本来権限のないコンテンツ ID=1 に対する更新ができてしまうことになります。

以上が、この脆弱性の根本的な原因です。

WordPress 4.7.2での改修内容

WordPress 4.7.2では、代わりに、get_postメソッドがget_post関数のラッパーとして作成され、権限チェックと更新の両方から呼ばれるようになりました。

527:  public function update_item_permissions_check( $request ) {
528: $post = $this->get_post( $request['id'] );
529: if ( is_wp_error( $post ) ) {
530: return $post;
531: }

get_postメソッドの冒頭は下記のとおりです。

318:  protected function get_post( $id ) {
319: $error = new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
320: if ( (int) $id<= 0 ) {
321: return $error;
322: }
323: $post = get_post( (int) $id );
324: if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
325: return $error;
326: }

$id を整数にキャストすることと、「存在しないコンテンツ」に対してエラーを返すようになりました。どちらか一方でも先の攻撃は防げるはずですが、とくに「存在しないコンテンツ」に対するチェックがいいですね(元々そうすべき内容ではありますが)。

教訓

この脆弱性は権限チェックの間違いの典型例です。チェックと更新とで、異なる入力を用いているわけですから、両者の不整合がチェック漏れの原因になります。
WordPress 4.7.2のソースを見ても、使用する度に int にキャストしている箇所があります。これは潜在的にバグや脆弱性の原因です。以下のいずれかにすべきであると考えます。
  • 入力値のバリデーションにて数値以外のものをエラーとする(推奨)
  • 入力時に整数にキャストし、以降の処理では一貫してキャスト後の値を用いる

まとめ

WordPress 4.7.1までのREST APIに存在した脆弱性について説明しました。Sucuriはこの脆弱性をPrivilege Escalation (権限昇格)としています。しかし、Privilege Escalation だと一般ユーザーが特権を悪用できるように感じますが、実際には認証しなくても権限が悪用できるため、「アクセス制御の欠落」の方がより適切であるように思います。
前述のように、極めて危険な脆弱性であるため、当該バージョンのWordPressをお使いのサイトは、至急のアップデートを推奨します。


【HASHコンサルティング広告】

HASHコンサルティング株式会社では、WordPressを用いたウェブサイトのセキュリティ強化支援サービスを提供しています。詳しくは以下を参照下さい。

WordPressサイトのセキュリティ強化支援 | HASHコンサルティング株式会社

WordPressのプラグインNextGEN GalleryのSQLインジェクション脆弱性について検証した

$
0
0

エグゼクティブサマリ

WordPressのプラグインNextGEN Gallery for WordPress 2.1.79未満にはSQLインジェクション脆弱性があり、早急なバージョンアップを推奨する。当該脆弱性は潜在的にWordPressの内部メソッドwpdb::prepareの仕様上の問題が原因であり、他のプラグインにも類似脆弱性の残存の可能性がある。このため、この問題の影響を受けない安全な実装方針を示した。

はじめに

Sucuriブログに、NextGEN Gallery for WordPressのSQLインジェクション脆弱性が報告されました。脆弱性のサマリは下記となります。
対象バージョンNextGEN Gallery for WordPress 2.1.79未満
影響SQLインジェクションによる情報漏えいなど
攻撃の認証要否不要
攻撃の難しさ困難
対策プラグインのアップデート (2.1.79にて修正)


脆弱なコードと検証

脆弱性はMixin_Displayed_Gallery_Queriesクラスのget_term_ids_for_tagsメソッドにあります。当該メソッドを下記に引用します。このメソッドは、タグを$tags配列として受け取り、タグ検索のSQL文を実行します。タグ検索のSQL文のIN句は、下記の※1(赤字)にて生成しています。エスケープなどがなされていないので心配になりますが、タグ中の引用符はHTMLエスケープ(SQLエスケープではない)され、バックスラッシュはフィルタリングで除去されるので、これら記号文字によるSQLインジェクション攻撃はできません。

function get_term_ids_for_tags($tags = FALSE)
{
global $wpdb;
// If no tags were provided, get them from the container_ids
if (!$tags || !is_array($tags)) {
$tags = $this->object->container_ids;
}
// Convert container ids to a string suitable for WHERE IN
$container_ids = array();
if (is_array($tags) && !in_array('all', array_map('strtolower', $tags))) {
foreach ($tags as $ndx => $container) { // ※1 タグ検索の IN 句の生成
$container_ids[] = "'{$container}'";
}
$container_ids = implode(',', $container_ids);
}
// Construct query
$query = "SELECT {$wpdb->term_taxonomy}.term_id FROM {$wpdb->term_taxonomy}\n
INNER JOIN {$wpdb->terms} ON {$wpdb->term_taxonomy}.term_id = {$wpdb->terms}.term_id\n
WHERE {$wpdb->term_taxonomy}.term_id = {$wpdb->terms}.term_id\n
AND {$wpdb->term_taxonomy}.taxonomy = %s";
if (!empty($container_ids)) {
$query .= " AND ({$wpdb->terms}.slug IN ({$container_ids}) OR {$wpdb->terms}.name IN ({$container_ids}))";
}
$query .= " ORDER BY {$wpdb->terms}.term_id";
$query = $wpdb->prepare($query, 'ngg_tag'); // ※2
// Get all term_ids for each image tag slug
$term_ids = array();
$results = $wpdb->get_results($query);
if (is_array($results) && !empty($results)) {
foreach ($results as $row) {
$term_ids[] = $row->term_id;
}
}
return $term_ids;
}

脆弱性の原因

WordPressのプラグインは、一般にWordPressがフレームワークとして提供するwpdbクラスのprepareメソッドを利用してSQL文を実行します。wpdb::prepareは、プレースホルダとしてsprintfに似た %s や %d を用いる独特の構文になっています。
では、タグとして%sを指定したら、何かおかしなことができるのではないでしょうか。やってみましょう。以下は、タグとして aaa%s を指定した場合に生成されるSQL文です。

$query string SELECT wp_term_taxonomy.term_id FROM wp_term_taxonomy
INNER JOIN wp_terms ON wp_term_taxonomy.term_id = wp_terms.term_id
WHERE wp_term_taxonomy.term_id = wp_terms.term_id
AND wp_term_taxonomy.taxonomy = %s
AND (wp_terms.slug IN ('aaa%s') OR wp_terms.name IN ('aaa%s')) ORDER BY wp_terms.term_id

しかし、これは実行時にエラーになります。prepareメソッドからPHPのvsprintf関数が呼び出される際に、%sが3個(上記赤字)あるのに、それに対する値が一つ('ngg_tag')しかないためです。
外部からのパラメータ指定で、SQL文生成のエラーになるという時点で、この箇所にバグがあることになります。何か攻撃の糸口はなんでしょうか。

実は、上記に関連して、以下の記事を書いたことがあります。

書式文字列によるSQLインジェクション攻撃例

上記の記事の中で、%s書式が余計に出てくるためにエラーになる状況について言及しており、そのエラー回避のテクニックについて、以下のように書いています。
 実はこのエラーを回避する方法があります。%sの代わりに、%1$sを指定するのです。これは、書式文字列上の位置に関わらず1番目のパラメータを受けるという意味です。
ということで、aaa%sの代わりに、aaa%1$s を指定してみましょう。今度はバインドまで行われ、生成されるSQL文は下記となります。

$query string SELECT wp_term_taxonomy.term_id FROM wp_term_taxonomy
INNER JOIN wp_terms ON wp_term_taxonomy.term_id = wp_terms.term_id
WHERE wp_term_taxonomy.term_id = wp_terms.term_id
AND wp_term_taxonomy.taxonomy = 'ngg_tag'
                  AND (wp_terms.slug IN ('aaangg_tag') OR wp_terms.name IN ('aaangg_tag')) ORDER BY wp_terms.term_id

エラーにはならない代わりに、とりたてて攻撃もできなさそうなSQL文ができてしまいました。 それでは、次に、%1$%s を指定してみます。バインド前のSQL文は下記となります。

$query string SELECT wp_term_taxonomy.term_id FROM wp_term_taxonomy
INNER JOIN wp_terms ON wp_term_taxonomy.term_id = wp_terms.term_id
WHERE wp_term_taxonomy.term_id = wp_terms.term_id
AND wp_term_taxonomy.taxonomy = %s
AND (wp_terms.slug IN ('aaa%1$%s') OR wp_terms.name IN ('aaa%1$%s'))

これがwpdb::prepareメソッドの中で、%sをシングルクォートで囲む処理が加わります。これは、SQL文の文字列リテラルはシングルクォートで囲むルールに対応するためです。この結果、SQL文は以下のように変形されます。最後の行のみを示します。

AND wp_term_taxonomy.taxonomy = '%s' AND (wp_terms.slug IN ('aaa%1$'%s'') OR wp_terms.name IN ('aaa%1$'%s''))

これがPHPのvsprintf関数で処理されるのですが、ここで上記の %1$'%s (上記赤字)に注目します。実は、これ全体で、vsprintfの書式になっています。1$は1番目のパラメータに対応するという意味、'%はパディングに%を用いるという意味です。ただし、桁指定子がないため、実際にはパティングの指定があっても何もしません。
この結果、バインド結果は下記になります。

AND wp_term_taxonomy.taxonomy = 'aaangg_tag' AND (wp_terms.slug IN ('aaangg_tag'') OR wp_terms.name IN ('aaangg_tag''))

なんということでしょう! シングルクォートの一つがパティング指定と解釈されたため、シングルクォートの対応が狂っています。SQLインジェクション攻撃ができそうな雰囲気が漂い始めました。
それでは、いようよ攻撃です。タグとして以下の文字列を指定します。

aaa%1$%s)) or 1=1#

生成されるSQL文(バインド前)は下記となります。

AND wp_term_taxonomy.taxonomy = %s AND (wp_terms.slug IN ('aaa%1$%s)) or 1=1#') OR wp_terms.name IN ('aaa%1$%s)) or 1=1#'))

wpdbクラスのprepareメソッドにより%sが'%s'とクォートされた結果

AND wp_term_taxonomy.taxonomy = '%s' AND (wp_terms.slug IN ('aaa%1$'%s')) or 1=1#') OR wp_terms.name IN ('aaa%1$'%s')) or 1=1#'))

そして、バインド後は下記のとおりです。

AND wp_term_taxonomy.taxonomy = 'ngg_tag' AND (wp_terms.slug IN ('aaangg_tag')) or 1=1#') OR wp_terms.name IN ('aaangg_tag')) or 1=1#')) ORDER BY wp_terms.term_id

赤字で示した部分が文字列リテラルをはみだし、SQL文の一部として解釈されています。SQLインジェクション攻撃の成功です。

脆弱性は誰のせい?

この問題は、NextGEN Gallery の脆弱性して報告されていますが、私は、wpdbクラスの仕様に問題があるように感じました。
一般に、プレースホルダの実装では、SQL文を構成する文字列リテラル中に「たまたま」プレースホルダ記号(? や :foo など)があってもプレースホルダとは解釈せず、文字列リテラルの一部であると解釈します。たとえば、PDOでは、下記の :user はプレースホルダとは解釈されません。

$db->prepare("SELECT * FROM employee WHERE user=':user'");

ところが、wpdbは、下記の %s がプレースホルダと解釈されることになります。

$wpdb->prepare("SELECT * FROM employee WHERE user='user%s'");

上記の場合、バインド値が'tanaka'の場合WHERE句は、WHERE user='user'tanaka''となり、「tanaka」が文字列リテラルを「はみだす」危なっかしい状態になります。しかし、下記ではそのような現象は起こりません。

$wpdb->prepare("SELECT * FROM employee WHERE user='%s'");

こうなる理由は、wpdb::prepareメソッドの内部で以下の置換処理が行われているからです。

$query = str_replace( "'%s'", '%s', $query ); // 間違ってプレースホルダをクォートしている場合に対処
$query = str_replace( '"%s"', '%s', $query ); // 同上(ダブルクォートの場合)
$query = preg_replace( '|(?<!%)%f|' , '%F', $query ); // ロケールの影響を避けるため浮動小数点数をクォートする
$query = preg_replace( '|(?<!%)%s|', "'%s'", $query ); // 文字列のプレースホルダをクォートするが、%%sはクォートしない

このあたり、いかにもアドホックで間違いが入りやすい状態と考えます。wpdb::prepareはSQL文をパースしていないわけで、同種の問題が他のプラグイン等にも潜在的に残っている可能性が高いと予想します。

対策

プラグイン利用者側の対策は、当該プラグイン(NextGEN Gallery for WordPress)を最新版(2.1.79以降) にバージョンアップすることです。
NextGEN Gallery for WordPress 2.1.79内部での対応は、tag指定された文字列中の % を %% と二重にすることで対策しています。これは書式文字列攻撃としての対策で、局所的には妥当なものと考えます。

いったんまとめ

Sucuriが発見した攻撃手法は極めて巧妙です。外部からはシングルクォートがフィルタリングされてSQL文内に指定できないところ、wpdb::prepareが%sの前後に挿入するシングルクォートを攻撃に使っています。しかも、挿入される2つのシングルクォートのうち一つを、vsprintfの書式(パディング指定)として解釈させることにより取り除いている点が巧妙です。
前述のように、この脆弱性は、WordPressの内部メソッドwpdb::prepareの潜在的な問題をNextGEN Galleryプラグインが踏み抜いてしまったものと考えます。

wpdb::prepareはどのように使えばよいか?

前述のように、この問題はwpdb::prepareメソッドの仕様(あるいは実装)がイケテナイ点が潜在的な原因になっていますが、WordPressのプラグイン等を使う場合は、このメソッドを使わないわけにはいきません。では、プラグイン開発者はどのようにwpdb::prepareメソッドを使えばよいでしょうか?
その答えは、「SQL文中に外部由来の値を混ぜない」という原則を徹底することです。
そんなこと言っても、IN句の中身は可変数なのでどうすればいいのという疑問が出てきますが、Drupalが採用している方法が参考になります。
Drupalでは、IN句の中身を生成する際に、可変個のプレースホルダを内部的に生成しています。それを真似ると、今回のケースでは、以下のようなSQL文を生成すればよかったことになります。
BEFORE : wp_terms.slug IN ('foo', 'bar', 'baz')
AFTER : wp_terms.slug IN (%s, %s, %s)
これに伴って、バインド値も調整することになります。このような実装にすることにより、SQL文中に外部からプレースホルダを指定させられる攻撃(一種の書式文字列攻撃)の防止を含め、SQLインジェクション脆弱性がないことを明確にできます。

まとめ

WordPressのプラグインNextGEN Galleryに対する巧妙なSQLインジェクション攻撃について説明しました。この問題の原因は、wpdb::prepareメソッドの潜在的なイケテナサが原因で、かつNextGEN Galleryの実装に外部から%が指定される場合の考慮漏れにあります。NextGEN Galleryの実装方針にも改善の余地があります。
前述のように、wpdb::prepareメソッドの潜在的な問題が原因ですので、他のWordPressプラグインにも同種の問題が残っている可能性があります。今後、可能な範囲で調査してみたいところです。

SQLiteのクォートにまつわる奇妙な仕様

$
0
0
SQLiteでは、ISO SQL標準同様に、文字列リテラルはシングルクォートで囲み、識別子をクォートする場合は、ダブルクォートで囲むことになっています。

'foo' : 文字列リテラル
"foo" : 識別子(テーブル名、列名等)

しかし、マニュアルによると、SQLiteのクォーティングには例外があります。それを実例で紹介しましょぅ。まずは、実験の準備として、列 a だけを持つテーブル a を作成します。
$ sqlite3 test.db
sqlite> CREATE TABLE a(a integer);
sqlite> INSERT INTO a VALUES(1);
sqlite> SELECT * FROM a;
1
sqlite>
続いて、以下を実行します。実行結果はどうなるでしょうか?
sqlite> SELECT 'a', "a", [a], `a`, "aa" FROM 'a'
これ、FROM 'a'のところが文法違反に見えますよね。FROMの後には表の名前が続くはずです。また、"aa"は列名だとするとそのような列は存在しないので、こちらもエラーになるはずです。
しかし、SQLiteの場合、上記SQL文はエラーにならず、以下の結果となります。
sqlite> SELECT 'a', "a", [a], `a`, "aa" FROM 'a';
a|1|1|1|aa
sqlite>
これはどういうことでしょうか?その答えは、SQLiteのマニュアルにあります。
For resilience when confronted with historical SQL statements, SQLite will sometimes bend the quoting rules above:
  • If a keyword in single quotes (ex: 'key' or 'glob') is used in a context where an identifier is allowed but where a string literal is not allowed, then the token is understood to be an identifier instead of a string literal.
  • If a keyword in double quotes (ex: "key" or "glob") is used in a context where it cannot be resolved to an identifier but where a string literal is allowed, then the token is understood to be a string literal instead of an identifier.
SQLite Query Language: SQLite Keywordsより引用
すなわち、シングルクォートで囲まれたキーワードが、文字列リテラルが許可されていない箇所に置かれている場合、識別子として認識されます。上記の FROM 'a'の 'a'がこれに該当します。
また、ダブルクォートで囲まれたキーワードであるのに、該当する列名等がない場合、文字列リテラルとして認識されます。上記の "aa"がこれに該当します。
SQLite はなんてすごいんだ! 僕達の気持ちをここまでくんでくれるなんて!!
というのはもちろん冗談で、こんなのは余計なお世話としか言いようがありません。開発者が間違った場合でもエラーにならず、見つけにくいバグの原因になりそうです。

ちなみに、SQLiteは、識別子を [] で囲む方式(MS SQL風)や、バッククォートで囲む方式(MySQL風)もサポートしていますが、これらの場合は、自動的に文字列リテラルとして認識されることはないようです。だったらこれらを使えば…という意見もありそうですが、ISO標準にはない書き方なので、移植性がそこなわれますよね。
ということで、SQLiteを使う場合、もしクォートする必要がなければ、識別子はクォートしない方が無難なのではないでしょうか? そうすれば、勝手に文字列リテラルとして認識されることもないし、移植性も損なわれません。

なお、この問題に起因するセキュリティ上の問題はないか考えてみましたが、思いつきませんでした。思いついた方があれば、ぜひ教えてください。

追記(20:35)

この問題が原因で脆弱性となる例を考えましたので紹介します。
ログイン処理で、「ログイン状態を保存」をトークンを用いて実装していたとします。SQL文は下記となります(SQLインジェクション対策はされていると想定)。
SELECT * FROM users WHERE "token" = $token
クッキーなどにセットされたトークンが、列 token に一致するものがあれば、そのユーザで自動ログインするという想定です。
しかし、列 token のつづりを以下のように間違えていたとします。
SELECT * FROM users WHERE "tokem" = $token
すると、通常はランダム文字列であるトークンとして、固定の文字列 'tokem'を指定するとログインできてしまうことになります。"tokem"という列が存在しないため、WHERE句は 'tokem' = $token と等価だからです。

し・か・し・な・が・ら、この例では正常系がまともに動かないため、テストさえしていれば、この脆弱性が入ったままリリースされることはないでしょう…ということで、あまり現実的ではない…しかし、絶対にないとも言い切れない…脆弱性の例でした。
Viewing all 194 articles
Browse latest View live